Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M8.prompt.md). Fill in{{d}},{{e_detail_value}},{{days}},{{stats_unreadCount}}before use. Copyright stays with the author.
M8 · 我的模块开发
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand 专家。请按照以下规范,逐步完成 M8 我的模块的全部开发内容。完成每个子任务后自行验证,确认无误再进入下一个。
当前项目状态
- M1–M6 已完成:全局样式、数据层、首页、列表页、新增/编辑表单、提醒详情面板
- 已有组件:
AppBar、BottomSheet、MedicineCard、StatusTag、ProgressBar、ReminderForm、ReminderDetail - 已有 hooks:
useDerivedList、useDerivedById、useHomeSummary、useReminderActions(均在src/hooks/useReminders.ts) - 已有本地存储工具:
src/utils/storage.ts(含loadReminders/saveReminders) - M7(通知记录)尚未开发,NotificationInbox 组件不存在;InboxEntry 点击时以 BottomSheet 空占位承载,等待 M7 接入
- 当前
src/pages/profile/index.tsx为占位页,本模块需完整替换 - 本模块需新增/修改:
src/utils/storage.ts:新增loadSettings/saveSettings函数src/constants/index.ts:新增STORAGE_KEY_SETTINGS、BEFORE_OPTIONS_LABELsrc/hooks/useReminders.ts:新增useProfileStatshooksrc/pages/profile/index.tsx:完整实现(替换占位页)src/pages/profile/index.scss:完整实现(替换占位样式)
taroify 组件使用规范
项目已安装 @taroify/core,通过 vite-plugin-style-import 配置了样式自动导入(引入组件时无需手动 import CSS)。
本模块适用的 taroify 组件:
| 用途 | taroify 组件 | import 路径 | 说明 |
|---|---|---|---|
| 底部弹层 | Popup |
@taroify/core/popup |
已由 BottomSheet 封装,直接复用 BottomSheet |
| 操作按钮 | Button |
@taroify/core/button |
通过 className 覆盖颜色匹配设计规范 soft 色板 |
表单选择器使用 Taro 原生组件(小程序兼容性最佳):
| 用途 | 组件 | import 路径 |
|---|---|---|
| 时间选择 | Picker |
@tarojs/components |
| 列表项选择 | Picker |
@tarojs/components |
⚠️ 小程序 Picker 说明:
mode="time"时onChange签名为(e: { detail: { value: string } }) => void,返回"HH:mm"字符串;mode="selector"时onChange.detail.value为选中项下标(number)。
复用已有组件(直接 import,无需重新实现):
| 组件 | import 路径 |
|---|---|
AppBar |
@/components/AppBar |
BottomSheet |
@/components/BottomSheet |
UI 设计参考
UI 参考 ui6.0/index.html 中 screen[data-screen="profile"] 区域。
页面整体结构:
profile 页面
├── AppBar(标题:"我的",副标题:"提醒授权、通知记录与默认设置",右侧设置图标)
└── 滚动内容区
├── ProfileCard(头像 + 昵称 + 提醒统计摘要)
├── MetricsRow(3格统计:启用中 / 未读通知 / 历史记录)
├── InboxEntry(通知记录入口卡片,点击打开通知记录 BottomSheet)
└── MenuList(设置菜单)
├── 订阅消息授权(Toggle Switch)
├── 默认提醒时间(展示值 + Picker 选择)
├── 默认提前天数(展示值 + Picker 选择)
└── 通知归档(总条数,只读)
UI 关键样式(与 ui6.0/index.html 一致):
- ProfileCard:
display: flex; gap: $spacing-5,头像 92px × 92px,border-radius: 32px,主色渐变背景,首字符作为 icon - MetricsRow:
display: grid; grid-template-columns: repeat(3, 1fr); gap: $spacing-4,每格min-height: 126px,强数字font-size: $text-2xl; font-weight: 900 - InboxEntry:
align-items: flex-start; padding: $spacing-5; border-radius: $radius-lg,内部有 entry-head(标题 + 箭头)和 entry-preview(摘要卡,绿色软色背景) - MenuList:
display: grid; gap: $spacing-4,每项min-height: 100px; padding: 0 $spacing-5; border-radius: $radius-lg - Toggle Switch:
width: 86px; height: 52px,.on时背景为$success,pill 内圆通过::before+transform: translateX(34px)实现(纯 CSS,不引入额外组件)
子任务 1:扩展 src/constants/index.ts
在文件末尾追加以下常量(不修改已有内容):
/** 设置的本地存储 key */
export const STORAGE_KEY_SETTINGS = 'settings_v1';
/** 提前天数选项的显示文本(与 BEFORE_OPTIONS 下标一一对应) */
export const BEFORE_OPTIONS_LABEL = BEFORE_OPTIONS.map((d) => `{{d}} 天`);
子任务 2:扩展 src/utils/storage.ts
在文件末尾追加以下代码(不修改已有内容):
import { STORAGE_KEY_SETTINGS } from '@/constants';
export interface AppSettings {
defaultTime: string; // HH:mm,默认提醒时间
defaultBefore: number; // 默认提前天数
subscribeEnabled: boolean; // 订阅消息授权开关
}
const DEFAULT_SETTINGS: AppSettings = {
defaultTime: '09:00',
defaultBefore: 7,
subscribeEnabled: false
};
/** 读取应用设置,不存在时返回默认值 */
export function loadSettings(): AppSettings {
try {
const data = Taro.getStorageSync(STORAGE_KEY_SETTINGS);
if (data && typeof data === 'object') {
return { ...DEFAULT_SETTINGS, ...data };
}
return { ...DEFAULT_SETTINGS };
} catch {
return { ...DEFAULT_SETTINGS };
}
}
/** 将应用设置写入本地存储 */
export function saveSettings(settings: AppSettings): void {
try {
Taro.setStorageSync(STORAGE_KEY_SETTINGS, settings);
} catch {
// 静默处理
}
}
⚠️
storage.ts文件顶部已有import Taro from "@tarojs/taro",追加时不重复 import。同理,STORAGE_KEY_SETTINGS的 import 需并入已有的 import 行。
子任务 3:在 src/hooks/useReminders.ts 中新增 useProfileStats
在文件末尾追加(不修改已有 hooks):
/** 获取"我的"页面所需的统计数据 */
export function useProfileStats() {
const reminders = useReminderStore().reminders;
return useMemo(() => {
const activeList = reminders.filter((r) => r.status === 'active');
const historyTotal = reminders.reduce(
(sum, r) => sum + r.history.length,
0
);
return {
/** 启用中的提醒数 */
activeCount: activeList.length,
/** 历史开药记录总条数(所有提醒的 history 之和) */
historyTotal,
/** 管理中的提醒总数(含暂停,不含删除) */
total: reminders.filter((r) => r.status !== 'deleted').length,
/** 未读通知数:M7 接入后从 notificationStore 获取;当前固定为 0 */
unreadCount: 0
};
}, [reminders]);
}
子任务 4:实现 src/pages/profile/index.scss
完整替换 src/pages/profile/index.scss:
@use '@/assets/styles/variables' as *;
.profile-page {
min-height: 100vh;
background: $bg;
// ─── 滚动内容区 ────────────────────────────────────────────────
&__content {
display: grid;
gap: $spacing-4;
padding: 0 $spacing-8 $spacing-9;
}
// ─── ProfileCard ───────────────────────────────────────────────
&__card {
display: flex;
align-items: center;
gap: $spacing-5;
padding: $spacing-6;
border-radius: $radius-lg;
background: $surface-raised;
border: 1px solid $line-soft;
box-shadow: $shadow-card;
}
&__avatar {
width: 92px;
height: 92px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 32px;
background: linear-gradient(135deg, $primary, $success);
color: #fff;
font-size: $text-2xl;
font-weight: 900;
flex-shrink: 0;
}
&__user-name {
display: block;
color: $ink-strong;
font-size: $text-xl;
font-weight: 900;
line-height: 1.2;
}
&__user-meta {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
line-height: 1.6;
}
// ─── MetricsRow ────────────────────────────────────────────────
&__metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $spacing-4;
}
&__metric {
min-height: 126px;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: $spacing-5 $spacing-5;
border-radius: $radius-lg;
background: $card;
border: 1px solid $line-soft;
box-shadow: $shadow-card;
gap: $spacing-2;
}
&__metric-value {
display: block;
color: $ink-strong;
font-size: $text-2xl;
line-height: 1.12;
font-weight: 900;
}
&__metric-label {
display: block;
color: $muted;
font-size: $text-xs;
font-weight: 700;
}
// ─── InboxEntry ────────────────────────────────────────────────
&__inbox-entry {
display: flex;
align-items: flex-start;
gap: $spacing-4;
padding: $spacing-5;
border-radius: $radius-lg;
background: $surface-raised;
border: 1px solid $line-soft;
box-shadow: $shadow-card;
width: 100%;
text-align: left;
}
&__inbox-main {
flex: 1;
display: grid;
gap: $spacing-3;
min-width: 0;
}
&__inbox-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-4;
}
&__inbox-title {
display: block;
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
}
&__inbox-subtitle {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
}
&__inbox-arrow {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
background: #fff;
color: $primary-deep;
border: 1px solid $line-soft;
box-shadow: $shadow-card;
flex-shrink: 0;
}
&__inbox-preview {
padding: $spacing-4 $spacing-5;
border-radius: $radius;
background: linear-gradient(
135deg,
rgba(216, 241, 232, 0.6),
rgba(228, 247, 237, 0.6)
);
border: 1px solid $line-soft;
}
&__inbox-preview-title {
display: block;
color: $ink-strong;
font-size: $text-sm;
font-weight: 900;
}
&__inbox-preview-meta {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
line-height: 1.55;
}
// ─── MenuList ──────────────────────────────────────────────────
&__menu {
display: grid;
gap: $spacing-4;
}
&__menu-item {
min-height: 100px;
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-5;
padding: 0 $spacing-5;
border-radius: $radius-lg;
background: $surface-raised;
border: 1px solid $line-soft;
width: 100%;
text-align: left;
}
&__menu-copy {
display: grid;
gap: 4px;
}
&__menu-label {
display: block;
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
}
&__menu-desc {
display: block;
color: $muted;
font-size: $text-xs;
}
&__menu-value {
color: $ink-strong;
font-size: $text-base;
font-weight: 900;
flex-shrink: 0;
}
// ─── Toggle Switch(纯 CSS,不依赖外部组件) ──────────────────
&__switch {
width: 86px;
height: 52px;
padding: 5px;
border-radius: $radius-pill;
background: #caded8;
transition: background $duration ease-out;
flex-shrink: 0;
border: none;
&::before {
content: '';
display: block;
width: 42px;
height: 42px;
border-radius: 50%;
background: #fff;
box-shadow: 0 4px 12px rgba(18, 51, 61, 0.14);
transition: transform $duration ease-out;
}
&--on {
background: $success;
&::before {
transform: translateX(34px);
}
}
}
// ─── 通知记录 BottomSheet 占位内容 ────────────────────────────
&__inbox-placeholder {
padding: $spacing-6;
text-align: center;
color: $muted;
font-size: $text-sm;
line-height: 1.6;
}
}
子任务 5:实现 src/pages/profile/index.tsx
完整替换 src/pages/profile/index.tsx:
import { useState } from "react";
import { Picker, Text, View } from "@tarojs/components";
import Taro, { useLoad } from "@tarojs/taro";
import AppBar from "@/components/AppBar";
import BottomSheet from "@/components/BottomSheet";
import { BEFORE_OPTIONS, BEFORE_OPTIONS_LABEL } from "@/constants";
import { useProfileStats } from "@/hooks/useReminders";
import { loadSettings, saveSettings } from "@/utils/storage";
import type { AppSettings } from "@/utils/storage";
import "./index.scss";
export default function Profile() {
const stats = useProfileStats();
const [settings, setSettings] = useState<AppSettings>(() => loadSettings());
const [inboxOpen, setInboxOpen] = useState(false);
useLoad(() => {
setSettings(loadSettings());
});
// ─── 设置更新辅助 ──────────────────────────────────────────────
const updateSettings = (patch: Partial<AppSettings>) => {
setSettings((prev) => {
const next = { ...prev, ...patch };
saveSettings(next);
return next;
});
};
// ─── 订阅消息授权 Toggle ───────────────────────────────────────
const handleToggleSubscribe = () => {
if (!settings.subscribeEnabled) {
// M9 接入后替换为真实的 requestSubscribeMessage 调用
Taro.showToast({
title: "M9 接入后开启微信推送",
icon: "none",
duration: 1500,
});
}
updateSettings({ subscribeEnabled: !settings.subscribeEnabled });
};
// ─── 默认提醒时间 Picker ───────────────────────────────────────
const handleTimeChange = (e: { detail: { value: string } }) => {
updateSettings({ defaultTime: e.detail.value });
Taro.showToast({
title: `默认提醒时间已设为 {{e_detail_value}}`,
icon: "none",
duration: 1200,
});
};
// ─── 默认提前天数 Picker ───────────────────────────────────────
const beforeIdx = BEFORE_OPTIONS.indexOf(
settings.defaultBefore as (typeof BEFORE_OPTIONS)[number],
);
const safeBeforeIdx = beforeIdx >= 0 ? beforeIdx : 0;
const handleBeforeChange = (e: { detail: { value: number } }) => {
const idx = e.detail.value;
const days = BEFORE_OPTIONS[idx] ?? BEFORE_OPTIONS[0];
updateSettings({ defaultBefore: days });
Taro.showToast({
title: `默认提前天数已设为 {{days}} 天`,
icon: "none",
duration: 1200,
});
};
return (
<View className="profile-page">
<AppBar
title="我的"
subtitle="提醒授权、通知记录与默认设置"
right={
<View className="profile-page__appbar-icon" aria-label="设置">
{/* 设置图标 SVG */}
<svg
viewBox="0 0 24 24"
fill="none"
style={{ width: 34, height: 34, strokeWidth: 2.1 }}
>
<path
d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19 13.5a7.8 7.8 0 0 0 0-3l2-1.5-2-3.5-2.4 1a8 8 0 0 0-2.6-1.5L13.7 2h-4l-.4 3a8 8 0 0 0-2.6 1.5l-2.4-1-2 3.5 2 1.5a7.8 7.8 0 0 0 0 3l-2 1.5 2 3.5 2.4-1a8 8 0 0 0 2.6 1.5l.4 3h4l.4-3a8 8 0 0 0 2.6-1.5l2.4 1 2-3.5-2.1-1.5Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</View>
}
/>
<View className="profile-page__content">
{/* ─── ProfileCard ─── */}
<View className="profile-page__card">
<View className="profile-page__avatar">
<Text>药</Text>
</View>
<View>
<Text className="profile-page__user-name">微信用户</Text>
<Text className="profile-page__user-meta">
管理中 {stats.total} 个提醒 · 历史 {stats.historyTotal} 次开药
</Text>
</View>
</View>
{/* ─── MetricsRow ─── */}
<View className="profile-page__metrics">
<View className="profile-page__metric">
<Text className="profile-page__metric-value">{stats.activeCount}</Text>
<Text className="profile-page__metric-label">启用中</Text>
</View>
<View className="profile-page__metric">
<Text className="profile-page__metric-value">{stats.unreadCount}</Text>
<Text className="profile-page__metric-label">未读通知</Text>
</View>
<View className="profile-page__metric">
<Text className="profile-page__metric-value">{stats.historyTotal}</Text>
<Text className="profile-page__metric-label">历史记录</Text>
</View>
</View>
{/* ─── InboxEntry(通知记录入口) ─── */}
<View
className="profile-page__inbox-entry"
onClick={() => setInboxOpen(true)}
role="button"
>
<View className="profile-page__inbox-main">
<View className="profile-page__inbox-head">
<View>
<Text className="profile-page__inbox-title">历史提醒记录</Text>
<Text className="profile-page__inbox-subtitle">
查看微信订阅消息与小程序内兜底通知
</Text>
</View>
<View className="profile-page__inbox-arrow">
<svg
viewBox="0 0 24 24"
fill="none"
style={{ width: 28, height: 28, strokeWidth: 2.1 }}
>
<path
d="m9 6 6 6-6 6"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</View>
</View>
<View className="profile-page__inbox-preview">
<Text className="profile-page__inbox-preview-title">
{stats.unreadCount > 0
? `{{stats_unreadCount}} 条未读通知`
: "暂无未读通知"}
</Text>
<Text className="profile-page__inbox-preview-meta">
消息入口用于兜底查看提醒,不替代首页和列表的任务操作。
</Text>
</View>
</View>
</View>
{/* ─── MenuList(设置列表) ─── */}
<View className="profile-page__menu">
{/* 订阅消息授权 */}
<View className="profile-page__menu-item">
<View className="profile-page__menu-copy">
<Text className="profile-page__menu-label">订阅消息授权</Text>
<Text className="profile-page__menu-desc">到点后自动发送微信提醒</Text>
</View>
<View
className={`profile-page__switch${settings.subscribeEnabled ? " profile-page__switch--on" : ""}`}
onClick={handleToggleSubscribe}
role="switch"
aria-checked={settings.subscribeEnabled}
/>
</View>
{/* 默认提醒时间 */}
<Picker mode="time" value={settings.defaultTime} onChange={handleTimeChange}>
<View className="profile-page__menu-item">
<View className="profile-page__menu-copy">
<Text className="profile-page__menu-label">默认提醒时间</Text>
<Text className="profile-page__menu-desc">新建提醒时自动带入</Text>
</View>
<Text className="profile-page__menu-value">{settings.defaultTime}</Text>
</View>
</Picker>
{/* 默认提前天数 */}
<Picker
mode="selector"
range={BEFORE_OPTIONS_LABEL}
value={safeBeforeIdx}
onChange={handleBeforeChange}
>
<View className="profile-page__menu-item">
<View className="profile-page__menu-copy">
<Text className="profile-page__menu-label">默认提前天数</Text>
<Text className="profile-page__menu-desc">建议首次使用默认 7 天</Text>
</View>
<Text className="profile-page__menu-value">{settings.defaultBefore} 天</Text>
</View>
</Picker>
{/* 通知归档(只读) */}
<View className="profile-page__menu-item">
<View className="profile-page__menu-copy">
<Text className="profile-page__menu-label">通知归档</Text>
<Text className="profile-page__menu-desc">包含已读与未读提醒消息</Text>
</View>
<Text className="profile-page__menu-value">{stats.historyTotal} 条</Text>
</View>
</View>
</View>
{/* ─── 通知记录 BottomSheet(M7 接入后替换内容) ─── */}
<BottomSheet
open={inboxOpen}
title="历史提醒记录"
onClose={() => setInboxOpen(false)}
>
<View className="profile-page__inbox-placeholder">
<Text>通知记录功能将在 M7 中实现。</Text>
</View>
</BottomSheet>
</View>
);
}
子任务 6:验证 AppBar 组件接口
在实现前,先确认 src/components/AppBar/index.tsx 当前接受的 props。
若 AppBar 不支持 right prop(显示右侧操作区),则:
- 方案 A(推荐):在 profile 页面的
AppBar下方自行渲染顶部区域,不依赖 AppBar 的 right prop,直接在profile-page的布局里用自定义appbar节点代替。 - 方案 B:修改 AppBar 组件,添加可选的
right?: React.ReactNodeprop。
选择哪种方案以最小改动为原则:若 AppBar 已有 right prop 则直接用;若没有,采用方案 A(不改动 AppBar)。
若采用方案 A,请将 AppBar 替换为以下自定义 JSX 片段(内联在 profile 页面顶部):
{
/* 顶部 AppBar 区域(方案 A:内联实现,不改动 AppBar 组件) */
}
<View className="profile-page__appbar">
<View>
<Text className="profile-page__appbar-title">我的</Text>
<Text className="profile-page__appbar-subtitle">
提醒授权、通知记录与默认设置
</Text>
</View>
<View className="profile-page__appbar-icon" aria-label="设置">
<svg
viewBox="0 0 24 24"
fill="none"
style={{ width: 34, height: 34, strokeWidth: 2.1 }}
>
<path
d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19 13.5a7.8 7.8 0 0 0 0-3l2-1.5-2-3.5-2.4 1a8 8 0 0 0-2.6-1.5L13.7 2h-4l-.4 3a8 8 0 0 0-2.6 1.5l-2.4-1-2 3.5 2 1.5a7.8 7.8 0 0 0 0 3l-2 1.5 2 3.5 2.4-1a8 8 0 0 0 2.6-1.5l2.4 1 2-3.5-2.1-1.5Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</View>
</View>;
对应追加的 SCSS(放入 profile-page 块内):
// ─── AppBar(方案 A 内联实现)──────────────────────────────────
&__appbar {
min-height: 112px;
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-4;
padding: $spacing-3 $spacing-8 $spacing-4;
}
&__appbar-title {
display: block;
color: $ink-strong;
font-size: $text-2xl;
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.14;
}
&__appbar-subtitle {
display: block;
margin-top: 6px;
color: $muted;
font-size: $text-base;
font-weight: 600;
}
&__appbar-icon {
width: 76px;
height: 76px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 24px;
color: $ink;
background: rgba(255, 255, 255, 0.96);
border: 1px solid $line-soft;
box-shadow: $shadow-card;
flex-shrink: 0;
}
验收标准
-
pnpm dev:weapp无报错启动,"我的"Tab 可正常切换进入 - ProfileCard 正确展示"管理中 N 个提醒 · 历史 N 次开药",数字与实际数据一致
- MetricsRow 三格数字(启用中 / 未读通知 / 历史记录)渲染正确
- InboxEntry 显示"暂无未读通知"(unreadCount = 0 时),点击后打开 BottomSheet,内部显示"通知记录功能将在 M7 中实现"
- 点击"订阅消息授权"Toggle,开关状态在视觉上切换,同时弹出 Toast 提示"M9 接入后开启微信推送",状态写入本地存储,重启后恢复
- 点击"默认提醒时间"行,弹出系统时间选择器,选择后行内展示新值,写入本地存储
- 点击"默认提前天数"行,弹出系统列表选择器(选项来自 BEFORE_OPTIONS_LABEL),选择后行内展示新值,写入本地存储
- 修改默认时间/提前天数后,在新增提醒表单(M5 ReminderForm)中重新打开时,默认值已更新为修改后的值(需验证 ReminderForm 中读取
loadSettings()或使用 DEFAULT_REMIND_TIME / DEFAULT_BEFORE 常量) - "通知归档"行显示历史记录总条数,只读无点击反馈
- 页面布局与
ui6.0/index.html的 profile screen 视觉一致(卡片圆角、色板、间距)
备注:M7 接入说明
M7(通知记录)开发时,需在本模块基础上完成以下接入:
- 新建
src/components/NotificationInbox/index.tsx,实现通知记录列表(含筛选 Tab、通知卡片、批量已读等) - 将
src/pages/profile/index.tsx中的profile-page__inbox-placeholder替换为<NotificationInbox />组件 - 在
reminderStore中引入notifications: NotificationRecord[]字段及相关操作(markNotificationRead、markAllNotificationsRead) - 更新
useProfileStats中unreadCount的计算逻辑,从 store 中读取真实未读数
