6 个 Task,2 个 Chunk: - Chunk 1: API service + 列表页 + 详情页(Taro/React) - Chunk 2: 路由注册 + 首页入口 + 集成验证
13 KiB
切片 3: 小程序 AI 报告查看 实施计划
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 患者可在微信小程序查看 AI 分析报告(只读),从首页入口进入列表页,点击查看详情。
Architecture: 后端 list_analysis / get_analysis 端点在切片 2 中已补全。小程序复用现有 services/request.ts 的 api 封装,新增 services/ai-analysis.ts 调用后端 API。新增 2 个页面(列表 + 详情),在首页添加入口卡片。
Tech Stack: Taro 4.2 + React 18 + TypeScript
设计规格: docs/superpowers/specs/2026-04-25-feature-completion-design.md §4
依赖: 切片 2 Task 1-4(后端 API 补全)必须先完成
Chunk 1: API 层 + 页面
Task 1: 创建 AI 分析 API service
Files:
- Create:
apps/miniprogram/src/services/ai-analysis.ts
参考模式: services/report.ts(化验报告 service)
- Step 1: 创建 service 文件
apps/miniprogram/src/services/ai-analysis.ts:
import { api } from './request';
export interface AiAnalysisItem {
id: string;
patient_id: string;
analysis_type: string;
model_used: string;
status: string;
result_content: string | null;
error_message: string | null;
created_at: string;
}
export async function listAiAnalysis(page = 1, pageSize = 20) {
return api.get<{ data: AiAnalysisItem[]; total: number }>(
'/ai/analysis/history',
{ page, page_size: pageSize },
);
}
export async function getAiAnalysisDetail(id: string) {
return api.get<AiAnalysisItem>(`/ai/analysis/${id}`);
}
注意: 后端 list_analysis 会根据 JWT 中的 user_id → patient_id 自动过滤(小程序端通过 X-Patient-Id header 传递)。若后端未自动过滤,需在请求参数中传 patient_id。
- Step 2: 验证编译
Run: cd apps/miniprogram && npx tsc --noEmit
Expected: 无错误
- Step 3: 提交
git add apps/miniprogram/src/services/ai-analysis.ts
git commit -m "feat(miniprogram): AI 分析 API service"
Task 2: AI 报告列表页
Files:
- Create:
apps/miniprogram/src/pages/ai-report/list/index.tsx - Create:
apps/miniprogram/src/pages/ai-report/list/index.scss
参考模式: pages/report/detail/index.tsx + pages/article/index.tsx
- Step 1: 创建列表页组件
apps/miniprogram/src/pages/ai-report/list/index.tsx:
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { listAiAnalysis, type AiAnalysisItem } from '@/services/ai-analysis';
import Loading from '@/components/Loading';
import EmptyState from '@/components/EmptyState';
import './index.scss';
const TYPE_LABELS: Record<string, string> = {
lab_report: '化验单解读',
trend: '趋势分析',
checkup_plan: '体检方案',
report_summary: '报告摘要',
};
const STATUS_MAP: Record<string, { text: string; className: string }> = {
completed: { text: '已完成', className: 'status-completed' },
streaming: { text: '分析中', className: 'status-streaming' },
failed: { text: '失败', className: 'status-failed' },
};
export default function AiReportList() {
const [list, setList] = useState<AiAnalysisItem[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
loadList(1);
}, []);
const loadList = async (p: number) => {
setLoading(true);
try {
const res = await listAiAnalysis(p, 20);
const items = res.data || [];
setList(p === 1 ? items : [...list, ...items]);
setPage(p);
setHasMore(items.length >= 20);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
};
const goDetail = (id: string) => {
Taro.navigateTo({ url: `/pages/ai-report/detail/index?id=${id}` });
};
const loadMore = () => {
if (hasMore && !loading) loadList(page + 1);
};
if (loading && list.length === 0) {
return <Loading />;
}
if (list.length === 0) {
return (
<View className='ai-report-page'>
<EmptyState text='暂无 AI 分析报告' />
</View>
);
}
return (
<View className='ai-report-page'>
<View className='page-title'>AI 分析报告</View>
<ScrollView scrollY className='report-scroll' onScrollToLower={loadMore}>
{list.map((item) => {
const statusInfo = STATUS_MAP[item.status] || { text: item.status, className: '' };
return (
<View
key={item.id}
className='report-card'
onClick={() => item.status === 'completed' && goDetail(item.id)}
>
<View className='card-header'>
<Text className='card-type'>{TYPE_LABELS[item.analysis_type] || item.analysis_type}</Text>
<Text className={`card-status ${statusInfo.className}`}>{statusInfo.text}</Text>
</View>
<View className='card-footer'>
<Text className='card-time'>{new Date(item.created_at).toLocaleString('zh-CN')}</Text>
<Text className='card-model'>{item.model_used}</Text>
</View>
</View>
);
})}
{loading && <Loading />}
{!hasMore && list.length > 0 && <Text className='no-more'>没有更多了</Text>}
</ScrollView>
</View>
);
}
- Step 2: 创建列表页样式
apps/miniprogram/src/pages/ai-report/list/index.scss:
.ai-report-page {
min-height: 100vh;
background: #f1f5f9;
padding: 16px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #0f172a;
margin-bottom: 16px;
}
.report-scroll {
height: calc(100vh - 80px);
}
.report-card {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.card-type {
font-size: 15px;
font-weight: 500;
color: #1e293b;
}
.card-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.status-completed {
color: #16a34a;
background: #dcfce7;
}
.status-streaming {
color: #2563eb;
background: #dbeafe;
}
.status-failed {
color: #dc2626;
background: #fee2e2;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-time {
font-size: 12px;
color: #94a3b8;
}
.card-model {
font-size: 11px;
color: #cbd5e1;
}
.no-more {
text-align: center;
font-size: 12px;
color: #94a3b8;
padding: 16px 0;
display: block;
}
- Step 3: 验证编译
Run: cd apps/miniprogram && npx tsc --noEmit
Expected: 无错误
- Step 4: 提交
git add apps/miniprogram/src/pages/ai-report/list/
git commit -m "feat(miniprogram): AI 报告列表页"
Task 3: AI 报告详情页
Files:
-
Create:
apps/miniprogram/src/pages/ai-report/detail/index.tsx -
Create:
apps/miniprogram/src/pages/ai-report/detail/index.scss -
Step 1: 创建详情页组件
apps/miniprogram/src/pages/ai-report/detail/index.tsx:
import React, { useState, useEffect } from 'react';
import { View, Text, RichText } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { getAiAnalysisDetail, type AiAnalysisItem } from '@/services/ai-analysis';
import Loading from '@/components/Loading';
import './index.scss';
const TYPE_LABELS: Record<string, string> = {
lab_report: '化验单解读',
trend: '趋势分析',
checkup_plan: '体检方案',
report_summary: '报告摘要',
};
function markdownToHtml(md: string): string {
return md
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n\n/g, '<br/><br/>')
.replace(/\n/g, '<br/>');
}
export default function AiReportDetail() {
const router = useRouter();
const id = router.params.id || '';
const [analysis, setAnalysis] = useState<AiAnalysisItem | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) return;
setLoading(true);
getAiAnalysisDetail(id)
.then((data) => setAnalysis(data))
.catch(() => Taro.showToast({ title: '加载失败', icon: 'none' }))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <Loading />;
if (!analysis) {
return (
<View className='detail-page'>
<Text className='empty-text'>报告不存在</Text>
</View>
);
}
const htmlContent = analysis.result_content
? markdownToHtml(analysis.result_content)
: '<p>暂无分析结果</p>';
return (
<View className='detail-page'>
<View className='detail-card'>
<Text className='detail-type'>{TYPE_LABELS[analysis.analysis_type] || analysis.analysis_type}</Text>
<View className='detail-meta'>
<Text className='meta-item'>模型: {analysis.model_used}</Text>
<Text className='meta-item'>{new Date(analysis.created_at).toLocaleString('zh-CN')}</Text>
</View>
</View>
<View className='content-card'>
<RichText className='report-content' nodes={htmlContent} />
</View>
</View>
);
}
- Step 2: 创建详情页样式
apps/miniprogram/src/pages/ai-report/detail/index.scss:
.detail-page {
min-height: 100vh;
background: #f1f5f9;
padding: 16px;
}
.detail-card {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.detail-type {
font-size: 18px;
font-weight: 600;
color: #0f172a;
display: block;
margin-bottom: 8px;
}
.detail-meta {
display: flex;
justify-content: space-between;
}
.meta-item {
font-size: 12px;
color: #94a3b8;
}
.content-card {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.report-content {
font-size: 14px;
line-height: 1.8;
color: #334155;
}
.empty-text {
display: block;
text-align: center;
padding: 60px 0;
color: #94a3b8;
font-size: 14px;
}
- Step 3: 验证编译
Run: cd apps/miniprogram && npx tsc --noEmit
Expected: 无错误
- Step 4: 提交
git add apps/miniprogram/src/pages/ai-report/detail/
git commit -m "feat(miniprogram): AI 报告详情页"
Chunk 2: 集成
Task 4: 注册页面路由
Files:
-
Modify:
apps/miniprogram/src/app.config.ts -
Step 1: 在 pages 数组中注册新页面
在 app.config.ts 的 pages 数组中,在 pages/report/detail/index 之后添加:
'pages/ai-report/list/index',
'pages/ai-report/detail/index',
- Step 2: 验证编译 + 提交
cd apps/miniprogram && npx tsc --noEmit
git add apps/miniprogram/src/app.config.ts
git commit -m "feat(miniprogram): 注册 AI 报告页面路由"
Task 5: 首页入口卡片
Files:
-
Modify:
apps/miniprogram/src/pages/index/index.tsx -
Modify:
apps/miniprogram/src/pages/index/index.scss -
Step 1: 在首页添加 AI 报告入口
在 pages/index/index.tsx 中,找到功能入口区域,添加 AI 报告卡片。
在页面 JSX 中添加一个导航卡片:
<View
className='feature-card ai-card'
onClick={() => Taro.navigateTo({ url: '/pages/ai-report/list/index' })}
>
<Text className='feature-icon'>🤖</Text>
<Text className='feature-title'>AI 分析报告</Text>
<Text className='feature-desc'>查看智能健康分析结果</Text>
</View>
- Step 2: 添加入口卡片样式(如需要)
在 pages/index/index.scss 中,根据现有功能卡片样式添加 .ai-card 样式(如果现有的 .feature-card 已足够,可跳过)。
- Step 3: 验证编译 + 提交
cd apps/miniprogram && npx tsc --noEmit
git add apps/miniprogram/src/pages/index/
git commit -m "feat(miniprogram): 首页添加 AI 报告入口卡片"
Task 6: 集成验证
- Step 1: 编译检查
Run: cd apps/miniprogram && npx tsc --noEmit
Expected: 无错误
- Step 2: 启动后端服务
Run: cd crates/erp-server && cargo run
确保 /api/v1/ai/analysis/history 和 /api/v1/ai/analysis/{id} 端点可用。
- Step 3: 小程序编译
Run: cd apps/miniprogram && pnpm build:weapp
Expected: 编译成功
- Step 4: 功能验证(微信开发者工具)
- 登录小程序(绑定有 AI 分析记录的患者)
- 首页可见"AI 分析报告"入口卡片
- 点击进入列表页 → 显示该患者的 AI 分析记录
- 点击一条已完成的记录 → 进入详情页,Markdown 内容正常渲染
- 无记录时显示空状态
- 失败记录显示"失败"标签,不可点击进入详情
- Step 5: 推送
git push