Prompt file imported from lxKylin/yucheng-mini (
.github/prompts/M2.prompt.md). Fill in{{y}},{{m}},{{d}},{{daysLeft}}before use. Copyright stays with the author.
M2 · 数据层 & 业务逻辑模块开发
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand 专家。请按照以下规范,逐步完成 M2 数据层与业务逻辑模块的全部开发内容。完成每个子任务后自行验证,确认无误再进入下一个。
当前项目状态
- M1 基础架构已完成:4 个 TabBar 页面占位、全局 SCSS 变量、
@taroify/core已安装、Zustand 已安装 src/types/index.ts仅含export {},需填充完整类型定义src/constants/index.ts仅含export {},需填充常量src/utils/目录为空,需创建dateUtils.ts和storage.tssrc/store/index.ts为示例代码,需替换为业务 Store(src/store/reminderStore.ts)- MVP 阶段使用本地存储(
Taro.setStorageSync)模拟数据库,云开发集成在 M9 替换
子任务 1:完整类型定义(src/types/index.ts)
完整替换 src/types/index.ts:
/** 提醒状态 */
export type ReminderStatus = 'active' | 'paused' | 'deleted';
/** 提醒紧急等级(用于 UI 颜色) */
export type ReminderLevel = 'danger' | 'warning' | 'good' | 'paused';
/** 药物提醒主体 */
export interface Reminder {
id: string;
name: string; // 药物名称
spec: string; // 规格描述
lastDate: string; // 最近开药日期 YYYY-MM-DD
interval: number; // 开药间隔(天)
before: number; // 提前提醒天数
time: string; // 提醒时间 HH:mm
status: ReminderStatus;
note: string; // 备注
history: string[]; // 历史开药日期列表(最近 5 条,YYYY-MM-DD)
createdAt: string; // ISO 字符串
updatedAt: string; // ISO 字符串
}
/** 派生计算数据(不持久化,每次从 Reminder 实时计算) */
export interface DerivedReminder extends Reminder {
nextDate: string; // 预计下次开药日期 YYYY-MM-DD
remindAt: string; // 实际触发提醒日期 YYYY-MM-DD
daysLeft: number; // 距今天数(负数=逾期,0=今日)
level: ReminderLevel;
levelLabel: string; // "5 天后" / "逾期 2 天" / "今日" / "已暂停"
progress: number; // 进度条百分比 0-100
}
/** 历史开药记录(为后续云开发扩展预留) */
export interface PrescriptionRecord {
id: string;
reminderId: string;
prescriptionDate: string; // YYYY-MM-DD
note: string;
createdAt: string;
}
子任务 2:常量定义(src/constants/index.ts)
完整替换 src/constants/index.ts:
/** 快捷间隔选项(天) */
export const INTERVAL_OPTIONS = [7, 14, 30, 45, 60, 90] as const;
/** 快捷提前提醒天数选项(天) */
export const BEFORE_OPTIONS = [1, 3, 7, 14] as const;
/** 默认提醒时间 */
export const DEFAULT_REMIND_TIME = '09:00';
/** 默认开药间隔(天) */
export const DEFAULT_INTERVAL = 30;
/** 默认提前提醒天数 */
export const DEFAULT_BEFORE = 3;
/** 本地存储 key */
export const STORAGE_KEY_REMINDERS = 'reminders_v1';
/** 历史记录最大保留条数 */
export const HISTORY_MAX = 5;
/** level 对应的排序权重(数字越小越靠前) */
export const LEVEL_ORDER: Record<string, number> = {
danger: 0,
warning: 1,
good: 2,
paused: 3
};
子任务 3:本地存储封装(src/utils/storage.ts)
创建 src/utils/storage.ts:
import Taro from '@tarojs/taro';
import type { Reminder } from '@/types';
import { STORAGE_KEY_REMINDERS } from '@/constants';
/** 从本地存储读取提醒列表,不存在时返回空数组 */
export function loadReminders(): Reminder[] {
try {
const data = Taro.getStorageSync(STORAGE_KEY_REMINDERS);
if (Array.isArray(data)) return data;
return [];
} catch {
return [];
}
}
/** 将提醒列表写入本地存储 */
export function saveReminders(reminders: Reminder[]): void {
try {
Taro.setStorageSync(STORAGE_KEY_REMINDERS, reminders);
} catch {
// 存储失败时静默处理,不影响内存中的数据
}
}
子任务 4:日期工具函数(src/utils/dateUtils.ts)
创建 src/utils/dateUtils.ts。此文件是核心业务逻辑,所有日期计算均在此完成:
import type { Reminder, DerivedReminder, ReminderLevel } from '@/types';
import { LEVEL_ORDER } from '@/constants';
// ─── 基础日期工具 ─────────────────────────────────────────────────
/** 将 YYYY-MM-DD 字符串解析为本地零时 Date,避免时区偏移 */
export function parseDate(dateStr: string): Date {
const [y, m, d] = dateStr.split('-').map(Number);
return new Date(y, m - 1, d);
}
/** 格式化 Date 为 YYYY-MM-DD */
export function formatDate(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `{{y}}-{{m}}-{{d}}`;
}
/** 格式化 Date 为 M月D日(展示用) */
export function formatDisplay(date: Date): string {
return `${date.getMonth() + 1}月${date.getDate()}日`;
}
/** 获取今天的 YYYY-MM-DD */
export function today(): string {
return formatDate(new Date());
}
/** 日期加 N 天 */
export function addDays(dateStr: string, days: number): string {
const d = parseDate(dateStr);
d.setDate(d.getDate() + days);
return formatDate(d);
}
/**
* 计算两个日期字符串之间的天数差
* @returns to - from 的天数(to 在 from 之后为正数)
*/
export function diffDays(from: string, to: string): number {
const msPerDay = 86400000;
return Math.round(
(parseDate(to).getTime() - parseDate(from).getTime()) / msPerDay
);
}
// ─── 业务计算 ─────────────────────────────────────────────────────
/** 计算下次开药日期 */
export function calcNextDate(lastDate: string, interval: number): string {
return addDays(lastDate, interval);
}
/** 计算提醒触发日期(下次开药日期 - 提前天数) */
export function calcRemindDate(nextDate: string, before: number): string {
return addDays(nextDate, -before);
}
/**
* 计算进度条百分比 0-100
* 逻辑:从 lastDate 到 nextDate 为一个周期,当前在周期内的位置
* 逾期时进度为 100,不超过 100
*/
function calcProgress(
lastDate: string,
nextDate: string,
todayStr: string
): number {
const total = diffDays(lastDate, nextDate);
if (total <= 0) return 100;
const elapsed = diffDays(lastDate, todayStr);
return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100)));
}
/**
* 根据 daysLeft 和 status 计算 level
* danger:逾期(<0)或今日(=0)
* warning:1-7 天内
* good:7 天以上
* paused:已暂停
*/
function calcLevel(
daysLeft: number,
status: Reminder['status']
): ReminderLevel {
if (status === 'paused') return 'paused';
if (daysLeft <= 0) return 'danger';
if (daysLeft <= 7) return 'warning';
return 'good';
}
/**
* 根据 daysLeft 和 level 生成展示文案
* 示例:"-2" → "逾期 2 天","0" → "今日","5" → "5 天后",paused → "已暂停"
*/
function calcLevelLabel(daysLeft: number, level: ReminderLevel): string {
if (level === 'paused') return '已暂停';
if (daysLeft === 0) return '今日';
if (daysLeft < 0) return `逾期 ${Math.abs(daysLeft)} 天`;
return `{{daysLeft}} 天后`;
}
/** 核心派生函数:将单条 Reminder 计算为 DerivedReminder */
export function derive(reminder: Reminder): DerivedReminder {
const todayStr = today();
const nextDate = calcNextDate(reminder.lastDate, reminder.interval);
const remindAt = calcRemindDate(nextDate, reminder.before);
const daysLeft = diffDays(todayStr, nextDate);
const level = calcLevel(daysLeft, reminder.status);
const levelLabel = calcLevelLabel(daysLeft, level);
const progress =
reminder.status === 'paused'
? calcProgress(reminder.lastDate, nextDate, todayStr)
: calcProgress(reminder.lastDate, nextDate, todayStr);
return {
...reminder,
nextDate,
remindAt,
daysLeft,
level,
levelLabel,
progress
};
}
/**
* 批量派生并按优先级排序
* 排序规则:danger < warning < good < paused,同级按 nextDate 升序
* 已删除(status=deleted)的条目过滤掉
*/
export function deriveAll(reminders: Reminder[]): DerivedReminder[] {
return reminders
.filter((r) => r.status !== 'deleted')
.map(derive)
.sort((a, b) => {
const levelDiff =
(LEVEL_ORDER[a.level] ?? 99) - (LEVEL_ORDER[b.level] ?? 99);
if (levelDiff !== 0) return levelDiff;
return a.nextDate.localeCompare(b.nextDate);
});
}
子任务 5:Zustand 状态管理(src/store/reminderStore.ts)
创建 src/store/reminderStore.ts。MVP 阶段使用本地存储,所有写操作完成后立即持久化:
import { create } from 'zustand';
import type { Reminder, DerivedReminder } from '@/types';
import {
DEFAULT_REMIND_TIME,
DEFAULT_INTERVAL,
DEFAULT_BEFORE,
HISTORY_MAX
} from '@/constants';
import { loadReminders, saveReminders } from '@/utils/storage';
import { derive, deriveAll, today } from '@/utils/dateUtils';
/** 生成唯一 ID(小程序环境不使用 crypto) */
function genId(): string {
return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
type AddPayload = Omit<Reminder, 'id' | 'createdAt' | 'updatedAt'>;
interface ReminderStore {
reminders: Reminder[];
// ─── CRUD ───────────────────────────────────────────────────────
addReminder: (payload: AddPayload) => void;
updateReminder: (id: string, payload: Partial<Reminder>) => void;
deleteReminder: (id: string) => void;
// ─── 业务操作 ────────────────────────────────────────────────────
/** 标记已开药:更新 lastDate,将今日日期插入 history 头部,最多保留 HISTORY_MAX 条 */
markDone: (id: string, date?: string) => void;
/** 切换暂停/启用 */
togglePause: (id: string) => void;
// ─── 查询(返回派生数据) ────────────────────────────────────────
getDerivedList: () => DerivedReminder[];
getById: (id: string) => DerivedReminder | null;
}
export const useReminderStore = create<ReminderStore>((set, get) => ({
reminders: loadReminders(),
addReminder(payload) {
const now = new Date().toISOString();
const newItem: Reminder = {
id: genId(),
createdAt: now,
updatedAt: now,
// 未传入字段使用默认值兜底
time: DEFAULT_REMIND_TIME,
interval: DEFAULT_INTERVAL,
before: DEFAULT_BEFORE,
note: '',
history: [],
status: 'active',
...payload
};
set((state) => {
const next = [...state.reminders, newItem];
saveReminders(next);
return { reminders: next };
});
},
updateReminder(id, payload) {
set((state) => {
const next = state.reminders.map((r) =>
r.id === id
? { ...r, ...payload, updatedAt: new Date().toISOString() }
: r
);
saveReminders(next);
return { reminders: next };
});
},
deleteReminder(id) {
// 软删除:标记 status=deleted,保留数据
set((state) => {
const next = state.reminders.map((r) =>
r.id === id
? {
...r,
status: 'deleted' as const,
updatedAt: new Date().toISOString()
}
: r
);
saveReminders(next);
return { reminders: next };
});
},
markDone(id, date) {
const doneDate = date ?? today();
set((state) => {
const next = state.reminders.map((r) => {
if (r.id !== id) return r;
const history = [doneDate, ...r.history].slice(0, HISTORY_MAX);
return {
...r,
lastDate: doneDate,
history,
updatedAt: new Date().toISOString()
};
});
saveReminders(next);
return { reminders: next };
});
},
togglePause(id) {
set((state) => {
const next = state.reminders.map((r) => {
if (r.id !== id) return r;
const status = r.status === 'paused' ? 'active' : 'paused';
return { ...r, status, updatedAt: new Date().toISOString() };
});
saveReminders(next);
return { reminders: next };
});
},
getDerivedList() {
return deriveAll(get().reminders);
},
getById(id) {
const r = get().reminders.find(
(r) => r.id === id && r.status !== 'deleted'
);
return r ? derive(r) : null;
}
}));
子任务 6:验证逻辑正确性
在任意占位页面(如 src/pages/home/index.tsx)中编写验证代码,运行后检查控制台输出,验证完毕后删除测试代码:
// 临时测试代码——验证完毕后删除
import { derive, deriveAll, diffDays, today } from '@/utils/dateUtils';
import type { Reminder } from '@/types';
const mockReminders: Reminder[] = [
{
id: '1',
name: '阿莫西林',
spec: '500mg',
status: 'active',
lastDate: '2026-04-01',
interval: 30,
before: 3,
time: '09:00',
note: '',
history: [],
createdAt: '',
updatedAt: ''
},
{
id: '2',
name: '复方感冒灵',
spec: '颗粒',
status: 'active',
lastDate: '2026-05-10',
interval: 7,
before: 1,
time: '09:00',
note: '',
history: [],
createdAt: '',
updatedAt: ''
},
{
id: '3',
name: '维生素C',
spec: '100mg',
status: 'paused',
lastDate: '2026-04-20',
interval: 60,
before: 7,
time: '09:00',
note: '',
history: [],
createdAt: '',
updatedAt: ''
}
];
// 打印派生结果,检查 level / daysLeft / progress
console.log('today:', today());
console.log('derived list:', JSON.stringify(deriveAll(mockReminders), null, 2));
// 边界:今日到期(daysLeft=0 → danger)
const todayReminder: Reminder = {
...mockReminders[0],
id: '4',
lastDate: ''
// 手动构造让 nextDate = today
};
验证要点:
status='paused'→level='paused',levelLabel='已暂停'daysLeft < 0(逾期)→level='danger',levelLabel='逾期 N 天'daysLeft === 0(今日)→level='danger',levelLabel='今日'0 < daysLeft <= 7→level='warning',levelLabel='N 天后'daysLeft > 7→level='good',levelLabel='N 天后'deriveAll结果按 danger → warning → good → paused 顺序排列progress值在 0-100 之间
子任务 7:自定义 Hook 封装(src/hooks/useReminders.ts)
创建 src/hooks/useReminders.ts,为页面组件提供便捷的数据访问接口:
import { useReminderStore } from '@/store/reminderStore';
import { deriveAll } from '@/utils/dateUtils';
/** 获取全部派生列表(已排序、已过滤删除项) */
export function useDerivedList() {
return useReminderStore((state) => deriveAll(state.reminders));
}
/** 获取单条派生数据 */
export function useDerivedById(id: string) {
return useReminderStore((state) => state.getById(id));
}
/** 获取首页摘要统计 */
export function useHomeSummary() {
return useReminderStore((state) => {
const list = deriveAll(state.reminders);
const overdueCount = list.filter((r) => r.level === 'danger').length;
const warningCount = list.filter((r) => r.level === 'warning').length;
const activeCount = list.filter((r) => r.status === 'active').length;
return { overdueCount, warningCount, activeCount, total: list.length };
});
}
/** 提醒操作 */
export function useReminderActions() {
return useReminderStore((state) => ({
addReminder: state.addReminder,
updateReminder: state.updateReminder,
deleteReminder: state.deleteReminder,
markDone: state.markDone,
togglePause: state.togglePause
}));
}
验收检查清单
完成所有子任务后,依次验证:
- 类型定义:
src/types/index.ts导出Reminder、DerivedReminder、PrescriptionRecord、ReminderStatus、ReminderLevel - 常量:
src/constants/index.ts导出INTERVAL_OPTIONS、BEFORE_OPTIONS、DEFAULT_REMIND_TIME、DEFAULT_INTERVAL、DEFAULT_BEFORE、STORAGE_KEY_REMINDERS - 存储工具:
src/utils/storage.ts的loadReminders/saveReminders可正常 import,无 TypeScript 报错 - 日期工具:
src/utils/dateUtils.ts的derive/deriveAll可正常 import,无 TypeScript 报错 - Store:
src/store/reminderStore.ts使用create创建,导出useReminderStore - level 计算正确:逾期/今日=danger,7天内=warning,7天以上=good,暂停=paused
- 排序正确:
deriveAll输出 danger 排最前,paused 排最后 - markDone 正确:调用后
lastDate更新,history头部插入今日日期,不超过 5 条 - togglePause 正确:active ↔ paused 互相切换
- 本地存储:调用
addReminder后模拟退出重进,loadReminders可读回数据 - Hook:
src/hooks/useReminders.ts导出 4 个 Hook,编译无报错 -
pnpm dev:weapp无编译报错
重要约束
- 不使用
window/localStorage/document:微信小程序环境不支持,存储统一使用Taro.setStorageSync/Taro.getStorageSync derive函数纯函数:不产生副作用,不修改传入的Reminder对象,每次调用基于当前日期实时计算status='deleted'软删除:deriveAll和getById均过滤掉deleted数据,保留原始数据供 M9 云同步使用history只保留最近HISTORY_MAX(5)条:markDone时截断- 进度条
progress区间 [0, 100]:逾期时进度为 100,不得超出 - Store 初始化立即读取本地存储:
reminders: loadReminders()在create时同步加载 - 所有写操作同步持久化:每次
set后立即调用saveReminders,不做批量延迟写入 - 禁止在 utils 中引用 store:
dateUtils.ts/storage.ts均为纯工具函数,不依赖 Zustand,避免循环依赖 - 不提前实现 UI:M2 阶段仅建立数据层,不修改任何页面组件的 UI 代码