chore(migration): 提交迁移收尾遗留——token 化补全、popup/tabs 组件删除、BLE 与配网页调整
This commit is contained in:
parent
0ca714bec5
commit
84942851cb
|
|
@ -25,3 +25,4 @@ graphify-out/
|
||||||
|
|
||||||
# Project-specific
|
# Project-specific
|
||||||
docs/superpowers/
|
docs/superpowers/
|
||||||
|
.superpowers/
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ export default defineConfig(async (merge) => {
|
||||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// css-loader v7 默认 exportLocalsConvention='as-is',kebab-case 类名
|
||||||
|
// (如 device-row)无法以 styles.deviceRow 访问,必须开启 camelCase 转换
|
||||||
|
cssLoaderOption: {
|
||||||
|
modules: {
|
||||||
|
exportLocalsConvention: 'camelCaseOnly'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// discovery.js(原 hooks/useBluetoothDiscovery.js)为保留的纯 JS 资产,
|
// discovery.js(原 hooks/useBluetoothDiscovery.js)为保留的纯 JS 资产,
|
||||||
// 不做类型化;此处只声明 useBle 需要的最小工厂签名。
|
// 不做类型化;此处只声明 useBle 与页面需要用到的最小工厂签名。
|
||||||
export interface DiscoveryOptions {
|
export interface DiscoveryOptions {
|
||||||
onSearchingChange?: (searching: boolean) => void
|
onSearchingChange?: (searching: boolean) => void
|
||||||
onConnectedChange?: (connected: boolean) => void
|
onConnectedChange?: (connected: boolean) => void
|
||||||
onDeviceListChange?: (deviceList: Array<{ deviceId: string; name?: string; RSSI?: number }>) => void
|
onDeviceListChange?: (deviceList: Array<Record<string, unknown> & { deviceId: string }>) => void
|
||||||
onProfileChange?: (profileId: string) => void
|
onProfileChange?: (profileId: string) => void
|
||||||
onStageChange?: (stage: string) => void
|
onStageChange?: (stage: string) => void
|
||||||
onDeviceInfoChange?: (deviceInfo: Record<string, unknown>) => void
|
onDeviceInfoChange?: (deviceInfo: Record<string, unknown>) => void
|
||||||
|
|
@ -12,17 +12,35 @@ export interface DiscoveryOptions {
|
||||||
onRadarData?: (data: unknown) => void
|
onRadarData?: (data: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiscoveryState {
|
||||||
|
isSearching: boolean
|
||||||
|
isConnected: boolean
|
||||||
|
currentDeviceId: string
|
||||||
|
currentProfileId: string
|
||||||
|
deviceMap: Record<string, Record<string, unknown>>
|
||||||
|
deviceList: Array<Record<string, unknown> & { deviceId: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WifiConfigResult {
|
||||||
|
success: boolean
|
||||||
|
stateCode: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiscoveryInstance {
|
export interface DiscoveryInstance {
|
||||||
startSearch: () => Promise<void>
|
startSearch: () => Promise<void>
|
||||||
stopSearch: () => Promise<void>
|
stopSearch: (showStopToast?: boolean) => Promise<void>
|
||||||
connectDevice: (deviceId: string) => Promise<void>
|
connectDevice: (deviceId?: string) => Promise<unknown>
|
||||||
prepareWifiConfig: (deviceId: string, options?: Record<string, unknown>) => Promise<void>
|
prepareWifiConfig: (deviceId: string, options?: Record<string, unknown>) => Promise<unknown>
|
||||||
disconnectDevice: () => Promise<void>
|
disconnectDevice: () => Promise<void>
|
||||||
configureWifi: (ssid: string, password: string, options?: Record<string, unknown>) => Promise<void>
|
configureWifi: (ssid: string, password: string, options?: Record<string, unknown>) => Promise<WifiConfigResult>
|
||||||
startRadar: () => Promise<void>
|
readDeviceInfo: () => Promise<unknown>
|
||||||
stopRadar: () => Promise<void>
|
startRadar: () => Promise<unknown>
|
||||||
|
stopRadar: () => Promise<unknown>
|
||||||
|
setNarrowMode: (enabled: boolean) => Promise<unknown>
|
||||||
|
writeFallParams: (rawParams: unknown) => Promise<unknown>
|
||||||
|
sendRadarCommand: (commandByte: number) => Promise<unknown>
|
||||||
cleanupDiscovery: () => void
|
cleanupDiscovery: () => void
|
||||||
[key: string]: unknown
|
getState: () => DiscoveryState
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function createBluetoothDiscovery(options?: DiscoveryOptions): DiscoveryInstance
|
export declare function createBluetoothDiscovery(options?: DiscoveryOptions): DiscoveryInstance
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ const DISCOVERY_POLL_INTERVAL_MS = 1200
|
||||||
const SCAN_DURATION_MS = 30000
|
const SCAN_DURATION_MS = 30000
|
||||||
const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', '713WQ', 'ED719', 'EP832'])
|
const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', '713WQ', 'ED719', 'EP832'])
|
||||||
|
|
||||||
function promisifyUniApi(apiName, params = {}) {
|
function promisifyTaroApi(apiName, params = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni[apiName]({
|
Taro[apiName]({
|
||||||
...params,
|
...params,
|
||||||
success: (res) => resolve(res),
|
success: (res) => resolve(res),
|
||||||
fail: (err) => reject(err)
|
fail: (err) => reject(err)
|
||||||
|
|
@ -545,7 +545,7 @@ export function createBluetoothDiscovery(options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await promisifyUniApi('getBluetoothDevices')
|
const result = await promisifyTaroApi('getBluetoothDevices')
|
||||||
const list = Array.isArray(result?.devices) ? result.devices : []
|
const list = Array.isArray(result?.devices) ? result.devices : []
|
||||||
list.forEach((device) => {
|
list.forEach((device) => {
|
||||||
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
|
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,36 @@
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { createBluetoothDiscovery } from '../discovery'
|
import { createBluetoothDiscovery } from '../discovery'
|
||||||
|
import type { DiscoveryInstance, DiscoveryOptions } from '../discovery'
|
||||||
|
|
||||||
|
// 搜索结果设备项:discovery.upsertDevice 合入 showName/匹配标记后的产物
|
||||||
|
export type BleDevice = Record<string, unknown> & { deviceId: string }
|
||||||
|
|
||||||
// 工厂回调 → React state 的映射产物
|
// 工厂回调 → React state 的映射产物
|
||||||
export interface BleUiState {
|
export interface BleUiState {
|
||||||
searching: boolean
|
searching: boolean
|
||||||
connected: boolean
|
connected: boolean
|
||||||
deviceList: Array<{ deviceId: string; name?: string; RSSI?: number }>
|
deviceList: BleDevice[]
|
||||||
profileId: string
|
profileId: string
|
||||||
stage: string
|
stage: string
|
||||||
deviceInfo: Record<string, unknown> | null
|
deviceInfo: Record<string, unknown> | null
|
||||||
authResult: unknown
|
authResult: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBle() {
|
interface BleSingleton {
|
||||||
const [state, setState] = useState<BleUiState>({
|
discovery: DiscoveryInstance
|
||||||
|
state: BleUiState
|
||||||
|
listeners: Set<(s: BleUiState) => void>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块级单例:add 页连接后 config 页必须共享同一 discovery 实例,
|
||||||
|
// 否则跨页后 connected=false、characteristics 丢失,配网流程断裂
|
||||||
|
let singleton: BleSingleton | null = null
|
||||||
|
|
||||||
|
function getSingleton(): BleSingleton {
|
||||||
|
if (singleton) return singleton
|
||||||
|
|
||||||
|
const listeners = new Set<(s: BleUiState) => void>()
|
||||||
|
const state: BleUiState = {
|
||||||
searching: false,
|
searching: false,
|
||||||
connected: false,
|
connected: false,
|
||||||
deviceList: [],
|
deviceList: [],
|
||||||
|
|
@ -21,34 +38,42 @@ export function useBle() {
|
||||||
stage: 'IDLE',
|
stage: 'IDLE',
|
||||||
deviceInfo: null,
|
deviceInfo: null,
|
||||||
authResult: null
|
authResult: null
|
||||||
})
|
|
||||||
|
|
||||||
// discovery 实例在整个组件生命周期内只创建一次;
|
|
||||||
// 回调里读最新 state 用 setState 函数式更新,不闭包旧值
|
|
||||||
const discoveryRef = useRef<ReturnType<typeof createBluetoothDiscovery> | null>(null)
|
|
||||||
if (discoveryRef.current === null) {
|
|
||||||
discoveryRef.current = createBluetoothDiscovery({
|
|
||||||
onSearchingChange: (searching) => setState((s) => ({ ...s, searching })),
|
|
||||||
onConnectedChange: (connected) => setState((s) => ({ ...s, connected })),
|
|
||||||
onDeviceListChange: (deviceList) => setState((s) => ({ ...s, deviceList })),
|
|
||||||
onProfileChange: (profileId) => setState((s) => ({ ...s, profileId })),
|
|
||||||
onStageChange: (stage) => setState((s) => ({ ...s, stage })),
|
|
||||||
onDeviceInfoChange: (deviceInfo) => setState((s) => ({ ...s, deviceInfo })),
|
|
||||||
onAuthChange: (authResult) => setState((s) => ({ ...s, authResult }))
|
|
||||||
// onError / onRadarData / onProtocolState / onProtocolMsg 按页面需要接入
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 卸载时统一清理(停扫描、断连接、解绑事件)
|
const patch = (partial: Partial<BleUiState>) => {
|
||||||
useEffect(() => {
|
Object.assign(state, partial)
|
||||||
const discovery = discoveryRef.current
|
listeners.forEach((fn) => fn({ ...state }))
|
||||||
return () => {
|
}
|
||||||
discovery?.cleanupDiscovery()
|
|
||||||
discoveryRef.current = null
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const discovery = discoveryRef.current!
|
const discovery = createBluetoothDiscovery({
|
||||||
|
onSearchingChange: (searching) => patch({ searching }),
|
||||||
|
onConnectedChange: (connected) => patch({ connected }),
|
||||||
|
onDeviceListChange: (deviceList) => patch({ deviceList }),
|
||||||
|
onProfileChange: (profileId) => patch({ profileId }),
|
||||||
|
onStageChange: (stage) => patch({ stage }),
|
||||||
|
onDeviceInfoChange: (deviceInfo) => patch({ deviceInfo }),
|
||||||
|
onAuthChange: (authResult) => patch({ authResult })
|
||||||
|
// onError / onRadarData / onProtocolState / onProtocolMsg 按页面需要接入
|
||||||
|
} as DiscoveryOptions)
|
||||||
|
|
||||||
return { state, discovery }
|
singleton = { discovery, state, listeners }
|
||||||
|
return singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBle() {
|
||||||
|
const { discovery, listeners, state } = getSingleton()
|
||||||
|
const [snapshot, setSnapshot] = useState<BleUiState>(() => ({ ...state }))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (next: BleUiState) => setSnapshot(next)
|
||||||
|
listeners.add(listener)
|
||||||
|
// 挂载时若单例状态已更新过(如另一页已连接),立即对齐
|
||||||
|
setSnapshot({ ...state })
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [listeners])
|
||||||
|
|
||||||
|
return { state: snapshot, discovery }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #9ea1a6;
|
color: $color-text-secondary;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
border: 4px solid #eef0f4;
|
border: 4px solid $color-bg-disabled;
|
||||||
border-top-color: #2ccb98;
|
border-top-color: $color-primary;
|
||||||
border-radius: 50%;
|
border-radius: $radius-circle;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #9ea1a6;
|
color: $color-text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
.navbar {
|
.navbar {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
|
|
@ -13,20 +14,26 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: -16px;
|
margin-left: -16px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backIcon {
|
.backIcon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绝对定位居中:与 back 按钮是否存在无关,免手动平衡宽度
|
||||||
.title {
|
.title {
|
||||||
flex: 1;
|
position: absolute;
|
||||||
|
left: 120px;
|
||||||
|
right: 120px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
margin-right: 44px; // 平衡 back 按钮宽度
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.35);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
position: fixed;
|
|
||||||
background: #ffffff;
|
|
||||||
z-index: 101;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pos-top {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
border-bottom-left-radius: 24px;
|
|
||||||
border-bottom-right-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pos-center {
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border-radius: 24px;
|
|
||||||
min-width: 80vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pos-bottom {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
border-top-left-radius: 24px;
|
|
||||||
border-top-right-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 40px;
|
|
||||||
color: #9ea1a6;
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import styles from './index.module.scss'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
visible: boolean
|
|
||||||
position?: 'top' | 'center' | 'bottom'
|
|
||||||
onClose: () => void
|
|
||||||
children?: ReactNode
|
|
||||||
maxHeight?: string // e.g. '60vh'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Popup({
|
|
||||||
visible,
|
|
||||||
position = 'bottom',
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
maxHeight = '60vh'
|
|
||||||
}: Props) {
|
|
||||||
if (!visible) return null
|
|
||||||
|
|
||||||
const positionClass = styles[`pos-${position}`]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View className={styles.mask} onClick={onClose} />
|
|
||||||
<View
|
|
||||||
className={`${styles.panel} ${positionClass}`}
|
|
||||||
style={{ maxHeight }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<View className={styles.close} onClick={onClose}>
|
|
||||||
<Text>×</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
background: #ffffff;
|
|
||||||
border-bottom: 1px solid #f0f1f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
padding: 28px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 30px;
|
|
||||||
color: #9ea1a6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active .label {
|
|
||||||
color: #2a2d33;
|
|
||||||
font-weight: 600;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active .label::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -8px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 56px;
|
|
||||||
height: 4px;
|
|
||||||
background: #2ccb98;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import styles from './index.module.scss'
|
|
||||||
|
|
||||||
export interface TabItem {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
items: TabItem[]
|
|
||||||
activeKey: string
|
|
||||||
onChange: (key: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Tabs({ items, activeKey, onChange }: Props) {
|
|
||||||
return (
|
|
||||||
<View className={styles.tabs}>
|
|
||||||
{items.map((item) => (
|
|
||||||
<View
|
|
||||||
key={item.key}
|
|
||||||
className={`${styles.tab} ${item.key === activeKey ? styles.active : ''}`}
|
|
||||||
onClick={() => onChange(item.key)}
|
|
||||||
>
|
|
||||||
<Text className={styles.label}>{item.label}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { BleDevice } from '@/ble/hooks/useBle'
|
||||||
|
|
||||||
|
// 设备型号 → 启用的按钮 key 列表。
|
||||||
|
// 后续如需让其他型号裁剪按钮,只需新增映射条目。
|
||||||
|
export const DEVICE_ACTION_KEYS: Record<string, readonly string[]> = Object.freeze({
|
||||||
|
EP832: ['WIFI'],
|
||||||
|
DEFAULT: ['WIFI', 'PARAMS', 'SERVER']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按钮注册表:label / 忙态文案。
|
||||||
|
// 未配置 handler 的 key 渲染为不可点击的占位项。
|
||||||
|
export const ACTION_REGISTRY = Object.freeze({
|
||||||
|
WIFI: {
|
||||||
|
label: '网络配置',
|
||||||
|
busyLabel: '连接中'
|
||||||
|
},
|
||||||
|
PARAMS: {
|
||||||
|
label: '参数配置'
|
||||||
|
},
|
||||||
|
SERVER: {
|
||||||
|
label: '服务器配置'
|
||||||
|
}
|
||||||
|
} as Record<string, { label: string; busyLabel?: string }>)
|
||||||
|
|
||||||
|
export function isEp832Device(device: BleDevice): boolean {
|
||||||
|
const name = String(device?.showName || device?.name || device?.localName || '').toUpperCase()
|
||||||
|
return name.includes('EP832')
|
||||||
|
}
|
||||||
|
|
@ -1,55 +1,168 @@
|
||||||
.page { min-height: 100vh; background: #f1f1f4; }
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
.body { padding: 32px; }
|
background: #e9eaed;
|
||||||
|
|
||||||
.tip {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #9ea1a6;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scanBtn {
|
.search-wrap {
|
||||||
height: 96px;
|
height: 500px;
|
||||||
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
|
background: $color-bg-white;
|
||||||
border-radius: 48px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
width: 260px;
|
||||||
|
height: 260px;
|
||||||
|
border-radius: 130px;
|
||||||
|
background: $color-primary;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 32px;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scanBtnText {
|
.search-btn:active {
|
||||||
color: #ffffff;
|
transform: scale(0.97);
|
||||||
font-size: 32px;
|
}
|
||||||
|
|
||||||
|
.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 30px rgba(67, 201, 150, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(67, 201, 150, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-row {
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #efeff1;
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: 34px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
.empty { padding: 80px 0; }
|
|
||||||
|
|
||||||
.list { display: flex; flex-direction: column; }
|
|
||||||
|
|
||||||
.item {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 32px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.list-header {
|
||||||
|
height: 116px;
|
||||||
|
background: $color-bg-white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 52px;
|
||||||
|
color: #101217;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-body {
|
||||||
|
background: #e9eaed;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-row {
|
||||||
|
padding: 36px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c9097;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: #2a2d33;
|
}
|
||||||
font-weight: 600;
|
|
||||||
max-width: 70%;
|
.device-card {
|
||||||
|
background: $color-bg-white;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-icon {
|
||||||
|
width: 92px;
|
||||||
|
height: 92px;
|
||||||
|
border-radius: 46px;
|
||||||
|
border: 2px solid #d8d8d8;
|
||||||
|
box-shadow: inset 0 0 0 10px #f1f1f1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 38px;
|
||||||
|
color: #30343a;
|
||||||
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rssi {
|
.device-tag {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
color: #9ea1a6;
|
color: $color-primary-deep;
|
||||||
|
border: 1px solid $color-primary-deep;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-tag {
|
||||||
|
color: $color-info-light;
|
||||||
|
border-color: $color-info-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-id-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #8a8f98;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid $color-primary;
|
||||||
|
color: $color-primary-deep;
|
||||||
|
font-size: 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8fffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.disabled {
|
||||||
|
border-color: $color-primary-light-border;
|
||||||
|
color: #7db89e;
|
||||||
|
background: #f5faf7;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,142 @@
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { Loading } from '@/components/loading'
|
|
||||||
import { useBle } from '@/ble/hooks/useBle'
|
import { useBle } from '@/ble/hooks/useBle'
|
||||||
import { useToast } from '@/hooks/useToast'
|
|
||||||
import { navigateTo } from '@/utils/navigation'
|
import { navigateTo } from '@/utils/navigation'
|
||||||
import { ROUTES } from '@/constants/routes'
|
import { ROUTES } from '@/constants/routes'
|
||||||
|
import { getErrorMessage } from '@/utils/error'
|
||||||
|
import { DEVICE_ACTION_KEYS, ACTION_REGISTRY, isEp832Device } from '@/constants/deviceActions'
|
||||||
|
import type { BleDevice } from '@/ble/hooks/useBle'
|
||||||
import styles from './add.module.scss'
|
import styles from './add.module.scss'
|
||||||
|
|
||||||
export default function DeviceAdd() {
|
export default function DeviceAdd() {
|
||||||
const toast = useToast()
|
|
||||||
const { state, discovery } = useBle()
|
const { state, discovery } = useBle()
|
||||||
const [connectingId, setConnectingId] = useState('')
|
const [preparingId, setPreparingId] = useState('')
|
||||||
|
|
||||||
|
// 页面隐藏/卸载时清理搜索(对齐旧版 onHide/onUnload),连接保持供配网页使用
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (discovery.getState().isSearching) {
|
||||||
|
discovery.stopSearch(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
|
if (state.searching) {
|
||||||
|
await discovery.stopSearch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// startSearch 内部自动完成:Android 定位权限检查 + 适配器初始化 +
|
||||||
|
// 服务 UUID 过滤扫描(00F4/F400/FFF4)+ 4s 无结果兜底全扫 + 30s 自动停止
|
||||||
try {
|
try {
|
||||||
// startSearch 内部自动完成:Android 定位权限检查 + 适配器初始化 +
|
|
||||||
// 服务 UUID 过滤扫描(00F4/F400/FFF4)+ 30s 自动停止
|
|
||||||
await discovery.startSearch()
|
await discovery.startSearch()
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('蓝牙搜索启动失败')
|
// startSearch 内部已 toast 具体错误码,此处不再重复提示
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中设备 → 连接(prepareWifiConfig:连接 → 发现服务特征 → 订阅 F301,厂家确认无需鉴权)
|
// 点击"网络配置":连接 → 发现服务特征 → 订阅 F301(厂家确认无需鉴权)→ 打开配网页
|
||||||
const handleSelect = async (deviceId: string) => {
|
const openWifiPanel = async (deviceId: string) => {
|
||||||
setConnectingId(deviceId)
|
if (preparingId) return
|
||||||
|
setPreparingId(deviceId)
|
||||||
|
Taro.showLoading({ title: '连接设备中', mask: true })
|
||||||
|
|
||||||
|
let prepError: unknown = null
|
||||||
try {
|
try {
|
||||||
await discovery.prepareWifiConfig(deviceId)
|
await discovery.prepareWifiConfig(deviceId)
|
||||||
navigateTo(ROUTES.DEVICE_CONFIG)
|
navigateTo(ROUTES.DEVICE_CONFIG)
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.error('连接失败,请重试')
|
prepError = error
|
||||||
} finally {
|
} finally {
|
||||||
setConnectingId('')
|
setPreparingId('')
|
||||||
|
// showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示
|
||||||
|
Taro.hideLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prepError) {
|
||||||
|
// 连接/订阅失败用模态弹窗,需用户主动确认,确保错误诊断信息可见可读
|
||||||
|
Taro.showModal({
|
||||||
|
title: '设备连接失败',
|
||||||
|
content: getErrorMessage(prepError),
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokeAction = (key: string, deviceId: string) => {
|
||||||
|
if (key === 'WIFI') openWifiPanel(deviceId)
|
||||||
|
// PARAMS/SERVER 仅占位,后续接入
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActionBusy = (key: string, deviceId: string) =>
|
||||||
|
key === 'WIFI' && preparingId === deviceId
|
||||||
|
|
||||||
|
const renderDevice = (device: BleDevice) => {
|
||||||
|
const actions = (DEVICE_ACTION_KEYS[isEp832Device(device) ? 'EP832' : 'DEFAULT'] ?? DEVICE_ACTION_KEYS.DEFAULT)
|
||||||
|
.map((key) => ({ key, ...ACTION_REGISTRY[key] }))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={device.deviceId} className={styles.deviceCard}>
|
||||||
|
<View className={styles.deviceIcon} />
|
||||||
|
<View className={styles.deviceMain}>
|
||||||
|
<View className={styles.deviceNameRow}>
|
||||||
|
<Text className={styles.deviceName}>{String(device.showName ?? device.deviceId)}</Text>
|
||||||
|
{device.hasTargetService ? (
|
||||||
|
<Text className={styles.deviceTag}>00F4</Text>
|
||||||
|
) : device.matchedByName || device.matchedByPayload ? (
|
||||||
|
<Text className={`${styles.deviceTag} ${styles.nameTag}`}>名称匹配</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<Text className={styles.deviceIdText}>{String(device.deviceId)}</Text>
|
||||||
|
<View className={styles.deviceActions}>
|
||||||
|
{actions.map((action) => {
|
||||||
|
const busy = isActionBusy(action.key, device.deviceId)
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={action.key}
|
||||||
|
className={`${styles.actionBtn} ${busy ? styles.disabled : ''}`}
|
||||||
|
onClick={() => !busy && invokeAction(action.key, device.deviceId)}
|
||||||
|
>
|
||||||
|
<Text>{busy ? action.busyLabel : action.label}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={styles.page}>
|
<View className={styles.page}>
|
||||||
<Navbar title="添加设备" />
|
<Navbar title="设备配置" />
|
||||||
<View className={styles.body}>
|
|
||||||
<Text className={styles.tip}>请确保设备处于配网模式,靠近手机</Text>
|
|
||||||
|
|
||||||
{!state.searching ? (
|
<View className={styles.searchWrap}>
|
||||||
<View className={styles.scanBtn} onClick={startScan}>
|
<View
|
||||||
<Text className={styles.scanBtnText}>开始扫描</Text>
|
className={`${styles.searchBtn} ${state.searching ? styles.isSearching : ''}`}
|
||||||
</View>
|
onClick={startScan}
|
||||||
|
>
|
||||||
|
<Text>{state.searching ? '搜索中' : '搜索'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.tipRow}>
|
||||||
|
<Text>*手机需开启蓝牙功能,Android手机还需开启定位功能</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.listHeader}>
|
||||||
|
<Text className={styles.listTitle}>搜索到的设备列表</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.listBody}>
|
||||||
|
{state.deviceList.length === 0 ? (
|
||||||
|
<View className={styles.emptyRow}><Text>暂无搜索结果</Text></View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
state.deviceList.map(renderDevice)
|
||||||
<View className={styles.scanBtn} onClick={() => discovery.stopSearch()}>
|
|
||||||
<Text className={styles.scanBtnText}>停止扫描</Text>
|
|
||||||
</View>
|
|
||||||
{state.deviceList.length === 0 ? (
|
|
||||||
<View className={styles.empty}><Loading text="搜索中..." /></View>
|
|
||||||
) : (
|
|
||||||
<View className={styles.list}>
|
|
||||||
{state.deviceList.map((d) => (
|
|
||||||
<View
|
|
||||||
key={d.deviceId}
|
|
||||||
className={styles.item}
|
|
||||||
onClick={() => handleSelect(d.deviceId)}
|
|
||||||
>
|
|
||||||
<Text className={styles.name}>{d.name || d.deviceId}</Text>
|
|
||||||
<Text className={styles.rssi}>
|
|
||||||
{connectingId === d.deviceId ? '连接中...' : `RSSI ${d.RSSI ?? '-'}`}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,123 @@
|
||||||
.page { min-height: 100vh; background: #f1f1f4; }
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.body { padding: 32px; }
|
.device-row {
|
||||||
|
min-height: 74px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 0 30px;
|
||||||
|
border-bottom: 1px solid #f4f4f4;
|
||||||
|
background: $color-bg-white;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.device-label {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #2a2d33;
|
color: #8a8f98;
|
||||||
margin-bottom: 16px;
|
|
||||||
margin-top: 24px;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.device-value {
|
||||||
height: 96px;
|
flex: 1;
|
||||||
background: #ffffff;
|
min-width: 0;
|
||||||
border-radius: 16px;
|
font-size: 27px;
|
||||||
padding: 0 32px;
|
color: #4a4f56;
|
||||||
font-size: 30px;
|
white-space: nowrap;
|
||||||
margin-bottom: 16px;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.wifi-row {
|
||||||
|
min-height: 102px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 30px;
|
||||||
|
gap: 20px;
|
||||||
|
background: $color-bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-label {
|
||||||
|
width: 170px;
|
||||||
|
font-size: 34px;
|
||||||
|
color: #363b45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4f4f;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 32px;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-input-ssid {
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 190px;
|
||||||
|
height: 62px;
|
||||||
|
border: 1px solid $color-primary-light-border;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
color: $color-primary-deep;
|
||||||
|
background: #effcf5;
|
||||||
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #9ea1a6;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: 48px;
|
|
||||||
height: 96px;
|
|
||||||
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
|
|
||||||
border-radius: 48px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submitDisabled { opacity: 0.5; }
|
.current-btn.disabled {
|
||||||
|
border-color: #b8d8ca;
|
||||||
|
color: #9bbbae;
|
||||||
|
background: #f3f6f4;
|
||||||
|
}
|
||||||
|
|
||||||
.submitText {
|
.import-row {
|
||||||
|
min-height: 100px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
font-size: 34px;
|
||||||
|
color: $color-primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $color-bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tip {
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 16px 30px;
|
||||||
|
font-size: 26px;
|
||||||
|
color: $color-danger-deep;
|
||||||
|
background: $color-danger-light-bg;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
height: 100px;
|
||||||
|
background: $color-primary;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 32px;
|
font-size: 36px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 40px 24px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn.disabled {
|
||||||
|
background: #8edcae;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,205 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text, Input } from '@tarojs/components'
|
import { View, Text, Input } from '@tarojs/components'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useBle } from '@/ble/hooks/useBle'
|
import { getErrorMessage } from '@/utils/error'
|
||||||
import { navigateBack } from '@/utils/navigation'
|
import { navigateBack } from '@/utils/navigation'
|
||||||
|
import { useBle } from '@/ble/hooks/useBle'
|
||||||
import styles from './config.module.scss'
|
import styles from './config.module.scss'
|
||||||
|
|
||||||
|
const WIFI_HISTORY_STORAGE_KEY = 'SMARTHOME_LAST_WIFI_CONFIG'
|
||||||
|
|
||||||
|
const WIFI_STATE_MESSAGES: Record<number, string> = Object.freeze({
|
||||||
|
1: 'WiFi 参数校验失败,请检查名称与密码',
|
||||||
|
2: '设备连接该 WiFi 失败,请确认密码是否正确',
|
||||||
|
3: 'WiFi 配置成功',
|
||||||
|
4: '设备正在连接 WiFi,请稍候'
|
||||||
|
})
|
||||||
|
|
||||||
|
function getWifiStateMessage(stateCode: number) {
|
||||||
|
return WIFI_STATE_MESSAGES[stateCode] || `WiFi 状态码异常: ${stateCode}`
|
||||||
|
}
|
||||||
|
|
||||||
export default function DeviceConfig() {
|
export default function DeviceConfig() {
|
||||||
const toast = useToast()
|
|
||||||
const { state, discovery } = useBle()
|
const { state, discovery } = useBle()
|
||||||
const [ssid, setSsid] = useState('')
|
const [ssid, setSsid] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [sending, setSending] = useState(false)
|
||||||
|
const [fetchingCurrentWifi, setFetchingCurrentWifi] = useState(false)
|
||||||
|
const [errorTip, setErrorTip] = useState('')
|
||||||
|
|
||||||
|
const selectedDevice = state.deviceList.find((d) => d.deviceId === discovery.getState().currentDeviceId)
|
||||||
|
const selectedDeviceName = (selectedDevice?.showName as string)
|
||||||
|
|| (selectedDevice?.deviceId as string)
|
||||||
|
|| '未选择设备'
|
||||||
|
|
||||||
|
// 启动 WiFi 模块并读取手机当前连接的 SSID(部分端必须先 startWifi)
|
||||||
|
const fetchCurrentWifiSsid = async () => {
|
||||||
|
if (fetchingCurrentWifi) return
|
||||||
|
setFetchingCurrentWifi(true)
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!state.connected) {
|
|
||||||
toast.error('设备未连接,请返回上一步重新连接')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!ssid || !password) {
|
|
||||||
toast.error('请填写 WiFi 信息')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSubmitting(true)
|
|
||||||
try {
|
try {
|
||||||
// 内部走 A7 分包写入 UUID_WIFI 特征;profile.wifi 校验 SSID/密码长度与非法字符
|
try {
|
||||||
await discovery.configureWifi(ssid, password)
|
await Taro.startWifi({})
|
||||||
toast.success('配网指令已发送')
|
} catch {
|
||||||
navigateBack()
|
// 已启动等非致命错误忽略,继续尝试读取当前连接
|
||||||
} catch (e) {
|
}
|
||||||
const msg = e instanceof Error ? e.message : '配置失败'
|
|
||||||
toast.error(msg)
|
const result = await Taro.getConnectedWifi({})
|
||||||
|
const info = (result as unknown as { wifi?: { SSID?: string; ssid?: string } }).wifi
|
||||||
|
const name = String(info?.SSID || info?.ssid || '').trim()
|
||||||
|
|
||||||
|
if (!name || name.toLowerCase().includes('unknown')) {
|
||||||
|
throw new Error('未获取到当前连接 WiFi 名称')
|
||||||
|
}
|
||||||
|
|
||||||
|
setSsid(name)
|
||||||
|
showToast('已填入当前连接WiFi')
|
||||||
|
} catch (error) {
|
||||||
|
showToast(getErrorMessage(error))
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setFetchingCurrentWifi(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importLastWifiConfig = () => {
|
||||||
|
try {
|
||||||
|
const cached = Taro.getStorageSync(WIFI_HISTORY_STORAGE_KEY) as
|
||||||
|
| { ssid?: string; password?: string }
|
||||||
|
| ''
|
||||||
|
|
||||||
|
if (!cached || typeof cached !== 'object') {
|
||||||
|
showToast('暂无上次 WiFi 配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = String(cached.ssid || '').trim()
|
||||||
|
const p = String(cached.password || '').trim()
|
||||||
|
|
||||||
|
if (!s || !p) {
|
||||||
|
showToast('暂无可用的历史 WiFi 配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSsid(s)
|
||||||
|
setPassword(p)
|
||||||
|
showToast('已导入上次 WiFi 配置')
|
||||||
|
} catch {
|
||||||
|
showToast('读取历史 WiFi 配置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheWifiConfig = (s: string, p: string) => {
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync(WIFI_HISTORY_STORAGE_KEY, { ssid: s, password: p, updatedAt: Date.now() })
|
||||||
|
} catch {
|
||||||
|
// 缓存失败不阻断配网结果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmWifiSettings = async () => {
|
||||||
|
if (sending) return
|
||||||
|
|
||||||
|
if (!state.connected) {
|
||||||
|
showToast('设备未连接,请重新配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = String(ssid || '').trim()
|
||||||
|
const p = String(password || '').trim()
|
||||||
|
|
||||||
|
if (!s) {
|
||||||
|
showToast('请输入 WiFi 名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!p) {
|
||||||
|
showToast('请输入 WiFi 密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSending(true)
|
||||||
|
setErrorTip('')
|
||||||
|
|
||||||
|
let needAutoClose = false
|
||||||
|
let tipMessage = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发送后等待设备经 UUID_STATE 返回最终态 1/2/3,才提示并自动返回
|
||||||
|
const result = await discovery.configureWifi(s, p, { timeout: 60000 })
|
||||||
|
tipMessage = getWifiStateMessage(result.stateCode)
|
||||||
|
needAutoClose = result.success
|
||||||
|
if (result.success) cacheWifiConfig(s, p)
|
||||||
|
} catch (error) {
|
||||||
|
// 超时/异常:错误持久展示在弹层内,避免瞬态 toast 被覆盖看不清
|
||||||
|
tipMessage = getErrorMessage(error)
|
||||||
|
setErrorTip(tipMessage)
|
||||||
|
} finally {
|
||||||
|
setSending(false)
|
||||||
|
if (tipMessage) showToast(tipMessage)
|
||||||
|
if (needAutoClose) navigateBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={styles.page}>
|
<View className={styles.page}>
|
||||||
<Navbar title="设备配置" />
|
<Navbar title="设备网络设置" />
|
||||||
<View className={styles.body}>
|
|
||||||
<Text className={styles.label}>WiFi 名称</Text>
|
<View className={styles.deviceRow}>
|
||||||
|
<Text className={styles.deviceLabel}>设备</Text>
|
||||||
|
<Text className={styles.deviceValue}>{selectedDeviceName}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.wifiRow}>
|
||||||
|
<Text className={styles.wifiLabel}>
|
||||||
|
<Text className={styles.required}>*</Text>WiFi名称
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
className={styles.input}
|
className={styles.wifiInputSsid}
|
||||||
placeholder="请输入 WiFi 名称"
|
placeholder="请输入 WiFi 名称"
|
||||||
|
maxlength={32}
|
||||||
|
confirmType="done"
|
||||||
value={ssid}
|
value={ssid}
|
||||||
onInput={(e) => setSsid(e.detail.value)}
|
onInput={(e) => setSsid(e.detail.value)}
|
||||||
/>
|
/>
|
||||||
<Text className={styles.label}>WiFi 密码</Text>
|
<View
|
||||||
|
className={`${styles.currentBtn} ${fetchingCurrentWifi ? styles.disabled : ''}`}
|
||||||
|
onClick={() => !fetchingCurrentWifi && fetchCurrentWifiSsid()}
|
||||||
|
>
|
||||||
|
<Text>{fetchingCurrentWifi ? '获取中...' : '一键获取当前连接WiFi'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.wifiRow}>
|
||||||
|
<Text className={styles.wifiLabel}>
|
||||||
|
<Text className={styles.required}>*</Text>WiFi密码
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
className={styles.input}
|
className={styles.wifiInput}
|
||||||
placeholder="请输入 WiFi 密码(8-63 位)"
|
placeholder="请输入密码"
|
||||||
password
|
password
|
||||||
|
maxlength={63}
|
||||||
|
confirmType="done"
|
||||||
value={password}
|
value={password}
|
||||||
onInput={(e) => setPassword(e.detail.value)}
|
onInput={(e) => setPassword(e.detail.value)}
|
||||||
/>
|
/>
|
||||||
<Text className={styles.hint}>
|
</View>
|
||||||
当前连接:{state.connected ? '已连接' : '未连接'}
|
|
||||||
{state.deviceInfo && state.profileId !== 'UNKNOWN' ? `(${state.profileId})` : ''}
|
<View className={styles.importRow} onClick={importLastWifiConfig}>
|
||||||
</Text>
|
<Text>一键导入上次 WiFi 配置</Text>
|
||||||
<View
|
</View>
|
||||||
className={`${styles.submit} ${submitting ? styles.submitDisabled : ''}`}
|
|
||||||
onClick={submitting ? undefined : handleSubmit}
|
{errorTip ? (
|
||||||
>
|
<View className={styles.errorTip}>
|
||||||
<Text className={styles.submitText}>
|
<Text>{errorTip}</Text>
|
||||||
{submitting ? '配置中...' : '开始配网'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View
|
||||||
|
className={`${styles.submitBtn} ${sending ? styles.disabled : ''}`}
|
||||||
|
onClick={() => !sending && confirmWifiSettings()}
|
||||||
|
>
|
||||||
|
<Text>{sending ? '发送中...' : '发送'}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.stateWrap {
|
.stateWrap {
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -9,7 +14,7 @@
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #2ccb98;
|
background: $color-primary;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -20,8 +25,9 @@
|
||||||
.heartCard {
|
.heartCard {
|
||||||
margin: 24px;
|
margin: 24px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-radius: 24px;
|
border-radius: $radius-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
@ -33,46 +39,46 @@
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heartIcon { font-size: 72px; color: #f5222d; line-height: 1; }
|
.heartIcon { font-size: 72px; color: $color-danger; line-height: 1; }
|
||||||
|
|
||||||
.heartValue {
|
.heartValue {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 96px;
|
font-size: 96px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heartUnit { margin-top: 8px; font-size: 30px; color: #9ea1a6; }
|
.heartUnit { margin-top: 8px; font-size: 30px; color: $color-text-secondary; }
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
border-radius: 999px;
|
border-radius: $radius-pill;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badgeOn { background: #f6ffed; color: #52c41a; }
|
.badgeOn { background: $color-success-light-bg; color: $color-success; }
|
||||||
.badgeOff { background: #f5f5f5; color: #999999; }
|
.badgeOff { background: $color-bg-disabled; color: $color-text-secondary; }
|
||||||
|
|
||||||
.badgeDot {
|
.badgeDot {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: $radius-circle;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badgeDotOn { background: #52c41a; }
|
.badgeDotOn { background: $color-success; }
|
||||||
.badgeDotOff { background: #999999; }
|
.badgeDotOff { background: $color-text-secondary; }
|
||||||
|
|
||||||
.heartRight { flex: 1; min-width: 0; margin-left: 32px; }
|
.heartRight { flex: 1; min-width: 0; margin-left: 32px; }
|
||||||
|
|
||||||
.heartHealthLabel {
|
.heartHealthLabel {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
@ -89,11 +95,12 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-radius: 16px;
|
border-radius: $radius-md;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metricLabel { font-size: 26px; color: #9ea1a6; }
|
.metricLabel { font-size: 26px; color: $color-text-secondary; }
|
||||||
|
|
||||||
.metricRow {
|
.metricRow {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
@ -106,14 +113,15 @@
|
||||||
.metricUnit {
|
.metricUnit {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #b5b9c0;
|
color: $color-text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoCard {
|
.infoCard {
|
||||||
margin: 4px 24px 24px;
|
margin: 4px 24px 24px;
|
||||||
padding: 8px 32px;
|
padding: 8px 32px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-radius: 24px;
|
border-radius: $radius-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoRow {
|
.infoRow {
|
||||||
|
|
@ -121,10 +129,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #f0f1f3;
|
border-bottom: 1px solid $color-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoRow:last-child { border-bottom: none; }
|
.infoRow:last-child { border-bottom: none; }
|
||||||
|
|
||||||
.infoLabel { font-size: 28px; color: #9ea1a6; }
|
.infoLabel { font-size: 28px; color: $color-text-secondary; }
|
||||||
.infoValue { font-size: 28px; color: #2a2d33; }
|
.infoValue { font-size: 28px; color: $color-text; }
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 66vh;
|
height: 66vh;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
|
box-shadow: $shadow-popup;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-bottom-left-radius: 24px;
|
border-bottom-left-radius: $radius-lg;
|
||||||
border-bottom-right-radius: 24px;
|
border-bottom-right-radius: $radius-lg;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,14 +20,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #f0f1f3;
|
border-bottom: 1px solid $color-border;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
|
|
@ -56,9 +56,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #f5f6f8;
|
background: $color-bg-gray;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 999px;
|
border-radius: $radius-pill;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
|
||||||
|
|
@ -66,16 +66,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-chip-active {
|
.quick-chip-active {
|
||||||
background: #e9fff3;
|
background: $color-primary-light-bg;
|
||||||
border-color: #2ccb98;
|
border-color: $color-primary;
|
||||||
color: #12a37a;
|
color: $color-primary-deep;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekdays {
|
.weekdays {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid #f0f1f3;
|
border-bottom: 1px solid $color-border;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-start,
|
.day-start,
|
||||||
.day-end { background: #2ccb98; }
|
.day-end { background: $color-primary; }
|
||||||
|
|
||||||
.day-start .day-num,
|
.day-start .day-num,
|
||||||
.day-end .day-num {
|
.day-end .day-num {
|
||||||
|
|
@ -141,35 +141,35 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-in {
|
.day-in {
|
||||||
background: #e9fff3;
|
background: $color-primary-light-bg;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-in .day-num { color: #12a37a; }
|
.day-in .day-num { color: $color-primary-deep; }
|
||||||
|
|
||||||
.day-num {
|
.day-num {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-tag {
|
.day-tag {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
line-height: 24px;
|
line-height: 26px;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 16px 24px calc(16px + env(safe-area-inset-bottom));
|
padding: 16px 24px calc(16px + env(safe-area-inset-bottom));
|
||||||
border-top: 1px solid #f0f1f3;
|
border-top: 1px solid $color-border;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-btn {
|
.confirm-btn {
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #2ccb98;
|
background: $color-primary;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-top: 1px solid #ededed;
|
border-top: 1px solid $color-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-cell {
|
.filter-cell {
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #f5f6f8;
|
background: $color-bg-gray;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
|
@ -37,9 +37,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-trigger-active {
|
.filter-trigger-active {
|
||||||
background: #e9fff3;
|
background: $color-primary-light-bg;
|
||||||
border-color: #2ccb98;
|
border-color: $color-primary;
|
||||||
color: #12a37a;
|
color: $color-primary-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-text {
|
.filter-text {
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
margin-top: -40px;
|
margin-top: -40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #b5b9c0;
|
color: $color-text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 数据层:三抽屉的定位锚点 */
|
/* 数据层:三抽屉的定位锚点 */
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
.content-mask {
|
.content-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: $color-mask;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,13 +87,13 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
|
box-shadow: $shadow-popup;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-bottom-left-radius: 24px;
|
border-bottom-left-radius: $radius-lg;
|
||||||
border-bottom-right-radius: 24px;
|
border-bottom-right-radius: $radius-lg;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
@ -103,13 +103,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #f0f1f3;
|
border-bottom: 1px solid $color-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-title {
|
.drawer-title {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-close {
|
.drawer-close {
|
||||||
|
|
@ -137,22 +137,22 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|
||||||
&:active { background: #f7f8fa; }
|
&:active { background: $color-bg-press; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-item-active { background: #e9fff3; }
|
.drawer-item-active { background: $color-primary-light-bg; }
|
||||||
|
|
||||||
.drawer-item-text {
|
.drawer-item-text {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-item-active .drawer-item-text {
|
.drawer-item-active .drawer-item-text {
|
||||||
color: #2ccb98;
|
color: $color-primary;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-icon {
|
.check-icon {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #2ccb98;
|
color: $color-primary;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
.title {
|
.title {
|
||||||
font-size: 60px;
|
font-size: 60px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 80px;
|
margin-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-radius: 16px;
|
border-radius: $radius-md;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
.submit {
|
.submit {
|
||||||
height: 96px;
|
height: 96px;
|
||||||
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
|
background: $color-primary-grad;
|
||||||
border-radius: 48px;
|
border-radius: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 80px 32px;
|
padding: 80px 32px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
@ -8,8 +13,8 @@
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
border-radius: 50%;
|
border-radius: $radius-circle;
|
||||||
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
|
background: $color-primary-grad;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -17,8 +22,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
text-align: center;
|
|
||||||
line-height: 120px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerInfo { display: flex; flex-direction: column; }
|
.headerInfo { display: flex; flex-direction: column; }
|
||||||
|
|
@ -26,13 +29,13 @@
|
||||||
.name {
|
.name {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uid {
|
.uid {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #9ea1a6;
|
color: $color-text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu { margin-top: 20px; }
|
.menu { margin-top: 20px; }
|
||||||
|
|
@ -40,16 +43,16 @@
|
||||||
.item {
|
.item {
|
||||||
height: 104px;
|
height: 104px;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
border-bottom: 1px solid #f0f1f3;
|
border-bottom: 1px solid $color-border;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: #2a2d33;
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
color: #c0c4cc;
|
color: $color-text-placeholder;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.stateWrap {
|
.stateWrap {
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -9,7 +14,7 @@
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #2ccb98;
|
background: $color-primary;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -23,25 +28,25 @@
|
||||||
height: 104px;
|
height: 104px;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
background: #ffffff;
|
background: $color-bg-white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemActive { background: #e9fff3; }
|
.itemActive { background: $color-primary-light-bg; }
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #222;
|
color: $color-text;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemActive .name { color: #26c996; }
|
.itemActive .name { color: $color-primary; }
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #c0c4cc;
|
color: $color-text-placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemActive .check { color: #26c996; }
|
.itemActive .check { color: $color-primary; }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
@import './variables.scss';
|
@import './variables.scss';
|
||||||
|
|
||||||
|
// designWidth=750 下 1px=1rpx,正文默认字号取 28rpx(=14物理px)
|
||||||
page {
|
page {
|
||||||
background-color: $color-bg;
|
background-color: $color-bg;
|
||||||
color: $color-text;
|
color: $color-text;
|
||||||
font-size: 14px;
|
font-size: 28px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view, text {
|
view, text {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function getErrorMessage(error: unknown): string {
|
||||||
|
if (!error) {
|
||||||
|
return '操作失败,请重试'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = error as { message?: string; errMsg?: string }
|
||||||
|
return err.message || err.errMsg || '操作失败,请重试'
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue