diff --git a/src/pages/device/add.module.scss b/src/pages/device/add.module.scss index 6ebd9cc..cf85641 100644 --- a/src/pages/device/add.module.scss +++ b/src/pages/device/add.module.scss @@ -1,168 +1,110 @@ .page { min-height: 100vh; - background: #e9eaed; + background: $color-bg; + padding-bottom: 40px; } -.search-wrap { - height: 500px; +.section { + margin: 24px; + padding: 40px 32px; background: $color-bg-white; - display: flex; - justify-content: center; - align-items: center; + border-radius: $radius-md; } -.search-btn { - width: 260px; - 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; +.sectionTitle { + font-size: 36px; font-weight: 700; + color: $color-text; + margin-bottom: 16px; + display: block; } -.list-body { - background: #e9eaed; - padding-bottom: 30px; +.sectionDesc { + font-size: 26px; + color: $color-text-secondary; + line-height: 1.5; + margin-bottom: 32px; + display: block; } -.empty-row { - padding: 36px; - text-align: center; - color: #8c9097; - font-size: 30px; +.input { + width: 100%; + height: 96px; + padding: 0 32px; + background: $color-bg-gray; + border-radius: $radius-md; + font-size: 28px; + color: $color-text; + box-sizing: border-box; + margin-bottom: 32px; } -.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 { +.submitBtn { + width: 100%; + height: 96px; + background: $color-primary-grad; + border-radius: $radius-pill; display: flex; align-items: center; - gap: 12px; + justify-content: center; } -.device-name { - flex: 1; - min-width: 0; - font-size: 38px; - color: #30343a; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.submitBtnPressed { + opacity: 0.85; } -.device-tag { - font-size: 22px; - color: $color-primary-deep; - border: 1px solid $color-primary-deep; - border-radius: 20px; - padding: 2px 10px; +.submitBtnText { + font-size: 32px; + font-weight: 600; + color: #ffffff; } -.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 { +.scanArea { + padding: 60px 0; 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; } -.action-btn { - padding: 8px 16px; - border: 2px solid $color-primary; - color: $color-primary-deep; - font-size: 30px; - border-radius: 6px; - background: #f8fffc; +.deviceBox { + width: 48%; + display: flex; + flex-direction: column; + align-items: center; } -.action-btn.disabled { - border-color: $color-primary-light-border; - color: #7db89e; - background: #f5faf7; +.deviceIcon1, +.deviceIcon2 { + width: 100%; + height: 200px; + background: $color-bg-gray; + border-radius: $radius-md; + margin-bottom: 16px; +} + +.deviceLabel { + font-size: 26px; + color: $color-text; } diff --git a/src/pages/device/add.tsx b/src/pages/device/add.tsx index 64c1362..aecba67 100644 --- a/src/pages/device/add.tsx +++ b/src/pages/device/add.tsx @@ -1,143 +1,79 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import Taro from '@tarojs/taro' -import { View, Text } from '@tarojs/components' +import { View, Text, Input } from '@tarojs/components' import { Navbar } from '@/components/navbar' -import { useBle } from '@/ble/hooks/useBle' -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 { showToast } from '@/utils/toast' import styles from './add.module.scss' export default function DeviceAdd() { - const { state, discovery } = useBle() - const [preparingId, setPreparingId] = useState('') + const [deviceId, setDeviceId] = useState('') - // 页面隐藏/卸载时清理搜索(对齐旧版 onHide/onUnload),连接保持供配网页使用 - useEffect(() => { - return () => { - if (discovery.getState().isSearching) { - discovery.stopSearch(false) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const startScan = async () => { - if (state.searching) { - await discovery.stopSearch() + const submitManual = () => { + if (!deviceId.trim()) { + showToast('请输入设备编号') return } - // startSearch 内部自动完成:Android 定位权限检查 + 适配器初始化 + - // 服务 UUID 过滤扫描(00F4/F400/FFF4)+ 4s 无结果兜底全扫 + 30s 自动停止 + showToast('设备添加功能即将接入') + } + + const scanQRCode = async () => { 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 { - // 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 ( - - - - - {String(device.showName ?? device.deviceId)} - {device.hasTargetService ? ( - 00F4 - ) : device.matchedByName || device.matchedByPayload ? ( - 名称匹配 - ) : null} - - {String(device.deviceId)} - - {actions.map((action) => { - const busy = isActionBusy(action.key, device.deviceId) - return ( - !busy && invokeAction(action.key, device.deviceId)} - > - {busy ? action.busyLabel : action.label} - - ) - })} - - - - ) - } - return ( - + - - - {state.searching ? '搜索中' : '搜索'} + + 手动输入 + + 将智能设备添加至当前家庭圈,可实时查看设备状态和告警信息。 + + setDeviceId(e.detail.value)} + /> + + 提交 - - *手机需开启蓝牙功能,Android手机还需开启定位功能 + + 扫码添加 + + ⊡ + 点击扫码 + - - 搜索到的设备列表 - - - - {state.deviceList.length === 0 ? ( - 暂无搜索结果 - ) : ( - state.deviceList.map(renderDevice) - )} + + 设备二维码/编号位置 + + + + 包装盒侧边 + + + + 设备侧面或背面 + + ) diff --git a/src/pages/device/index.module.scss b/src/pages/device/index.module.scss index 39d98d0..ce3a73d 100644 --- a/src/pages/device/index.module.scss +++ b/src/pages/device/index.module.scss @@ -5,26 +5,6 @@ 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 { margin-top: 80px; display: flex; @@ -32,11 +12,102 @@ 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 { display: flex; flex-wrap: wrap; justify-content: space-between; - padding: 24px; + padding: 0 24px; } .card { @@ -116,3 +187,86 @@ } .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; +} diff --git a/src/pages/device/index.tsx b/src/pages/device/index.tsx index 30eb830..e0c007a 100644 --- a/src/pages/device/index.tsx +++ b/src/pages/device/index.tsx @@ -6,10 +6,12 @@ import { setDeviceList, type Device } from '@/store/deviceSlice' import { Navbar } from '@/components/navbar' import { Loading } from '@/components/loading' import { StateView } from '@/components/state-view' +import { BottomSheet } from '@/components/bottom-sheet' import { getDeviceList } from '@/api/device' import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums' import { ROUTES } from '@/constants/routes' import { navigateTo } from '@/utils/navigation' +import { getFamilies, type Family } from '@/api/family' import styles from './index.module.scss' const FALLBACK_ICON = '⌂' @@ -22,6 +24,11 @@ export default function DeviceList() { const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) + const [families, setFamilies] = useState([]) + const [currentFamily, setCurrentFamily] = useState(null) + const [familySheetOpen, setFamilySheetOpen] = useState(false) + const [pendingFamily, setPendingFamily] = useState(null) + // 在线状态需每次进入页面都新鲜:useDidShow 覆盖 // 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景 const loadDevices = useCallback(async () => { @@ -37,8 +44,20 @@ export default function DeviceList() { } }, [dispatch, scene?.id]) + const loadFamilies = useCallback(async () => { + try { + const list = await getFamilies() + setFamilies(list) + // 若当前未选中,默认选第一条 + setCurrentFamily((prev) => prev ?? list[0] ?? null) + } catch { + // 静默:家庭圈加载失败不阻断设备列表 + } + }, []) + useDidShow(() => { loadDevices() + loadFamilies() }) 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}`) } 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 openFamilySheet = () => { + setPendingFamily(currentFamily) + setFamilySheetOpen(true) + } + const closeFamilySheet = () => { + setFamilySheetOpen(false) + setPendingFamily(null) + } + const confirmFamilySheet = () => { + if (pendingFamily) setCurrentFamily(pendingFamily) + closeFamilySheet() + } + return ( - - {scene?.name || '请选择场所'} - › + {/* 设备应用家庭信息卡 */} + + + 设备应用家庭 + {currentFamily?.name ?? scene?.name ?? '未选择'} + + + 关爱对象 + {currentFamily?.caredLabel ?? '--'} + + + 关爱地址 + {currentFamily?.address ?? '--'} + + + 切换家庭圈 + + {/* 房间管理 + 添加设备 两张并排 */} + + + ⌂ + 房间管理 + + + + + 添加设备 + + + + {/* 设备网格 */} {loading ? ( ) : loadError ? ( @@ -87,6 +146,38 @@ export default function DeviceList() { + + + {/* 切换家庭圈弹层 */} + + + {families.map((family) => { + const active = pendingFamily?.id === family.id + return ( + setPendingFamily(family)} + > + + {family.name} + {family.role && ( + + {family.role} + + )} + + + 关爱对象 {family.caredLabel} + 设备数量 {family.deviceCount}台 + + + ) + })} + + + 确定 + + ) }