Prompt file imported from lxKylin/yucheng-mini (
.codex/prompts/M3.prompt.md). Fill in{{level}},{{barLevel}},{{doneColor}},{{item_id}}before use. Copyright stays with the author.
M3 · 首页模块开发
任务概述
你是一名熟练的 Taro 4 + React 18 + TypeScript + Zustand 专家。请按照以下规范,逐步完成 M3 首页模块的全部开发内容。完成每个子任务后自行验证,确认无误再进入下一个。
当前项目状态
- M1 基础架构已完成:全局 SCSS 变量(
src/styles/variables.scss)、全局基础样式(src/app.scss)、AppBar 组件骨架(已实现) - M2 数据层已完成:
src/types/index.ts、src/utils/dateUtils.ts、src/utils/storage.ts、src/store/reminderStore.ts、src/hooks/useReminders.ts均已实现 - 以下组件为空骨架,需要在本模块实现:
src/components/StatusTag/index.tsx+index.scsssrc/components/ProgressBar/index.tsx+index.scsssrc/components/MedicineCard/index.tsx+index.scss
src/pages/home/index.tsx当前为占位页,需要完整替换src/pages/home/index.scss当前为占位样式,需要完整替换src/pages/home/index.config.ts已正确配置,不修改- M5(新增表单)和 M6(详情面板)尚未开发,FAB 和"查看详情"按钮本模块用 Toast 占位
taroify 组件使用规范
项目已安装 @taroify/core,且通过 vite-plugin-style-import 配置了样式自动导入(引入组件时无需手动 import CSS 文件)。
优先级规则:
- 优先使用 taroify 提供的组件,通过
className+ SCSS 覆盖将其视觉调整为项目设计规范(soft 色板) - 若 taroify 组件的交互结构与设计差异过大,再考虑手写组件
- 可通过
@taroify/mcpMCP 检索可用组件列表和 Props
本模块适用的 taroify 组件:
| 用途 | taroify 组件 | import 路径 | 说明 |
|---|---|---|---|
| 状态标签 | Tag |
@taroify/core/tag |
shape="rounded" 实现 pill 形,className 覆盖颜色 |
| 进度条 | Progress |
@taroify/core/progress |
label={false} 隐藏文字,className 覆盖颜色和高度 |
| 操作按钮 | Button |
@taroify/core/button |
className 覆盖颜色匹配设计规范 |
⚠️ 颜色覆盖说明:taroify 默认为 solid 色板(白色文字 + 纯色背景),项目设计规范为 soft 色板(彩色文字 + 浅色背景)。需通过组件的
classNameprop + SCSS 中更高优先级的选择器(必要时用!important)进行覆盖。
UI 设计参考
UI 参考 ui5.0/index.html 中 data-screen="home" 区域:
首页结构:
AppBar(首页总览 + 今日日期副标题 + 右侧 + 按钮)
↓
HeroCard(风险摘要卡片:逾期数量 + 今日数量 + 说明文字)
→ 有逾期/今日:danger 渐变背景
→ 无紧急:primary 渐变背景
↓
MetricsRow(三格统计:已逾期 / 今日处理 / 管理中)
↓
小节标题 "最近待开药提醒" + "查看全部" 按钮
↓
UrgentCards(最多展示前 3 条紧急药物卡片,每张包含状态标签、进度条、操作按钮)
↓
小节标题 "近期时间线" + "下次开药节点" 标注
↓
Timeline(与 UrgentCards 相同的 3 条,展示日期节点)
↓
FAB(右下角固定新增按钮,M5 接入前显示 Toast)
子任务 1:实现 StatusTag 组件(使用 taroify Tag)
使用 taroify Tag:提供 pill 形状和语义色,通过 className 覆盖为设计规范的 soft 色板。
完整替换 src/components/StatusTag/index.tsx:
import Tag from '@taroify/core/tag'
import type { ReminderLevel } from '@/types'
import './index.scss'
interface StatusTagProps {
level: ReminderLevel
label: string
}
// ReminderLevel 到 taroify Tag color 的映射
// 'paused' 无对应语义色,映射到 'default'(再由 SCSS 覆盖为紫色)
const colorMap = {
danger: 'danger',
warning: 'warning',
good: 'success',
paused: 'default',
} as const
export default function StatusTag({ level, label }: StatusTagProps) {
return (
<Tag
shape='rounded'
color={colorMap[level]}
className={`status-tag status-tag--{{level}}`}
>
{label}
</Tag>
)
}
完整替换 src/components/StatusTag/index.scss:
@use '@/assets/styles/variables' as *;
// taroify Tag 默认样式为 solid 色板(白色文字 + 纯色背景)
// 通过高优先级选择器覆盖为设计规范的 soft 色板(彩色文字 + 浅色背景)
.status-tag {
font-size: $text-xs;
font-weight: 900;
// danger:红色文字 + 浅红背景
&--danger {
color: $danger !important;
background: $danger-soft !important;
}
// warning:橙色文字 + 浅橙背景
&--warning {
color: $warning !important;
background: $warning-soft !important;
}
// good:绿色文字 + 浅绿背景
&--good {
color: $success !important;
background: $success-soft !important;
}
// paused:紫色文字 + 浅紫背景(taroify 无 violet,覆盖 default 灰色)
&--paused {
color: $violet !important;
background: $violet-soft !important;
}
}
子任务 2:实现 ProgressBar 组件(使用 taroify Progress)
使用 taroify Progress:提供进度动画结构,通过 className 覆盖高度和颜色为设计规范值。
完整替换 src/components/ProgressBar/index.tsx:
import Progress from '@taroify/core/progress'
import type { ReminderLevel } from '@/types'
import './index.scss'
interface ProgressBarProps {
/** 进度百分比 0–100 */
progress: number
level: ReminderLevel
}
// ReminderLevel 到 taroify Progress color 的映射
// 'paused' 复用 'success'(绿色),视觉上不突出
const colorMap = {
danger: 'danger',
warning: 'warning',
good: 'success',
paused: 'success',
} as const
export default function ProgressBar({ progress, level }: ProgressBarProps) {
const barLevel = level === 'paused' ? 'good' : level
const safePercent = Math.min(100, Math.max(0, progress))
return (
<Progress
className={`progress-bar progress-bar--{{barLevel}}`}
percent={safePercent}
color={colorMap[barLevel]}
label={false}
/>
)
}
完整替换 src/components/ProgressBar/index.scss:
@use '@/assets/styles/variables' as *;
// taroify Progress 默认高度 28px、颜色与项目设计规范不同
// 通过 className 组合选择器覆盖高度和填充条精确颜色
.progress-bar {
// 覆盖高度和轨道背景色
&.taroify-progress {
height: 8px;
background: #ecf3f5;
}
// 覆盖各状态填充条颜色为设计规范精确色值
&--danger .taroify-progress__portion {
background: $danger;
}
&--warning .taroify-progress__portion {
background: $warning;
}
&--good .taroify-progress__portion {
background: $success;
}
}
说明:taroify Progress 默认 danger 颜色为
#ee0a24,项目设计值为$danger: #c94040,必须通过 SCSS 覆盖.taroify-progress__portion的 background 以精确匹配设计稿。
子任务 3:实现 MedicineCard 组件(操作按钮使用 taroify Button)
使用 taroify Button 实现操作按钮,通过 className 覆盖为设计规范颜色;卡片布局为自定义(taroify 无对应药物卡片组件)。
完整替换 src/components/MedicineCard/index.tsx:
import { View, Text } from '@tarojs/components'
import Button from '@taroify/core/button'
import type { DerivedReminder } from '@/types'
import StatusTag from '@/components/StatusTag'
import ProgressBar from '@/components/ProgressBar'
import './index.scss'
interface MedicineCardProps {
item: DerivedReminder
/** 是否显示操作按钮(首页/列表页均为 true,详情面板中为 false) */
showActions?: boolean
onDone?: () => void
onDetail?: () => void
}
export default function MedicineCard({
item,
showActions = true,
onDone,
onDetail,
}: MedicineCardProps) {
// 已开药按钮:逾期/今日用 danger 色,其余用 success 色
const doneColor = item.level === 'danger' ? 'danger' : 'success'
return (
<View
className={`medicine-card${item.status === 'paused' ? ' medicine-card--paused' : ''}`}
>
{/* 顶部:药物名 + 状态标签 */}
<View className='medicine-card__row'>
<View className='medicine-card__info'>
<Text className='medicine-card__name'>{item.name}</Text>
<Text className='medicine-card__meta'>
最近开药 {item.lastDate} · 周期 {item.interval} 天 · {item.before} 天前 {item.time} 提醒
</Text>
</View>
<StatusTag level={item.level} label={item.levelLabel} />
</View>
{/* 进度条 */}
<View className='medicine-card__progress'>
<ProgressBar progress={item.progress} level={item.level} />
</View>
{/* 操作按钮(taroify Button) */}
{showActions && (
<View className='medicine-card__actions'>
<Button
className={`medicine-card__btn medicine-card__btn--{{doneColor}}`}
color={doneColor}
onClick={onDone}
>
✓ 已开药
</Button>
<Button
className='medicine-card__btn medicine-card__btn--ghost'
onClick={onDetail}
>
查看详情
</Button>
</View>
)}
</View>
)
}
完整替换 src/components/MedicineCard/index.scss:
@use '@/assets/styles/variables' as *;
.medicine-card {
padding: $spacing-3;
border-radius: $radius;
background: $card;
border: 1px solid rgba(216, 235, 239, 0.86);
box-shadow: 0 8px 20px rgba(16, 74, 91, 0.07);
&--paused {
opacity: 0.72;
}
&__row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: $spacing-3;
}
&__info {
flex: 1;
min-width: 0;
}
&__name {
display: block;
color: $ink-strong;
font-size: $text-lg;
font-weight: 900;
line-height: 1.3;
}
&__meta {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
line-height: 1.65;
}
&__progress {
margin-top: $spacing-5;
}
&__actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $spacing-3;
margin-top: $spacing-3;
}
// 覆盖 taroify Button 样式,匹配设计规范
&__btn {
width: 100% !important;
min-height: 36px !important;
font-size: $text-sm !important;
font-weight: 900 !important;
border-radius: $radius !important;
border: none !important;
&--danger {
color: #fff !important;
background: $danger !important;
}
&--success {
color: #fff !important;
background: $success !important;
}
// 次按钮:ghost 风格(主题色文字 + 浅主题背景)
&--ghost {
color: $primary-deep !important;
background: $primary-soft !important;
}
}
}
子任务 4:实现首页(src/pages/home/)
4.1 完整替换 src/pages/home/index.tsx
import Taro, { useLoad } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { useDerivedList, useReminderActions } from '@/hooks/useReminders'
import { formatDisplay, parseDate } from '@/utils/dateUtils'
import AppBar from '@/components/AppBar'
import MedicineCard from '@/components/MedicineCard'
import './index.scss'
export default function Home() {
const allItems = useDerivedList()
const { markDone } = useReminderActions()
// 统计数据(基于派生列表实时计算)
const overdueCount = allItems.filter(x => x.status !== 'paused' && x.daysLeft < 0).length
const todayCount = allItems.filter(x => x.status !== 'paused' && x.daysLeft === 0).length
const total = allItems.length
const hasDanger = overdueCount > 0 || todayCount > 0
// 前 3 条紧急活跃提醒(deriveAll 已按优先级排序)
const urgent = allItems.filter(x => x.status !== 'paused').slice(0, 3)
// AppBar 副标题:今日日期
const todayStr = (() => {
const now = new Date()
return `${now.getMonth() + 1}月${now.getDate()}日,先处理最紧急的开药任务`
})()
const handleMarkDone = (id: string) => {
markDone(id)
Taro.showToast({ title: '已记录开药', icon: 'success', duration: 1500 })
}
const handleViewAll = () => {
Taro.switchTab({ url: '/pages/list/index' })
}
/** M5 实现后替换为打开新增 BottomSheet */
const handleAddNew = () => {
Taro.showToast({ title: '功能开发中', icon: 'none', duration: 1200 })
}
/** M6 实现后替换为打开详情 BottomSheet */
const handleDetail = (_id: string) => {
Taro.showToast({ title: '详情开发中', icon: 'none', duration: 1200 })
}
useLoad(() => {
console.log('home page loaded')
})
return (
<View className='home-page'>
{/* 顶部标题栏 */}
<AppBar
title='首页总览'
caption={todayStr}
right={
<View className='home-page__header-btn' onClick={handleAddNew}>
<Text>+</Text>
</View>
}
/>
{/* 风险摘要卡片 */}
<View className={`home-hero${hasDanger ? ' home-hero--danger' : ''}`}>
<Text className='home-hero__eyebrow'>今日待办</Text>
<Text className='home-hero__title'>
{overdueCount} 个已逾期,{todayCount} 个今天到期
</Text>
<Text className='home-hero__desc'>
{hasDanger
? '建议先完成逾期或今日到期事项,再检查未来 7 天内需要提前挂号的药物。'
: '近期没有紧急开药任务,继续保持当前记录节奏。'}
</Text>
</View>
{/* 统计行:三格数据 */}
<View className='home-metrics'>
<View className='home-metric'>
<Text className='home-metric__value'>{overdueCount}</Text>
<Text className='home-metric__label'>已逾期</Text>
</View>
<View className='home-metric'>
<Text className='home-metric__value'>{todayCount}</Text>
<Text className='home-metric__label'>今日处理</Text>
</View>
<View className='home-metric'>
<Text className='home-metric__value'>{total}</Text>
<Text className='home-metric__label'>管理中</Text>
</View>
</View>
{/* 最近待开药提醒 */}
<View className='home-subhead'>
<Text className='home-subhead__title'>最近待开药提醒</Text>
<View className='home-subhead__action' onClick={handleViewAll}>
<Text>查看全部</Text>
</View>
</View>
<View className='home-cards'>
{urgent.length > 0
? urgent.map(item => (
<MedicineCard
key={item.id}
item={item}
onDone={() => handleMarkDone(item.id)}
onDetail={() => handleDetail(item.id)}
/>
))
: (
<View className='home-empty'>
<Text>暂无待处理提醒</Text>
</View>
)
}
</View>
{/* 近期时间线 */}
<View className='home-subhead'>
<Text className='home-subhead__title'>近期时间线</Text>
<Text className='home-subhead__note'>下次开药节点</Text>
</View>
<View className='home-timeline'>
{urgent.map(item => {
const dateLabel =
item.daysLeft <= 0 ? '今天' : formatDisplay(parseDate(item.nextDate))
return (
<View key={`tl-{{item_id}}`} className='home-timeline__item'>
<View className='home-timeline__date'>
<Text>{dateLabel}</Text>
</View>
<View className='home-timeline__body'>
<Text className='home-timeline__name'>{item.name}开药提醒</Text>
<Text className='home-timeline__desc'>
{item.levelLabel} · 预计下次 {item.nextDate},提醒 {item.remindAt} {item.time}
</Text>
</View>
</View>
)
})}
</View>
{/* FAB 新增按钮(M5 接入前为 Toast 占位) */}
<View className='home-fab' onClick={handleAddNew}>
<Text className='home-fab__icon'>+</Text>
</View>
</View>
)
}
4.2 完整替换 src/pages/home/index.scss
@use '@/assets/styles/variables' as *;
.home-page {
min-height: 100vh;
background: $bg;
padding: $spacing-4;
// 为 FAB 和原生 TabBar 预留底部空间
padding-bottom: 140px;
&__header-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: $primary-soft;
color: $primary-deep;
font-size: 20px;
font-weight: 300;
line-height: 1;
}
}
// ─── 风险摘要卡片 ──────────────────────────────────────────────
.home-hero {
margin-top: $spacing-3;
padding: $spacing-4;
border-radius: $radius-lg;
background: linear-gradient(135deg, $primary, $primary-deep);
color: #fff;
&--danger {
background: linear-gradient(
135deg,
$danger,
mix($danger, $ink-strong, 65%)
);
}
&__eyebrow {
display: block;
font-size: $text-xs;
font-weight: 900;
opacity: 0.85;
letter-spacing: 0.04em;
text-transform: uppercase;
}
&__title {
display: block;
margin-top: $spacing-2;
font-size: $text-xl;
font-weight: 900;
line-height: 1.2;
}
&__desc {
display: block;
margin-top: $spacing-2;
font-size: $text-sm;
line-height: 1.6;
opacity: 0.88;
}
}
// ─── 统计行 ────────────────────────────────────────────────────
.home-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $spacing-3;
margin-top: $spacing-3;
}
.home-metric {
padding: 10px;
border-radius: $radius;
background: $card;
border: 1px solid $line-soft;
box-shadow: 0 8px 18px rgba(16, 74, 91, 0.06);
text-align: center;
&__value {
display: block;
color: $ink-strong;
font-size: $text-xl;
font-weight: 900;
line-height: 1.15;
}
&__label {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
font-weight: 700;
}
}
// ─── 小节标题 ──────────────────────────────────────────────────
.home-subhead {
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-3;
margin-top: $spacing-5;
&__title {
color: $ink-strong;
font-size: $text-md;
font-weight: 900;
}
&__action {
min-height: 30px;
padding: 0 $spacing-3;
border-radius: $radius-pill;
color: $primary-deep;
background: $primary-soft;
font-size: $text-xs;
font-weight: 800;
display: flex;
align-items: center;
}
&__note {
color: $muted;
font-size: $text-xs;
font-weight: 700;
}
}
// ─── 卡片列表 ──────────────────────────────────────────────────
.home-cards {
display: grid;
gap: $spacing-3;
margin-top: $spacing-3;
}
.home-empty {
padding: $spacing-6 $spacing-4;
border-radius: $radius;
color: $muted;
background: rgba(255, 255, 255, 0.88);
border: 1px dashed $line;
text-align: center;
font-size: $text-base;
}
// ─── 时间线 ────────────────────────────────────────────────────
.home-timeline {
display: grid;
gap: $spacing-3;
margin-top: $spacing-3;
&__item {
display: grid;
grid-template-columns: 60px 1fr;
gap: $spacing-3;
padding: $spacing-3;
border-radius: $radius;
background: rgba(255, 255, 255, 0.82);
border: 1px solid $line-soft;
}
&__date {
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius;
color: $primary-deep;
background: $primary-soft;
font-size: $text-xs;
font-weight: 900;
text-align: center;
min-height: 50px;
padding: 4px;
}
&__name {
display: block;
font-size: $text-base;
font-weight: 900;
color: $ink-strong;
}
&__desc {
display: block;
margin-top: 4px;
color: $muted;
font-size: $text-xs;
line-height: 1.6;
}
}
// ─── FAB 新增按钮 ──────────────────────────────────────────────
.home-fab {
position: fixed;
right: $spacing-4;
bottom: 80px;
z-index: 8;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
background: linear-gradient(135deg, $primary, $primary-deep);
box-shadow: 0 16px 32px rgba(12, 100, 120, 0.28);
&__icon {
color: #fff;
font-size: 28px;
font-weight: 300;
line-height: 1;
}
}
子任务 5:验证
在完成全部子任务后,运行 pnpm dev:weapp 并在微信开发者工具中验证:
- 添加若干条测试数据(可直接在 Store 初始化时写入 mock 数据,验证完毕后删除):
// 在 src/store/reminderStore.ts 的 create 中临时加入,验证后删除
// reminders: [
// { id: 't1', name: '二甲双胍', spec: '0.5g', lastDate: '2026-04-13', interval: 30,
// before: 3, time: '09:00', status: 'active', note: '', history: [], createdAt: '', updatedAt: '' },
// { id: 't2', name: '阿托伐他汀', spec: '20mg', lastDate: '2026-05-10', interval: 7,
// before: 3, time: '09:00', status: 'active', note: '', history: [], createdAt: '', updatedAt: '' },
// { id: 't3', name: '优甲乐', spec: '50ug', lastDate: '2026-05-01', interval: 45,
// before: 7, time: '08:30', status: 'paused', note: '', history: [], createdAt: '', updatedAt: '' },
// ],
验收检查清单
完成所有子任务后,依次验证:
- StatusTag:
src/components/StatusTag/index.tsx接收level和labelprops,渲染正确颜色的标签 - ProgressBar:
src/components/ProgressBar/index.tsx接收progress(0-100)和level,渲染对应颜色的进度条 - MedicineCard:
src/components/MedicineCard/index.tsx组合使用 StatusTag 和 ProgressBar,显示药物名、规格、操作按钮 - 首页编译:
pnpm dev:weapp无 TypeScript / SCSS 编译报错 - HeroCard 颜色:有逾期或今日到期时显示红色渐变,否则显示蓝绿色渐变
- HeroCard 文案:正确显示"X 个已逾期,Y 个今天到期"数字
- MetricsRow:三格数字随数据变化正确更新
- UrgentCards:最多显示 3 条,暂停状态(paused)不出现在列表中
- 卡片排序:danger(逾期/今日)排最前,warning 次之,good 最后
- StatusTag 颜色:danger=红、warning=橙、good=绿、paused=紫
- ProgressBar 颜色:与 StatusTag level 匹配
- "已开药"按钮:点击后 Toast 显示"已记录开药",卡片数据刷新(daysLeft 重置)
- "查看全部":点击后跳转到提醒列表页
- Timeline:与 UrgentCards 相同的前 3 条,日期正确(逾期/今日显示"今天",其余显示 M月D日)
- FAB 按钮:固定在右下角,点击显示"功能开发中"Toast
- 空状态:没有活跃提醒时,UrgentCards 区域显示"暂无待处理提醒"
重要约束
- taroify 优先:有合适的 taroify 组件时必须优先使用,通过
className+ SCSS 覆盖匹配设计规范;手写组件仅在 taroify 无对应组件时使用 - taroify 样式自动导入:
vite-plugin-style-import已配置,引入 taroify 组件时无需手动importCSS 文件 - 禁止内联色值:所有颜色通过 SCSS 变量引用,
#fff(白色文字)为字面量例外;taroify 覆盖场景允许使用!important - Taro 组件:布局层使用
@tarojs/components中的View、Text,不使用 HTML 标签(div、p、ul等) - 不修改 M1/M2 文件:
src/styles/variables.scss、src/app.scss、src/types/index.ts、src/utils/、src/store/、src/hooks/均不修改 - 不修改
index.config.ts:首页的页面配置在 M1 已完成 - 不实现 M5/M6 功能:FAB 和"查看详情"按钮使用
Taro.showToast占位,并留清晰的 TODO 注释 - 组件样式独立:StatusTag、ProgressBar、MedicineCard 各自的样式文件独立,通过
@use '@/assets/styles/variables' as *引入,不依赖app.scss中的全局 class position: fixed适用:微信小程序支持position: fixed,FAB 使用此定位方式Taro.switchTab路径:TabBar 页面切换使用Taro.switchTab({ url: '/pages/list/index' }),必须带/前缀
