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}台
+
+
+ )
+ })}
+
+
+ 确定
+
+
)
}