diff --git a/src/api/device.ts b/src/api/device.ts deleted file mode 100644 index 2e83d36..0000000 --- a/src/api/device.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { request } from '@/http' -import type { Device } from '@/store/deviceSlice' - -const USE_MOCK = true - -const MOCK_DEVICES: Device[] = [ - { id: 1, name: '客厅体征监测仪', model: 'ED713', online: true }, - { id: 2, name: '卧室体征监测仪', model: 'ED713', online: false }, - { id: 3, name: '书房跌倒检测雷达', model: 'ED719', online: true }, - { id: 4, name: '走廊跌倒检测雷达', model: 'ED719', online: false } -] - -export function getDeviceList(sceneId: number | null): Promise { - if (USE_MOCK) { - return new Promise((resolve) => setTimeout(() => resolve(MOCK_DEVICES), 500)) - } - return request.get('/device/list', { sceneId }) -} - -// getVitalSigns / getHeartRateHistory 在 P3 Task 3.2 加入本文件 - -const MOCK_VITALS = { - heartRate: 72, - respiratoryRate: 16, - bodyMovement: 3, - moveState: 3, - onbedStatus: 1, - bodyStatus: 2, - bodyPosition: 8 -} - -const heartState: { series: number[]; value: number } = { series: [], value: 72 } - -function driftHeartRate(): number { - heartState.value += Math.round((Math.random() - 0.5) * 4) - heartState.value = Math.min(90, Math.max(60, heartState.value)) - return heartState.value -} - -for (let i = 0; i < 50; i++) heartState.series.push(driftHeartRate()) - -function pushHeartReadings() { - const n = 2 + Math.round(Math.random()) - for (let i = 0; i < n; i++) heartState.series.push(driftHeartRate()) - if (heartState.series.length > 50) heartState.series = heartState.series.slice(-50) -} - -export interface VitalSigns { - heartRate: number - respiratoryRate: number - bodyMovement: number - moveState: number - onbedStatus: number - bodyStatus: number - bodyPosition: number - deviceId: string -} - -export function getVitalSigns(deviceId: string): Promise { - if (USE_MOCK) { - const heartRate = driftHeartRate() - return new Promise((resolve) => - setTimeout(() => resolve({ ...MOCK_VITALS, heartRate, deviceId }), 300) - ) - } - return request.get('/device/vitals', { deviceId }) -} - -export function getHeartRateHistory(deviceId: string): Promise { - if (USE_MOCK) { - pushHeartReadings() - return new Promise((resolve) => setTimeout(() => resolve([...heartState.series]), 300)) - } - return request.get('/device/heart-history', { deviceId }) -} diff --git a/src/api/homeDevice.ts b/src/api/homeDevice.ts index b0cd40f..c0ecfc3 100644 --- a/src/api/homeDevice.ts +++ b/src/api/homeDevice.ts @@ -1,24 +1,43 @@ import { request } from '@/http' +// 入参对齐 apifox YlHomeDeviceListReq:homeId 与 roomId 至少填一个 +export interface HomeDeviceListReq { + homeId?: string + roomId?: string // 传入则只查该房间设备 +} + +// 入参对齐 apifox YlHomeDeviceAddReq +export interface HomeDeviceAddReq { + deviceName: string // 设备序列号 + homeId: string + roomId: string +} + +// 入参对齐 apifox YlHomeDeviceUnbindReq +export interface HomeDeviceUnbindReq { + installId: string // 设备安装 ID +} + // 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7) -// 注意与 api/device.ts(mock 设备列表)并存,切换时由页面改引用并删除旧模块 export interface HomeDevice { + installId: string // 设备安装 ID(unbind 入参,按文档推断) deviceId: string homeId: string roomId: string + roomName: string deviceName: string deviceType: string online: boolean } -export function addHomeDevice(params: Record): Promise { - return request.post('/yanglao/app/ylHomeDevice/add', params) +export function getHomeDeviceList(params: HomeDeviceListReq): Promise { + return request.post('/yanglao/app/ylHomeDevice/getList', params as unknown as Record) } -export function unbindHomeDevice(params: Record): Promise { - return request.post('/yanglao/app/ylHomeDevice/unbind', params) +export function addHomeDevice(params: HomeDeviceAddReq): Promise { + return request.post('/yanglao/app/ylHomeDevice/add', params as unknown as Record) } -export function getHomeDeviceList(homeId: string): Promise { - return request.get('/yanglao/app/ylHomeDevice/getList', { homeId }) +export function unbindHomeDevice(params: HomeDeviceUnbindReq): Promise { + return request.post('/yanglao/app/ylHomeDevice/unbind', params as unknown as Record) } diff --git a/src/api/scene.ts b/src/api/scene.ts deleted file mode 100644 index 786d37f..0000000 --- a/src/api/scene.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { request } from '@/http' -import { storage } from '@/utils/storage' -import type { Scene } from '@/store/sceneSlice' - -const USE_MOCK = true - -const SCENE_STORAGE_KEY = 'sceneData' -const MOCK_ID_SEED = 9000 - -const SEED_SCENES: Scene[] = [ - { id: 1, name: '客厅' }, - { id: 2, name: '卧室' }, - { id: 3, name: '书房' }, - { id: 4, name: '走廊' } -] - -// 读全量列表;storage 为空时写入种子(首次启动初始化) -function loadScenes(): Scene[] { - const saved = storage.get(SCENE_STORAGE_KEY) - if (Array.isArray(saved) && saved.length > 0) return saved - storage.set(SCENE_STORAGE_KEY, SEED_SCENES) - return SEED_SCENES -} - -function saveScenes(scenes: Scene[]): void { - storage.set(SCENE_STORAGE_KEY, scenes) -} - -// mock 异步语义:与真实 API 调用形式一致 -const delay = (value: T, ms = 200): Promise => - new Promise((resolve) => setTimeout(() => resolve(value), ms)) - -export function getSceneList(): Promise { - if (USE_MOCK) { - return delay(loadScenes()) - } - return request.get('/scene/list') -} - -export function addScene(name: string): Promise { - const trimmed = name.trim() - if (!trimmed) return Promise.reject(new Error('请输入房间名称')) - if (USE_MOCK) { - const scenes = loadScenes() - if (scenes.some((s) => s.name === trimmed)) { - return Promise.reject(new Error('该房间已存在')) - } - const scene: Scene = { id: MOCK_ID_SEED + scenes.length, name: trimmed } - saveScenes([...scenes, scene]) - return delay(scene) - } - return request.post('/scene/add', { name: trimmed } as unknown as Record) -} diff --git a/src/pages/device/add.module.scss b/src/pages/device/add.module.scss index 99262bb..fdbd83b 100644 --- a/src/pages/device/add.module.scss +++ b/src/pages/device/add.module.scss @@ -39,6 +39,39 @@ margin-bottom: 32px; } +// 安装房间选择:label + 横向 chip 组 +.roomPicker { + margin-bottom: 32px; +} + +.roomLabel { + display: block; + font-size: 28px; + color: $color-text-secondary; + margin-bottom: 16px; +} + +.roomOptions { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.roomChip { + padding: 12px 32px; + background: $color-bg-gray; + border: 1px solid transparent; + border-radius: $radius-pill; + font-size: 26px; + color: $color-text-secondary; +} + +.roomChipActive { + background: $color-primary-light-bg; + border-color: $color-primary; + color: $color-primary-deep; +} + .submitBtn { width: 100%; height: 96px; @@ -49,6 +82,11 @@ justify-content: center; } +// 提交中防重复点击的视觉降级 +.submitBtnDisabled { + opacity: 0.6; +} + .submitBtnPressed { opacity: 0.85; } diff --git a/src/pages/device/add.tsx b/src/pages/device/add.tsx index b6e122e..7ed5ae1 100644 --- a/src/pages/device/add.tsx +++ b/src/pages/device/add.tsx @@ -1,28 +1,90 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import Taro from '@tarojs/taro' +import { useDidShow } from '@tarojs/taro' import { View, Text, Input } from '@tarojs/components' import { Navbar } from '@/components/navbar' +import { Loading } from '@/components/loading' +import { StateView } from '@/components/state-view' +import { addHomeDevice } from '@/api/homeDevice' +import { getHomeList, type HomeInfo } from '@/api/home' +import { getRoomList, type HomeRoom } from '@/api/homeRoom' import { showToast } from '@/utils/toast' +import { navigateBack } from '@/utils/navigation' import styles from './add.module.scss' export default function DeviceAdd() { const [deviceId, setDeviceId] = useState('') + const [submitting, setSubmitting] = useState(false) - const submitManual = () => { - if (!deviceId.trim()) { + const [loading, setLoading] = useState(true) + const [loadError, setLoadError] = useState(false) + const [home, setHome] = useState(null) + const [rooms, setRooms] = useState([]) + const [roomId, setRoomId] = useState('') + + // 数据加载体:useDidShow 首次与错误重试共用(state 变化触发 useDidShow 不可行,用计数器驱动 effect) + const [reload, setReload] = useState(0) + const [loaded, setLoaded] = useState(false) + + // 默认家庭 + 房间列表:addHomeDevice 需 homeId + roomId + useEffect(() => { + let cancelled = false + setLoading(true) + setLoadError(false) + getHomeList() + .then(async (list) => { + const first = list[0] ?? null + if (cancelled) return + setHome(first) + if (!first) return + const roomList = await getRoomList(first.homeId) + if (cancelled) return + setRooms(roomList ?? []) + setRoomId((prev) => prev || roomList[0]?.roomId || '') + }) + .catch(() => !cancelled && setLoadError(true)) + .finally(() => { + if (cancelled) return + setLoading(false) + setLoaded(true) + }) + return () => { + cancelled = true + } + }, [reload]) + + // 从房间管理等页面返回时刷新(房间可能已变更) + useDidShow(() => { + if (loaded) setReload((n) => n + 1) + }) + + const submitManual = async () => { + if (submitting) return + const code = deviceId.trim() + if (!code) { showToast('请输入设备编号') return } - showToast('设备添加功能即将接入') + if (!home || !roomId) { + showToast('请选择安装房间') + return + } + setSubmitting(true) + try { + await addHomeDevice({ deviceName: code, homeId: home.homeId, roomId }) + showToast('添加成功') + navigateBack() + } catch (e) { + showToast(e instanceof Error ? e.message : '添加失败') + } finally { + setSubmitting(false) + } } // 点击扫码区拉起相机扫码,识别结果回填到输入框,确认后提交 const scanQRCode = async () => { - // 测试日志:完整输出扫码回调,用于真机验证扫描功能(上线前移除) - console.log('[SCAN] scanCode invoked') try { const res = await Taro.scanCode({ scanType: ['qrCode', 'barCode'] }) - console.log('[SCAN] success:', JSON.stringify(res)) const code = String(res.result ?? '').trim() if (!code) { showToast('未识别到有效编号') @@ -30,12 +92,35 @@ export default function DeviceAdd() { } setDeviceId(code) showToast('已填入扫码结果') - } catch (error) { - console.log('[SCAN] fail:', JSON.stringify(error)) + } catch { showToast('扫码取消') } } + if (loading) { + return ( + + + + + ) + } + + if (loadError || !home) { + return ( + + + (loadError ? setReload((n) => n + 1) : navigateBack())} + /> + + ) + } + return ( @@ -43,7 +128,7 @@ export default function DeviceAdd() { 手动输入 - 将智能设备添加至当前家庭圈,可实时查看设备状态和告警信息。 + 将智能设备添加至「{home.name}」,可实时查看设备状态和告警信息。 setDeviceId(e.detail.value)} /> - - 提交 + {rooms.length > 0 && ( + + 安装房间 + + {rooms.map((room) => ( + setRoomId(room.roomId)} + > + {room.roomName} + + ))} + + + )} + + {submitting ? '提交中...' : '提交'} diff --git a/src/pages/device/detail.tsx b/src/pages/device/detail.tsx index aa10b03..204075e 100644 --- a/src/pages/device/detail.tsx +++ b/src/pages/device/detail.tsx @@ -1,26 +1,14 @@ -import { useState, useEffect, useRef, useMemo } from 'react' -import Taro, { useRouter, useDidShow, useDidHide } from '@tarojs/taro' +import { useRouter } from '@tarojs/taro' import { View, Text } from '@tarojs/components' import { Navbar } from '@/components/navbar' import { ListRow, ListRowGroup } from '@/components/list-row' -import { Loading } from '@/components/loading' -import { StateView } from '@/components/state-view' -import { VitalWave } from '@/components/vital-wave' -import { getVitalSigns, getHeartRateHistory, type VitalSigns } from '@/api/device' -import { MOVE_STATE_MAP, BODY_STATUS_MAP, ONBED_STATUS_MAP, type DeviceModel } from '@/constants/enums' +import { ROUTES } from '@/constants/routes' +import { navigateTo } from '@/utils/navigation' +import type { DeviceModel } from '@/constants/enums' import styles from './detail.module.scss' -const REFRESH_INTERVAL = 3000 - -const METRIC_COLORS = { - breath: '#1677ff', - motion: '#fa8c16', - moveState: '#52c41a', - bedState: '#f5222d', - bodyState: '#1677ff', - bodyPosition: '#52c41a' -} - +// 体征数据后端暂无对应接口:详情页先保留设备信息静态展示, +// 心率/呼吸等实时体征待接口定型后接入 export default function Detail() { const router = useRouter() const params = router.params as { id?: string; name?: string; model?: DeviceModel; online?: string } @@ -30,124 +18,36 @@ export default function Detail() { const deviceModel = params.model ?? '' const online = params.online === '1' - const [loading, setLoading] = useState(true) - const [loadError, setLoadError] = useState(false) - const [vitals, setVitals] = useState({} as VitalSigns) - const [wavePoints, setWavePoints] = useState([]) - - const timerRef = useRef | null>(null) - - const valueOf = (v: unknown): string | number => - v === undefined || v === null || v === '' ? '-' : (v as string | number) - - const metrics = useMemo(() => { - const v = vitals as unknown as Record - return [ - { label: '呼吸率', value: valueOf(v.respiratoryRate), unit: '/min', color: METRIC_COLORS.breath }, - { label: '体动', value: valueOf(v.bodyMovement), unit: '', color: METRIC_COLORS.motion }, - { label: '运动状态', value: MOVE_STATE_MAP[v.moveState as number] ?? valueOf(v.moveState), unit: '', color: METRIC_COLORS.moveState }, - { label: '在床状态', value: ONBED_STATUS_MAP[v.onbedStatus as number] ?? valueOf(v.onbedStatus), unit: '', color: METRIC_COLORS.bedState }, - { label: '人体状态', value: BODY_STATUS_MAP[v.bodyStatus as number] ?? valueOf(v.bodyStatus), unit: '', color: METRIC_COLORS.bodyState }, - { label: '人体位置', value: valueOf(v.bodyPosition), unit: '分米', color: METRIC_COLORS.bodyPosition } - ] - }, [vitals]) - - const fetchVitals = async () => { - const [v, history] = await Promise.all([ - getVitalSigns(deviceId), - getHeartRateHistory(deviceId) - ]) - setVitals(v) - setWavePoints(Array.isArray(history) ? history : []) - } - - const loadVitals = async () => { - setLoading(true) - setLoadError(false) - try { - await fetchVitals() - } catch { - setLoadError(true) - } finally { - setLoading(false) - } - } - - // 轮询生命周期与 uni-app 版对齐(I1 修复): - // 首次挂载拉数据;useDidShow 启动轮询 / useDidHide 停止轮询—— - // 页面被压栈后台(navigateTo 其它页)时轮询不空跑,回到前台恢复 - useEffect(() => { - loadVitals() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [deviceId]) - - useDidShow(() => { - if (timerRef.current) clearInterval(timerRef.current) - timerRef.current = setInterval(() => { - fetchVitals().catch(() => {}) - }, REFRESH_INTERVAL) - }) - - useDidHide(() => { - if (timerRef.current) { - clearInterval(timerRef.current) - timerRef.current = null - } - }) - - // 组件卸载兜底清理 - useEffect(() => { - return () => { - if (timerRef.current) clearInterval(timerRef.current) - } - }, []) + const goBleConfig = () => navigateTo(ROUTES.DEVICE_BLE_CONFIG) + const goWifiConfig = () => navigateTo(ROUTES.DEVICE_BLE_CONFIG) return ( - {loading ? ( - - ) : loadError ? ( - - ) : ( - <> - - - ❤ - {valueOf(vitals.heartRate)} - BPM - - - {online ? '正常' : '离线'} - - - - 心率健康 - - + + + ❤ + -- + BPM + + + {online ? '在线' : '离线'} + + + 体征数据接入中 + + - - {metrics.map((m) => ( - - {m.label} - - {m.value} - {!!m.unit && {m.unit}} - - - ))} - - - - - - - - - - )} + + + + + + + + ) } diff --git a/src/pages/device/index.module.scss b/src/pages/device/index.module.scss index 3929aee..5c4286f 100644 --- a/src/pages/device/index.module.scss +++ b/src/pages/device/index.module.scss @@ -119,7 +119,7 @@ .card { width: 48.5%; margin-bottom: 24px; - padding: 36px 20px 28px; + padding: 36px 20px 24px; box-sizing: border-box; background: $color-bg-white; border-radius: $radius-lg; @@ -129,6 +129,14 @@ align-items: center; } +// 上半区(图标/名称/状态)点击进详情,下方解绑按钮独立命中 +.cardTop { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + .icon { width: 108px; height: 108px; @@ -178,6 +186,27 @@ .statusText { font-size: 24px; color: $color-primary-deep; } .statusTextOff { color: $color-text-secondary; } +// 解绑按钮:卡片底部通栏浅红描边,与详情点击区分 +.unbindBtn { + width: 100%; + margin-top: 20px; + padding: 12px 0; + border: 1px solid $color-danger; + border-radius: $radius-pill; + display: flex; + align-items: center; + justify-content: center; +} + +.unbindBtnPressed { + background: $color-danger-light-bg; +} + +.unbindBtnText { + font-size: 24px; + color: $color-danger; +} + // 切换家庭圈弹层内的列表项 .sheetList { padding-bottom: 16px; diff --git a/src/pages/device/index.tsx b/src/pages/device/index.tsx index 74dd6d7..2f9c833 100644 --- a/src/pages/device/index.tsx +++ b/src/pages/device/index.tsx @@ -1,73 +1,111 @@ import { useState, useCallback } from 'react' import { useDidShow } from '@tarojs/taro' +import Taro from '@tarojs/taro' import { View, Text } from '@tarojs/components' -import { useAppSelector, useAppDispatch } from '@/store/hooks' -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 { getHomeDeviceList, unbindHomeDevice, type HomeDevice } from '@/api/homeDevice' +import { getHomeList, type HomeInfo } from '@/api/home' import { ROUTES } from '@/constants/routes' import { navigateTo } from '@/utils/navigation' -import { getHomeList, type HomeInfo } from '@/api/home' +import { showToast } from '@/utils/toast' import styles from './index.module.scss' const FALLBACK_ICON = '⌂' export default function DeviceList() { - const dispatch = useAppDispatch() - const scene = useAppSelector((s) => s.scene.current) - const deviceList = useAppSelector((s) => s.device.list) - const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) + const [deviceList, setDeviceList] = useState([]) + const [unbinding, setUnbinding] = 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 () => { + // 设备列表随家庭联动:useDidShow 覆盖首次进入/返回/tab 切回三种场景 + const loadDevices = useCallback(async (homeId?: string) => { + const targetHomeId = homeId ?? currentFamily?.homeId + if (!targetHomeId) { + setDeviceList([]) + setLoading(false) + return + } setLoading(true) setLoadError(false) try { - const data = await getDeviceList(scene?.id ?? null) - dispatch(setDeviceList(data)) + const data = await getHomeDeviceList({ homeId: targetHomeId }) + setDeviceList(data ?? []) } catch { setLoadError(true) } finally { setLoading(false) } - }, [dispatch, scene?.id]) + }, [currentFamily?.homeId]) const loadFamilies = useCallback(async () => { try { const list = await getHomeList() setFamilies(list) // 若当前未选中,默认选第一条 - setCurrentFamily((prev) => prev ?? list[0] ?? null) + setCurrentFamily((prev) => { + const next = prev ?? list[0] ?? null + if (next && next.homeId !== prev?.homeId) loadDevices(next.homeId) + return next + }) } catch { // 静默:家庭圈加载失败不阻断设备列表 } - }, []) + }, [loadDevices]) useDidShow(() => { loadDevices() loadFamilies() }) - const goDetail = (device: Device) => { - const name = encodeURIComponent(device.name) - navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${device.id}&name=${name}&model=${device.model}&online=${device.online ? 1 : 0}`) + const goDetail = (device: HomeDevice) => { + const name = encodeURIComponent(device.deviceName) + navigateTo( + `${ROUTES.DEVICE_DETAIL}?id=${device.deviceId}&name=${name}&model=${device.deviceType}&online=${device.online ? 1 : 0}` + ) } const goAdd = () => navigateTo(ROUTES.DEVICE_ADD) const goRoom = () => navigateTo(ROUTES.SCENE_SELECT) - const iconOf = (model: DeviceModel) => DEVICE_ICON_MAP[model] ?? FALLBACK_ICON + // 解绑确认:防误触,成功后原地刷新列表 + const confirmUnbind = (device: HomeDevice) => { + if (unbinding) return + Taro.showModal({ + title: '解绑设备', + content: `确定解绑「${device.deviceName}」吗?解绑后需重新添加。`, + confirmText: '解绑', + confirmColor: '#EE4B4B', + success: (res) => { + if (!res.confirm) return + doUnbind(device) + } + }) + } + + const doUnbind = async (device: HomeDevice) => { + if (!device.installId) { + showToast('缺少设备安装 ID,无法解绑') + return + } + setUnbinding(true) + try { + await unbindHomeDevice({ installId: device.installId }) + showToast('已解绑') + await loadDevices() + } catch (e) { + showToast(e instanceof Error ? e.message : '解绑失败') + } finally { + setUnbinding(false) + } + } const openFamilySheet = () => { setPendingFamily(currentFamily) @@ -78,7 +116,10 @@ export default function DeviceList() { setPendingFamily(null) } const confirmFamilySheet = () => { - if (pendingFamily) setCurrentFamily(pendingFamily) + if (pendingFamily && pendingFamily.homeId !== currentFamily?.homeId) { + setCurrentFamily(pendingFamily) + loadDevices(pendingFamily.homeId) + } closeFamilySheet() } @@ -93,7 +134,7 @@ export default function DeviceList() { 设备应用家庭 - {currentFamily?.name ?? scene?.name ?? '未选择'} + {currentFamily?.name ?? '未选择'} 关爱对象 @@ -126,22 +167,27 @@ export default function DeviceList() { {loading ? ( ) : loadError ? ( - + loadDevices()} /> ) : deviceList.length === 0 ? ( ) : ( {deviceList.map((device) => ( - goDetail(device)}> - - {iconOf(device.model)} + + goDetail(device)}> + + {FALLBACK_ICON} + + {device.deviceName} + + + + {device.online ? '在线' : '离线'} + + - {device.name} - - - - {device.online ? '在线' : '离线'} - + confirmUnbind(device)}> + 解绑 ))} diff --git a/src/pages/scene/select.tsx b/src/pages/scene/select.tsx index 38afc42..90b52d5 100644 --- a/src/pages/scene/select.tsx +++ b/src/pages/scene/select.tsx @@ -1,15 +1,13 @@ import { useState, useEffect } from 'react' import Taro from '@tarojs/taro' import { View, Text } from '@tarojs/components' -import { useAppDispatch, useAppSelector } from '@/store/hooks' -import { setScene, type Scene } from '@/store/sceneSlice' import { Navbar } from '@/components/navbar' import { Loading } from '@/components/loading' import { Empty } from '@/components/empty' import { StateView } from '@/components/state-view' -import { getSceneList, addScene } from '@/api/scene' +import { getHomeList, type HomeInfo } from '@/api/home' +import { getRoomList, addRoom, updateRoom, deleteRoom, type HomeRoom } from '@/api/homeRoom' import { showToast } from '@/utils/toast' -import { navigateBack } from '@/utils/navigation' import styles from './select.module.scss' // Taro 类型未跟进 wx.showModal 的 editable 扩展(微信 2.17.1+),运行时真实存在 @@ -25,20 +23,29 @@ interface EditableModalResult extends Taro.showModal.SuccessCallbackResult { const showEditableModal = (option: EditableModalOption) => Taro.showModal(option as Taro.showModal.Option) as Promise -export default function SceneSelect() { - const dispatch = useAppDispatch() - const current = useAppSelector((s) => s.scene.current) +// 房间操作菜单:编辑名称 / 删除 +const showActionSheet = () => + Taro.showActionSheet({ itemList: ['编辑名称', '删除房间'] }) as Promise +export default function SceneSelect() { const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) - const [list, setList] = useState([]) + const [home, setHome] = useState(null) + const [list, setList] = useState([]) const load = async () => { setLoading(true) setLoadError(false) try { - const data = await getSceneList() - setList(data ?? []) + const homes = await getHomeList() + const first = homes[0] ?? null + setHome(first) + if (!first) { + setList([]) + return + } + const rooms = await getRoomList(first.homeId) + setList(rooms ?? []) } catch { setLoadError(true) } finally { @@ -48,21 +55,16 @@ export default function SceneSelect() { useEffect(() => { load() }, []) - const selectScene = (scene: Scene) => { - dispatch(setScene(scene)) - navigateBack() - } - // 添加房间:可编辑弹窗输入名称,成功后刷新列表 - const addRoom = () => { + const addRoomByName = () => { showEditableModal({ title: '添加房间', editable: true, placeholderText: '请输入房间名称(如:阳台)' }).then(async ({ confirm, content }) => { - if (!confirm) return + if (!confirm || !home) return try { - await addScene(content ?? '') + await addRoom({ homeId: home.homeId, name: (content ?? '').trim() }) showToast('添加成功') await load() } catch (error) { @@ -71,6 +73,40 @@ export default function SceneSelect() { }) } + // 长按/操作:编辑名称或删除 + const onRoomAction = (room: HomeRoom) => { + if (!home) return + showActionSheet() + .then(async ({ tapIndex }) => { + if (tapIndex === 0) { + const { confirm, content } = await showEditableModal({ + title: '编辑房间名称', + editable: true, + content: room.roomName, + placeholderText: '请输入房间名称' + }) + if (!confirm) return + await updateRoom({ roomId: room.roomId, name: (content ?? '').trim() }) + showToast('已修改') + await load() + } else if (tapIndex === 1) { + const { confirm } = await Taro.showModal({ + title: '删除房间', + content: `确定删除「${room.roomName}」吗?`, + confirmText: '删除', + confirmColor: '#EE4B4B' + }) + if (!confirm) return + await deleteRoom({ roomId: room.roomId }) + showToast('已删除') + await load() + } + }) + .catch(() => { + // 取消操作静默 + }) + } + return ( @@ -80,21 +116,21 @@ export default function SceneSelect() { ) : ( - {list.map((scene) => ( + {list.map((room) => ( selectScene(scene)} + key={room.roomId} + className={styles.item} + onLongPress={() => onRoomAction(room)} > - {scene.name} - {scene.id === current?.id ? '✓' : '›'} + {room.roomName} + onRoomAction(room)}>编辑 ))} {/* 空列表时也保留添加入口 */} {list.length === 0 && ( )} - + + 添加房间 diff --git a/src/store/deviceSlice.ts b/src/store/deviceSlice.ts deleted file mode 100644 index 9dc6a4a..0000000 --- a/src/store/deviceSlice.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import type { DeviceModel } from '@/constants/enums' - -export interface Device { - id: number - name: string - model: DeviceModel - online: boolean -} - -interface State { - list: Device[] -} - -const slice = createSlice({ - name: 'device', - initialState: { list: [] } as State, - reducers: { - setDeviceList(state, action: PayloadAction) { - state.list = action.payload - } - } -}) - -export const { setDeviceList } = slice.actions -export default slice.reducer diff --git a/src/store/index.ts b/src/store/index.ts index fae5348..db7327c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,15 +1,9 @@ import { configureStore } from '@reduxjs/toolkit' -import sceneReducer from './sceneSlice' import authReducer from './authSlice' -import deviceReducer from './deviceSlice' -import eventReducer from './eventSlice' export const store = configureStore({ reducer: { - scene: sceneReducer, - auth: authReducer, - device: deviceReducer, - event: eventReducer + auth: authReducer } }) diff --git a/src/store/sceneSlice.ts b/src/store/sceneSlice.ts deleted file mode 100644 index 515904c..0000000 --- a/src/store/sceneSlice.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { storage } from '@/utils/storage' - -// Scene 类型唯一定义处:api/scene.ts 与页面均从此导入(避免双定义漂移) -export interface Scene { - id: number - name: string -} - -interface State { - current: Scene | null -} - -const STORAGE_KEY = 'current_scene' - -const initialState: State = { - current: storage.get(STORAGE_KEY) -} - -const slice = createSlice({ - name: 'scene', - initialState, - reducers: { - setScene(state, action: PayloadAction) { - state.current = action.payload - storage.set(STORAGE_KEY, action.payload) - }, - clearScene(state) { - state.current = null - storage.remove(STORAGE_KEY) - } - } -}) - -export const { setScene, clearScene } = slice.actions -export default slice.reducer