feat(device-tab): 智能设备页改造(信息卡+切换家庭圈+功能卡片)+ 添加设备页
智能设备页重构:原"请选择场所"行删除,新增设备应用家庭信息卡(家庭圈名/关爱对象/关爱地址+切换家庭圈按钮)、房间管理/添加设备两张并排功能卡片;切换家庭圈走 BottomSheet 弹层(pending 确认制,取消不生效),本地 useState 管理选中家庭圈。新"添加设备"页(pages/device/add)含手动输入+扫码+二维码位置说明三区块,纯 UI 阶段,提交/扫码均弹 toast。
This commit is contained in:
parent
d6333beda9
commit
fbdcdfa6ce
|
|
@ -1,168 +1,110 @@
|
||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #e9eaed;
|
background: $color-bg;
|
||||||
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-wrap {
|
.section {
|
||||||
height: 500px;
|
margin: 24px;
|
||||||
|
padding: 40px 32px;
|
||||||
background: $color-bg-white;
|
background: $color-bg-white;
|
||||||
display: flex;
|
border-radius: $radius-md;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn {
|
.sectionTitle {
|
||||||
width: 260px;
|
font-size: 36px;
|
||||||
height: 260px;
|
|
||||||
border-radius: 130px;
|
|
||||||
background: $color-primary;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 56px;
|
|
||||||
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 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;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
font-weight: 700;
|
||||||
|
color: $color-text;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-body {
|
.sectionDesc {
|
||||||
background: #e9eaed;
|
font-size: 26px;
|
||||||
padding-bottom: 30px;
|
color: $color-text-secondary;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-row {
|
.input {
|
||||||
padding: 36px;
|
width: 100%;
|
||||||
text-align: center;
|
height: 96px;
|
||||||
color: #8c9097;
|
padding: 0 32px;
|
||||||
font-size: 30px;
|
background: $color-bg-gray;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
font-size: 28px;
|
||||||
|
color: $color-text;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card {
|
.submitBtn {
|
||||||
background: $color-bg-white;
|
width: 100%;
|
||||||
margin-top: 12px;
|
height: 96px;
|
||||||
padding: 24px;
|
background: $color-primary-grad;
|
||||||
display: flex;
|
border-radius: $radius-pill;
|
||||||
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-name {
|
.submitBtnPressed {
|
||||||
flex: 1;
|
opacity: 0.85;
|
||||||
min-width: 0;
|
|
||||||
font-size: 38px;
|
|
||||||
color: #30343a;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-tag {
|
.submitBtnText {
|
||||||
font-size: 22px;
|
font-size: 32px;
|
||||||
color: $color-primary-deep;
|
font-weight: 600;
|
||||||
border: 1px solid $color-primary-deep;
|
color: #ffffff;
|
||||||
border-radius: 20px;
|
|
||||||
padding: 2px 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-tag {
|
.scanArea {
|
||||||
color: $color-info-light;
|
padding: 60px 0;
|
||||||
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;
|
display: flex;
|
||||||
gap: 10px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanAreaPressed {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanIcon {
|
||||||
|
font-size: 140px;
|
||||||
|
color: $color-primary;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanText {
|
||||||
|
font-size: 30px;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.deviceBox {
|
||||||
padding: 8px 16px;
|
width: 48%;
|
||||||
border: 2px solid $color-primary;
|
display: flex;
|
||||||
color: $color-primary-deep;
|
flex-direction: column;
|
||||||
font-size: 30px;
|
align-items: center;
|
||||||
border-radius: 6px;
|
|
||||||
background: #f8fffc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.disabled {
|
.deviceIcon1,
|
||||||
border-color: $color-primary-light-border;
|
.deviceIcon2 {
|
||||||
color: #7db89e;
|
width: 100%;
|
||||||
background: #f5faf7;
|
height: 200px;
|
||||||
|
background: $color-bg-gray;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceLabel {
|
||||||
|
font-size: 26px;
|
||||||
|
color: $color-text;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,79 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text, Input } from '@tarojs/components'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { useBle } from '@/ble/hooks/useBle'
|
import { showToast } from '@/utils/toast'
|
||||||
import { navigateTo } from '@/utils/navigation'
|
|
||||||
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 { state, discovery } = useBle()
|
const [deviceId, setDeviceId] = useState('')
|
||||||
const [preparingId, setPreparingId] = useState('')
|
|
||||||
|
|
||||||
// 页面隐藏/卸载时清理搜索(对齐旧版 onHide/onUnload),连接保持供配网页使用
|
const submitManual = () => {
|
||||||
useEffect(() => {
|
if (!deviceId.trim()) {
|
||||||
return () => {
|
showToast('请输入设备编号')
|
||||||
if (discovery.getState().isSearching) {
|
|
||||||
discovery.stopSearch(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const startScan = async () => {
|
|
||||||
if (state.searching) {
|
|
||||||
await discovery.stopSearch()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// startSearch 内部自动完成:Android 定位权限检查 + 适配器初始化 +
|
showToast('设备添加功能即将接入')
|
||||||
// 服务 UUID 过滤扫描(00F4/F400/FFF4)+ 4s 无结果兜底全扫 + 30s 自动停止
|
}
|
||||||
|
|
||||||
|
const scanQRCode = async () => {
|
||||||
try {
|
try {
|
||||||
await discovery.startSearch()
|
// @ts-ignore Taro 类型未跟进 wx.scanCode 的完整回调(实际运行时存在)
|
||||||
|
const res = await Taro.scanCode({ onlyFromCamera: false })
|
||||||
|
const code = String(res?.result ?? '').trim()
|
||||||
|
if (!code) {
|
||||||
|
showToast('未识别到有效编号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDeviceId(code)
|
||||||
|
showToast('已填入扫码结果')
|
||||||
} catch {
|
} catch {
|
||||||
// startSearch 内部已 toast 具体错误码,此处不再重复提示
|
showToast('扫码取消')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击"网络配置":连接 → 发现服务特征 → 订阅 F301(厂家确认无需鉴权)→ 打开配网页
|
|
||||||
const openWifiPanel = async (deviceId: string) => {
|
|
||||||
if (preparingId) return
|
|
||||||
setPreparingId(deviceId)
|
|
||||||
Taro.showLoading({ title: '连接设备中', mask: true })
|
|
||||||
|
|
||||||
let prepError: unknown = null
|
|
||||||
try {
|
|
||||||
await discovery.prepareWifiConfig(deviceId)
|
|
||||||
navigateTo(ROUTES.DEVICE_CONFIG)
|
|
||||||
} catch (error) {
|
|
||||||
prepError = error
|
|
||||||
} finally {
|
|
||||||
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="设备配置" variant="hero" />
|
<Navbar title="添加设备" autoBack={true} variant="hero" />
|
||||||
|
|
||||||
<View className={styles.searchWrap}>
|
<View className={styles.section}>
|
||||||
<View
|
<Text className={styles.sectionTitle}>手动输入</Text>
|
||||||
className={`${styles.searchBtn} ${state.searching ? styles.isSearching : ''}`}
|
<Text className={styles.sectionDesc}>
|
||||||
onClick={startScan}
|
将智能设备添加至当前家庭圈,可实时查看设备状态和告警信息。
|
||||||
>
|
</Text>
|
||||||
<Text>{state.searching ? '搜索中' : '搜索'}</Text>
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="请输入设备编号"
|
||||||
|
maxlength={32}
|
||||||
|
confirmType="done"
|
||||||
|
value={deviceId}
|
||||||
|
onInput={(e) => setDeviceId(e.detail.value)}
|
||||||
|
/>
|
||||||
|
<View className={styles.submitBtn} hoverClass={styles.submitBtnPressed} onClick={submitManual}>
|
||||||
|
<Text className={styles.submitBtnText}>提交</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className={styles.tipRow}>
|
<View className={styles.section}>
|
||||||
<Text>*手机需开启蓝牙功能,Android手机还需开启定位功能</Text>
|
<Text className={styles.sectionTitle}>扫码添加</Text>
|
||||||
|
<View className={styles.scanArea} hoverClass={styles.scanAreaPressed} onClick={scanQRCode}>
|
||||||
|
<Text className={styles.scanIcon}>⊡</Text>
|
||||||
|
<Text className={styles.scanText}>点击扫码</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className={styles.listHeader}>
|
<View className={styles.section}>
|
||||||
<Text className={styles.listTitle}>搜索到的设备列表</Text>
|
<Text className={styles.sectionTitle}>设备二维码/编号位置</Text>
|
||||||
|
<View className={styles.deviceRow}>
|
||||||
|
<View className={styles.deviceBox}>
|
||||||
|
<View className={styles.deviceIcon1} />
|
||||||
|
<Text className={styles.deviceLabel}>包装盒侧边</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.deviceBox}>
|
||||||
|
<View className={styles.deviceIcon2} />
|
||||||
|
<Text className={styles.deviceLabel}>设备侧面或背面</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className={styles.listBody}>
|
|
||||||
{state.deviceList.length === 0 ? (
|
|
||||||
<View className={styles.emptyRow}><Text>暂无搜索结果</Text></View>
|
|
||||||
) : (
|
|
||||||
state.deviceList.map(renderDevice)
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,6 @@
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteRow {
|
|
||||||
height: 108px;
|
|
||||||
padding: 0 32px;
|
|
||||||
background: $color-bg-white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteText {
|
|
||||||
font-size: 34px;
|
|
||||||
color: $color-text;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteArrow {
|
|
||||||
font-size: 36px;
|
|
||||||
color: $color-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stateWrap {
|
.stateWrap {
|
||||||
margin-top: 80px;
|
margin-top: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -32,11 +12,102 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设备应用家庭信息卡:白色卡片浮在绿色头部下,浅灰行内分隔
|
||||||
|
.familyCard {
|
||||||
|
margin: -40px 24px 0;
|
||||||
|
padding: 8px 32px 32px;
|
||||||
|
background: $color-bg-white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.familyRow {
|
||||||
|
min-height: 104px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.familyRow:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.familyLabel {
|
||||||
|
font-size: 30px;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.familyValue {
|
||||||
|
font-size: 30px;
|
||||||
|
color: $color-text;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchBtn {
|
||||||
|
margin-top: 24px;
|
||||||
|
height: 88px;
|
||||||
|
background: $color-primary-grad;
|
||||||
|
border-radius: $radius-pill;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchBtnPressed {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchBtnText {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 房间管理 / 添加设备 两张并排功能卡
|
||||||
|
.funcRow {
|
||||||
|
display: flex;
|
||||||
|
padding: 24px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funcCard {
|
||||||
|
flex: 1;
|
||||||
|
height: 144px;
|
||||||
|
background: $color-bg-white;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funcCardPressed {
|
||||||
|
background: $color-bg-press;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funcIcon {
|
||||||
|
font-size: 56px;
|
||||||
|
color: $color-primary;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funcText {
|
||||||
|
font-size: 28px;
|
||||||
|
color: $color-text;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备网格
|
||||||
.grid {
|
.grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -116,3 +187,86 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fabIcon { font-size: 48px; color: #ffffff; font-weight: 300; }
|
.fabIcon { font-size: 48px; color: #ffffff; font-weight: 300; }
|
||||||
|
|
||||||
|
// 切换家庭圈弹层内的列表项
|
||||||
|
.sheetList {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetItem {
|
||||||
|
padding: 24px 32px;
|
||||||
|
background: $color-bg-gray;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetItemActive {
|
||||||
|
background: $color-primary-light-bg;
|
||||||
|
border: 2px solid $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetHead {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetName {
|
||||||
|
font-size: 32px;
|
||||||
|
color: $color-text;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetRole {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: $color-bg-disabled;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetRoleText {
|
||||||
|
font-size: 22px;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetItemActive .sheetRole {
|
||||||
|
background: $color-bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetItemActive .sheetRoleText {
|
||||||
|
color: $color-primary-deep;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetMeta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetMetaItem {
|
||||||
|
font-size: 24px;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetItemActive .sheetMetaItem {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetConfirm {
|
||||||
|
margin: 16px 0 32px;
|
||||||
|
height: 96px;
|
||||||
|
background: $color-primary-grad;
|
||||||
|
border-radius: $radius-pill;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetConfirmPressed {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheetConfirmText {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import { setDeviceList, type Device } from '@/store/deviceSlice'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { Loading } from '@/components/loading'
|
import { Loading } from '@/components/loading'
|
||||||
import { StateView } from '@/components/state-view'
|
import { StateView } from '@/components/state-view'
|
||||||
|
import { BottomSheet } from '@/components/bottom-sheet'
|
||||||
import { getDeviceList } from '@/api/device'
|
import { getDeviceList } from '@/api/device'
|
||||||
import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums'
|
import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums'
|
||||||
import { ROUTES } from '@/constants/routes'
|
import { ROUTES } from '@/constants/routes'
|
||||||
import { navigateTo } from '@/utils/navigation'
|
import { navigateTo } from '@/utils/navigation'
|
||||||
|
import { getFamilies, type Family } from '@/api/family'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
const FALLBACK_ICON = '⌂'
|
const FALLBACK_ICON = '⌂'
|
||||||
|
|
@ -22,6 +24,11 @@ export default function DeviceList() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [loadError, setLoadError] = useState(false)
|
const [loadError, setLoadError] = useState(false)
|
||||||
|
|
||||||
|
const [families, setFamilies] = useState<Family[]>([])
|
||||||
|
const [currentFamily, setCurrentFamily] = useState<Family | null>(null)
|
||||||
|
const [familySheetOpen, setFamilySheetOpen] = useState(false)
|
||||||
|
const [pendingFamily, setPendingFamily] = useState<Family | null>(null)
|
||||||
|
|
||||||
// 在线状态需每次进入页面都新鲜:useDidShow 覆盖
|
// 在线状态需每次进入页面都新鲜:useDidShow 覆盖
|
||||||
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景
|
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景
|
||||||
const loadDevices = useCallback(async () => {
|
const loadDevices = useCallback(async () => {
|
||||||
|
|
@ -37,8 +44,20 @@ export default function DeviceList() {
|
||||||
}
|
}
|
||||||
}, [dispatch, scene?.id])
|
}, [dispatch, scene?.id])
|
||||||
|
|
||||||
|
const loadFamilies = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const list = await getFamilies()
|
||||||
|
setFamilies(list)
|
||||||
|
// 若当前未选中,默认选第一条
|
||||||
|
setCurrentFamily((prev) => prev ?? list[0] ?? null)
|
||||||
|
} catch {
|
||||||
|
// 静默:家庭圈加载失败不阻断设备列表
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
loadDevices()
|
loadDevices()
|
||||||
|
loadFamilies()
|
||||||
})
|
})
|
||||||
|
|
||||||
const goDetail = (device: Device) => {
|
const goDetail = (device: Device) => {
|
||||||
|
|
@ -46,19 +65,59 @@ export default function DeviceList() {
|
||||||
navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${device.id}&name=${name}&model=${device.model}&online=${device.online ? 1 : 0}`)
|
navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${device.id}&name=${name}&model=${device.model}&online=${device.online ? 1 : 0}`)
|
||||||
}
|
}
|
||||||
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
||||||
const goSceneSelect = () => navigateTo(ROUTES.SCENE_SELECT)
|
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
||||||
|
|
||||||
const iconOf = (model: DeviceModel) => DEVICE_ICON_MAP[model] ?? FALLBACK_ICON
|
const iconOf = (model: DeviceModel) => DEVICE_ICON_MAP[model] ?? FALLBACK_ICON
|
||||||
|
|
||||||
|
const openFamilySheet = () => {
|
||||||
|
setPendingFamily(currentFamily)
|
||||||
|
setFamilySheetOpen(true)
|
||||||
|
}
|
||||||
|
const closeFamilySheet = () => {
|
||||||
|
setFamilySheetOpen(false)
|
||||||
|
setPendingFamily(null)
|
||||||
|
}
|
||||||
|
const confirmFamilySheet = () => {
|
||||||
|
if (pendingFamily) setCurrentFamily(pendingFamily)
|
||||||
|
closeFamilySheet()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={styles.page}>
|
<View className={styles.page}>
|
||||||
<Navbar title="智能设备" autoBack={false} variant="hero" />
|
<Navbar title="智能设备" autoBack={false} variant="hero" />
|
||||||
|
|
||||||
<View className={styles.siteRow} onClick={goSceneSelect}>
|
{/* 设备应用家庭信息卡 */}
|
||||||
<Text className={styles.siteText}>{scene?.name || '请选择场所'}</Text>
|
<View className={styles.familyCard}>
|
||||||
<Text className={styles.siteArrow}>›</Text>
|
<View className={styles.familyRow}>
|
||||||
|
<Text className={styles.familyLabel}>设备应用家庭</Text>
|
||||||
|
<Text className={styles.familyValue}>{currentFamily?.name ?? scene?.name ?? '未选择'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.familyRow}>
|
||||||
|
<Text className={styles.familyLabel}>关爱对象</Text>
|
||||||
|
<Text className={styles.familyValue}>{currentFamily?.caredLabel ?? '--'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.familyRow}>
|
||||||
|
<Text className={styles.familyLabel}>关爱地址</Text>
|
||||||
|
<Text className={styles.familyValue}>{currentFamily?.address ?? '--'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.switchBtn} hoverClass={styles.switchBtnPressed} onClick={openFamilySheet}>
|
||||||
|
<Text className={styles.switchBtnText}>切换家庭圈</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 房间管理 + 添加设备 两张并排 */}
|
||||||
|
<View className={styles.funcRow}>
|
||||||
|
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goRoom}>
|
||||||
|
<Text className={styles.funcIcon}>⌂</Text>
|
||||||
|
<Text className={styles.funcText}>房间管理</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goAdd}>
|
||||||
|
<Text className={styles.funcIcon}>+</Text>
|
||||||
|
<Text className={styles.funcText}>添加设备</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 设备网格 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className={styles.stateWrap}><Loading /></View>
|
<View className={styles.stateWrap}><Loading /></View>
|
||||||
) : loadError ? (
|
) : loadError ? (
|
||||||
|
|
@ -87,6 +146,38 @@ export default function DeviceList() {
|
||||||
<View className={styles.fab} onClick={goAdd}>
|
<View className={styles.fab} onClick={goAdd}>
|
||||||
<Text className={styles.fabIcon}>+</Text>
|
<Text className={styles.fabIcon}>+</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 切换家庭圈弹层 */}
|
||||||
|
<BottomSheet visible={familySheetOpen} title="切换家庭圈" onClose={closeFamilySheet}>
|
||||||
|
<View className={styles.sheetList}>
|
||||||
|
{families.map((family) => {
|
||||||
|
const active = pendingFamily?.id === family.id
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={family.id}
|
||||||
|
className={`${styles.sheetItem} ${active ? styles.sheetItemActive : ''}`}
|
||||||
|
onClick={() => setPendingFamily(family)}
|
||||||
|
>
|
||||||
|
<View className={styles.sheetHead}>
|
||||||
|
<Text className={styles.sheetName}>{family.name}</Text>
|
||||||
|
{family.role && (
|
||||||
|
<View className={styles.sheetRole}>
|
||||||
|
<Text className={styles.sheetRoleText}>{family.role}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className={styles.sheetMeta}>
|
||||||
|
<Text className={styles.sheetMetaItem}>关爱对象 {family.caredLabel}</Text>
|
||||||
|
<Text className={styles.sheetMetaItem}>设备数量 {family.deviceCount}台</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
<View className={styles.sheetConfirm} hoverClass={styles.sheetConfirmPressed} onClick={confirmFamilySheet}>
|
||||||
|
<Text className={styles.sheetConfirmText}>确定</Text>
|
||||||
|
</View>
|
||||||
|
</BottomSheet>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue