Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M12.prompt.md). Fill in{{medicine_expiryDate}}before use. Copyright stays with the author.
M12 · 药箱 Tab & MedicineInventoryCard 组件
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand + @taroify/core 专家。请按照以下规范,逐步完成 M12 药箱 Tab 与 MedicineInventoryCard 组件开发。完成每个子任务后自行验证,确认无误再进入下一个。
本模块承接 M10 / M10a / M11 的统一 Medicine 数据层与 MedicineComposer:新增「药箱」Tab 的完整页面,用于展示所有未删除药品,包含开启开药提醒的药品和仅药箱备忘的药品。药箱页不承担风险中心职责,不展示开药倒计时、逾期天数、进度条、库存估算或库存风险。
参考资料
- 开发计划:
docs/3.开发计划.md的「V1.1 药箱模块开发计划」与「M12 · 药箱 Tab & MedicineInventoryCard 组件」 - UI 参考:
ui7.0/index.htmlrenderMedicines():药箱搜索、全部 / 已开提醒 / 未开提醒筛选、空状态inventoryCard(item):药品资料卡片结构、服药信息标签、提醒启用状态和编辑入口- FAB 行为:在药箱页点击右下角新增时打开
MedicineComposer,默认reminderEnabled=false
- 前置模块:
- M10:已建立
Medicine/DerivedMedicine类型、常量和药箱选择器 - M10a:已完成兼容迁移,提醒视角与药箱视角数据口径分离
- M11:已完成
MedicineComposer统一新增 / 编辑 Sheet 和useReminderSheet的defaultReminderEnabled支持
- M10:已建立
当前项目状态
src/app.config.ts已包含pages/medicines/index,Tab 顺序当前为:首页 / 提醒 / 药箱 / 我的src/pages/medicines/index.tsx当前仍是占位页,仅展示“药箱 — 开发中”src/pages/medicines/index.config.ts已配置标题“药箱”src/pages/medicines/index.scss当前需要替换为药箱页样式src/components/MedicineComposer/已存在:- 支持
medicineId - 支持
defaultReminderEnabled onSuccess后页面负责关闭 Sheet
- 支持
src/hooks/useReminderSheet.ts已支持:openCreate(defaultReminderEnabled = true)openEdit(id)sheetTitlesheetActivehandleFormSuccess
src/hooks/useReminders.ts已提供:useAllDerivedMedicines():药箱视角,返回所有未删除药品useReminderActions():包含deleteReminder
src/utils/dateUtils.ts已提供:deriveAllMedicines():过滤 deleted,reminderEnabled=true优先,同级按name升序scheduleLabel:固定时间时展示固定时间 HH:mm
src/types/index.ts已定义Medicine/DerivedMedicine
技术约束
- 保持函数式组件 + Hooks + TypeScript 类型安全
- 优先使用
@taroify/core组件;没有合适组件时再使用@tarojs/components - 不新增云数据库集合,不修改云函数扫描逻辑,不重写 M9 云开发认证和订阅消息
- 不实现首页 / 提醒列表适配新数据层的 M13 内容
- 不改变
deriveAll()的提醒视角过滤规则 - 不做库存估算,不引入
stockPerPrescription、estimatedStock、stockDays等字段 - 药箱卡片不展示开药倒计时、逾期天数、进度条、库存风险或剩余量估算
- 不删除
ReminderForm,不回退 M11 已完成的MedicineComposer入口 - 修改保持小步精准,不顺手重构首页、提醒列表、我的页或云端服务
本模块新增 / 修改文件
src/
├── components/
│ └── MedicineInventoryCard/
│ ├── index.tsx ← 新建:药品资料卡片
│ └── index.scss ← 新建:卡片样式
└── pages/
└── medicines/
├── index.tsx ← 修改:药箱主页面
├── index.config.ts ← 检查:页面标题与分享配置
└── index.scss ← 修改:页面样式
src/app.config.ts ← 检查:药箱 Tab 顺序和图标,不必要不改
如发现 src/app.config.ts 已符合:首页 / 提醒 / 药箱 / 我的,且图标路径为 assets/tabbar/medicines.png、assets/tabbar/medicines-active.png,不要为了格式化而修改它。
子任务 1:新建 MedicineInventoryCard 组件
新建:
src/components/MedicineInventoryCard/index.tsxsrc/components/MedicineInventoryCard/index.scss
1.1 Props
interface MedicineInventoryCardProps {
medicine: DerivedMedicine;
onClick?: () => void;
}
说明:
onClick用于进入编辑 Sheet- 不在卡片内部直接调用 store
- 不在卡片内部打开
BottomSheet - 卡片只负责展示药品资料和点击入口
1.2 展示结构
卡片结构按以下信息组织:
MedicineInventoryCard
├── 药品名称(粗体 16px)
├── 第二栏:规格 · 剂型 · 过期 YYYY-MM-DD
├── 服药信息标签:
│ ├── 每次 {dosagePerUse}{dosageUnit}
│ ├── 每日 {timesPerDay} 次
│ └── {scheduleLabel}
└── 底部操作行
├── reminderEnabled=true:"提醒已启用" + "已进入提醒列表"
├── reminderEnabled=false:"未启用提醒" + "仅药箱备忘"
└── 编辑按钮 / 点击提示
字段规则:
- 药品名称:
medicine.name || '未命名药品' - 规格为空时显示:
未填写规格 - 剂型展示使用
MEDICINE_FORM_OPTIONS的 label,不直接展示tablet/capsule等 value - 过期时间:
- 有值:
过期 {{medicine_expiryDate}} - 无值:
未填过期时间
- 有值:
- 服用时机直接使用
medicine.scheduleLabel - 如果
medicine.scheduleTiming === '固定时间'但scheduleLabel不可用,兜底为固定时间 ${medicine.scheduleTime || '08:00'}
1.3 组件行为
- 整张卡片可点击,触发
onClick - 编辑按钮点击同样触发
onClick - 避免点击编辑按钮时触发两次,可使用
stopPropagation - 添加可访问性信息:
- 卡片使用
role="button"或等效可点击语义 - 编辑按钮有明确文案或
aria-label - 状态标签文字不能只靠颜色表达
- 卡片使用
子任务 2:实现药箱页数据、搜索和筛选
修改:
src/pages/medicines/index.tsxsrc/pages/medicines/index.scss
2.1 页面数据
使用:
const medicines = useAllDerivedMedicines();
药箱页必须展示所有未删除药品,包括:
reminderEnabled=true的开药提醒药品reminderEnabled=false的纯药箱备忘药品
不要使用 useDerivedList(),因为它是首页 / 提醒列表视角,只返回开启提醒的药品。
2.2 搜索
页面顶部添加搜索框,按以下字段过滤:
- 药品名称:
name - 规格:
spec - 剂型:
MEDICINE_FORM_OPTIONSlabel 或form - 服用时机:
scheduleTiming - 固定时间:
scheduleTime - 派生服用文案:
scheduleLabel - 过期时间:
expiryDate
搜索要求:
- 本地状态维护关键词
- 输入为空时展示筛选后的全部结果
- 使用
trim().toLowerCase()做宽松匹配 - 中文字段直接
includes即可,不需要拼音搜索
2.3 筛选
筛选仅包含:
all:全部enabled:已开提醒disabled:未开提醒
筛选 UI 可使用轻量按钮 / chip 风格,并展示数量:
- 全部:所有未删除药品数量
- 已开提醒:
reminderEnabled=true - 未开提醒:
reminderEnabled=false
不要加入逾期、7 天内、正常、暂停等提醒风险筛选;这些属于提醒列表,不属于药箱页。
2.4 排序
优先沿用 useAllDerivedMedicines() / deriveAllMedicines() 已经提供的排序:
reminderEnabled=true优先- 同级按
name升序
页面内过滤后不要再引入新的风险排序。
子任务 3:接入新增 / 编辑 Sheet
药箱页使用现有:
BottomSheetMedicineComposeruseReminderSheet
3.1 新增药品
页面右下角添加悬浮新增按钮:
- 文案或可访问性名称:
新增药品 - 点击调用:
openCreate(false) - 打开的 Sheet 标题应为:
新增药品 MedicineComposer传入defaultReminderEnabled={false}
参考调用:
<MedicineComposer
key={formKey}
medicineId={editReminderId}
defaultReminderEnabled={defaultReminderEnabled}
onSuccess={handleFormSuccess}
onCancel={closeSheet}
/>
注意:不要在药箱页把 defaultReminderEnabled 写死为 true。
3.2 编辑药品
- 点击
MedicineInventoryCard调用openEdit(medicine.id) - 打开的 Sheet 标题应为:
编辑药品 - 编辑模式由
medicineId={editReminderId}回填 - 编辑时
defaultReminderEnabled实际由已有药品数据决定,不要强行覆盖提醒开关
3.3 删除确认
药箱页需要提供删除入口,并有确认弹窗。
实现方式可二选一,优先选对当前代码改动最小的方案:
- 在药箱页为编辑 Sheet 增加一个轻量“删除药品”操作区,调用
deleteReminder(editReminderId) - 或在
MedicineInventoryCard增加onDeleteprop 和删除按钮
删除要求:
- 调用
Taro.showModal - 标题建议:
删除药品 - 内容建议:
删除后将不再出现在药箱和提醒列表,历史记录也不会继续展示。 - 用户确认后调用
useReminderActions().deleteReminder(id) - 删除成功后关闭 Sheet 或保持列表刷新,并 Toast:
药品已删除 - 删除取消时不做任何数据修改
不要硬删除本地数组;使用 store 的 deleteReminder,保持云端和本地状态一致。
子任务 4:空状态和页面状态
4.1 无药品
当 useAllDerivedMedicines() 返回空数组时展示空状态:
- 标题:
药箱还是空的 - 辅助文案:
先保存一个药品备忘,之后也可以随时开启开药提醒。 - 主按钮:
新增药品,点击openCreate(false)
4.2 有药品但筛选无结果
当存在药品,但当前搜索 / 筛选没有结果:
- 标题:
当前筛选下没有药品 - 辅助文案:
换个关键词,或切换筛选查看全部药箱记录。
4.3 加载和云端刷新
如果当前页面模式和首页 / 列表一致,且项目中已有云端加载入口,可保持现状。
本模块不要求新增全局 loading 体系;但不要阻断已有 store 云端同步。药箱页应消费 store 当前数据,并随 store 更新自动刷新。
子任务 5:样式要求
src/pages/medicines/index.scss 和 src/components/MedicineInventoryCard/index.scss 需符合现有小程序风格。
要求:
- 页面背景、搜索框、筛选 chip、卡片风格与首页 / 提醒列表保持一致
- 顶部布局适合小程序安全区和 TabBar 页面
- 控件点击区域不小于 44px
- 搜索框、筛选、空状态、悬浮按钮在小屏下不重叠
- 药品名称、规格、剂型、过期时间较长时可换行或省略,不溢出容器
- 卡片标签数量固定且稳定,不因文本变化造成布局跳动明显
reminderEnabled=true和reminderEnabled=false有清晰视觉区别,但不能只依赖颜色- 不使用大面积装饰渐变,不引入新的大色系
- 不要把页面 section 包成多层嵌套卡片;卡片只用于单个药品项和空状态
子任务 6:页面配置检查
检查 src/pages/medicines/index.config.ts:
navigationBarTitleText应为药箱或我的药箱- 保持现有分享配置即可,不必要不改
检查 src/app.config.ts:
pages包含pages/medicines/index- TabBar 中药箱位于提醒之后、我的之前
- 图标路径:
iconPath: 'assets/tabbar/medicines.png';
selectedIconPath: 'assets/tabbar/medicines-active.png';
如已经符合,不要修改。
子任务 7:回归检查
完成后至少检查:
7.1 静态扫描
运行:
rg -n "MedicineInventoryCard|useAllDerivedMedicines|openCreate\\(false\\)|defaultReminderEnabled" src/pages/medicines src/components src/hooks
确认:
- 药箱页使用
useAllDerivedMedicines() - 药箱新增入口调用
openCreate(false) MedicineComposer在药箱页收到defaultReminderEnabled={defaultReminderEnabled}或等效传递MedicineInventoryCard已被药箱页渲染
再运行:
rg -n "useDerivedList|progress|daysLeft|levelLabel|estimatedStock|stockDays|stockPerPrescription" src/pages/medicines src/components/MedicineInventoryCard
期望:
- 药箱页不使用
useDerivedList MedicineInventoryCard不展示倒计时、进度、库存估算或库存风险- 如果
levelLabel只在误扫注释中出现,也应移除,避免后续误用
7.2 TypeScript 检查
优先运行:
pnpm exec tsc --noEmit
如项目当前没有独立 tsc 配置或存在环境限制,运行项目等效检查,并在最终说明中写清楚。
7.3 启动检查
运行:
pnpm dev:weapp
看到编译成功或无 M12 新增错误后即可停止。
验收标准
页面
- TabBar 可正常切换到药箱页
-
src/pages/medicines/index.tsx不再是占位页 - 药箱页展示所有未删除药品,包含
reminderEnabled=false的药品 - 药箱页不使用
useDerivedList()作为数据源 - 药箱页支持搜索药品名称、规格、剂型、服用时机、固定时间和过期时间
- 药箱筛选仅包含:全部 / 已开提醒 / 未开提醒
- 空药箱和筛选无结果分别有明确空状态
卡片
-
src/components/MedicineInventoryCard/index.tsx和index.scss存在 - 卡片展示药品名称、规格、剂型、过期时间
- 卡片展示每次用量、每日次数、服用时机
- 固定时间服用展示具体时间点
-
reminderEnabled=true展示“提醒已启用 / 已进入提醒列表” -
reminderEnabled=false展示“未启用提醒 / 仅药箱备忘” - 卡片不展示开药倒计时、逾期天数、进度条、剩余量估算或库存风险
交互
- 点击悬浮按钮打开
MedicineComposer,标题为“新增药品” - 药箱新增时
defaultReminderEnabled=false,默认不展开提醒区 - 点击卡片打开编辑 Sheet,回填数据正确
- 编辑后列表自动刷新并保持药箱视角展示
- 删除有确认弹窗,确认后药品从药箱列表移除
- 删除
reminderEnabled=true的药品后,也不会继续出现在首页 / 提醒列表
回归
- 首页和提醒列表仍只展示
reminderEnabled=true的提醒视角数据 - M11 的首页 / 列表新增开药提醒入口仍默认开启提醒
- 已开药、暂停 / 启用、详情查看不因 M12 回归
-
pnpm exec tsc --noEmit或等效 TypeScript 检查无 M12 新增错误 -
pnpm dev:weapp可启动,无 M12 引入的编译错误
开发提示
- 先实现
MedicineInventoryCard,再实现药箱页搜索 / 筛选,最后接入新增 / 编辑 / 删除 Sheet - 药箱页是资料管理视角,风险状态、进度和倒计时都应留在首页 / 提醒列表
- 剂型 label 可以从
MEDICINE_FORM_OPTIONS映射得到,避免直接把枚举 value 暴露给用户 useReminderSheet已能根据openCreate(false)给出“新增药品”标题,药箱页应复用它,不要另写一套 Sheet 状态机- 删除操作使用
deleteReminder,该 API 会把状态置为 deleted 并同步云端;不要在页面内手动过滤后假装删除成功