From eb79424305e71bae6251718c8cb84feed549e331 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 22:47:33 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=E5=88=87=E7=89=87=203=20?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=20AI=20=E6=8A=A5=E5=91=8A=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 个 Task,2 个 Chunk: - Chunk 1: API service + 列表页 + 详情页(Taro/React) - Chunk 2: 路由注册 + 首页入口 + 集成验证 --- ...2026-04-25-slice3-miniprogram-ai-report.md | 539 ++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-25-slice3-miniprogram-ai-report.md diff --git a/docs/superpowers/plans/2026-04-25-slice3-miniprogram-ai-report.md b/docs/superpowers/plans/2026-04-25-slice3-miniprogram-ai-report.md new file mode 100644 index 0000000..e01b09a --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-slice3-miniprogram-ai-report.md @@ -0,0 +1,539 @@ +# 切片 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(`/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 = { + lab_report: '化验单解读', + trend: '趋势分析', + checkup_plan: '体检方案', + report_summary: '报告摘要', +}; + +const STATUS_MAP: Record = { + completed: { text: '已完成', className: 'status-completed' }, + streaming: { text: '分析中', className: 'status-streaming' }, + failed: { text: '失败', className: 'status-failed' }, +}; + +export default function AiReportList() { + const [list, setList] = useState([]); + 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 ; + } + + if (list.length === 0) { + return ( + + + + ); + } + + return ( + + AI 分析报告 + + {list.map((item) => { + const statusInfo = STATUS_MAP[item.status] || { text: item.status, className: '' }; + return ( + item.status === 'completed' && goDetail(item.id)} + > + + {TYPE_LABELS[item.analysis_type] || item.analysis_type} + {statusInfo.text} + + + {new Date(item.created_at).toLocaleString('zh-CN')} + {item.model_used} + + + ); + })} + {loading && } + {!hasMore && list.length > 0 && 没有更多了} + + + ); +} +``` + +- [ ] **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 = { + lab_report: '化验单解读', + trend: '趋势分析', + checkup_plan: '体检方案', + report_summary: '报告摘要', +}; + +function markdownToHtml(md: string): string { + return md + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/(
  • .*<\/li>)/s, '
      $1
    ') + .replace(/\n\n/g, '

    ') + .replace(/\n/g, '
    '); +} + +export default function AiReportDetail() { + const router = useRouter(); + const id = router.params.id || ''; + + const [analysis, setAnalysis] = useState(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 ; + + if (!analysis) { + return ( + + 报告不存在 + + ); + } + + const htmlContent = analysis.result_content + ? markdownToHtml(analysis.result_content) + : '

    暂无分析结果

    '; + + return ( + + + {TYPE_LABELS[analysis.analysis_type] || analysis.analysis_type} + + 模型: {analysis.model_used} + {new Date(analysis.created_at).toLocaleString('zh-CN')} + + + + + + + + ); +} +``` + +- [ ] **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 + Taro.navigateTo({ url: '/pages/ai-report/list/index' })} +> + 🤖 + AI 分析报告 + 查看智能健康分析结果 + +``` + +- [ ] **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 +```