diff --git a/src/api/scene.ts b/src/api/scene.ts index 5b73b8e..786d37f 100644 --- a/src/api/scene.ts +++ b/src/api/scene.ts @@ -1,18 +1,53 @@ import { request } from '@/http' +import { storage } from '@/utils/storage' import type { Scene } from '@/store/sceneSlice' const USE_MOCK = true -const MOCK_SCENES: Scene[] = [ +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 new Promise((resolve) => setTimeout(() => resolve(MOCK_SCENES), 300)) + 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/api/user.ts b/src/api/user.ts index 93097c4..8018d2a 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -6,6 +6,8 @@ export interface UserInfo { id: string name: string avatar: string + mobile: string + loginType: 'mobile' | 'wechat' } export function getUserInfo(): Promise { @@ -13,7 +15,9 @@ export function getUserInfo(): Promise { return Promise.resolve({ id: 'u-001', name: '张大爷', - avatar: '' + avatar: '', + mobile: '', + loginType: 'mobile' }) } return request.get('/user/info') diff --git a/src/components/family-card/index.module.scss b/src/components/family-card/index.module.scss index dc9b38a..e8abbf0 100644 --- a/src/components/family-card/index.module.scss +++ b/src/components/family-card/index.module.scss @@ -6,7 +6,7 @@ background: $color-primary-light-bg; border: 1px solid $color-primary-light-border; border-radius: $radius-lg; - padding: 32px; + padding: 36px 32px 12px; } .cardPressed { @@ -27,23 +27,24 @@ .head { display: flex; align-items: center; - margin-bottom: 24px; + margin-bottom: 28px; } // 圆图标:品牌渐变底 + 白字首字,对齐设备网格 icon 风格 .icon { - width: 88px; - height: 88px; + width: 92px; + height: 92px; border-radius: $radius-circle; background: $color-primary-grad; display: flex; align-items: center; justify-content: center; - margin-right: 20px; + margin-right: 24px; + flex-shrink: 0; } .iconText { - font-size: 36px; + font-size: 38px; font-weight: 600; color: #ffffff; } @@ -51,14 +52,18 @@ .headMain { flex: 1; min-width: 0; + display: flex; + flex-direction: column; +} + +.nameRow { display: flex; align-items: center; } .name { - flex: 1; min-width: 0; - font-size: 32px; + font-size: 34px; font-weight: 600; color: $color-text; overflow: hidden; @@ -66,11 +71,11 @@ white-space: nowrap; } -// 人数徽标:白底胶囊——浅绿卡身上无米色 token,白底对比最清晰 -.memberBadge { +// 角色徽标:白底胶囊,紧贴名称右侧 +.roleBadge { flex-shrink: 0; - margin-left: 16px; - padding: 4px 16px; + margin-left: 12px; + padding: 4px 14px; background: $color-bg-white; border: 1px solid $color-primary-light-border; border-radius: $radius-pill; @@ -78,20 +83,24 @@ color: $color-primary-deep; } +// 成员/设备合并为头部副标题,避免与下方信息行重复占行 +.subCount { + margin-top: 10px; + font-size: 24px; + color: $color-text-secondary; +} + // 信息行:浅绿卡身上用更淡的绿分割线区分,不用灰白 $color-border .rows { display: flex; align-items: center; justify-content: space-between; - min-height: 60px; - border-top: 1px solid $color-primary-light-border; -} - -.rows:first-of-type { - border-top: none; + min-height: 72px; + border-top: 1px solid rgba(159, 220, 195, 0.45); } .rowLabel { + flex-shrink: 0; font-size: 26px; color: $color-text-secondary; } diff --git a/src/components/family-card/index.tsx b/src/components/family-card/index.tsx index c1ac523..ac407a3 100644 --- a/src/components/family-card/index.tsx +++ b/src/components/family-card/index.tsx @@ -23,8 +23,13 @@ export function FamilyCard({ family, onClick }: Props) { {first} - {family.name} - {family.memberCount} 人 + + {family.name} + {family.role && {family.role}} + + + {family.memberCount} 位成员 · {family.deviceCount} 台设备 + @@ -35,10 +40,6 @@ export function FamilyCard({ family, onClick }: Props) { 关爱地址 {family.address} - - 设备数量 - {family.deviceCount} - ) } diff --git a/src/pages/device/add.module.scss b/src/pages/device/add.module.scss index cf85641..99262bb 100644 --- a/src/pages/device/add.module.scss +++ b/src/pages/device/add.module.scss @@ -60,7 +60,7 @@ } .scanArea { - padding: 60px 0; + padding: 24px 0 40px; display: flex; flex-direction: column; align-items: center; @@ -70,41 +70,73 @@ opacity: 0.85; } -.scanIcon { - font-size: 140px; - color: $color-primary; - line-height: 1; - margin-bottom: 24px; +// 扫码取景框:四角 L 形 + 中间横线(纯 CSS 绘制,颜色取品牌主色) +.scanFrame { + position: relative; + width: 240px; + height: 240px; + margin-bottom: 32px; +} + +// 四角 L 形:每个角直接写完整 border 简写(wxss 会把 border:0 当清边框, +// 之后再单独设 border-*-width 不生效,导致四角不渲染) +.scanCorner { + position: absolute; + width: 72px; + height: 72px; +} + +.scanCornerTopLeft { + top: 0; + left: 0; + border-top: 12px solid $color-primary; + border-left: 12px solid $color-primary; + border-top-left-radius: 20px; +} + +.scanCornerTopRight { + top: 0; + right: 0; + border-top: 12px solid $color-primary; + border-right: 12px solid $color-primary; + border-top-right-radius: 20px; +} + +.scanCornerBottomLeft { + bottom: 0; + left: 0; + border-bottom: 12px solid $color-primary; + border-left: 12px solid $color-primary; + border-bottom-left-radius: 20px; +} + +.scanCornerBottomRight { + bottom: 0; + right: 0; + border-bottom: 12px solid $color-primary; + border-right: 12px solid $color-primary; + border-bottom-right-radius: 20px; +} + +// 中间横线:贯穿取景框,与四角同色 +.scanLine { + position: absolute; + left: 0; + right: 0; + top: 50%; + height: 12px; + background: $color-primary; + transform: translateY(-50%); } .scanText { font-size: 30px; + font-weight: 600; color: $color-primary; + margin-bottom: 8px; } -.deviceRow { - display: flex; - justify-content: space-between; - margin-top: 16px; -} - -.deviceBox { - width: 48%; - display: flex; - flex-direction: column; - align-items: center; -} - -.deviceIcon1, -.deviceIcon2 { - width: 100%; - height: 200px; - background: $color-bg-gray; - border-radius: $radius-md; - margin-bottom: 16px; -} - -.deviceLabel { - font-size: 26px; - color: $color-text; +.scanHint { + font-size: 24px; + color: $color-text-tertiary; } diff --git a/src/pages/device/add.tsx b/src/pages/device/add.tsx index aecba67..b6e122e 100644 --- a/src/pages/device/add.tsx +++ b/src/pages/device/add.tsx @@ -16,18 +16,22 @@ export default function DeviceAdd() { showToast('设备添加功能即将接入') } + // 点击扫码区拉起相机扫码,识别结果回填到输入框,确认后提交 const scanQRCode = async () => { + // 测试日志:完整输出扫码回调,用于真机验证扫描功能(上线前移除) + console.log('[SCAN] scanCode invoked') try { - // @ts-ignore Taro 类型未跟进 wx.scanCode 的完整回调(实际运行时存在) - const res = await Taro.scanCode({ onlyFromCamera: false }) - const code = String(res?.result ?? '').trim() + const res = await Taro.scanCode({ scanType: ['qrCode', 'barCode'] }) + console.log('[SCAN] success:', JSON.stringify(res)) + const code = String(res.result ?? '').trim() if (!code) { showToast('未识别到有效编号') return } setDeviceId(code) showToast('已填入扫码结果') - } catch { + } catch (error) { + console.log('[SCAN] fail:', JSON.stringify(error)) showToast('扫码取消') } } @@ -56,23 +60,17 @@ export default function DeviceAdd() { 扫码添加 + 扫描设备机身或包装盒上的二维码,自动填入编号。 - ⊡ - 点击扫码 - - - - - 设备二维码/编号位置 - - - - 包装盒侧边 - - - - 设备侧面或背面 + + + + + + + 点击开始扫码 + 支持二维码 / 条形码 diff --git a/src/pages/mine/index.tsx b/src/pages/mine/index.tsx index 7a4453b..5376c08 100644 --- a/src/pages/mine/index.tsx +++ b/src/pages/mine/index.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { View, Text } from '@tarojs/components' -import { useAppDispatch, useAppSelector } from '@/store/hooks' +import { useAppDispatch } from '@/store/hooks' import { clearAuth } from '@/store/authSlice' import { Navbar } from '@/components/navbar' import { ListRow, ListRowGroup } from '@/components/list-row' @@ -13,7 +13,6 @@ import styles from './index.module.scss' export default function Mine() { const dispatch = useAppDispatch() const toast = useToast() - const auth = useAppSelector((s) => s.auth) const [user, setUser] = useState(null) useEffect(() => { @@ -23,6 +22,7 @@ export default function Mine() { const logout = () => { dispatch(clearAuth()) + toast.show('已退出登录') reLaunch(ROUTES.LOGIN) } @@ -35,13 +35,18 @@ export default function Mine() { {user?.name ?? '加载中...'} - {auth.userId ? `ID: ${auth.userId}` : ''} + + {user?.loginType === 'mobile' ? '手机号验证码登录' : '微信登录'} + navigateTo(ROUTES.DEVICE_BLE_CONFIG)} /> + navigateTo(ROUTES.SCENE_SELECT)} /> + + diff --git a/src/pages/scene/select.module.scss b/src/pages/scene/select.module.scss index d01843c..df01e99 100644 --- a/src/pages/scene/select.module.scss +++ b/src/pages/scene/select.module.scss @@ -38,3 +38,29 @@ } .itemActive .check { color: $color-primary; } + +// 添加房间:虚线边框卡,与家庭圈添加卡风格一致 +.addBtn { + margin-top: 24px; + height: 104px; + border: 1px dashed $color-primary-light-border; + border-radius: $radius-md; + background: $color-bg-white; + display: flex; + align-items: center; + justify-content: center; +} + +.addBtnPressed { opacity: 0.8; } + +.addIcon { + font-size: 36px; + font-weight: 300; + color: $color-primary; + margin-right: 8px; +} + +.addText { + font-size: 28px; + color: $color-primary; +} diff --git a/src/pages/scene/select.tsx b/src/pages/scene/select.tsx index 7880f10..38afc42 100644 --- a/src/pages/scene/select.tsx +++ b/src/pages/scene/select.tsx @@ -1,4 +1,5 @@ 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' @@ -6,10 +7,24 @@ import { Navbar } from '@/components/navbar' import { Loading } from '@/components/loading' import { Empty } from '@/components/empty' import { StateView } from '@/components/state-view' -import { getSceneList } from '@/api/scene' +import { getSceneList, addScene } from '@/api/scene' +import { showToast } from '@/utils/toast' import { navigateBack } from '@/utils/navigation' import styles from './select.module.scss' +// Taro 类型未跟进 wx.showModal 的 editable 扩展(微信 2.17.1+),运行时真实存在 +interface EditableModalOption extends Taro.showModal.Option { + editable?: boolean + placeholderText?: string +} + +interface EditableModalResult extends Taro.showModal.SuccessCallbackResult { + content?: string +} + +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) @@ -38,15 +53,31 @@ export default function SceneSelect() { navigateBack() } + // 添加房间:可编辑弹窗输入名称,成功后刷新列表 + const addRoom = () => { + showEditableModal({ + title: '添加房间', + editable: true, + placeholderText: '请输入房间名称(如:阳台)' + }).then(async ({ confirm, content }) => { + if (!confirm) return + try { + await addScene(content ?? '') + showToast('添加成功') + await load() + } catch (error) { + showToast(error instanceof Error ? error.message : '添加失败') + } + }) + } + return ( - + {loading ? ( ) : loadError ? ( - ) : list.length === 0 ? ( - ) : ( {list.map((scene) => ( @@ -59,6 +90,14 @@ export default function SceneSelect() { {scene.id === current?.id ? '✓' : '›'} ))} + {/* 空列表时也保留添加入口 */} + {list.length === 0 && ( + + )} + + + + 添加房间 + )}