Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M7.prompt.md). Fill in{{dateStr}},{{all_length}},{{unreadCount}},{{readCount}},{{badgeClass}},{{stats_latestNotification_title}},{{stats_latestNotification_date}},{{stats_latestNotification_time}}before use. Copyright stays with the author.
M7 · 通知记录模块开发
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand 专家。请按照以下规范,逐步完成 M7 通知记录模块的全部开发内容。完成每个子任务后自行验证,确认无误再进入下一个。
当前项目状态
- M1–M6、M8 已完成:全局样式、数据层、首页、列表页、新增/编辑表单、提醒详情面板、我的页面
- 已有组件:
AppBar、BottomSheet、MedicineCard、StatusTag、ProgressBar、ReminderForm、ReminderDetail - 已有 hooks(均在
src/hooks/useReminders.ts):useDerivedList、useDerivedById、useHomeSummary、useReminderActionsuseProfileStats(当前unreadCount固定返回0,本模块需替换为真实值)
- 已有本地存储工具:
src/utils/storage.ts(含loadReminders/saveReminders、loadSettings/saveSettings) - 已有常量:
src/constants/index.ts(含STORAGE_KEY_REMINDERS、STORAGE_KEY_SETTINGS等) src/pages/profile/index.tsx中,InboxEntry点击已可打开 BottomSheet,但 BottomSheet 内容为占位文字(profile-page__inbox-placeholder),本模块需替换为真实的 NotificationInbox 组件- 本模块需新增/修改:
src/types/index.ts:追加NotificationRecord类型src/constants/index.ts:追加STORAGE_KEY_NOTIFICATIONSsrc/utils/storage.ts:追加loadNotifications、saveNotificationssrc/store/reminderStore.ts:扩展 store,增加 notifications 状态与操作src/hooks/useReminders.ts:新增useInboxList、useNotificationActions;更新useProfileStats返回真实 unreadCount 和 latestNotificationsrc/components/NotificationInbox/index.tsx(新建)src/components/NotificationInbox/index.scss(新建)src/pages/profile/index.tsx:替换占位内容,接入 NotificationInbox;新增详情 BottomSheet 状态支持"查看提醒"跳转
taroify 组件使用规范
项目已安装 @taroify/core,通过 vite-plugin-style-import 配置了样式自动导入(引入组件时无需手动 import CSS)。
本模块适用的 taroify 组件:
| 用途 | taroify 组件 | import 路径 | 说明 |
|---|---|---|---|
| 操作按钮 | Button |
@taroify/core/button |
通过 className 覆盖颜色匹配设计规范 soft 色板 |
复用已有组件(直接 import,无需重新实现):
| 组件 | import 路径 |
|---|---|
BottomSheet |
@/components/BottomSheet |
ReminderDetail |
@/components/ReminderDetail |
UI 设计参考
UI 参考 ui6.0/index.html 中 openInbox() 函数渲染的 Sheet 内容以及 renderInboxList() 函数的通知卡片结构。
NotificationInbox 整体结构(作为 BottomSheet 的 children 使用):
NotificationInbox
├── 通知说明卡(notice card)
│ ├── 未读数量标题("N 条未读通知")
│ └── 说明文字(引导用户知晓此处为兜底查看入口)
├── 筛选标题行(subhead)
│ ├── 左侧标题文字:"通知筛选"
│ └── 右侧"全部已读"按钮
├── 筛选标签组(filters)
│ └── 全部 / 未读 / 已读 / 紧急 / 近期(5 个标签,active 态高亮)
└── 消息列表(message-list,垂直排列)
└── NotificationCard(×n)
├── 左侧日期徽章(date-badge)
│ ├── 日期(M月D日格式)
│ └── 时间(HH:mm)
└── 右侧内容区(message-stack)
├── 标签行(已读/未读 mini-tag + 类别 mini-tag)
├── 标题(medicine 字号,font-weight: 900)
├── 摘要文字(meta 色,小字)
└── 操作按钮行
- 未读:「标记已读」(primary 样式)+ 「查看提醒」(ghost 样式)
- 已读:「查看提醒」(ghost 样式)
UI 关键样式:
notification-inbox__notice:padding: $spacing-5; border-radius: $radius-lg; background: $primary-soft; border: 1px solid $line-soft;标题font-size: $text-base; font-weight: 900; color: $ink-strong;正文font-size: $text-xs; color: $mutednotification-inbox__subhead:display: flex; align-items: center; justify-content: space-between; margin-top: $spacing-4; font-size: $text-base; font-weight: 900; color: $ink-strong;"全部已读"按钮min-height: 48px; padding: 0 $spacing-4; border-radius: $radius-pill; color: $primary-deep; background: $primary-soft; font-size: $text-xs; font-weight: 800; border: nonenotification-inbox__filters:display: flex; gap: $spacing-3; overflow-x: auto; margin-top: $spacing-3; padding-bottom: $spacing-2;每个 filtermin-height: 48px; padding: 0 $spacing-4; border-radius: $radius-pill; font-size: $text-xs; font-weight: 800; background: $surface-soft; color: $muted; border: none;.is-active态background: $primary-deep; color: #fffnotification-inbox__list:display: grid; gap: $spacing-4; margin-top: $spacing-4notification-inbox__card:display: flex; gap: $spacing-4; padding: $spacing-4 $spacing-5; border-radius: $radius-lg; background: $card; border: 1px solid $line-soft; box-shadow: $shadow-cardnotification-inbox__date-badge:min-width: 80px; padding: $spacing-3 $spacing-2; border-radius: $radius; text-align: center; font-size: $text-xs; font-weight: 800; line-height: 1.5; flex-shrink: 0;.is-urgent→background: $danger-soft; color: $danger;.is-upcoming→background: $warning-soft; color: $warning;.is-archive→background: $success-soft; color: $successnotification-inbox__tag:display: inline-flex; align-items: center; min-height: 32px; padding: 0 $spacing-3; border-radius: $radius-pill; font-size: 20px; font-weight: 900(注意:小程序 px 单位,字体用 rpx 等效,这里保持与其他组件一致使用已有 SCSS 变量);.is-unread→background: $danger-soft; color: $danger;.is-read→background: $surface-soft; color: $mutednotification-inbox__actions:display: flex; gap: $spacing-3; margin-top: $spacing-3; flex-wrap: wrapnotification-inbox__empty:padding: $spacing-8; text-align: center; color: $muted; font-size: $text-sm
子任务 1:追加 NotificationRecord 类型到 src/types/index.ts
在文件末尾追加(不修改已有内容):
/** 通知类别 */
export type NotificationCategory = 'urgent' | 'upcoming' | 'archive';
/** 站内通知记录(微信订阅消息失败时的兜底收件箱) */
export interface NotificationRecord {
id: string;
reminderId: string;
title: string;
summary: string;
date: string; // YYYY-MM-DD
time: string; // HH:mm
category: NotificationCategory;
read: boolean;
createdAt: string; // ISO 字符串
}
子任务 2:追加常量到 src/constants/index.ts
在文件末尾追加(不修改已有内容):
/** 通知记录的本地存储 key */
export const STORAGE_KEY_NOTIFICATIONS = 'notification_inbox_v1';
子任务 3:追加通知存储函数到 src/utils/storage.ts
在文件末尾追加(不修改已有内容,import 行合并到已有 import 中):
import type { NotificationRecord } from '@/types';
import { STORAGE_KEY_NOTIFICATIONS } from '@/constants';
/** 从本地存储读取通知记录列表,不存在时返回空数组 */
export function loadNotifications(): NotificationRecord[] {
try {
const data = Taro.getStorageSync(STORAGE_KEY_NOTIFICATIONS);
if (Array.isArray(data)) return data;
return [];
} catch {
return [];
}
}
/** 将通知记录列表写入本地存储 */
export function saveNotifications(notifications: NotificationRecord[]): void {
try {
Taro.setStorageSync(STORAGE_KEY_NOTIFICATIONS, notifications);
} catch {
// 静默处理
}
}
⚠️
storage.ts文件顶部已有import Taro from "@tarojs/taro",不重复 import。NotificationRecord和STORAGE_KEY_NOTIFICATIONS的 import 需并入已有对应 import 行。
子任务 4:扩展 src/store/reminderStore.ts,增加通知状态与操作
4.1 在文件顶部 import 区追加(合并进已有 import,不重复):
import type { NotificationRecord } from '@/types';
import { loadNotifications, saveNotifications } from '@/utils/storage';
4.2 在文件中 MOCK_REMINDERS 声明之前,追加 Mock 通知数据:
const MOCK_NOTIFICATIONS: NotificationRecord[] = [
{
id: 'n1',
reminderId: 'mock-danger-overdue',
title: '洛拉替尼开药事项已逾期',
summary: '预计下次开药 2026-05-13,提醒已触发,请尽快安排复诊。',
date: '2026-05-14',
time: '09:00',
category: 'urgent',
read: false,
createdAt: MOCK_TIMESTAMP
},
{
id: 'n2',
reminderId: 'mock-danger-today',
title: '瑞舒伐他汀 今日需要开药',
summary: '建议提前挂号或线上复诊,避免断药风险。',
date: '2026-05-14',
time: '09:00',
category: 'urgent',
read: false,
createdAt: MOCK_TIMESTAMP
},
{
id: 'n3',
reminderId: 'mock-warning',
title: '地舒单抗已进入提前提醒窗口',
summary: '预计 5/20 需要重新开药,当前是 7 天前提醒节点。',
date: '2026-05-13',
time: '08:30',
category: 'upcoming',
read: false,
createdAt: MOCK_TIMESTAMP
},
{
id: 'n4',
reminderId: 'mock-good',
title: '维生素D3当前节奏正常',
summary: '这是一条排程确认消息,帮助你核对下一次提醒是否正确。',
date: '2026-05-08',
time: '10:00',
category: 'archive',
read: true,
createdAt: MOCK_TIMESTAMP
}
];
4.3 在 initialReminders 声明之后,追加:
const initialNotifications = (() => {
const loaded = loadNotifications();
return loaded.length > 0 ? loaded : MOCK_NOTIFICATIONS;
})();
4.4 在 ReminderStore interface 中追加(在已有 getDerivedList 之前):
notifications: NotificationRecord[];
markNotificationRead: (id: string) => void;
markAllNotificationsRead: () => void;
getInboxList: () => NotificationRecord[];
4.5 在 createStore 的初始 state 对象中追加(与 reminders: initialReminders 同级):
notifications: initialNotifications,
4.6 在 createStore 的方法实现中,追加以下三个方法(在 getDerivedList 之前):
markNotificationRead(id) {
set((state) => {
const next = state.notifications.map((n) =>
n.id === id ? { ...n, read: true } : n,
);
saveNotifications(next);
return { notifications: next };
});
},
markAllNotificationsRead() {
set((state) => {
const next = state.notifications.map((n) => ({ ...n, read: true }));
saveNotifications(next);
return { notifications: next };
});
},
getInboxList() {
// 按日期降序排列,未读优先
return [...get().notifications].sort((a, b) => {
if (a.read !== b.read) return a.read ? 1 : -1;
return b.date.localeCompare(a.date) || b.time.localeCompare(a.time);
});
},
子任务 5:更新 src/hooks/useReminders.ts
5.1 更新 useProfileStats,替换 unreadCount: 0 为真实值,并增加 latestNotification 字段:
将已有的 useProfileStats 函数完整替换为以下内容:
/** 获取"我的"页面所需的统计数据 */
export function useProfileStats() {
const { reminders, notifications } = useReminderStore();
return useMemo(() => {
const activeList = reminders.filter((r) => r.status === 'active');
const historyTotal = reminders.reduce(
(sum, r) => sum + r.history.length,
0
);
const unreadCount = notifications.filter((n) => !n.read).length;
// 最新通知:未读优先,否则取最近一条
const latestNotification =
notifications.find((n) => !n.read) ?? notifications[0] ?? null;
return {
activeCount: activeList.length,
historyTotal,
total: reminders.filter((r) => r.status !== 'deleted').length,
unreadCount,
latestNotification
};
}, [reminders, notifications]);
}
5.2 在文件末尾追加以下两个 hook(不修改已有内容):
/** 获取通知收件箱列表(支持筛选) */
export function useInboxList(
filter: 'all' | 'unread' | 'read' | 'urgent' | 'upcoming'
) {
const notifications = useReminderStore().notifications;
return useMemo(() => {
const sorted = [...notifications].sort((a, b) => {
if (a.read !== b.read) return a.read ? 1 : -1;
return b.date.localeCompare(a.date) || b.time.localeCompare(a.time);
});
if (filter === 'all') return sorted;
if (filter === 'unread') return sorted.filter((n) => !n.read);
if (filter === 'read') return sorted.filter((n) => n.read);
return sorted.filter((n) => n.category === filter);
}, [notifications, filter]);
}
/** 通知操作 */
export function useNotificationActions() {
const state = useReminderStore();
return useMemo(
() => ({
markNotificationRead: state.markNotificationRead,
markAllNotificationsRead: state.markAllNotificationsRead
}),
[state]
);
}
子任务 6:新建 src/components/NotificationInbox/index.scss
@use '@/assets/styles/variables' as *;
.notification-inbox {
display: grid;
gap: $spacing-4;
// ─── 通知说明卡 ───────────────────────────────────────────────
&__notice {
padding: $spacing-5;
border-radius: $radius-lg;
background: $primary-soft;
border: 1px solid $line-soft;
}
&__notice-title {
display: block;
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
}
&__notice-desc {
display: block;
margin-top: 6px;
color: $muted;
font-size: $text-xs;
line-height: 1.6;
}
// ─── 筛选标题行 ──────────────────────────────────────────────
&__subhead {
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-4;
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
}
&__mark-all {
min-height: 48px;
padding: 0 $spacing-4;
border-radius: $radius-pill;
color: $primary-deep;
background: $primary-soft;
font-size: $text-xs;
font-weight: 800;
border: none;
}
// ─── 筛选标签组 ──────────────────────────────────────────────
&__filters {
display: flex;
gap: $spacing-3;
overflow-x: auto;
padding-bottom: $spacing-2;
&::-webkit-scrollbar {
display: none;
}
}
&__filter {
min-height: 48px;
padding: 0 $spacing-4;
border-radius: $radius-pill;
font-size: $text-xs;
font-weight: 800;
color: $muted;
background: $surface-soft;
border: none;
white-space: nowrap;
flex-shrink: 0;
&.is-active {
color: #fff;
background: $primary-deep;
}
}
// ─── 消息列表 ────────────────────────────────────────────────
&__list {
display: grid;
gap: $spacing-4;
}
// ─── 单条通知卡片 ─────────────────────────────────────────────
&__card {
display: flex;
gap: $spacing-4;
padding: $spacing-4 $spacing-5;
border-radius: $radius-lg;
background: $card;
border: 1px solid $line-soft;
box-shadow: $shadow-card;
}
// ─── 日期徽章 ────────────────────────────────────────────────
&__date-badge {
min-width: 80px;
padding: $spacing-3 $spacing-2;
border-radius: $radius;
text-align: center;
font-size: $text-xs;
font-weight: 800;
line-height: 1.5;
flex-shrink: 0;
background: $surface-soft;
color: $muted;
&.is-urgent {
background: $danger-soft;
color: $danger;
}
&.is-upcoming {
background: $warning-soft;
color: $warning;
}
&.is-archive {
background: $success-soft;
color: $success;
}
}
// ─── 右侧内容区 ──────────────────────────────────────────────
&__message-stack {
flex: 1;
min-width: 0;
display: grid;
gap: $spacing-2;
}
&__tags {
display: flex;
gap: $spacing-2;
flex-wrap: wrap;
}
&__tag {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 $spacing-3;
border-radius: $radius-pill;
font-size: $text-xs;
font-weight: 900;
&.is-unread {
background: $danger-soft;
color: $danger;
}
&.is-read {
background: $surface-soft;
color: $muted;
}
&.is-category {
background: $surface-soft;
color: $ink-strong;
}
}
&__title {
display: block;
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
line-height: 1.3;
}
&__summary {
display: block;
color: $muted;
font-size: $text-xs;
line-height: 1.6;
}
&__actions {
display: flex;
gap: $spacing-3;
flex-wrap: wrap;
margin-top: $spacing-2;
}
&__btn {
min-height: 60px;
padding: 0 $spacing-4;
border-radius: $radius;
font-size: $text-xs;
font-weight: 800;
border: none;
&.is-primary {
color: $primary-deep;
background: $primary-soft;
}
&.is-ghost {
color: $ink;
background: $surface-soft;
border: 1px solid $line-soft;
}
}
// ─── 空状态 ──────────────────────────────────────────────────
&__empty {
padding: $spacing-8;
text-align: center;
color: $muted;
font-size: $text-sm;
}
}
子任务 7:新建 src/components/NotificationInbox/index.tsx
import { useState } from "react";
import { Text, View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import {
useInboxList,
useNotificationActions,
useReminderStore,
} from "@/hooks/useReminders";
import type { NotificationCategory } from "@/types";
import "./index.scss";
// 本地复用 useReminderStore 需要导出,见子任务 5 的说明
// 实际上直接从 hooks 文件取到 notifications 总数
// 但此处为了获取 unreadCount,我们直接从 useInboxList 派生
type InboxFilterKey = "all" | "unread" | "read" | "urgent" | "upcoming";
interface NotificationInboxProps {
/** 点击"查看提醒"时,由父组件负责打开对应提醒的详情 */
onViewReminder: (reminderId: string) => void;
}
const CATEGORY_LABEL: Record<NotificationCategory, string> = {
urgent: "紧急提醒",
upcoming: "即将到期",
archive: "归档通知",
};
/** 将 YYYY-MM-DD 格式化为 M月D日 */
function fmtDate(dateStr: string): string {
const d = new Date(`{{dateStr}}T00:00:00`);
return `${d.getMonth() + 1}月${d.getDate()}日`;
}
export default function NotificationInbox({
onViewReminder,
}: NotificationInboxProps) {
const [activeFilter, setActiveFilter] = useState<InboxFilterKey>("all");
const list = useInboxList(activeFilter);
const { markNotificationRead, markAllNotificationsRead } =
useNotificationActions();
// 计算各筛选标签数量
const all = useInboxList("all");
const unreadCount = all.filter((n) => !n.read).length;
const readCount = all.filter((n) => n.read).length;
const FILTERS: [InboxFilterKey, string][] = [
["all", `全部 {{all_length}}`],
["unread", `未读 {{unreadCount}}`],
["read", `已读 {{readCount}}`],
["urgent", "紧急"],
["upcoming", "近期"],
];
const handleMarkRead = (id: string) => {
markNotificationRead(id);
Taro.showToast({ title: "已标记为已读", icon: "none", duration: 1000 });
};
const handleMarkAll = () => {
markAllNotificationsRead();
Taro.showToast({ title: "全部已标记为已读", icon: "none", duration: 1000 });
};
return (
<View className="notification-inbox">
{/* 通知说明卡 */}
<View className="notification-inbox__notice">
<Text className="notification-inbox__notice-title">
{unreadCount > 0 ? `{{unreadCount}} 条未读通知` : "通知都已读完"}
</Text>
<Text className="notification-inbox__notice-desc">
这里仅做通知查看和已读管理;开药、暂停、编辑等任务操作仍建议从首页卡片或列表详情进入。
</Text>
</View>
{/* 筛选标题行 */}
<View className="notification-inbox__subhead">
<Text>通知筛选</Text>
<View
className="notification-inbox__mark-all"
onClick={handleMarkAll}
role="button"
>
全部已读
</View>
</View>
{/* 筛选标签组 */}
<View className="notification-inbox__filters">
{FILTERS.map(([key, label]) => (
<View
key={key}
className={`notification-inbox__filter${activeFilter === key ? " is-active" : ""}`}
onClick={() => setActiveFilter(key)}
role="button"
>
{label}
</View>
))}
</View>
{/* 消息列表 */}
<View className="notification-inbox__list">
{list.length === 0 ? (
<View className="notification-inbox__empty">
<Text>当前筛选下没有通知记录</Text>
</View>
) : (
list.map((item) => {
const badgeClass =
item.category === "urgent"
? "is-urgent"
: item.category === "upcoming"
? "is-upcoming"
: "is-archive";
return (
<View key={item.id} className="notification-inbox__card">
{/* 日期徽章 */}
<View
className={`notification-inbox__date-badge {{badgeClass}}`}
>
<Text>{fmtDate(item.date)}</Text>
{"\n"}
<Text>{item.time}</Text>
</View>
{/* 右侧内容区 */}
<View className="notification-inbox__message-stack">
<View className="notification-inbox__tags">
<View
className={`notification-inbox__tag ${item.read ? "is-read" : "is-unread"}`}
>
{item.read ? "已读" : "未读"}
</View>
<View className="notification-inbox__tag is-category">
{CATEGORY_LABEL[item.category]}
</View>
</View>
<Text className="notification-inbox__title">
{item.title}
</Text>
<Text className="notification-inbox__summary">
{item.summary}
</Text>
<View className="notification-inbox__actions">
{!item.read && (
<View
className="notification-inbox__btn is-primary"
onClick={() => handleMarkRead(item.id)}
role="button"
>
标记已读
</View>
)}
<View
className="notification-inbox__btn is-ghost"
onClick={() => onViewReminder(item.reminderId)}
role="button"
>
查看提醒
</View>
</View>
</View>
</View>
);
})
)}
</View>
</View>
);
}
⚠️
useInboxList在组件内被调用了两次(一次带 filter,一次带 "all" 用于统计),这会触发两次订阅。如果性能有顾虑,可改为只调用useInboxList("all")一次,在组件内自行做 filter 过滤。但 MVP 阶段通知量少,保持简洁即可。
⚠️
useReminderStore目前未从useReminders.ts直接导出,useNotificationActions和useInboxList在 hook 文件内部通过闭包访问 store。因此组件只需 import 这两个 hook,无需直接 import store。
子任务 8:更新 src/pages/profile/index.tsx,接入 NotificationInbox
8.1 在 import 区追加(与已有 import 合并):
import NotificationInbox from '@/components/NotificationInbox';
import ReminderDetail from '@/components/ReminderDetail';
8.2 在已有 state 声明区追加详情 state:
const [detailId, setDetailId] = useState<string | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
8.3 追加 handleViewReminder 回调函数(在 handleBeforeChange 之后):
const handleViewReminder = (reminderId: string) => {
setInboxOpen(false);
setDetailId(reminderId);
setDetailOpen(true);
};
const closeDetail = () => {
setDetailOpen(false);
setDetailId(null);
};
const handleEditFromDetail = (_id: string) => {
// M7 不含编辑入口,暂不实现,关闭详情即可
closeDetail();
};
8.4 将 InboxEntry 预览文字改为读取真实通知摘要
将已有的 inboxPreviewMeta 部分的硬编码文字替换为:
<Text className="profile-page__inbox-preview-meta">
{stats.latestNotification
? `{{stats_latestNotification_title}} · {{stats_latestNotification_date}} {{stats_latestNotification_time}}`
: '消息入口用于兜底查看提醒,不替代首页和列表的任务操作。'}
</Text>
8.5 将占位 BottomSheet 内容替换为 NotificationInbox,并在其后增加详情 BottomSheet
将:
<BottomSheet
open={inboxOpen}
title="历史提醒记录"
onClose={() => setInboxOpen(false)}
>
<View className="profile-page__inbox-placeholder">
<Text>通知记录功能将在 M7 中实现。</Text>
</View>
</BottomSheet>
替换为:
<BottomSheet
open={inboxOpen}
title="历史提醒记录"
onClose={() => setInboxOpen(false)}
onAfterOpen={() => Taro.hideTabBar({ animation: true })}
onAfterClose={() => Taro.showTabBar({ animation: true })}
>
<NotificationInbox onViewReminder={handleViewReminder} />
</BottomSheet>
<BottomSheet
open={detailOpen}
title="提醒详情"
onClose={closeDetail}
onAfterOpen={() => Taro.hideTabBar({ animation: true })}
onAfterClose={() => Taro.showTabBar({ animation: true })}
>
{detailId ? (
<ReminderDetail
reminderId={detailId}
onClose={closeDetail}
onEdit={handleEditFromDetail}
/>
) : null}
</BottomSheet>
8.6 删除 profile-page__inbox-placeholder 相关的 SCSS 规则
在 src/pages/profile/index.scss 末尾找到 &__inbox-placeholder(如存在)并删除该规则块。
验收标准
-
src/types/index.ts中NotificationRecord类型存在,字段完整 -
src/store/reminderStore.ts中notifications初始化为 Mock 数据(共 4 条),markNotificationRead、markAllNotificationsRead、getInboxList可用 -
src/hooks/useReminders.ts中useProfileStats返回的unreadCount为真实未读数(Mock 数据中为 3),latestNotification为最新未读通知对象 - "我的"页 MetricsRow 中"未读通知"数字显示为 3(与 Mock 数据一致)
- "我的"页 InboxEntry 预览文字显示最新未读通知标题和时间(不再是硬编码文案)
- 点击 InboxEntry,BottomSheet 打开,内容为通知记录列表(4 条),TabBar 隐藏
- 5 个筛选标签各自计数准确,点击后列表即时过滤
- "全部已读"点击后,所有通知变为已读,unreadCount 归零,"我的"页数字同步刷新
- 单条"标记已读"后,该通知状态更新,其余不受影响
- 日期徽章颜色匹配类别:urgent=红、upcoming=橙、archive=绿
- 点击"查看提醒"后,通知记录 BottomSheet 关闭,提醒详情 BottomSheet 打开并展示正确数据
- 关闭详情 BottomSheet 后返回正常视图,TabBar 恢复显示
- 通知记录数据在小程序重启后可从本地存储恢复(已读状态持久化)
- 不存在独立的"通知记录"TabBar Tab 或独立路由页面
