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

411 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<view class="login-tabs">
<!-- 左:密码登录 -->
<view
class="login-tab-item"
@click="switchTab(1)"
>
<text class="login-tab-text" :class="currentTab == 1 ? 'active-text' : '' ">Password login</text>
<view class="login-tab-underline" v-if="currentTab == 1"></view>
</view>
<!-- 右:短信登录 -->
<view
class="login-tab-item"
@click="switchTab(0)"
>
<text class="login-tab-text" :class="currentTab == 0 ? 'active-text' : '' ">SMS login</text>
<view class="login-tab-underline" v-if="currentTab == 0"></view>
</view>
</view>
<view class="page-box">
<!-- SMS login 表单 -->
<view v-if="currentTab == 0">
<view class="input-wrapper">
<uni-icons type="person" size="20" color="#3A6FA1" class="input-icon" />
<input
class="inputType"
type="text"
v-model="phoneNumber"
prefixIcon="person"
placeholder="mobile phone number"
/>
</view>
<view class="input-wrapper">
<uni-icons type="locked" size="20" color="#3A6FA1" class="input-icon" />
<input
class="inputType"
v-model="verificationCode"
prefixIcon="locked"
placeholder="verification code"
/>
<view class="code-btn" @click="onGetCode"> get code </view>
</view>
</view>
<!-- Password login 表单 -->
<view v-else>
<view class="input-wrapper">
<uni-icons type="person" size="20" color="#3A6FA1" class="input-icon" />
<input
class="inputType"
type="text"
v-model="account"
prefixIcon="person"
placeholder="account"
/>
</view>
<view class="input-wrapper">
<uni-icons type="locked" size="20" color="#3A6FA1" 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" color="#3A6FA1" />
</view>
</view>
<!-- 记住密码 -->
<view class="remember-row" @tap="rememberPwd = !rememberPwd">
<uni-icons :type="rememberPwd ? 'checkbox-filled' : 'circle'" size="18" :color="rememberPwd ? '#3A6FA1' : '#999999'" />
<text class="remember-text">remember password</text>
</view>
<!-- 历史账号 -->
<view class="history-row" v-if="historyList.length > 0" @tap="historyOpen = !historyOpen">
<uni-icons :type="historyOpen ? 'up' : 'down'" size="16" color="#3A6FA1" />
<text class="history-text">saved accounts ({{ historyList.length }})</text>
</view>
<view class="history-list" v-if="historyOpen && historyList.length > 0">
<view class="history-item" v-for="(item, idx) in historyList" :key="idx" @tap="onPickHistory(item)">
<uni-icons type="person" size="16" color="#3A6FA1" />
<text class="history-account">{{ item.account }}</text>
<view class="history-del" @tap.stop="onRemoveHistory(item)">
<uni-icons type="closeempty" size="16" color="#999999" />
</view>
</view>
</view>
</view>
</view>
<view class="buttonType">
<button class="buttonSize" @click="LoginRequest">login</button>
<text class="registerText" @click="onRegister">Don't have an account yet? Quick registration</text>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { LoginReqApi } from '../../api/requestApi'
import { setToken, setUserName, setSavedAccount, getSavedAccount, getSavedPassword, clearSavedAccount, AccountRecord, getAccountHistory, addAccountHistory, removeAccountHistory } from '../../common/auth'
const account = ref("")
const password = ref("")
const currentTab = ref(1)
const phoneNumber = ref("")
const verificationCode = ref("")
const showPwd = ref(false)
const rememberPwd = ref(false)
const historyList = ref<AccountRecord[]>([])
const historyOpen = ref(false)
// 页面加载时回填已保存的账号密码
onLoad(() => {
const savedPassword = getSavedPassword()
if (savedPassword.length > 0) {
rememberPwd.value = true
account.value = getSavedAccount()
password.value = savedPassword
}
historyList.value = getAccountHistory()
})
// 选择历史账号回填
function onPickHistory(item: AccountRecord) {
account.value = item.account
password.value = item.password
rememberPwd.value = true
historyOpen.value = false
}
// 删除一条历史账号
function onRemoveHistory(item: AccountRecord) {
removeAccountHistory(item.account)
historyList.value = getAccountHistory()
}
function switchTab(index: number) {
if (currentTab.value !== index) {
currentTab.value = index
}
}
function onGetCode() {
// TODO: 这里写请求验证码的逻辑
uni.showToast({
title: "Send verification code",
icon: "none"
})
}
async function GetSmsLoginReq(){
uni.showToast({
title: "in development",
icon: "error"
})
}
async function onRegister() {
uni.redirectTo({
url: "/pages/register/register"
})
}
async function GetPasswordLoginReq(){
try {
const res = await LoginReqApi({
username: account.value,
password: password.value
})
// console.log("res = ", JSON.stringify((res)))
const statusCode = res["statusCode"] as number
if(statusCode == 200) {
const data = res["data"] as Record<string, any>
const token = data["token"] as string | null
const userName = data["username"] as string | null
if(token != null && token.length > 0){
setToken(token)
}
if(userName != null && userName.length > 0) {
setUserName(userName)
}
// 勾选了记住密码则保存账号密码,否则清除
if(rememberPwd.value) {
setSavedAccount(account.value, password.value)
} else {
clearSavedAccount()
}
// 保存到历史账号,方便下次直接选择
addAccountHistory(account.value, password.value)
uni.switchTab({
url: "/pages/tabbar/my"
})
} else {
const data = res["data"] as Record<string, any>
const msg = (data["result"] as string | null) ?? "login fail"
uni.showToast({
title: msg,
icon: 'error',
duration: 2000
})
}
} catch (e: any) {
console.log('login error = ', e)
uni.showToast({
title: "login error",
icon: "error",
duration: 2000,
})
} finally {
}
}
async function LoginRequest() {
if(currentTab.value == 0) {
GetSmsLoginReq()
}
else if(currentTab.value == 1) {
GetPasswordLoginReq()
}
}
</script>
<style>
.page{
flex: 1;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.buttonType {
width: 100%;
padding: 0 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 80%;
}
.buttonSize {
width: 100%;
border-radius: 10rpx;
margin: 40rpx auto;
background-color: #6d91b3;
}
.input-wrapper {
width: 600rpx;
margin: 20rpx auto;
border-bottom: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 2rpx solid #6d91b3;
box-sizing: border-box;
margin-top: 30rpx;
}
.inputType {
flex: 1;
border: none;
background-color: transparent;
font-size: 32rpx;
padding-left: 70rpx;
padding-right: 20rpx;
}
.input-icon {
position: absolute;
left: 10rpx;
width: 40rpx;
top: 50%;
transform: translateY(-50%);
}
.toggle {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.page-box {
flex: 1;
margin-top: 15rpx;
}
.login-tabs {
margin-top: 40rpx;
padding: 0 60rpx;
display: flex;
flex-direction: row;
justify-content: center;
}
.login-tab-item {
margin-right: 100rpx;
display: flex;
flex-direction: column;
align-items: flex-start; /* 让文字和下划线左对齐 */
}
.login-tab-text {
font-size: 30rpx;
color: #6d91b3; /* 未选中浅色 */
}
/* 选中时文字变深、加粗 */
.active-text {
color: #3c2c14;
}
/* 下划线小块 */
.login-tab-underline {
margin-top: 6rpx;
width: 100rpx;
height: 4rpx;
background-color: #3A6FA1;
border-radius: 2rpx;
}
.code-btn {
margin-left: 16rpx;
padding: 8rpx 16rpx;
border-radius: 24rpx;
border-width: 1rpx;
border-style: solid;
}
.registerText {
margin-top: 30rpx;
font-weight: 700;
color: #3A6FA1;
text-align: center;
}
/* 记住密码 */
.remember-row {
width: 600rpx;
margin: 20rpx auto;
display: flex;
flex-direction: row;
align-items: center;
}
.remember-text {
font-size: 26rpx;
color: #666666;
margin-left: 10rpx;
}
/* 历史账号 */
.history-row {
width: 600rpx;
margin: 10rpx auto;
display: flex;
flex-direction: row;
align-items: center;
}
.history-text {
font-size: 26rpx;
color: #3A6FA1;
margin-left: 10rpx;
}
.history-list {
width: 600rpx;
margin: 0 auto;
border: 2rpx solid #e6e6e6;
border-radius: 16rpx;
background-color: #ffffff;
}
.history-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.history-account {
flex: 1;
min-width: 0;
font-size: 28rpx;
color: #333333;
margin-left: 12rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-del {
padding: 6rpx;
}
</style>