diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index 66122e6..fe14b45 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -12,6 +12,8 @@ export default defineAppConfig({ 'pages/article/index', 'pages/article/detail/index', 'pages/report/detail/index', + 'pages/ai-report/list/index', + 'pages/ai-report/detail/index', 'pages/followup/detail/index', 'pages/consultation/index', 'pages/mall/index', diff --git a/apps/miniprogram/src/pages/ai-report/detail/index.scss b/apps/miniprogram/src/pages/ai-report/detail/index.scss new file mode 100644 index 0000000..a34dd62 --- /dev/null +++ b/apps/miniprogram/src/pages/ai-report/detail/index.scss @@ -0,0 +1,52 @@ +.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; +} diff --git a/apps/miniprogram/src/pages/ai-report/detail/index.tsx b/apps/miniprogram/src/pages/ai-report/detail/index.tsx new file mode 100644 index 0000000..42a2e53 --- /dev/null +++ b/apps/miniprogram/src/pages/ai-report/detail/index.tsx @@ -0,0 +1,73 @@ +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_interpretation: '化验单解读', + health_trend_analysis: '趋势分析', + personalized_checkup_plan: '体检方案', + report_summary_generation: '报告摘要', +}; + +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(/(
  • [\s\S]*?<\/li>)/g, '') + .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')} + + + + + + + + ); +} diff --git a/apps/miniprogram/src/pages/ai-report/list/index.scss b/apps/miniprogram/src/pages/ai-report/list/index.scss new file mode 100644 index 0000000..8b20cc3 --- /dev/null +++ b/apps/miniprogram/src/pages/ai-report/list/index.scss @@ -0,0 +1,87 @@ +.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; +} + +.status-pending { + color: #d97706; + background: #fef3c7; +} + +.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; +} diff --git a/apps/miniprogram/src/pages/ai-report/list/index.tsx b/apps/miniprogram/src/pages/ai-report/list/index.tsx new file mode 100644 index 0000000..d31140e --- /dev/null +++ b/apps/miniprogram/src/pages/ai-report/list/index.tsx @@ -0,0 +1,97 @@ +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_interpretation: '化验单解读', + health_trend_analysis: '趋势分析', + personalized_checkup_plan: '体检方案', + report_summary_generation: '报告摘要', +}; + +const STATUS_MAP: Record = { + completed: { text: '已完成', className: 'status-completed' }, + streaming: { text: '分析中', className: 'status-streaming' }, + failed: { text: '失败', className: 'status-failed' }, + pending: { text: '等待中', className: 'status-pending' }, +}; + +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 : (prev) => [...prev, ...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 && 没有更多了} + + + ); +} diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 54da82c..408e727 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -79,6 +79,7 @@ export default function Index() { { label: '健康录入', icon: '📊', path: '/pages/health/input/index' }, { label: '健康趋势', icon: '📈', path: '/pages/health/trend/index' }, { label: '资讯文章', icon: '📰', path: '/pages/article/index' }, + { label: 'AI 报告', icon: '🤖', path: '/pages/ai-report/list/index' }, ]; const handleServiceClick = (path: string) => { diff --git a/apps/miniprogram/src/services/ai-analysis.ts b/apps/miniprogram/src/services/ai-analysis.ts new file mode 100644 index 0000000..a6f2627 --- /dev/null +++ b/apps/miniprogram/src/services/ai-analysis.ts @@ -0,0 +1,23 @@ +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}`); +}