初始化
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