From 93f60c74abd26f6e8f476c9f8983df1dfbfa0fcd Mon Sep 17 00:00:00 2001 From: czz <862977248@qq.com> Date: Thu, 7 May 2026 16:44:13 +0800 Subject: [PATCH 1/2] docs: add message page design spec --- .../specs/2026-05-07-message-page-design.md | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-07-message-page-design.md diff --git a/docs/superpowers/specs/2026-05-07-message-page-design.md b/docs/superpowers/specs/2026-05-07-message-page-design.md new file mode 100644 index 0000000..5424ec7 --- /dev/null +++ b/docs/superpowers/specs/2026-05-07-message-page-design.md @@ -0,0 +1,173 @@ +# 消息页面设计说明 + +## 1. 目标 + +在当前微信小程序项目中新增一个“消息”页面,页面风格参考提供的设计图,并与现有首页形成双向页面跳转。 + +本次目标是让项目从“只有首页”扩展为“首页 + 消息页”的可预览状态,同时保留后续继续新增“报告”“我的”等页面的扩展空间。 + +## 2. 本次范围 + +本次实现包含: + +- 新增消息页面 +- 首页与消息页之间的底部导航跳转 +- 抽取一个可复用的底部导航组件 +- 在消息页中实现顶部双标签、消息卡片列表、未读红点视觉 +- 更新页面注册配置 +- 更新 README,补充当前页面结构和消息页说明 + +本次不包含: + +- 真正的后端消息接口 +- 消息已读、删除、下拉刷新、分页加载 +- “系统消息”标签下的真实内容切换接口 +- “报告”“小e”“我的”真实业务页 + +## 3. 设计原则 + +- 保持项目结构简单,适合新手继续修改 +- 尽量复用现有首页的深色视觉语言 +- 不额外引入新的大型依赖 +- 先完成静态页面和页面跳转,再为后续真实接口预留清晰结构 + +## 4. 页面与组件设计 + +### 4.1 消息页面 + +新增目录: + +- `src/pages/message/index.tsx` +- `src/pages/message/index.scss` +- `src/pages/message/index.config.ts` + +页面内容分为三块: + +1. 顶部标签区 +2. 消息卡片列表区 +3. 底部导航区 + +顶部标签区使用两个标签: + +- `体征消息` +- `系统消息` + +默认高亮 `体征消息`,`系统消息` 右侧显示一个小红点作为未读提示。首版只做前端本地切换,切换后可展示不同的本地占位数据,不接接口。 + +消息卡片列表区使用 3 条演示数据,卡片字段参考设计图: + +- 标题 +- 设备 ID +- 使用人员 +- 消息类型 +- 检测数值 +- 发生时间 + +页面整体继续使用深色背景、圆角卡片、浅色文字和青绿色高亮色,尽量与首页统一。 + +### 4.2 底部导航组件 + +新增目录: + +- `src/components/tab-bar/index.tsx` +- `src/components/tab-bar/index.scss` + +组件负责: + +- 渲染统一的 5 个底部导航项 +- 根据传入的当前页面 key 控制高亮状态 +- 处理首页与消息页之间的跳转 +- 对暂未实现的页面继续保留轻提示 + +组件入参保持简单,建议至少包含: + +- 当前激活项 key + +组件内部统一维护导航项配置,避免首页和消息页各自重复维护一份。 + +## 5. 交互设计 + +### 5.1 页面跳转 + +- 首页点击底部 `消息`,跳转到消息页 +- 消息页点击底部 `首页`,跳转到首页 +- 点击当前已激活 tab 不重复跳转 +- 点击 `报告`、`小e`、`我的` 时,继续提示“功能待接入” + +为了避免多层页面堆栈,首页与消息页之间的 tab 切换优先使用 `Taro.redirectTo`。 + +### 5.2 消息标签切换 + +- 点击 `体征消息`,显示体征消息列表 +- 点击 `系统消息`,显示系统消息占位列表 +- 当前仅做前端状态切换,不持久化 + +### 5.3 消息列表滚动 + +如果卡片数量超过一屏,允许页面自然纵向滚动。底部导航固定在底部,页面内容区域保留足够底部内边距,避免被遮挡。 + +## 6. 数据设计 + +消息页首版使用本地静态数据。建议定义明确的 TypeScript 类型,例如: + +- `MessageTabKey` +- `MessageItem` + +数据至少包含: + +- `id` +- `title` +- `deviceId` +- `userName` +- `messageType` +- `valueText` +- `occurredAt` +- `category` + +其中 `category` 用于区分 `vital` 和 `system` 两类消息,便于本地筛选。 + +## 7. 文件修改范围 + +预计涉及文件: + +- `src/app.config.ts` +- `src/pages/index/index.tsx` +- `src/pages/index/index.scss` +- `src/pages/message/index.tsx` +- `src/pages/message/index.scss` +- `src/pages/message/index.config.ts` +- `src/components/tab-bar/index.tsx` +- `src/components/tab-bar/index.scss` +- `README.md` + +## 8. 测试与验证 + +至少验证以下内容: + +- 项目可以正常编译 +- 首页底部 `消息` 可跳转到消息页 +- 消息页底部 `首页` 可跳回首页 +- 消息页顶部双标签可以切换 +- 当前激活 tab 样式正确 +- 页面底部内容不会被固定导航遮挡 + +## 9. 风险与控制 + +风险 1:底部导航从首页内联实现改为组件后,可能影响首页现有视觉。 +控制方式:保留原有视觉结构,优先抽取已有样式命名和布局逻辑,避免大改。 + +风险 2:tab 切换若使用 `navigateTo`,可能导致页面堆栈不断增加。 +控制方式:首页与消息页之间统一使用 `redirectTo`。 + +风险 3:消息页完全照搬图片可能与当前项目样式不一致。 +控制方式:保留设计图核心布局,但颜色、阴影、圆角与首页现有风格保持同一体系。 + +## 10. 完成标准 + +满足以下条件即可认为本次任务完成: + +- 小程序内已存在独立消息页 +- 首页与消息页可通过底部导航双向切换 +- 消息页主要视觉与提供的参考图接近 +- 代码结构对新手友好,文件职责清晰 +- README 已同步说明当前新增页面 From c63f29e500ad35c189cdcbc705e19883e25cd96c Mon Sep 17 00:00:00 2001 From: czz <862977248@qq.com> Date: Thu, 7 May 2026 17:13:00 +0800 Subject: [PATCH 2/2] feat: add message page --- src/app.config.ts | 2 +- src/components/tab-bar/index.scss | 164 ++++++++++++++++++++++++++++++ src/components/tab-bar/index.tsx | 63 ++++++++++++ src/pages/index/index.scss | 86 ---------------- src/pages/index/index.tsx | 24 +---- src/pages/message/index.config.ts | 4 + src/pages/message/index.scss | 129 +++++++++++++++++++++++ src/pages/message/index.tsx | 145 ++++++++++++++++++++++++++ 8 files changed, 508 insertions(+), 109 deletions(-) create mode 100644 src/components/tab-bar/index.scss create mode 100644 src/components/tab-bar/index.tsx create mode 100644 src/pages/message/index.config.ts create mode 100644 src/pages/message/index.scss create mode 100644 src/pages/message/index.tsx diff --git a/src/app.config.ts b/src/app.config.ts index 4e2430c..8fb4fc0 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,5 +1,5 @@ export default defineAppConfig({ - pages: ["pages/index/index"], + pages: ["pages/index/index", "pages/message/index"], window: { navigationBarTitleText: "新手小程序", navigationBarBackgroundColor: "#1AAD19", diff --git a/src/components/tab-bar/index.scss b/src/components/tab-bar/index.scss new file mode 100644 index 0000000..aded8a7 --- /dev/null +++ b/src/components/tab-bar/index.scss @@ -0,0 +1,164 @@ +.app-tabbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding: 16rpx 24rpx calc(20rpx + env(safe-area-inset-bottom)); + background: rgba(24, 30, 43, 0.98); + box-shadow: 0 -8rpx 24rpx rgba(4, 8, 20, 0.34); +} + +.app-tabbar__item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + min-width: 88rpx; +} + +.app-tabbar__icon { + position: relative; + width: 34rpx; + height: 34rpx; + margin-bottom: 8rpx; + color: #8f97a9; +} + +.app-tabbar__icon::before, +.app-tabbar__icon::after { + position: absolute; + content: ""; +} + +.app-tabbar__icon--home { + border: 2rpx solid currentColor; + border-top: none; + border-radius: 0 0 8rpx 8rpx; +} + +.app-tabbar__icon--home::before { + left: 4rpx; + top: -2rpx; + width: 22rpx; + height: 22rpx; + border-top: 2rpx solid currentColor; + border-left: 2rpx solid currentColor; + transform: rotate(45deg); + background: transparent; +} + +.app-tabbar__icon--report::before, +.app-tabbar__icon--report::after { + bottom: 4rpx; + width: 6rpx; + border-radius: 999rpx; + background: currentColor; +} + +.app-tabbar__icon--report::before { + left: 7rpx; + height: 16rpx; +} + +.app-tabbar__icon--report::after { + left: 19rpx; + height: 24rpx; +} + +.app-tabbar__icon--assistant { + border: 2rpx solid currentColor; + border-radius: 50%; +} + +.app-tabbar__icon--assistant::before { + left: 6rpx; + top: 8rpx; + width: 18rpx; + height: 10rpx; + border-bottom: 2rpx solid currentColor; + border-radius: 0 0 12rpx 12rpx; +} + +.app-tabbar__icon--assistant::after { + left: 12rpx; + top: 6rpx; + width: 4rpx; + height: 4rpx; + border-radius: 50%; + background: currentColor; + box-shadow: 8rpx 0 0 currentColor; +} + +.app-tabbar__icon--message { + border: 2rpx solid currentColor; + border-radius: 10rpx; +} + +.app-tabbar__icon--message::before, +.app-tabbar__icon--message::after { + left: 8rpx; + right: 8rpx; + height: 2rpx; + background: currentColor; +} + +.app-tabbar__icon--message::before { + top: 10rpx; +} + +.app-tabbar__icon--message::after { + bottom: 10rpx; +} + +.app-tabbar__icon--mine { + border: 2rpx solid currentColor; + border-radius: 50%; +} + +.app-tabbar__icon--mine::before { + left: 9rpx; + top: 5rpx; + width: 12rpx; + height: 12rpx; + border-radius: 50%; + background: currentColor; +} + +.app-tabbar__icon--mine::after { + left: 5rpx; + bottom: 5rpx; + width: 20rpx; + height: 10rpx; + border-radius: 10rpx 10rpx 6rpx 6rpx; + border: 2rpx solid currentColor; + background: transparent; +} + +.app-tabbar__icon--active { + color: #36e4aa; +} + +.app-tabbar__badge { + position: absolute; + top: -4rpx; + right: -4rpx; + width: 10rpx; + height: 10rpx; + border-radius: 50%; + background: #ff4d4f; + box-shadow: 0 0 0 4rpx rgba(24, 30, 43, 0.98); +} + +.app-tabbar__label { + color: #b0b6c4; + font-size: 20rpx; + line-height: 1.2; +} + +.app-tabbar__label--active { + color: #36e4aa; +} diff --git a/src/components/tab-bar/index.tsx b/src/components/tab-bar/index.tsx new file mode 100644 index 0000000..e41ab21 --- /dev/null +++ b/src/components/tab-bar/index.tsx @@ -0,0 +1,63 @@ +import { Text, View } from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import "./index.scss"; + +export type TabBarKey = "home" | "report" | "assistant" | "message" | "mine"; + +type TabBarProps = { + activeKey: TabBarKey; +}; + +type TabItem = { + key: TabBarKey; + label: string; + hasBadge?: boolean; + pagePath?: string; +}; + +const tabItems: TabItem[] = [ + { key: "home", label: "首页", pagePath: "/pages/index/index" }, + { key: "report", label: "报告" }, + { key: "assistant", label: "小e" }, + { key: "message", label: "消息", hasBadge: true, pagePath: "/pages/message/index" }, + { key: "mine", label: "我的" } +]; + +export default function TabBar({ activeKey }: TabBarProps) { + const showToast = (title: string) => { + Taro.showToast({ + title, + icon: "none" + }); + }; + + const handleTabClick = (item: TabItem) => { + if (item.key === activeKey) { + return; + } + + if (item.pagePath) { + Taro.redirectTo({ url: item.pagePath }); + return; + } + + showToast(`${item.label}功能待接入`); + }; + + return ( + + {tabItems.map((item) => { + const isActive = item.key === activeKey; + + return ( + handleTabClick(item)}> + + {item.hasBadge ? : null} + + {item.label} + + ); + })} + + ); +} diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss index 92ba0f8..376d594 100644 --- a/src/pages/index/index.scss +++ b/src/pages/index/index.scss @@ -363,89 +363,3 @@ line-height: 1.7; } -.device-tabbar { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 5; - display: flex; - align-items: flex-start; - justify-content: space-around; - padding: 16rpx 24rpx calc(20rpx + env(safe-area-inset-bottom)); - background: rgba(30, 35, 48, 0.98); - box-shadow: 0 -8rpx 24rpx rgba(4, 8, 20, 0.34); -} - -.device-tabbar__item { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - min-width: 88rpx; -} - -.device-tabbar__icon { - position: relative; - width: 34rpx; - height: 34rpx; - margin-bottom: 8rpx; - border-radius: 10rpx; - border: 2rpx solid #8f97a9; - opacity: 0.86; -} - -.device-tabbar__icon::before, -.device-tabbar__icon::after { - position: absolute; - content: ""; -} - -.device-tabbar__icon::before { - left: 8rpx; - right: 8rpx; - top: 10rpx; - height: 2rpx; - background: currentColor; - color: #8f97a9; -} - -.device-tabbar__icon::after { - left: 8rpx; - right: 8rpx; - bottom: 10rpx; - height: 2rpx; - background: currentColor; - color: #8f97a9; -} - -.device-tabbar__icon--active { - border-color: #36e4aa; - background: rgba(54, 228, 170, 0.12); -} - -.device-tabbar__icon--active::before, -.device-tabbar__icon--active::after { - color: #36e4aa; -} - -.device-tabbar__badge { - position: absolute; - top: -4rpx; - right: -4rpx; - width: 10rpx; - height: 10rpx; - border-radius: 50%; - background: #ff4d4f; - box-shadow: 0 0 0 4rpx rgba(30, 35, 48, 0.98); -} - -.device-tabbar__label { - color: #b0b6c4; - font-size: 20rpx; - line-height: 1.2; -} - -.device-tabbar__label--active { - color: #36e4aa; -} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 76f5c6c..82011b7 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,6 +1,7 @@ import { Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; import { useEffect, useRef, useState } from "react"; +import TabBar from "../../components/tab-bar"; import "./index.scss"; type BluetoothStatus = "idle" | "searching" | "empty" | "success"; @@ -17,14 +18,6 @@ const notices = [ "3. 扫码功能需开启相机权限" ]; -const navItems = [ - { key: "home", label: "首页", active: true }, - { key: "report", label: "报告", active: false }, - { key: "assistant", label: "小e", active: false }, - { key: "message", label: "消息", active: false, badge: true }, - { key: "mine", label: "我的", active: false } -]; - const mockBluetoothDevices: DeviceCandidate[] = [ { id: "mock-thermo-01", name: "体征监测设备 A1", source: "mock" }, { id: "mock-thermo-02", name: "体征监测设备 B2", source: "mock" } @@ -109,10 +102,6 @@ export default function Index() { showToast("后续可从这里进入设备管理页"); }; - const handleTabClick = (label: string) => { - showToast(`${label}功能待接入`); - }; - const handleScanBind = async () => { try { setStatusHint("等待扫码识别设备编码。"); @@ -334,16 +323,7 @@ export default function Index() { - - {navItems.map((item) => ( - handleTabClick(item.label)}> - - {item.badge ? : null} - - {item.label} - - ))} - + ); } diff --git a/src/pages/message/index.config.ts b/src/pages/message/index.config.ts new file mode 100644 index 0000000..c344b06 --- /dev/null +++ b/src/pages/message/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: "custom", + navigationBarTitleText: "消息" +}); diff --git a/src/pages/message/index.scss b/src/pages/message/index.scss new file mode 100644 index 0000000..acc9b20 --- /dev/null +++ b/src/pages/message/index.scss @@ -0,0 +1,129 @@ +.message-page { + position: relative; + min-height: 100vh; + padding: 24rpx 24rpx 156rpx; + box-sizing: border-box; + background: linear-gradient(180deg, #1d2331 0%, #171d29 100%); + overflow: hidden; +} + +.message-page__glow { + position: absolute; + border-radius: 50%; + pointer-events: none; +} + +.message-page__glow--top { + top: -80rpx; + left: -40rpx; + width: 280rpx; + height: 280rpx; + background: radial-gradient(circle, rgba(55, 228, 171, 0.1), transparent 70%); +} + +.message-page__glow--side { + top: 160rpx; + right: -90rpx; + width: 240rpx; + height: 320rpx; + background: radial-gradient(circle, rgba(86, 116, 184, 0.12), transparent 72%); +} + +.message-tabs { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 110rpx; + padding: 20rpx 0 28rpx; +} + +.message-tabs__item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-width: 120rpx; +} + +.message-tabs__label { + color: #8f97ac; + font-size: 24rpx; + line-height: 1.3; +} + +.message-tabs__label--active { + color: #36e4aa; + font-weight: 600; +} + +.message-tabs__dot { + position: absolute; + top: 6rpx; + right: -10rpx; + width: 10rpx; + height: 10rpx; + border-radius: 50%; + background: #ff4d4f; +} + +.message-tabs__line { + position: absolute; + left: 50%; + bottom: -10rpx; + width: 52rpx; + height: 4rpx; + border-radius: 999rpx; + background: linear-gradient(90deg, #35e5b3 0%, #20c9bf 100%); + transform: translateX(-50%); +} + +.message-list { + position: relative; + z-index: 1; +} + +.message-card { + padding: 24rpx 26rpx; + border-radius: 26rpx; + background: rgba(42, 48, 66, 0.96); + box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.03); +} + +.message-card + .message-card { + margin-top: 22rpx; +} + +.message-card__title { + display: block; + margin-bottom: 18rpx; + color: #f3f7ff; + font-size: 28rpx; + font-weight: 600; + line-height: 1.5; +} + +.message-card__row { + display: flex; + align-items: flex-start; + line-height: 1.7; +} + +.message-card__row + .message-card__row { + margin-top: 6rpx; +} + +.message-card__label { + width: 104rpx; + flex-shrink: 0; + color: #7f889d; + font-size: 22rpx; +} + +.message-card__value { + flex: 1; + color: #d9dfeb; + font-size: 22rpx; + word-break: break-all; +} diff --git a/src/pages/message/index.tsx b/src/pages/message/index.tsx new file mode 100644 index 0000000..c7064da --- /dev/null +++ b/src/pages/message/index.tsx @@ -0,0 +1,145 @@ +import { Text, View } from "@tarojs/components"; +import { useState } from "react"; +import TabBar from "../../components/tab-bar"; +import "./index.scss"; + +type MessageTabKey = "vital" | "system"; + +type MessageItem = { + id: string; + title: string; + deviceId: string; + userName: string; + messageType: string; + valueText: string; + occurredAt: string; + category: MessageTabKey; +}; + +const tabs: Array<{ key: MessageTabKey; label: string; hasDot?: boolean }> = [ + { key: "vital", label: "体征消息" }, + { key: "system", label: "系统消息", hasDot: true } +]; + +const messageList: MessageItem[] = [ + { + id: "vital-1", + title: "实时监测结果通知", + deviceId: "A54984651", + userName: "1201/李小北", + messageType: "心率异常", + valueText: "106", + occurredAt: "2024-07-30 01:15", + category: "vital" + }, + { + id: "vital-2", + title: "睡眠报告分析通知", + deviceId: "A54984651", + userName: "1201/李小北", + messageType: "HRV异常", + valueText: "89", + occurredAt: "2024-07-30 01:15", + category: "vital" + }, + { + id: "vital-3", + title: "睡眠月报分析通知", + deviceId: "A54984651", + userName: "1201/李小北", + messageType: "睡眠得分", + valueText: "有20天低于60分", + occurredAt: "2024-07-30 01:15", + category: "vital" + }, + { + id: "system-1", + title: "系统升级提醒", + deviceId: "平台消息", + userName: "全部用户", + messageType: "版本更新", + valueText: "建议升级到最新版本", + occurredAt: "2024-07-30 09:30", + category: "system" + }, + { + id: "system-2", + title: "服务时间调整通知", + deviceId: "平台消息", + userName: "全部用户", + messageType: "运营公告", + valueText: "周日 02:00-04:00 系统维护", + occurredAt: "2024-07-29 18:00", + category: "system" + } +]; + +const fieldLabels = { + deviceId: "设备ID", + userName: "使用人员", + messageType: "消息类型", + valueText: "检测数值", + occurredAt: "发生时间" +}; + +export default function MessagePage() { + const [activeTab, setActiveTab] = useState("vital"); + + const currentMessages = messageList.filter((item) => item.category === activeTab); + + return ( + + + + + + {tabs.map((item) => { + const isActive = item.key === activeTab; + + return ( + setActiveTab(item.key)}> + {item.label} + {item.hasDot ? : null} + {isActive ? : null} + + ); + })} + + + + {currentMessages.map((item) => ( + + {item.title} + + + {fieldLabels.deviceId} + {item.deviceId} + + + + {fieldLabels.userName} + {item.userName} + + + + {fieldLabels.messageType} + {item.messageType} + + + + {fieldLabels.valueText} + {item.valueText} + + + + {fieldLabels.occurredAt} + {item.occurredAt} + + + ))} + + + + + ); +}