Files
Karlcarlo ded7ea1228 feat: 新增可配置的路径前缀以支持子路径部署
添加前后端环境变量配置示例,新增后端路径前缀配置项,更新后端路由与静态文件挂载逻辑,调整前端代理配置与 API 调用代码,适配子路径部署场景。
2026-05-21 20:56:22 +08:00

620 lines
27 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>孕育无忧 - 孕期报告智能解读</title>
<style>
:root{--pink:#e91e8c;--pink-light:#fce4f3;--pink-bg:#fff5fb;--green:#27ae60;--orange:#f39c12;--red:#e74c3c;--gray:#999;--border:#eee;--shadow:0 2px 12px rgba(0,0,0,.08);--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB",sans-serif;background:#f5f5f5;color:#333;max-width:480px;margin:0 auto;min-height:100vh;position:relative}
.loading-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,.85);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999}
.spinner{width:40px;height:40px;border:4px solid var(--pink-light);border-top-color:var(--pink);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.loading-overlay p{margin-top:12px;color:var(--pink);font-size:14px}
/* Header */
.header{background:linear-gradient(135deg,var(--pink),#c2185b);color:#fff;padding:20px;text-align:center;position:sticky;top:0;z-index:100}
.header h1{font-size:20px;font-weight:600}
.header p{font-size:12px;opacity:.85;margin-top:4px}
/* Tab bar */
.tab-bar{display:flex;background:#fff;border-bottom:1px solid var(--border);position:sticky;top:72px;z-index:99}
.tab-bar button{flex:1;padding:12px 0;border:none;background:none;font-size:13px;color:var(--gray);cursor:pointer;border-bottom:2px solid transparent;transition:.2s}
.tab-bar button.active{color:var(--pink);border-bottom-color:var(--pink);font-weight:600}
/* Pages */
.page{display:none;padding:16px}
.page.active{display:block}
/* Cards */
.card{background:#fff;border-radius:var(--radius);padding:16px;margin-bottom:12px;box-shadow:var(--shadow)}
.card h3{font-size:15px;margin-bottom:8px;color:#333}
.card p{font-size:13px;color:#666;line-height:1.6}
/* Upload area */
.upload-area{border:2px dashed #ddd;border-radius:var(--radius);padding:40px 20px;text-align:center;cursor:pointer;transition:.2s;background:#fafafa}
.upload-area:hover{border-color:var(--pink);background:var(--pink-bg)}
.upload-area .icon{font-size:48px;margin-bottom:12px}
.upload-area p{color:var(--gray);font-size:14px}
.upload-area input{display:none}
/* Image preview */
.preview-img{max-width:100%;border-radius:8px;margin:12px 0}
/* Status badges */
.badge{display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:500}
.badge-normal{background:#e8f5e9;color:var(--green)}
.badge-attention{background:#fff3e0;color:var(--orange)}
.badge-warning{background:#ffebee;color:var(--red)}
.badge-high{background:#ffebee;color:var(--red)}
.badge-low{background:#fff3e0;color:var(--orange)}
/* Buttons */
.btn{display:inline-block;padding:10px 24px;border-radius:24px;border:none;font-size:14px;cursor:pointer;transition:.2s;font-weight:500}
.btn-primary{background:var(--pink);color:#fff}
.btn-primary:hover{background:#c2185b}
.btn-primary:disabled{background:#ccc;cursor:not-allowed}
.btn-outline{border:1px solid var(--pink);color:var(--pink);background:none}
.btn-block{display:block;width:100%;text-align:center}
/* Data items table */
.data-table{width:100%;border-collapse:collapse;font-size:13px;margin:8px 0}
.data-table th{text-align:left;padding:8px 6px;border-bottom:2px solid var(--border);color:var(--gray);font-weight:500}
.data-table td{padding:8px 6px;border-bottom:1px solid var(--border)}
/* Chat */
.chat-container{display:flex;flex-direction:column;height:calc(100vh - 170px)}
.chat-messages{flex:1;overflow-y:auto;padding:12px 0}
.chat-msg{margin-bottom:12px;display:flex}
.chat-msg.user{justify-content:flex-end}
.chat-bubble{max-width:80%;padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.6;white-space:pre-wrap}
.chat-msg.user .chat-bubble{background:var(--pink);color:#fff;border-bottom-right-radius:4px}
.chat-msg.assistant .chat-bubble{background:#fff;color:#333;border-bottom-left-radius:4px;box-shadow:var(--shadow)}
.chat-input-area{display:flex;gap:8px;padding:12px 0}
.chat-input-area input{flex:1;padding:10px 16px;border:1px solid #ddd;border-radius:24px;font-size:14px;outline:none}
.chat-input-area input:focus{border-color:var(--pink)}
.quick-questions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.quick-q{padding:6px 12px;background:var(--pink-bg);color:var(--pink);border-radius:16px;font-size:12px;cursor:pointer;border:none;transition:.2s}
.quick-q:hover{background:var(--pink-light)}
/* Report list */
.report-item{display:flex;align-items:center;padding:12px;border-bottom:1px solid var(--border);cursor:pointer;transition:.1s}
.report-item:hover{background:#fafafa}
.report-item .info{flex:1;margin-left:12px}
.report-item .info h4{font-size:14px;margin-bottom:4px}
.report-item .info p{font-size:12px;color:var(--gray)}
.report-icon{width:40px;height:40px;border-radius:10px;background:var(--pink-light);display:flex;align-items:center;justify-content:center;font-size:20px}
/* Analysis result */
.analysis-section{margin-bottom:16px}
.analysis-section h4{font-size:14px;margin-bottom:8px;color:var(--pink)}
.indicator-card{background:#fafafa;border-radius:8px;padding:12px;margin-bottom:8px}
.indicator-card .name{font-weight:600;font-size:14px}
.indicator-card .nickname{color:var(--pink);font-size:12px;margin-left:6px}
.indicator-card .detail{font-size:13px;color:#555;margin-top:6px;line-height:1.6}
/* Action items */
.action-item{display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-bottom:1px solid var(--border)}
.action-dot{width:8px;height:8px;border-radius:50%;margin-top:5px;flex-shrink:0}
.action-dot.high{background:var(--red)}
.action-dot.medium{background:var(--orange)}
.action-dot.low{background:var(--green)}
/* Empty state */
.empty-state{text-align:center;padding:60px 20px;color:var(--gray)}
.empty-state .icon{font-size:64px;margin-bottom:16px}
/* Verify form */
.verify-item{display:flex;gap:8px;margin-bottom:8px;align-items:center;flex-wrap:wrap}
.verify-item input,.verify-item select{padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}
.verify-item input{width:80px}
.verify-item .item-name{font-size:13px;font-weight:500;min-width:100px}
textarea{width:100%;padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;resize:vertical;min-height:60px}
</style>
</head>
<body>
<div class="loading-overlay" id="globalLoading" style="display:none">
<div class="spinner"></div>
<p id="loadingText">加载中...</p>
</div>
<div class="header">
<h1>孕育无忧</h1>
<p>AI 智能解读孕期检查报告</p>
</div>
<div class="tab-bar">
<button class="active" onclick="showPage('home')">首页</button>
<button onclick="showPage('reports')">报告</button>
<button onclick="showPage('chat')">AI问答</button>
<button onclick="showPage('me')">我的</button>
</div>
<!-- ===== 首页 ===== -->
<div class="page active" id="page-home">
<div class="card">
<h3>📸 上传检查报告</h3>
<p style="margin-bottom:12px">拍照或选择孕期检查报告图片,AI 自动识别并解读</p>
<div class="upload-area" id="uploadArea">
<div class="icon">📋</div>
<p>点击上传报告图片</p>
<p style="font-size:12px;margin-top:4px;color:#bbb">支持 JPG、PNG 格式</p>
<input type="file" id="fileInput" accept="image/*">
</div>
</div>
<div id="ocrResult" style="display:none">
<div class="card">
<h3>📝 OCR 识别结果</h3>
<p style="margin-bottom:8px;color:var(--gray);font-size:12px">请核对以下识别数据,如有误请修正后确认</p>
<img id="previewImg" class="preview-img" alt="报告图片">
<div id="ocrDataItems"></div>
<div style="margin-top:12px">
<label style="font-size:13px;font-weight:500">自觉症状(可选)</label>
<textarea id="symptomsInput" placeholder="如:头晕、水肿、腰痛等..." style="margin-top:6px"></textarea>
</div>
<div style="margin-top:12px;text-align:center">
<button class="btn btn-primary btn-block" onclick="verifyReport()">确认数据,生成 AI 解读</button>
</div>
</div>
</div>
<div id="analysisResult" style="display:none"></div>
</div>
<!-- ===== 报告列表 ===== -->
<div class="page" id="page-reports">
<div class="card">
<h3>📋 我的报告</h3>
<div id="reportList">
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无报告,去首页上传吧</p>
</div>
</div>
</div>
</div>
<!-- ===== AI 对话 ===== -->
<div class="page" id="page-chat">
<div class="chat-container">
<div id="chatReportSelect" class="card" style="padding:10px 14px">
<label style="font-size:13px;color:var(--gray)">选择报告进行追问:</label>
<select id="chatReportId" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;margin-top:6px;font-size:13px">
<option value="">请先上传报告</option>
</select>
</div>
<div id="quickQuestions" class="quick-questions"></div>
<div class="chat-messages" id="chatMessages">
<div class="empty-state">
<div class="icon">💬</div>
<p>选择报告后可以向 AI 追问</p>
</div>
</div>
<div class="chat-input-area">
<input type="text" id="chatInput" placeholder="输入你的问题..." onkeydown="if(event.key==='Enter')sendChat()">
<button class="btn btn-primary" onclick="sendChat()">发送</button>
</div>
</div>
</div>
<!-- ===== 我的 ===== -->
<div class="page" id="page-me">
<div class="card" style="text-align:center;padding:30px">
<div style="width:64px;height:64px;border-radius:50%;background:var(--pink-light);margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:28px">👩</div>
<h3 id="userName">体验用户</h3>
<p style="color:var(--gray);margin-top:4px" id="userInfo">加载中...</p>
</div>
<div class="card">
<h3>🍎 营养建议</h3>
<p style="margin-bottom:12px;font-size:13px;color:var(--gray)">根据报告选择对应的营养建议</p>
<select id="nutritionReportId" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;font-size:13px">
<option value="">选择报告</option>
</select>
<button class="btn btn-outline btn-block" style="margin-top:8px" onclick="getNutrition()">获取营养建议</button>
<div id="nutritionResult" style="margin-top:12px"></div>
</div>
<div class="card">
<h3>🏃‍♀️ 运动建议</h3>
<button class="btn btn-outline btn-block" onclick="getExercise()">获取运动建议</button>
<div id="exerciseResult" style="margin-top:12px"></div>
</div>
</div>
<script>
const API = '';
const API_PREFIX = window.location.pathname.startsWith('/app_03') ? '/app_03/api' : '/api';
let TOKEN = '';
let currentReportId = '';
let ocrDataItems = [];
// ---- Helpers ----
function showLoading(text='加载中...'){
document.getElementById('loadingText').textContent=text;
document.getElementById('globalLoading').style.display='flex';
}
function hideLoading(){document.getElementById('globalLoading').style.display='none'}
async function api(path,opts={}){
if(path.startsWith('/api/')) path = API_PREFIX + path.slice(4);
const headers = {'Authorization':'Bearer '+TOKEN, ...(opts.headers||{})};
if(opts.body && !(opts.body instanceof FormData)){
headers['Content-Type']='application/json';
opts.body=JSON.stringify(opts.body);
}
const res = await fetch(API+path, {...opts, headers});
if(!res.ok){
const err = await res.json().catch(()=>({detail:'请求失败'}));
throw new Error(err.detail||'请求失败');
}
return res.json();
}
function showPage(name){
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.getElementById('page-'+name).classList.add('active');
document.querySelectorAll('.tab-bar button').forEach((b,i)=>{
b.classList.toggle('active', ['home','reports','chat','me'][i]===name);
});
if(name==='reports') loadReportList();
if(name==='chat') loadChatReportSelect();
if(name==='me') loadProfile();
}
function badgeHtml(status){
const map={normal:'正常',attention:'注意',warning:'警告',high:'偏高',low:'偏低'};
return `<span class="badge badge-${status}">${map[status]||status}</span>`;
}
// ---- Auth ----
async function demoLogin(){
showLoading('正在登录...');
try{
const data = await api('/api/auth/demo-login',{method:'POST'});
TOKEN = data.access_token;
hideLoading();
}catch(e){
hideLoading();
alert('登录失败:'+e.message);
}
}
// ---- Upload ----
document.getElementById('uploadArea').onclick=()=>document.getElementById('fileInput').click();
document.getElementById('fileInput').onchange=async function(){
const file = this.files[0];
if(!file) return;
showLoading('AI 正在识别报告...');
const fd = new FormData();
fd.append('file', file);
try{
const data = await api('/api/report/upload',{method:'POST',body:fd});
currentReportId = data.report_id;
ocrDataItems = data.ocr_result.data_items || [];
// Show preview
document.getElementById('previewImg').src = API + '/' + data.image_url;
renderOcrItems(ocrDataItems, data.ocr_result.report_type);
document.getElementById('ocrResult').style.display='block';
document.getElementById('analysisResult').style.display='none';
hideLoading();
}catch(e){
hideLoading();
alert('上传失败:'+e.message);
}
};
function renderOcrItems(items, reportType){
let html = `<div class="verify-item" style="margin-bottom:12px">
<span class="item-name">报告类型</span>
<input id="reportTypeInput" value="${reportType||''}" style="flex:1" placeholder="如:血常规">
</div>`;
html += '<table class="data-table"><thead><tr><th>指标</th><th>数值</th><th>单位</th><th>参考范围</th><th>状态</th></tr></thead><tbody>';
items.forEach((item,i)=>{
html += `<tr>
<td><input data-idx="${i}" data-field="name" value="${item.name||''}" style="width:80px;border:1px solid #eee;padding:4px;border-radius:4px"></td>
<td><input data-idx="${i}" data-field="value" value="${item.value||''}" style="width:60px;border:1px solid #eee;padding:4px;border-radius:4px"></td>
<td><input data-idx="${i}" data-field="unit" value="${item.unit||''}" style="width:50px;border:1px solid #eee;padding:4px;border-radius:4px"></td>
<td><input data-idx="${i}" data-field="reference_range" value="${item.reference_range||''}" style="width:80px;border:1px solid #eee;padding:4px;border-radius:4px"></td>
<td><select data-idx="${i}" data-field="status" style="border:1px solid #eee;padding:4px;border-radius:4px">
<option value="normal" ${item.status==='normal'?'selected':''}>正常</option>
<option value="high" ${item.status==='high'?'selected':''}>偏高</option>
<option value="low" ${item.status==='low'?'selected':''}>偏低</option>
</select></td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('ocrDataItems').innerHTML = html;
}
async function verifyReport(){
// Collect edited data
const items = ocrDataItems.map((_,i)=>{
const get = f => document.querySelector(`[data-idx="${i}"][data-field="${f}"]`).value;
return {name:get('name'),value:get('value'),unit:get('unit'),reference_range:get('reference_range'),status:get('status'),category:''};
});
const reportType = document.getElementById('reportTypeInput').value;
const symptoms = document.getElementById('symptomsInput').value;
showLoading('正在确认数据...');
try{
await api(`/api/report/${currentReportId}/verify`,{
method:'PUT',
body:{data_items:items,symptoms:symptoms||null,notes:null,report_type:reportType||null}
});
hideLoading();
generateAnalysis();
}catch(e){
hideLoading();
alert('确认失败:'+e.message);
}
}
async function generateAnalysis(){
showLoading('AI 正在解读报告,请稍候...');
try{
const data = await api(`/api/analysis/${currentReportId}/generate`,{method:'POST'});
renderAnalysis(data);
hideLoading();
}catch(e){
hideLoading();
alert('解读失败:'+e.message);
}
}
function renderAnalysis(data){
let html = '<div class="card"><h3>🤖 AI 解读报告</h3>';
// Overall
html += `<div class="analysis-section">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
${badgeHtml(data.overall_status)}
<span style="font-size:13px;color:var(--gray)">风险等级:${({low:'低',medium:'中',high:'高'})[data.risk_level]||data.risk_level}</span>
</div>
<p style="font-size:14px;line-height:1.6">${data.overall_summary}</p>
<p style="font-size:14px;color:var(--pink);margin-top:6px">${data.emotional_summary}</p>
</div>`;
// Categories
if(data.categories && data.categories.length){
data.categories.forEach(cat=>{
html += `<div class="analysis-section">
<h4>${cat.category} ${badgeHtml(cat.status)}</h4>`;
(cat.indicators||[]).forEach(ind=>{
html += `<div class="indicator-card">
<div><span class="name">${ind.name}</span><span class="nickname">${ind.nickname||''}</span> ${badgeHtml(ind.status)} <span style="font-size:12px;color:var(--gray);margin-left:6px">${ind.value||''}</span></div>
<div class="detail">
${ind.what_is_it?'<div>📖 '+ind.what_is_it+'</div>':''}
${ind.impact?'<div>👶 '+ind.impact+'</div>':''}
${ind.suggestion?'<div>💡 '+ind.suggestion+'</div>':''}
${ind.recheck_reminder?'<div>🔔 '+ind.recheck_reminder+'</div>':''}
</div>
</div>`;
});
html += '</div>';
});
}
// Action items
if(data.action_items && data.action_items.length){
html += '<div class="analysis-section"><h4>📋 行动建议</h4>';
data.action_items.forEach(a=>{
html += `<div class="action-item"><div class="action-dot ${a.priority}"></div><div>
<div style="font-size:13px">${a.content}</div>
<div style="font-size:11px;color:var(--gray)">${a.category||''}</div>
</div></div>`;
});
html += '</div>';
}
// Tips
if(data.tips && data.tips.length){
html += '<div class="analysis-section"><h4>💝 温馨提示</h4>';
data.tips.forEach(t=>{html += `<p style="font-size:13px;padding:4px 0;color:#555">• ${t}</p>`});
html += '</div>';
}
html += `<div style="margin-top:12px;display:flex;gap:8px">
<button class="btn btn-outline" style="flex:1" onclick="showPage('chat')">💬 追问 AI</button>
<button class="btn btn-outline" style="flex:1" onclick="getDoctorQuestions()">🩺 问诊清单</button>
</div>`;
html += '</div>';
document.getElementById('analysisResult').innerHTML = html;
document.getElementById('analysisResult').style.display = 'block';
}
// ---- Report List ----
async function loadReportList(){
try{
const data = await api('/api/report/list');
if(!data.length){
document.getElementById('reportList').innerHTML='<div class="empty-state"><div class="icon">📭</div><p>暂无报告,去首页上传吧</p></div>';
return;
}
let html='';
data.forEach(r=>{
const date = new Date(r.created_at).toLocaleDateString('zh-CN');
html += `<div class="report-item" onclick="viewReport('${r.id}')">
<div class="report-icon">📋</div>
<div class="info"><h4>${r.report_type||'检查报告'}</h4><p>${date} · 孕${r.pregnancy_week||'?'}周 · ${r.item_count}项指标</p></div>
${badgeHtml(r.overall_status)}
</div>`;
});
document.getElementById('reportList').innerHTML=html;
}catch(e){}
}
async function viewReport(id){
currentReportId = id;
showLoading('加载报告...');
try{
const data = await api(`/api/analysis/${id}`);
renderAnalysis(data);
showPage('home');
document.getElementById('ocrResult').style.display='none';
document.getElementById('analysisResult').style.display='block';
hideLoading();
}catch(e){
hideLoading();
// No analysis yet, show report detail
try{
const report = await api(`/api/report/${id}`);
currentReportId = id;
ocrDataItems = report.data_items||[];
renderOcrItems(ocrDataItems, report.report_type);
showPage('home');
document.getElementById('ocrResult').style.display='block';
document.getElementById('analysisResult').style.display='none';
}catch(e2){alert(e2.message)}
}
}
// ---- Chat ----
async function loadChatReportSelect(){
try{
const data = await api('/api/report/list');
const sel = document.getElementById('chatReportId');
sel.innerHTML = data.length ? data.map(r=>`<option value="${r.id}">${r.report_type||'报告'} - ${new Date(r.created_at).toLocaleDateString('zh-CN')}</option>`).join('') : '<option value="">请先上传报告</option>';
if(currentReportId) sel.value=currentReportId;
loadChatHistory();
}catch(e){}
}
async function loadChatHistory(){
const rid = document.getElementById('chatReportId').value;
if(!rid) return;
try{
const data = await api(`/api/chat/${rid}/history`);
const container = document.getElementById('chatMessages');
if(!data.messages.length){
container.innerHTML='<div class="empty-state" style="padding:30px"><div class="icon">💬</div><p>有任何疑问都可以问我哦~</p></div>';
}else{
container.innerHTML = data.messages.map(m=>`<div class="chat-msg ${m.role}"><div class="chat-bubble">${m.content}</div></div>`).join('');
container.scrollTop = container.scrollHeight;
}
// Quick questions
const qqs = data.quick_questions||["这个指标偏高会影响宝宝吗?","我需要注意什么饮食?","什么时候需要复查?"];
document.getElementById('quickQuestions').innerHTML = qqs.map(q=>`<button class="quick-q" onclick="askQuick('${q}')">${q}</button>`).join('');
}catch(e){}
}
document.getElementById('chatReportId').onchange=loadChatHistory;
function askQuick(q){document.getElementById('chatInput').value=q;sendChat()}
async function sendChat(){
const rid = document.getElementById('chatReportId').value;
const input = document.getElementById('chatInput');
const msg = input.value.trim();
if(!rid||!msg) return;
input.value='';
const container = document.getElementById('chatMessages');
// Clear empty state
if(container.querySelector('.empty-state')) container.innerHTML='';
container.innerHTML+=`<div class="chat-msg user"><div class="chat-bubble">${msg}</div></div>`;
container.innerHTML+=`<div class="chat-msg assistant" id="typingIndicator"><div class="chat-bubble">思考中...</div></div>`;
container.scrollTop = container.scrollHeight;
try{
const data = await api(`/api/chat/${rid}/message`,{method:'POST',body:{content:msg}});
document.getElementById('typingIndicator').remove();
container.innerHTML+=`<div class="chat-msg assistant"><div class="chat-bubble">${data.ai_message.content}</div></div>`;
container.scrollTop = container.scrollHeight;
}catch(e){
document.getElementById('typingIndicator').remove();
container.innerHTML+=`<div class="chat-msg assistant"><div class="chat-bubble" style="color:var(--red)">回复失败:${e.message}</div></div>`;
}
}
// ---- Profile ----
async function loadProfile(){
try{
const data = await api('/api/user/profile');
document.getElementById('userName').textContent = data.name||'体验用户';
document.getElementById('userInfo').textContent = `${data.age||'?'}岁 · 孕${data.pregnancy_week||'?'}周${data.pregnancy_day||0}天 · ${data.blood_type||'?'}型血`;
// Also update nutrition select
const reports = await api('/api/report/list');
document.getElementById('nutritionReportId').innerHTML = reports.length ? reports.map(r=>`<option value="${r.id}">${r.report_type||'报告'} - ${new Date(r.created_at).toLocaleDateString('zh-CN')}</option>`).join('') : '<option value="">暂无报告</option>';
}catch(e){}
}
// ---- Nutrition ----
async function getNutrition(){
const rid = document.getElementById('nutritionReportId').value;
if(!rid){alert('请先选择报告');return}
showLoading('AI 正在生成营养建议...');
try{
const data = await api(`/api/nutrition/${rid}`);
let html='';
(data.recipes||[]).forEach(r=>{
html+=`<div style="background:#fafafa;border-radius:8px;padding:12px;margin-bottom:8px">
<div style="font-weight:600;font-size:14px">🍳 ${r.name}</div>
<p style="font-size:12px;color:var(--gray);margin-top:4px">${r.description||''}</p>
<p style="font-size:12px;margin-top:4px">⏱ ${r.time||''} · ${r.difficulty||''}</p>
<p style="font-size:12px;color:var(--pink);margin-top:4px">📌 ${r.reason||''}</p>
${r.ingredients?'<p style="font-size:12px;margin-top:4px">食材:'+r.ingredients.join('、')+'</p>':''}
</div>`;
});
if(data.dietary_tips&&data.dietary_tips.length){
html+='<div style="margin-top:8px"><h4 style="font-size:13px;color:var(--orange)">⚠️ 饮食提醒</h4>';
data.dietary_tips.forEach(t=>{html+=`<p style="font-size:13px;padding:2px 0">• ${t}</p>`});
html+='</div>';
}
document.getElementById('nutritionResult').innerHTML=html;
hideLoading();
}catch(e){hideLoading();alert('获取失败:'+e.message)}
}
// ---- Exercise ----
async function getExercise(){
showLoading('AI 正在生成运动建议...');
try{
const data = await api('/api/exercise/recommendations');
let html='';
(data.exercises||[]).forEach(ex=>{
html+=`<div style="background:#fafafa;border-radius:8px;padding:12px;margin-bottom:8px">
<div style="font-weight:600;font-size:14px">🏃 ${ex.name}</div>
<p style="font-size:12px;color:var(--gray);margin-top:4px">${ex.description||''}</p>
<p style="font-size:12px;margin-top:4px">⏱ ${ex.duration||''} · 🔥 ${ex.calories||''} · ${ex.difficulty||''}</p>
${ex.benefits?'<p style="font-size:12px;color:var(--green);margin-top:4px">✅ '+ex.benefits.join(' · ')+'</p>':''}
${ex.precautions?'<p style="font-size:12px;color:var(--orange);margin-top:4px">⚠️ '+ex.precautions.join(' · ')+'</p>':''}
</div>`;
});
if(data.daily_schedule&&data.daily_schedule.length){
html+='<div style="margin-top:8px"><h4 style="font-size:13px;color:var(--pink)">📅 每日运动安排</h4>';
data.daily_schedule.forEach(s=>{html+=`<p style="font-size:13px;padding:2px 0">${s.time} ${s.activity} (${s.duration})</p>`});
html+='</div>';
}
document.getElementById('exerciseResult').innerHTML=html;
hideLoading();
}catch(e){hideLoading();alert('获取失败:'+e.message)}
}
// ---- Doctor Questions ----
async function getDoctorQuestions(){
if(!currentReportId){alert('请先选择报告');return}
showLoading('AI 正在生成问诊清单...');
try{
const data = await api(`/api/doctor/questions/${currentReportId}`);
let html='<div class="card"><h3>🩺 问诊清单</h3><p style="font-size:12px;color:var(--gray);margin-bottom:12px">带着这些问题去看医生,沟通更高效</p>';
(data.questions||[]).forEach(q=>{
html+=`<div class="action-item"><div class="action-dot ${q.priority}"></div><div>
<div style="font-size:14px;font-weight:500">"${q.question}"</div>
<div style="font-size:12px;color:var(--gray);margin-top:2px">💡 ${q.reason}</div>
</div></div>`;
});
html+='</div>';
document.getElementById('analysisResult').innerHTML+=html;
hideLoading();
}catch(e){hideLoading();alert('获取失败:'+e.message)}
}
// ---- Init ----
demoLogin();
</script>
</body>
</html>