evan.liu 07509a2387 迁移uni-app x到uni-app vue3,蓝牙去第三方插件改uni内置BLE API
- uni-app x官方无内置蓝牙API(文档明确见插件市场),为实现去插件化,工程整体迁移为uni-app vue3(vue/ts),App端内置完整BLE API,标准基座即可运行
- 新建src/BluetoothManager.ts:基于uni内置BLE API(createBLEConnection/notifyBLECharacteristicValueChange/writeBLECharacteristicValue(writeNoResponse)/setBLEMTU/getBLEDeviceRSSI等),保持原插件type约定,页面业务逻辑不变
- 删除uni_modules/android-ble及未使用的lime-*、uni-wifi;保留uni-icons、uni-scss
- uts模块全部移植为ts(src/mcm、api、common):UTSJSONObject改Record<string,any>,适配vue3 getStorageSync空值返回空串的差异
- 7个页面uvue转vue:模板不变,显式import ref与生命周期钩子;OTA固件读取由java.io改为plus.io+uni.base64ToArrayBuffer
- manifest.json去uni-app-x改app-plus结构,补Android 12+蓝牙权限;删除platformConfig.json;运行改用标准基座
2026-09-04 06:02:29 +08:00

346 lines
7.2 KiB
Vue

<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<view v-if="registerFlag == false">
<view class="textType">
<text >email</text>
</view>
<view class="input-wrapper">
<uni-icons type="person" size="20" class="input-icon" />
<input
class="inputType"
type="text"
v-model="account"
prefixIcon="person"
placeholder="email"
/>
</view>
<view class="textType">
<text >pinCode</text>
</view>
<view class="input-wrapper">
<uni-icons type="starhalf" size="20" class="input-icon" />
<input
class="inputType"
type="text"
v-model="pinCode"
prefixIcon="person"
placeholder="pinCode"
/>
<view
class="code-btn"
:class="{ disabled: isCountingDown }"
@tap="onGetCodeTap"
>
<text class="textCode">{{ isCountingDown ? `${countdown}s` : "get code" }}</text>
</view>
</view>
<view class="buttonType">
<button class="buttonSize" @tap="onNextStep">next</button>
</view>
</view>
<view v-else>
<view class="textType">
<text>password</text>
</view>
<view class="input-wrapper">
<uni-icons type="locked" size="20" class="input-icon" />
<input
class="inputType"
type="text"
:password="!showPwd"
v-model="password"
placeholder="password"
/>
<!-- 右侧“眼睛”按钮 -->
<view class="toggle" @tap="showPwd = !showPwd">
<uni-icons :type="showPwd ? 'eye' : 'eye-filled'" size="20" />
</view>
</view>
<view class="textType">
<text>enter the password again</text>
</view>
<view class="input-wrapper">
<uni-icons type="locked" size="20" class="input-icon" />
<input
class="inputType"
type="text"
:password="!showAgainPwd"
v-model="passwordAgain"
placeholder="password"
/>
<!-- 右侧“眼睛”按钮 -->
<view class="toggle" @tap="showAgainPwd = !showAgainPwd">
<uni-icons :type="showAgainPwd ? 'eye' : 'eye-filled'" size="20" />
</view>
</view>
<view class="textTip">
<text class="textSize">tips: The password should be at least six characters long and include both uppercase and lowercase letters as well as numbers.</text>
</view>
<view class="buttonType">
<button class="buttonSize" @tap="GetRegisteredUserReq">register</button>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { RegisteredUserReqApi, GetPindCodeReqApi } from '../../api/requestApi'
const showPwd = ref(false)
const showAgainPwd = ref(false)
const account = ref("")
const password = ref("")
const passwordAgain = ref("")
const pinCode = ref("")
const resPinCode = ref("")
const registerFlag = ref(false)
async function GetpindCode() {
try {
const res = await GetPindCodeReqApi({
userName: account.value,
userPassword: password.value,
rndcode: "123"
})
// console.log("res = ", res)
const statusCode = res["statusCode"] as number
const data = res["data"] as Record<string, any>
if(statusCode == 200) {
resPinCode.value = (data["pinCode"] as string) ?? ""
uni.showToast({
title: "send pinCode success",
icon: 'success',
duration: 2000
})
} else {
const msg = (data["result"] as string | null) ?? "The user has registered"
uni.showToast({
title: msg + ": " + statusCode,
icon: 'error',
duration: 2000
})
}
} catch (e) {
} finally {
}
}
async function GetRegisteredUserReq(){
if(password.value != passwordAgain.value) {
uni.showToast({
title: "The password is inconsistent",
icon: 'error',
duration: 2000
})
return
}
try {
const res = await RegisteredUserReqApi({
userName: account.value,
userPassword: password.value,
rndcode: "123"
})
const statusCode = res["statusCode"] as number
const data = res["data"] as Record<string, any>
// console.log("res = ", res)
if(statusCode == 200) {
const msg = (data["result"] as string | null) ?? "Registration successful"
uni.showToast({
title: msg,
icon: "success",
duration: 2000,
mask: true
})
uni.redirectTo({
url: "/pages/login/login"
})
} else{
const msg = (data["result"] as string | null) ?? "The user has registered"
uni.showToast({
title: msg + ": " + statusCode,
icon: 'error',
duration: 2000
})
}
} catch (e) {
} finally {
}
}
function onNextStep() {
console.log("pinCode.value = ", pinCode.value)
console.log("resPinCode.value = ", resPinCode.value)
if (pinCode.value.length == 0 || pinCode.value != resPinCode.value) {
uni.showToast({
title: "pindCode error",
icon: 'error',
duration: 2000
})
return
}
registerFlag.value = true
}
const isCountingDown = ref(false)
const countdown = ref(0)
let countdownTimer: number = 0
function startCountdown(sec: number) {
// 防止重复启动
if (countdownTimer != 0) {
clearInterval(countdownTimer)
countdownTimer = 0
}
isCountingDown.value = true
countdown.value = sec
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
countdownTimer = 0
isCountingDown.value = false
countdown.value = 0
}
}, 1000)
}
async function onGetCodeTap() {
// 倒计时期间不允许再点
if (isCountingDown.value) return
if(account.value.length == 0) {
uni.showToast({
title: "please enter email",
icon: 'error',
duration: 2000
})
return
}
// 先调你的接口
await GetpindCode()
startCountdown(60) // 60秒倒计时
}
</script>
<style scoped>
.page{
flex: 1;
display: flex;
flex-direction: column;
padding: 20rpx;
}
.buttonType {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.buttonSize {
width: 90%;
border-radius: 10rpx;
background-color: #6d91b3;
}
.buttonBack {
width: 80%;
border-radius: 10rpx;
background-color: #eee;
}
.input-wrapper {
position: relative;
width: 90%;
height: 80rpx;
margin: 20rpx auto;
}
.inputType {
width: 100%;
height: 100%;
box-sizing: border-box;
padding-left: 80rpx; /* 给左边图标腾位置 */
padding-right: 230rpx;
border: 2rpx solid #ddd;
border-radius: 12rpx;
font-size: 28rpx;
}
.input-icon {
position: absolute;
left: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.toggle {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.textType {
width: 600rpx;
margin: 10rpx auto 0;
}
.textTip {
width: 90%;
margin: 20rpx auto;
}
.textSize {
font-size: 32rpx;
color: #6d91b3;
}
.code-btn {
display: flex;
transform: none;
position: absolute;
right: 5rpx;
top: 5rpx;
background-color: #f8f8f8;
height: 70rpx;
width: 200rpx;
align-items: center;
justify-content: center;
border-radius: 12rpx;
}
.code-btn.disabled {
opacity: 0.5;
pointer-events: none;
}
.textCode {
font-size: 32rpx;
color: #6d91b3;
line-height: 1.4;
}
</style>