Files
hms/docs/superpowers/plans/2026-04-25-slice3-miniprogram-ai-report.md
iven eb79424305
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(plan): 切片 3 小程序 AI 报告查看实施计划
6 个 Task,2 个 Chunk:
- Chunk 1: API service + 列表页 + 详情页(Taro/React)
- Chunk 2: 路由注册 + 首页入口 + 集成验证
2026-04-25 22:47:33 +08:00

540 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 切片 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`:
```typescript
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: 提交**
```bash
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`:
```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`:
```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: 提交**
```bash
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`:
```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`:
```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: 提交**
```bash
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` 之后添加:
```typescript
'pages/ai-report/list/index',
'pages/ai-report/detail/index',
```
- [ ] **Step 2: 验证编译 + 提交**
```bash
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 中添加一个导航卡片:
```tsx
<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: 验证编译 + 提交**
```bash
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: 功能验证(微信开发者工具)**
1. 登录小程序(绑定有 AI 分析记录的患者)
2. 首页可见"AI 分析报告"入口卡片
3. 点击进入列表页 → 显示该患者的 AI 分析记录
4. 点击一条已完成的记录 → 进入详情页Markdown 内容正常渲染
5. 无记录时显示空状态
6. 失败记录显示"失败"标签,不可点击进入详情
- [ ] **Step 5: 推送**
```bash
git push
```