初始化
This commit is contained in:
commit
e4a9775327
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
unpackage/
|
||||
.hbuilderx/
|
||||
.DS_Store
|
||||
.codex-tasks/
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { ensureCurrentPageAuth } from './utils/auth'
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
ensureCurrentPageAuth()
|
||||
},
|
||||
onHide() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, SUCCESS_CODE } from '@/constants/request'
|
||||
import { clearAuth, getToken, isAuthEnabled, redirectToLogin } from '@/utils/auth'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
let baseURL = DEFAULT_BASE_URL
|
||||
|
||||
function buildUrl(url = '') {
|
||||
if (!url) {
|
||||
return baseURL
|
||||
}
|
||||
|
||||
if (/^https?:\/\//.test(url)) {
|
||||
return url
|
||||
}
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
return `${baseURL}${url}`
|
||||
}
|
||||
|
||||
return `${baseURL}/${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造请求头。
|
||||
* @param {Record<string, string>} customHeader 自定义请求头
|
||||
* @param {boolean} needAuth 是否需要携带 token
|
||||
* @param {boolean} withJson 是否设置 JSON Content-Type
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function buildHeaders(customHeader = {}, needAuth = true, withJson = true) {
|
||||
const headers = {
|
||||
...(withJson ? { 'Content-Type': 'application/json' } : {}),
|
||||
...customHeader
|
||||
}
|
||||
|
||||
if (needAuth && isAuthEnabled()) {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
function handleUnauthorized() {
|
||||
if (!isAuthEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
clearAuth()
|
||||
redirectToLogin()
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一解析后端响应结构。
|
||||
* 约定:`code === 0` 为成功,最终返回 `data` 字段。
|
||||
*/
|
||||
function resolveResponseData(responseData) {
|
||||
const code = typeof responseData?.code === 'number' ? responseData.code : SUCCESS_CODE
|
||||
|
||||
if (code !== SUCCESS_CODE) {
|
||||
const error = new Error(responseData?.message || '业务处理失败')
|
||||
error.code = code
|
||||
throw error
|
||||
}
|
||||
|
||||
return responseData?.data !== undefined ? responseData.data : responseData
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法。
|
||||
* @param {object} options uni.request 参数扩展
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function request(options = {}) {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
data,
|
||||
header,
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
needAuth = true,
|
||||
showErrorToast = true,
|
||||
showToast: showToastLegacy
|
||||
} = options
|
||||
|
||||
const shouldShowErrorToast = typeof showToastLegacy === 'boolean'
|
||||
? showToastLegacy
|
||||
: showErrorToast
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: buildUrl(url),
|
||||
method: String(method).toUpperCase(),
|
||||
data,
|
||||
timeout,
|
||||
header: buildHeaders(header, needAuth, true),
|
||||
success: (res) => {
|
||||
const { statusCode = 0, data: responseData = {} } = res
|
||||
|
||||
if (statusCode === 401) {
|
||||
handleUnauthorized()
|
||||
reject(new Error('登录状态已失效'))
|
||||
return
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
const message = responseData?.message || `网络错误(${statusCode})`
|
||||
if (shouldShowErrorToast) {
|
||||
showToast(message)
|
||||
}
|
||||
reject(new Error(message))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const finalData = resolveResponseData(responseData)
|
||||
resolve(finalData)
|
||||
} catch (error) {
|
||||
if (error.code === 401) {
|
||||
handleUnauthorized()
|
||||
}
|
||||
if (shouldShowErrorToast) {
|
||||
showToast(error.message)
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (shouldShowErrorToast) {
|
||||
showToast('网络异常,请稍后重试')
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
request.get = (url, params = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data: params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
request.post = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
request.put = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
request.delete = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
request.upload = (url, filePath, name = 'file', formData = {}, config = {}) => {
|
||||
const {
|
||||
header = {},
|
||||
needAuth = true,
|
||||
showErrorToast = true,
|
||||
showToast: showToastLegacy
|
||||
} = config
|
||||
|
||||
const shouldShowErrorToast = typeof showToastLegacy === 'boolean'
|
||||
? showToastLegacy
|
||||
: showErrorToast
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: buildUrl(url),
|
||||
filePath,
|
||||
name,
|
||||
formData,
|
||||
header: buildHeaders(header, needAuth, false),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 401) {
|
||||
handleUnauthorized()
|
||||
reject(new Error('登录状态已失效'))
|
||||
return
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const message = `上传失败(${res.statusCode})`
|
||||
if (shouldShowErrorToast) {
|
||||
showToast(message)
|
||||
}
|
||||
reject(new Error(message))
|
||||
return
|
||||
}
|
||||
|
||||
let responseData = {}
|
||||
try {
|
||||
responseData = JSON.parse(res.data || '{}')
|
||||
} catch (error) {
|
||||
if (shouldShowErrorToast) {
|
||||
showToast('上传返回数据格式错误')
|
||||
}
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const finalData = resolveResponseData(responseData)
|
||||
resolve(finalData)
|
||||
} catch (error) {
|
||||
if (error.code === 401) {
|
||||
handleUnauthorized()
|
||||
}
|
||||
if (shouldShowErrorToast) {
|
||||
showToast(error.message)
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (shouldShowErrorToast) {
|
||||
showToast('上传失败,请稍后重试')
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态设置 API 基础地址。
|
||||
*/
|
||||
request.setBaseUrl = (url) => {
|
||||
if (!url || typeof url !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
baseURL = url.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
request.getBaseUrl = () => baseURL
|
||||
|
||||
export default request
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import request from '@/api/http/client'
|
||||
|
||||
/**
|
||||
* 发送短信验证码。
|
||||
* @param {string} mobile 手机号
|
||||
*/
|
||||
export function sendSmsCode(mobile) {
|
||||
return request.post('/auth/sms-code', { mobile }, { needAuth: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号 + 验证码登录。
|
||||
*/
|
||||
export function loginByMobile(mobile, code) {
|
||||
return request.post('/auth/mobile-login', { mobile, code }, { needAuth: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信登录。
|
||||
*/
|
||||
export function loginByWechat(loginCode) {
|
||||
return request.post('/auth/wechat-login', { code: loginCode }, { needAuth: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息。
|
||||
*/
|
||||
export function getUserProfile() {
|
||||
return request.get('/user/profile')
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* 本地存储 key。
|
||||
*/
|
||||
export const AUTH_STORAGE_KEYS = Object.freeze({
|
||||
TOKEN: 'smarthome_token',
|
||||
USER: 'smarthome_user'
|
||||
})
|
||||
|
||||
/**
|
||||
* 登录开关:当前为 false,便于功能联调。
|
||||
* 恢复鉴权时改为 true 即可。
|
||||
*/
|
||||
export const AUTH_ENABLED = false
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* 目标服务 UUID(用于优先识别特定设备)。
|
||||
*/
|
||||
export const TARGET_SERVICE_UUID = '0000F400-0000-1000-8000-00805F9B34FB'
|
||||
export const TARGET_SHORT_UUID = '00F4'
|
||||
|
||||
/**
|
||||
* 蓝牙常见错误提示映射。
|
||||
*/
|
||||
export const BLUETOOTH_ERROR_MESSAGES = Object.freeze({
|
||||
10001: '蓝牙不可用,请先开启手机蓝牙',
|
||||
10003: '连接失败,请重试',
|
||||
10012: '操作超时,请重试'
|
||||
})
|
||||
|
||||
export const DEFAULT_BLUETOOTH_ERROR_MESSAGE = '蓝牙搜索启动失败'
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* 接口请求默认配置。
|
||||
*/
|
||||
export const SUCCESS_CODE = 0
|
||||
export const DEFAULT_TIMEOUT = 15000
|
||||
export const DEFAULT_BASE_URL = 'https://api.example.com'
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* 统一维护页面路由,避免页面内散落硬编码字符串。
|
||||
*/
|
||||
export const ROUTES = Object.freeze({
|
||||
LOGIN: '/pages/login/index',
|
||||
HOME: '/pages/index/index',
|
||||
EVENT: '/pages/event/index',
|
||||
MINE: '/pages/mine/index',
|
||||
DEVICE_ADD: '/pages/device/add',
|
||||
DEVICE_CONFIG: '/pages/device/config'
|
||||
})
|
||||
|
||||
/**
|
||||
* TabBar 页面集合,供登录跳转与路由守卫判断使用。
|
||||
*/
|
||||
export const TAB_ROUTES = Object.freeze([ROUTES.HOME, ROUTES.EVENT, ROUTES.MINE])
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import {
|
||||
BLUETOOTH_ERROR_MESSAGES,
|
||||
DEFAULT_BLUETOOTH_ERROR_MESSAGE,
|
||||
TARGET_SERVICE_UUID,
|
||||
TARGET_SHORT_UUID
|
||||
} from '@/constants/bluetooth'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
/**
|
||||
* 蓝牙设备搜索逻辑封装。
|
||||
* 通过回调把状态同步给页面,减少页面文件复杂度。
|
||||
*/
|
||||
export function createBluetoothDiscovery(options = {}) {
|
||||
const {
|
||||
onSearchingChange = () => {},
|
||||
onDeviceMapChange = () => {},
|
||||
onDeviceListChange = () => {}
|
||||
} = options
|
||||
|
||||
let isSearching = false
|
||||
let deviceMap = {}
|
||||
let deviceFoundHandler = null
|
||||
|
||||
function setSearching(value) {
|
||||
isSearching = Boolean(value)
|
||||
onSearchingChange(isSearching)
|
||||
}
|
||||
|
||||
function setDeviceMap(nextMap) {
|
||||
deviceMap = nextMap
|
||||
onDeviceMapChange({ ...deviceMap })
|
||||
emitDeviceList()
|
||||
}
|
||||
|
||||
function emitDeviceList() {
|
||||
const list = Object.values(deviceMap).sort((a, b) => {
|
||||
if (a.hasTargetService !== b.hasTargetService) {
|
||||
return a.hasTargetService ? -1 : 1
|
||||
}
|
||||
|
||||
const aSignal = typeof a.RSSI === 'number' ? a.RSSI : -999
|
||||
const bSignal = typeof b.RSSI === 'number' ? b.RSSI : -999
|
||||
return bSignal - aSignal
|
||||
})
|
||||
|
||||
onDeviceListChange(list)
|
||||
}
|
||||
|
||||
function normalizeUuid(uuid) {
|
||||
return String(uuid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
function hasTargetService(device) {
|
||||
if (!Array.isArray(device.advertisServiceUUIDs)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const targetUuid = normalizeUuid(TARGET_SERVICE_UUID)
|
||||
return device.advertisServiceUUIDs.some((uuid) => {
|
||||
const value = normalizeUuid(uuid)
|
||||
return value === targetUuid || value.endsWith(TARGET_SHORT_UUID)
|
||||
})
|
||||
}
|
||||
|
||||
function openBluetoothAdapter() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.openBluetoothAdapter({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startDiscoveryWithServices(services = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
allowDuplicatesKey: true,
|
||||
interval: 0,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
}
|
||||
|
||||
if (services.length) {
|
||||
options.services = services
|
||||
}
|
||||
|
||||
uni.startBluetoothDevicesDiscovery(options)
|
||||
})
|
||||
}
|
||||
|
||||
function upsertDevice(rawDevice) {
|
||||
if (!rawDevice || !rawDevice.deviceId) {
|
||||
return
|
||||
}
|
||||
|
||||
const existing = deviceMap[rawDevice.deviceId] || {}
|
||||
const merged = {
|
||||
...existing,
|
||||
...rawDevice,
|
||||
showName: rawDevice.name || rawDevice.localName || existing.showName || '未命名设备',
|
||||
hasTargetService: hasTargetService(rawDevice) || existing.hasTargetService
|
||||
}
|
||||
|
||||
setDeviceMap({
|
||||
...deviceMap,
|
||||
[rawDevice.deviceId]: merged
|
||||
})
|
||||
}
|
||||
|
||||
function registerDeviceFoundHandler() {
|
||||
if (deviceFoundHandler) {
|
||||
return
|
||||
}
|
||||
|
||||
deviceFoundHandler = (res) => {
|
||||
const devices = Array.isArray(res.devices) ? res.devices : [res]
|
||||
devices.forEach((device) => {
|
||||
upsertDevice(device)
|
||||
})
|
||||
}
|
||||
|
||||
uni.onBluetoothDeviceFound(deviceFoundHandler)
|
||||
}
|
||||
|
||||
function stopSearch(showStopToast = true) {
|
||||
return new Promise((resolve) => {
|
||||
uni.stopBluetoothDevicesDiscovery({
|
||||
complete: () => {
|
||||
setSearching(false)
|
||||
if (showStopToast) {
|
||||
showToast('已停止搜索')
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function cleanupDiscovery() {
|
||||
if (isSearching) {
|
||||
uni.stopBluetoothDevicesDiscovery({
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
|
||||
setSearching(false)
|
||||
|
||||
if (deviceFoundHandler && uni.offBluetoothDeviceFound) {
|
||||
uni.offBluetoothDeviceFound(deviceFoundHandler)
|
||||
}
|
||||
|
||||
deviceFoundHandler = null
|
||||
}
|
||||
|
||||
function getBluetoothErrorText(error) {
|
||||
const errCode = error && error.errCode
|
||||
return BLUETOOTH_ERROR_MESSAGES[errCode] || DEFAULT_BLUETOOTH_ERROR_MESSAGE
|
||||
}
|
||||
|
||||
async function startSearch() {
|
||||
setDeviceMap({})
|
||||
|
||||
try {
|
||||
await openBluetoothAdapter()
|
||||
registerDeviceFoundHandler()
|
||||
|
||||
try {
|
||||
await startDiscoveryWithServices([TARGET_SERVICE_UUID])
|
||||
} catch (error) {
|
||||
await startDiscoveryWithServices([])
|
||||
}
|
||||
|
||||
setSearching(true)
|
||||
} catch (error) {
|
||||
setSearching(false)
|
||||
showToast(getBluetoothErrorText(error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startSearch,
|
||||
stopSearch,
|
||||
cleanupDiscovery
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 创建一个简单倒计时控制器,适配 Vue2 Options API。
|
||||
* @param {object} options
|
||||
* @param {number} options.duration 倒计时总秒数
|
||||
* @param {(value:number)=>void} options.onChange 每次变化回调
|
||||
*/
|
||||
export function createCountdown(options = {}) {
|
||||
const {
|
||||
duration = 60,
|
||||
onChange = () => {}
|
||||
} = options
|
||||
|
||||
let timer = null
|
||||
let current = 0
|
||||
|
||||
function emit() {
|
||||
onChange(current)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
current = duration
|
||||
emit()
|
||||
|
||||
timer = setInterval(() => {
|
||||
current -= 1
|
||||
if (current <= 0) {
|
||||
current = 0
|
||||
emit()
|
||||
clear()
|
||||
return
|
||||
}
|
||||
|
||||
emit()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
clear
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import App from './App'
|
||||
import { setupAuthInterceptors } from './utils/auth'
|
||||
|
||||
setupAuthInterceptors()
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name" : "smarthome",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx7f272a672efe6c04",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/add",
|
||||
"style": {
|
||||
"navigationBarTitleText": "添加设备"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/config",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "智慧养老",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F1F1F4"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#666666",
|
||||
"selectedColor": "#2CCB98",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/event/index",
|
||||
"text": "事件"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<view class="page page-base">
|
||||
<view class="card card-base">
|
||||
<text class="title">添加设备(静态)</text>
|
||||
<text class="desc">这里用于后续接入设备连接流程,例如蓝牙/Wi-Fi 配网。</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #666666;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
<template>
|
||||
<view class="page">
|
||||
<view class="top-nav top-nav-base">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">设备配置</text>
|
||||
</view>
|
||||
|
||||
<view class="search-wrap">
|
||||
<view class="search-btn" :class="{ 'is-searching': isSearching }" @click="toggleSearch">
|
||||
<text>{{ searchBtnText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip-row">
|
||||
*手机需开启蓝牙功能,Android手机还需开启定位功能
|
||||
</view>
|
||||
|
||||
<view class="list-header">
|
||||
<text class="list-title">搜索到的设备列表</text>
|
||||
</view>
|
||||
|
||||
<view class="list-body">
|
||||
<view v-if="!deviceList.length" class="empty-row">暂无搜索结果</view>
|
||||
<view v-for="device in deviceList" :key="device.deviceId" class="device-card">
|
||||
<view class="device-icon"></view>
|
||||
<view class="device-main">
|
||||
<view class="device-name-row">
|
||||
<text class="device-name">{{ device.showName }}</text>
|
||||
<text v-if="device.hasTargetService" class="device-tag">00F4</text>
|
||||
</view>
|
||||
<text class="device-id">{{ device.deviceId }}</text>
|
||||
<view class="device-actions">
|
||||
<view class="action-btn">网络配置</view>
|
||||
<view class="action-btn">参数配置</view>
|
||||
<view class="action-btn">服务器配置</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createBluetoothDiscovery } from '@/hooks/useBluetoothDiscovery'
|
||||
import { navigateBack } from '@/utils/navigation'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSearching: false,
|
||||
deviceList: [],
|
||||
deviceMap: {},
|
||||
discoveryController: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
searchBtnText() {
|
||||
return this.isSearching ? '搜索中' : '搜索'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
/**
|
||||
* 蓝牙搜索控制器:统一处理设备发现、排序、清理逻辑。
|
||||
*/
|
||||
this.discoveryController = createBluetoothDiscovery({
|
||||
onSearchingChange: (value) => {
|
||||
this.isSearching = value
|
||||
},
|
||||
onDeviceMapChange: (value) => {
|
||||
this.deviceMap = value
|
||||
},
|
||||
onDeviceListChange: (value) => {
|
||||
this.deviceList = value
|
||||
}
|
||||
})
|
||||
},
|
||||
onHide() {
|
||||
this.cleanupDiscovery()
|
||||
},
|
||||
onUnload() {
|
||||
this.cleanupDiscovery()
|
||||
},
|
||||
methods: {
|
||||
cleanupDiscovery() {
|
||||
if (!this.discoveryController) {
|
||||
return
|
||||
}
|
||||
|
||||
this.discoveryController.cleanupDiscovery()
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.cleanupDiscovery()
|
||||
navigateBack(1)
|
||||
},
|
||||
|
||||
async toggleSearch() {
|
||||
if (!this.discoveryController) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isSearching) {
|
||||
await this.discoveryController.stopSearch(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.discoveryController.startSearch()
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #e9eaed;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
border-bottom: 1rpx solid #ececec;
|
||||
}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
top: 30rpx;
|
||||
font-size: 64rpx;
|
||||
color: #111111;
|
||||
width: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
color: #2a2d33;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
height: 500rpx;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
border-radius: 130rpx;
|
||||
background: #43c996;
|
||||
color: #ffffff;
|
||||
font-size: 56rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.search-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.search-btn.is-searching {
|
||||
animation: searchPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes searchPulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(67, 201, 150, 0.42);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 0 0 30rpx rgba(67, 201, 150, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(67, 201, 150, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.tip-row {
|
||||
min-height: 78rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #efeff1;
|
||||
color: #ff0000;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
height: 116rpx;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 38rpx;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 52rpx;
|
||||
color: #101217;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
min-height: calc(100vh - 814rpx);
|
||||
background: #e9eaed;
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-row {
|
||||
padding: 36rpx;
|
||||
text-align: center;
|
||||
color: #8c9097;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: #ffffff;
|
||||
margin-top: 12rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
border: 2rpx solid #d8d8d8;
|
||||
box-shadow: inset 0 0 0 10rpx #f1f1f1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 38rpx;
|
||||
color: #30343a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.device-tag {
|
||||
font-size: 22rpx;
|
||||
color: #17b474;
|
||||
border: 1rpx solid #17b474;
|
||||
border-radius: 20rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 30rpx;
|
||||
color: #8a8f98;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
border: 2rpx solid #42c995;
|
||||
color: #22b47a;
|
||||
font-size: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #f8fffc;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<view class="page page-base">
|
||||
<view class="top-nav top-nav-base">
|
||||
<view class="title top-nav-title-base">事件</view>
|
||||
</view>
|
||||
|
||||
<view class="filters">
|
||||
<view class="filter-item">全部场所</view>
|
||||
<view class="filter-item">全部状态</view>
|
||||
<view class="filter-item">时间</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-area empty-state">
|
||||
<view class="empty-box empty-state-box"></view>
|
||||
<text class="empty-text empty-state-text">暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.filters {
|
||||
height: 92rpx;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid #ededed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 42rpx;
|
||||
color: #3a3a3a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-area {
|
||||
min-height: 980rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<view class="page page-base">
|
||||
<view class="top-nav top-nav-base">
|
||||
<view class="title">智慧养老</view>
|
||||
</view>
|
||||
|
||||
<view class="banner">
|
||||
<view class="banner-left">
|
||||
<text class="banner-title">智慧养老云助手</text>
|
||||
<text class="banner-sub">一键开启智慧好生活</text>
|
||||
</view>
|
||||
<view class="banner-illus">
|
||||
<view class="phone"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="site-row" @click="goConfig">
|
||||
<text class="menu">☰</text>
|
||||
<text class="site-text">请选择场所</text>
|
||||
<text class="down">⌄</text>
|
||||
</view>
|
||||
|
||||
<view class="empty-area empty-state">
|
||||
<view class="empty-box empty-state-box"></view>
|
||||
<text class="empty-text empty-state-text">暂无数据</text>
|
||||
</view>
|
||||
|
||||
<view class="fab" @click="goAdd">+</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { navigateTo } from '@/utils/navigation'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
goAdd() {
|
||||
navigateTo(ROUTES.DEVICE_ADD)
|
||||
},
|
||||
goConfig() {
|
||||
navigateTo(ROUTES.DEVICE_CONFIG)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: 0;
|
||||
height: 380rpx;
|
||||
background: #28cda0;
|
||||
padding: 56rpx 52rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 58rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.banner-sub {
|
||||
font-size: 46rpx;
|
||||
color: #e9fff8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.banner-illus {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.phone {
|
||||
width: 120rpx;
|
||||
height: 170rpx;
|
||||
border: 8rpx solid rgba(255, 255, 255, 0.92);
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
|
||||
.site-row {
|
||||
height: 108rpx;
|
||||
padding: 0 36rpx;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 46rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin-right: 22rpx;
|
||||
}
|
||||
|
||||
.site-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.down {
|
||||
margin-left: 10rpx;
|
||||
color: #26c996;
|
||||
}
|
||||
|
||||
.empty-area {
|
||||
margin-top: 40rpx;
|
||||
min-height: 760rpx;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 38rpx;
|
||||
bottom: 220rpx;
|
||||
width: 118rpx;
|
||||
height: 118rpx;
|
||||
border-radius: 59rpx;
|
||||
background: #2cc998;
|
||||
color: #ffffff;
|
||||
font-size: 88rpx;
|
||||
line-height: 110rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 12rpx 24rpx rgba(29, 177, 130, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
<template>
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<text class="title">欢迎使用智慧养老</text>
|
||||
<text class="sub">登录后可管理设备与查看事件</text>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="form-row">
|
||||
<text class="prefix">+86</text>
|
||||
<input
|
||||
v-model="mobile"
|
||||
class="input"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-row code-row">
|
||||
<input
|
||||
v-model="code"
|
||||
class="input"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<button class="code-btn" :disabled="countdown > 0" @click="sendCode">
|
||||
{{ codeBtnText }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<button class="login-btn" @click="loginByMobileSubmit">手机号验证码登录</button>
|
||||
|
||||
<view class="split">
|
||||
<view class="line"></view>
|
||||
<text class="split-text">或</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
<button class="wx-btn" @click="loginByWechatSubmit">微信一键登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loginByMobile, loginByWechat, sendSmsCode } from '@/api/user'
|
||||
import { createCountdown } from '@/hooks/useCountdown'
|
||||
import { goAfterLogin, isLoggedIn, setAuth } from '@/utils/auth'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { isValidMobile, isValidSmsCode } from '@/utils/validators'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mobile: '',
|
||||
code: '',
|
||||
countdown: 0,
|
||||
redirect: '',
|
||||
countdownController: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
codeBtnText() {
|
||||
return this.countdown > 0 ? `${this.countdown}s后重试` : '获取验证码'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.countdownController = createCountdown({
|
||||
duration: 60,
|
||||
onChange: (value) => {
|
||||
this.countdown = value
|
||||
}
|
||||
})
|
||||
},
|
||||
onLoad(query) {
|
||||
this.redirect = query.redirect || ''
|
||||
|
||||
if (isLoggedIn()) {
|
||||
goAfterLogin(this.redirect)
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
this.clearCountdown()
|
||||
},
|
||||
methods: {
|
||||
clearCountdown() {
|
||||
if (!this.countdownController) {
|
||||
return
|
||||
}
|
||||
|
||||
this.countdownController.clear()
|
||||
},
|
||||
|
||||
validateMobileOrToast() {
|
||||
if (isValidMobile(this.mobile)) {
|
||||
return true
|
||||
}
|
||||
|
||||
showToast('请输入正确手机号')
|
||||
return false
|
||||
},
|
||||
|
||||
finishLogin(payload = {}) {
|
||||
const token = payload.token || `mock-token-${Date.now()}`
|
||||
const user = payload.user || {
|
||||
name: '微信用户',
|
||||
mobile: '',
|
||||
loginType: 'wechat'
|
||||
}
|
||||
|
||||
setAuth({ token, user })
|
||||
goAfterLogin(this.redirect)
|
||||
},
|
||||
|
||||
async sendCode() {
|
||||
if (!this.validateMobileOrToast()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sendSmsCode(this.mobile)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
showToast('验证码已发送')
|
||||
this.countdownController.start()
|
||||
},
|
||||
|
||||
async loginByMobileSubmit() {
|
||||
if (!this.validateMobileOrToast()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidSmsCode(this.code)) {
|
||||
showToast('请输入6位验证码')
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '登录中'
|
||||
})
|
||||
|
||||
try {
|
||||
const data = await loginByMobile(this.mobile, this.code)
|
||||
this.finishLogin({
|
||||
token: data?.token,
|
||||
user: data?.user || {
|
||||
name: `用户${this.mobile.slice(-4)}`,
|
||||
mobile: this.mobile,
|
||||
loginType: 'mobile'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
async loginByWechatSubmit() {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.showLoading({
|
||||
title: '微信登录中'
|
||||
})
|
||||
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (res) => {
|
||||
try {
|
||||
const data = await loginByWechat(res.code || '')
|
||||
this.finishLogin({
|
||||
token: data?.token,
|
||||
user: data?.user || {
|
||||
name: '微信用户',
|
||||
mobile: '',
|
||||
loginType: 'wechat'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading()
|
||||
showToast('微信登录失败,请重试')
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.showLoading({
|
||||
title: '微信登录中'
|
||||
})
|
||||
|
||||
try {
|
||||
const data = await loginByWechat('')
|
||||
this.finishLogin({
|
||||
token: data?.token,
|
||||
user: data?.user || {
|
||||
name: '微信用户',
|
||||
mobile: '',
|
||||
loginType: 'wechat'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #ecf9f5 0%, #f5f6f8 48%, #f1f1f4 100%);
|
||||
padding: 80rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin: 30rpx 8rpx 46rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 54rpx;
|
||||
color: #18242f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #6f7782;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 38rpx 30rpx 42rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(10, 63, 48, 0.08);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
min-height: 98rpx;
|
||||
border: 1rpx solid #e8eaef;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-row + .form-row {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
width: 86rpx;
|
||||
color: #1f2b37;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
height: 98rpx;
|
||||
font-size: 30rpx;
|
||||
color: #1f2b37;
|
||||
}
|
||||
|
||||
.code-row {
|
||||
padding-right: 12rpx;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
width: 190rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
border-radius: 36rpx;
|
||||
background: #e9faf4;
|
||||
color: #15b880;
|
||||
font-size: 26rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.code-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.code-btn[disabled] {
|
||||
color: #8f98a3;
|
||||
background: #f1f3f6;
|
||||
}
|
||||
|
||||
.login-btn,
|
||||
.wx-btn {
|
||||
margin-top: 26rpx;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
line-height: 92rpx;
|
||||
border-radius: 16rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: linear-gradient(90deg, #23c797, #11b880);
|
||||
}
|
||||
|
||||
.login-btn::after,
|
||||
.wx-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.split {
|
||||
margin: 34rpx 0 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: #eceef1;
|
||||
}
|
||||
|
||||
.split-text {
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #9aa2ad;
|
||||
}
|
||||
|
||||
.wx-btn {
|
||||
margin-top: 24rpx;
|
||||
background: #07c160;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<view class="page page-base">
|
||||
<view class="top-nav top-nav-base">
|
||||
<view class="title top-nav-title-base">我的</view>
|
||||
</view>
|
||||
|
||||
<view class="user-card" @click="goLogin">
|
||||
<view class="avatar"></view>
|
||||
<view>
|
||||
<text class="user-name">{{ userName }}</text>
|
||||
<text class="user-tip">{{ loginTip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-card">
|
||||
<view class="item" @click="goConfig">
|
||||
<text class="left">设备配置</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">分享的设备</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">场所管理</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">接警人管理</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">手机号</text>
|
||||
<text class="mid">{{ phoneText }}</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">语音广播</text>
|
||||
<text class="right">›</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="left">当前版本</text>
|
||||
<text class="mid">2.8.44</text>
|
||||
</view>
|
||||
<view class="item" @click="logout">
|
||||
<text class="left">退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { clearAuth, getUser, isLoggedIn, LOGIN_PAGE, redirectToLogin } from '@/utils/auth'
|
||||
import { navigateTo } from '@/utils/navigation'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userName() {
|
||||
return this.userInfo?.name || '微信用户'
|
||||
},
|
||||
phoneText() {
|
||||
return this.userInfo?.mobile || '未绑定'
|
||||
},
|
||||
loginTip() {
|
||||
return this.userInfo?.loginType === 'mobile' ? '手机号验证码登录' : '微信登录'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.userInfo = getUser()
|
||||
},
|
||||
methods: {
|
||||
goConfig() {
|
||||
navigateTo(ROUTES.DEVICE_CONFIG)
|
||||
},
|
||||
goLogin() {
|
||||
if (isLoggedIn()) {
|
||||
return
|
||||
}
|
||||
|
||||
navigateTo(LOGIN_PAGE)
|
||||
},
|
||||
logout() {
|
||||
clearAuth()
|
||||
showToast('已退出登录')
|
||||
redirectToLogin(ROUTES.HOME)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.page {
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
margin: 24rpx;
|
||||
min-height: 200rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 34rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
border-radius: 66rpx;
|
||||
background: radial-gradient(circle at 35% 35%, #95d347 0 32%, #6ba83a 33% 100%);
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: block;
|
||||
font-size: 46rpx;
|
||||
color: #2a2d33;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-tip {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #9ea1a6;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
margin: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item {
|
||||
min-height: 94rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #efefef;
|
||||
}
|
||||
|
||||
.item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
font-size: 48rpx;
|
||||
color: #2f3135;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mid {
|
||||
margin-right: 16rpx;
|
||||
color: #9ea1a6;
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.right {
|
||||
color: #9ea1a6;
|
||||
font-size: 54rpx;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,46 @@
|
|||
.page-base {
|
||||
min-height: 100vh;
|
||||
background: #f1f1f4;
|
||||
}
|
||||
|
||||
.top-nav-base {
|
||||
height: 120rpx;
|
||||
padding: 24rpx 24rpx 0;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-nav-title-base {
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a7a9ad;
|
||||
}
|
||||
|
||||
.empty-state-box {
|
||||
width: 220rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(180deg, #eef0f4, #e3e6eb);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
font-size: 52rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-base {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
getUserProfile,
|
||||
loginByMobile,
|
||||
loginByWechat,
|
||||
sendSmsCode
|
||||
} from '@/api/user'
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import { AUTH_ENABLED, AUTH_STORAGE_KEYS } from '@/constants/auth'
|
||||
import { ROUTES, TAB_ROUTES } from '@/constants/routes'
|
||||
|
||||
export const LOGIN_PAGE = ROUTES.LOGIN
|
||||
export const HOME_PAGE = ROUTES.HOME
|
||||
export const TAB_PAGES = TAB_ROUTES
|
||||
|
||||
let hasSetupInterceptors = false
|
||||
|
||||
/**
|
||||
* 规范化路由,保证以 `/` 开头。
|
||||
*/
|
||||
function normalizeUrl(url = '') {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
return url
|
||||
}
|
||||
|
||||
return `/${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取不带 query 的路径。
|
||||
*/
|
||||
function getPath(url = '') {
|
||||
return normalizeUrl(url).split('?')[0]
|
||||
}
|
||||
|
||||
function safeDecode(value = '') {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把页面 options 组装成 query 字符串。
|
||||
*/
|
||||
function buildQuery(options = {}) {
|
||||
const keys = Object.keys(options)
|
||||
if (!keys.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const query = keys
|
||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(options[key] || '')}`)
|
||||
.join('&')
|
||||
|
||||
return query ? `?${query}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面完整地址(path + query)。
|
||||
*/
|
||||
function getCurrentPageUrl() {
|
||||
const pages = getCurrentPages()
|
||||
if (!pages.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const current = pages[pages.length - 1]
|
||||
const currentPath = current.route ? `/${current.route}` : ''
|
||||
const currentQuery = current.options ? buildQuery(current.options) : ''
|
||||
|
||||
return `${currentPath}${currentQuery}`
|
||||
}
|
||||
|
||||
function isLoginPage(url = '') {
|
||||
return getPath(url) === LOGIN_PAGE
|
||||
}
|
||||
|
||||
function needLogin(url = '') {
|
||||
return !isLoginPage(url)
|
||||
}
|
||||
|
||||
export function isAuthEnabled() {
|
||||
return AUTH_ENABLED
|
||||
}
|
||||
|
||||
export function isTabPage(url = '') {
|
||||
return TAB_PAGES.includes(getPath(url))
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return uni.getStorageSync(AUTH_STORAGE_KEYS.TOKEN) || ''
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
if (!AUTH_ENABLED) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(getToken())
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
return uni.getStorageSync(AUTH_STORAGE_KEYS.USER) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存登录态。
|
||||
*/
|
||||
export function setAuth({ token, user } = {}) {
|
||||
if (token) {
|
||||
uni.setStorageSync(AUTH_STORAGE_KEYS.TOKEN, token)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
uni.setStorageSync(AUTH_STORAGE_KEYS.USER, user)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
uni.removeStorageSync(AUTH_STORAGE_KEYS.TOKEN)
|
||||
uni.removeStorageSync(AUTH_STORAGE_KEYS.USER)
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页并携带重定向地址。
|
||||
*/
|
||||
export function redirectToLogin(targetUrl = '') {
|
||||
if (!AUTH_ENABLED) {
|
||||
return
|
||||
}
|
||||
|
||||
const redirect = normalizeUrl(targetUrl || getCurrentPageUrl())
|
||||
if (isLoginPage(redirect)) {
|
||||
return
|
||||
}
|
||||
|
||||
const loginUrl = redirect
|
||||
? `${LOGIN_PAGE}?redirect=${encodeURIComponent(redirect)}`
|
||||
: LOGIN_PAGE
|
||||
|
||||
uni.reLaunch({
|
||||
url: loginUrl
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后按 redirect 返回目标页。
|
||||
*/
|
||||
export function goAfterLogin(redirect = '') {
|
||||
const target = normalizeUrl(safeDecode(redirect) || HOME_PAGE)
|
||||
const targetPath = getPath(target)
|
||||
|
||||
if (isLoginPage(targetPath)) {
|
||||
uni.switchTab({
|
||||
url: HOME_PAGE
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isTabPage(targetPath)) {
|
||||
uni.switchTab({
|
||||
url: targetPath
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.reLaunch({
|
||||
url: target
|
||||
})
|
||||
}
|
||||
|
||||
function guardPage(url = '') {
|
||||
if (!AUTH_ENABLED) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!needLogin(url) || isLoggedIn()) {
|
||||
return true
|
||||
}
|
||||
|
||||
redirectToLogin(url)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 uni 路由拦截器,只注册一次。
|
||||
*/
|
||||
export function setupAuthInterceptors() {
|
||||
if (hasSetupInterceptors) {
|
||||
return
|
||||
}
|
||||
|
||||
const methods = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']
|
||||
methods.forEach((method) => {
|
||||
uni.addInterceptor(method, {
|
||||
invoke(args) {
|
||||
return guardPage(args.url)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
hasSetupInterceptors = true
|
||||
}
|
||||
|
||||
export function ensureCurrentPageAuth() {
|
||||
const currentUrl = getCurrentPageUrl()
|
||||
if (!currentUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
guardPage(currentUrl)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 统一路由跳转封装,减少页面层重复样板代码。
|
||||
*/
|
||||
export function navigateTo(url) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
}
|
||||
|
||||
export function navigateBack(delta = 1) {
|
||||
uni.navigateBack({
|
||||
delta
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import request from '@/api/http/client'
|
||||
|
||||
/**
|
||||
* 兼容层:保留旧导入路径 `@/utils/request`。
|
||||
*/
|
||||
export default request
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 统一的轻提示封装。
|
||||
* @param {string} title 提示文案
|
||||
*/
|
||||
export function showToast(title = '') {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* 校验中国大陆手机号。
|
||||
* @param {string} value 手机号
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidMobile(value = '') {
|
||||
return /^1\d{10}$/.test(String(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 6 位短信验证码。
|
||||
* @param {string} value 验证码
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidSmsCode(value = '') {
|
||||
return /^\d{6}$/.test(String(value))
|
||||
}
|
||||
Loading…
Reference in New Issue