# 切片 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 ```