Commit 99b20067 authored by 阮涛's avatar 阮涛

Add new file

parents
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>慢病健康管理系统 · ChronicCare</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--sage:#2d6a4f;--sage-mid:#40916c;--sage-light:#74c69d;--sage-pale:#d8f3dc;
--mint:#b7e4c7;--white:#ffffff;--off:#f8faf9;
--stone:#1b2d27;--stone-mid:#354f47;--stone-light:#5c7a6e;
--text:#1e3a32;--text-mid:#4a6b60;--text-light:#7a9e94;--text-faint:#afc8bf;
--border:#ddeee6;--border-strong:#b7d9c8;
--amber:#e76f00;--amber-pale:#fff3e0;
--rose:#c0392b;--rose-pale:#fdecea;
--sky:#1565c0;--sky-pale:#e3f2fd;
--gold:#b8860b;--gold-pale:#fffde7;
--shadow:0 2px 20px rgba(45,106,79,.09);
--shadow-lg:0 8px 40px rgba(45,106,79,.14);
--r:12px;
--sidebar:230px;
--header:58px;
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;font-family:'Noto Sans SC',sans-serif;background:var(--off);color:var(--text);font-size:14px;}
/* ── LAYOUT ── */
.app{display:flex;height:100vh;overflow:hidden;}
/* ── SIDEBAR ── */
.sidebar{
width:var(--sidebar);min-width:var(--sidebar);
background:var(--stone);
display:flex;flex-direction:column;overflow:hidden;
}
.brand{
padding:18px 16px 14px;
border-bottom:1px solid rgba(255,255,255,.07);
}
.brand-row{display:flex;align-items:center;gap:10px;}
.brand-icon{
width:36px;height:36px;border-radius:10px;
background:linear-gradient(135deg,var(--sage-light),var(--sage-mid));
display:flex;align-items:center;justify-content:center;
font-size:18px;flex-shrink:0;
}
.brand-title{font-family:'Lora',serif;font-size:16px;font-weight:700;color:#fff;line-height:1.1;}
.brand-sub{font-size:10px;color:rgba(255,255,255,.4);letter-spacing:1px;margin-top:2px;}
.nav{flex:1;padding:10px 10px;overflow-y:auto;}
.nav::-webkit-scrollbar{width:0;}
.nav-group{margin-bottom:18px;}
.nav-group-label{font-size:10px;color:rgba(255,255,255,.3);letter-spacing:1.5px;text-transform:uppercase;padding:0 8px;margin-bottom:6px;}
.nav-item{
display:flex;align-items:center;gap:9px;
padding:8px 10px;border-radius:8px;
cursor:pointer;color:rgba(255,255,255,.55);
font-size:13px;transition:all .15s;margin-bottom:1px;
}
.nav-item:hover{background:rgba(255,255,255,.07);color:rgba(255,255,255,.85);}
.nav-item.active{background:rgba(116,198,157,.18);color:var(--sage-light);}
.nav-item .ni{font-size:15px;width:20px;text-align:center;flex-shrink:0;}
.nav-badge{margin-left:auto;background:var(--amber);color:#fff;font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px;}
.sidebar-user{
padding:12px 14px;border-top:1px solid rgba(255,255,255,.07);
display:flex;align-items:center;gap:10px;
}
.su-avatar{
width:32px;height:32px;border-radius:50%;
background:linear-gradient(135deg,var(--sage-light),var(--sage));
display:flex;align-items:center;justify-content:center;
font-size:13px;font-weight:700;color:#fff;flex-shrink:0;
}
.su-name{font-size:13px;font-weight:500;color:rgba(255,255,255,.85);}
.su-role{font-size:10px;color:rgba(255,255,255,.35);}
/* ── MAIN ── */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;}
.topbar{
height:var(--header);min-height:var(--header);
background:var(--white);border-bottom:1px solid var(--border);
display:flex;align-items:center;padding:0 24px;gap:16px;
}
.topbar-title{font-family:'Lora',serif;font-size:19px;font-weight:600;color:var(--text);}
.topbar-sub{font-size:12px;color:var(--text-light);margin-top:1px;}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:10px;}
.live-indicator{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-light);}
.live-dot{width:7px;height:7px;border-radius:50%;background:var(--sage-mid);animation:pulse 2s infinite;}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
.content{flex:1;overflow-y:auto;padding:20px 24px;}
.content::-webkit-scrollbar{width:4px;}
.content::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:4px;}
.page{display:none;}
.page.active{display:block;animation:rise .2s ease;}
@keyframes rise{from{opacity:0;transform:translateY(5px);}to{opacity:1;transform:none;}}
/* ── BUTTONS ── */
.btn{
display:inline-flex;align-items:center;gap:6px;
padding:8px 16px;border:none;border-radius:8px;
font-family:'Noto Sans SC',sans-serif;
font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;
}
.btn-primary{background:var(--sage-mid);color:#fff;}
.btn-primary:hover{background:var(--sage);box-shadow:0 4px 12px rgba(64,145,108,.3);}
.btn-outline{background:transparent;color:var(--sage-mid);border:1.5px solid var(--sage-mid);}
.btn-outline:hover{background:var(--sage-pale);}
.btn-ghost{background:var(--off);color:var(--text-mid);border:1px solid var(--border);}
.btn-ghost:hover{border-color:var(--sage-light);color:var(--sage-mid);}
.btn-danger{background:var(--rose-pale);color:var(--rose);border:1px solid #f5b7b1;}
.btn-danger:hover{background:#fad7d4;}
.btn-amber{background:var(--amber-pale);color:var(--amber);border:1px solid #ffd9a0;}
.btn-sm{padding:5px 12px;font-size:12px;}
.btn-xs{padding:3px 8px;font-size:11px;}
/* ── CARDS ── */
.card{
background:var(--white);border:1px solid var(--border);
border-radius:var(--r);padding:20px 22px;
margin-bottom:16px;box-shadow:var(--shadow);
}
.card-hd{
display:flex;align-items:center;justify-content:space-between;
margin-bottom:16px;padding-bottom:12px;
border-bottom:1px solid var(--border);
}
.card-title{font-size:14px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:7px;}
.card-title .ct-icon{font-size:16px;}
/* KPI */
.kpi-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px;}
.kpi{
background:var(--white);border:1px solid var(--border);
border-radius:var(--r);padding:16px 18px;
box-shadow:var(--shadow);position:relative;overflow:hidden;
transition:transform .2s;
}
.kpi:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg);}
.kpi::after{
content:'';position:absolute;bottom:0;left:0;right:0;height:3px;
}
.kpi.green::after{background:linear-gradient(90deg,var(--sage-mid),var(--sage-light));}
.kpi.amber::after{background:var(--amber);}
.kpi.rose::after{background:var(--rose);}
.kpi.sky::after{background:var(--sky);}
.kpi-label{font-size:11px;color:var(--text-light);letter-spacing:.5px;margin-bottom:6px;}
.kpi-val{font-family:'Lora',serif;font-size:28px;font-weight:700;color:var(--text);line-height:1;}
.kpi-val span{font-size:13px;color:var(--text-light);font-family:'Noto Sans SC',sans-serif;margin-left:3px;}
.kpi-sub{font-size:11px;color:var(--text-faint);margin-top:4px;}
.kpi-glyph{position:absolute;right:14px;top:12px;font-size:26px;opacity:.1;}
/* PILLS */
.pill{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:500;}
.pill-green{background:var(--sage-pale);color:var(--sage);}
.pill-amber{background:var(--amber-pale);color:var(--amber);}
.pill-rose{background:var(--rose-pale);color:var(--rose);}
.pill-sky{background:var(--sky-pale);color:var(--sky);}
.pill-gold{background:var(--gold-pale);color:var(--gold);}
.pill-gray{background:#f0f4f2;color:var(--text-light);}
.pill-dot{width:5px;height:5px;border-radius:50%;background:currentColor;}
/* TABLE */
.tbl-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;}
thead th{padding:9px 12px;text-align:left;font-size:11px;color:var(--text-light);letter-spacing:.8px;text-transform:uppercase;border-bottom:1.5px solid var(--border);white-space:nowrap;font-weight:500;}
tbody td{padding:11px 12px;border-bottom:1px solid var(--border);color:var(--text);font-size:13px;vertical-align:middle;}
tbody tr:last-child td{border-bottom:none;}
tbody tr:hover td{background:var(--off);}
/* FORMS */
.fg{display:flex;flex-direction:column;gap:5px;}
.fg-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
.fg-grid.c3{grid-template-columns:1fr 1fr 1fr;}
.fg.full{grid-column:1/-1;}
.fg label{font-size:11px;color:var(--text-mid);font-weight:500;letter-spacing:.5px;text-transform:uppercase;}
.fi{
padding:8px 11px;border:1.5px solid var(--border);border-radius:8px;
background:var(--off);color:var(--text);
font-family:'Noto Sans SC',sans-serif;font-size:13px;
outline:none;transition:border-color .15s,background .15s;
}
.fi:focus{border-color:var(--sage-mid);background:var(--white);}
.fi-select option{background:var(--white);}
.fi-ta{resize:vertical;min-height:72px;}
.form-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}
/* MODAL */
.overlay{
display:none;position:fixed;inset:0;
background:rgba(27,45,39,.5);backdrop-filter:blur(3px);
z-index:600;align-items:center;justify-content:center;
}
.overlay.show{display:flex;animation:fadeIn .2s;}
@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
.modal{
background:var(--white);border:1px solid var(--border);
border-radius:16px;padding:26px 30px;
width:90%;max-width:560px;max-height:88vh;overflow-y:auto;
box-shadow:var(--shadow-lg);
}
.modal-lg{max-width:720px;}
.modal-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;}
.modal-title{font-family:'Lora',serif;font-size:18px;font-weight:600;color:var(--text);}
.modal-close{background:none;border:none;color:var(--text-light);font-size:20px;cursor:pointer;line-height:1;}
.modal-close:hover{color:var(--text);}
/* PROGRESS */
.prog{height:6px;background:var(--border);border-radius:3px;overflow:hidden;}
.prog-bar{height:100%;border-radius:3px;transition:width .4s;}
.pb-green{background:linear-gradient(90deg,var(--sage-mid),var(--sage-light));}
.pb-amber{background:var(--amber);}
.pb-rose{background:var(--rose);}
.pb-sky{background:var(--sky);}
/* SEARCH */
.search-wrap{
display:flex;align-items:center;gap:8px;
background:var(--off);border:1.5px solid var(--border);
border-radius:8px;padding:7px 12px;flex:1;max-width:280px;
}
.search-wrap input{background:none;border:none;outline:none;color:var(--text);font-family:'Noto Sans SC',sans-serif;font-size:13px;flex:1;}
.search-wrap input::placeholder{color:var(--text-faint);}
/* TABS */
.tabs{display:flex;gap:0;border-bottom:1.5px solid var(--border);margin-bottom:16px;}
.tab{padding:8px 16px;cursor:pointer;font-size:13px;color:var(--text-light);border-bottom:2px solid transparent;margin-bottom:-1.5px;transition:all .15s;}
.tab:hover{color:var(--text);}
.tab.active{color:var(--sage-mid);border-bottom-color:var(--sage-mid);font-weight:500;}
/* TIMELINE */
.tl{padding-left:18px;border-left:2px solid var(--border);}
.tl-item{position:relative;padding:0 0 14px 18px;}
.tl-item::before{content:'';position:absolute;left:-23px;top:5px;width:9px;height:9px;border-radius:50%;background:var(--sage-mid);border:2px solid var(--white);}
.tl-time{font-size:11px;color:var(--text-faint);margin-bottom:2px;}
.tl-title{font-size:13px;font-weight:500;margin-bottom:2px;}
.tl-desc{font-size:12px;color:var(--text-light);}
/* HEALTH CHART BOXES */
.chart-box{position:relative;height:200px;}
.chart-box-lg{position:relative;height:260px;}
/* ALERT */
.alert{
display:flex;align-items:flex-start;gap:10px;
padding:11px 14px;border-radius:9px;margin-bottom:10px;
}
.alert-rose{background:var(--rose-pale);border:1px solid #f5c6c6;}
.alert-amber{background:var(--amber-pale);border:1px solid #ffd9a0;}
.alert-green{background:var(--sage-pale);border:1px solid var(--mint);}
.alert-sky{background:var(--sky-pale);border:1px solid #90caf9;}
.alert-icon{font-size:16px;flex-shrink:0;margin-top:1px;}
.alert-body{flex:1;}
.alert-title{font-size:13px;font-weight:600;margin-bottom:2px;}
.alert-desc{font-size:12px;color:var(--text-mid);}
/* MEDICINE CARDS */
.med-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;}
.med-card{
background:var(--off);border:1.5px solid var(--border);
border-radius:10px;padding:14px 16px;
transition:border-color .15s,transform .15s;
}
.med-card:hover{border-color:var(--sage-light);transform:translateY(-1px);}
.med-card.taken{border-color:var(--sage-light);background:var(--sage-pale);}
.med-card.missed{border-color:#f5c6c6;background:var(--rose-pale);}
.med-name{font-size:14px;font-weight:600;margin-bottom:4px;}
.med-dose{font-size:12px;color:var(--text-mid);}
.med-time{font-size:11px;color:var(--text-faint);margin-top:2px;}
.med-actions{margin-top:10px;display:flex;gap:6px;}
/* DRAWER */
.drawer{
position:fixed;right:-500px;top:0;bottom:0;width:480px;
background:var(--white);border-left:1px solid var(--border);
z-index:500;transition:right .3s cubic-bezier(.4,0,.2,1);
display:flex;flex-direction:column;box-shadow:var(--shadow-lg);
}
.drawer.open{right:0;}
.drawer-hd{padding:18px 22px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.drawer-body{flex:1;overflow-y:auto;padding:18px 22px;}
.drawer-body::-webkit-scrollbar{width:3px;}
.drawer-body::-webkit-scrollbar-thumb{background:var(--border-strong);}
.drawer-footer{padding:14px 22px;border-top:1px solid var(--border);display:flex;gap:8px;}
/* EDUCATION CARDS */
.edu-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;}
.edu-card{
background:var(--white);border:1px solid var(--border);
border-radius:var(--r);overflow:hidden;cursor:pointer;
transition:transform .2s,box-shadow .2s;
box-shadow:var(--shadow);
}
.edu-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);}
.edu-banner{height:80px;display:flex;align-items:center;justify-content:center;font-size:36px;}
.edu-body{padding:14px 16px;}
.edu-tag{font-size:10px;letter-spacing:.8px;text-transform:uppercase;font-weight:600;margin-bottom:6px;}
.edu-title{font-size:14px;font-weight:600;margin-bottom:6px;color:var(--text);}
.edu-desc{font-size:12px;color:var(--text-mid);line-height:1.6;}
.edu-footer{padding:10px 16px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
/* TOAST */
.toast-wrap{position:fixed;bottom:22px;right:22px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
.toast{
background:var(--white);border:1px solid var(--border);border-radius:10px;
padding:11px 16px;display:flex;align-items:center;gap:10px;
box-shadow:0 8px 30px rgba(0,0,0,.12);animation:slideUp .3s ease;
font-size:13px;min-width:220px;max-width:320px;
}
@keyframes slideUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
.toast.success{border-left:3px solid var(--sage-mid);}
.toast.error{border-left:3px solid var(--rose);}
.toast.info{border-left:3px solid var(--sky);}
.toast.warning{border-left:3px solid var(--amber);}
/* VITAL SIGN CARDS */
.vital-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;}
.vital-card{
background:var(--off);border:1.5px solid var(--border);
border-radius:10px;padding:14px 16px;
}
.vital-label{font-size:11px;color:var(--text-light);letter-spacing:.5px;margin-bottom:6px;}
.vital-val{font-family:'Lora',serif;font-size:24px;font-weight:700;color:var(--text);}
.vital-unit{font-size:12px;color:var(--text-light);font-family:'Noto Sans SC',sans-serif;margin-left:3px;}
.vital-status{font-size:11px;margin-top:4px;}
.vital-normal{color:var(--sage-mid);}
.vital-high{color:var(--rose);}
.vital-low{color:var(--amber);}
/* EMPTY */
.empty{text-align:center;padding:36px 20px;color:var(--text-faint);}
.empty-icon{font-size:36px;margin-bottom:8px;opacity:.4;}
.empty-text{font-size:13px;}
/* EXPORT GRID */
.export-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:14px;}
.exp-card{
background:var(--off);border:1.5px solid var(--border);
border-radius:10px;padding:18px;cursor:pointer;
text-align:center;transition:all .2s;
}
.exp-card:hover{border-color:var(--sage-mid);transform:translateY(-2px);background:var(--sage-pale);}
.exp-icon{font-size:30px;margin-bottom:8px;}
.exp-name{font-size:14px;font-weight:600;color:var(--text);margin-bottom:3px;}
.exp-desc{font-size:11px;color:var(--text-light);}
/* SECTION DIVIDER */
.sec{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px;}
.sec-label{font-size:12px;font-weight:600;color:var(--text-light);text-transform:uppercase;letter-spacing:.8px;}
/* HEALTH SCORE RING */
.score-wrap{text-align:center;padding:8px 0;}
.score-big{font-family:'Lora',serif;font-size:52px;font-weight:700;color:var(--sage-mid);line-height:1;}
.score-label{font-size:12px;color:var(--text-light);margin-top:4px;}
/* RESPONSIVE */
@media(max-width:900px){
.kpi-grid{grid-template-columns:repeat(2,1fr);}
.edu-grid{grid-template-columns:repeat(2,1fr);}
.med-grid{grid-template-columns:1fr 1fr;}
.fg-grid,.fg-grid.c3{grid-template-columns:1fr;}
}
</style>
</head>
<body>
<div class="app">
<!-- ══════════════ SIDEBAR ══════════════ -->
<aside class="sidebar">
<div class="brand">
<div class="brand-row">
<div class="brand-icon">🌿</div>
<div>
<div class="brand-title">ChronicCare</div>
<div class="brand-sub">慢病管理系统</div>
</div>
</div>
</div>
<nav class="nav">
<div class="nav-group">
<div class="nav-group-label">核心功能</div>
<div class="nav-item active" onclick="nav(this,'dashboard')"><span class="ni">🏠</span>健康总览</div>
<div class="nav-item" onclick="nav(this,'patients')"><span class="ni">👥</span>患者档案</div>
<div class="nav-item" onclick="nav(this,'vitals')"><span class="ni">📊</span>健康监测</div>
<div class="nav-item" onclick="nav(this,'medicine')"><span class="ni">💊</span>用药管理<span class="nav-badge">3</span></div>
<div class="nav-item" onclick="nav(this,'followup')"><span class="ni">📅</span>远程随访<span class="nav-badge">5</span></div>
</div>
<div class="nav-group">
<div class="nav-group-label">工具</div>
<div class="nav-item" onclick="nav(this,'alerts')"><span class="ni">🔔</span>异常提醒<span class="nav-badge" id="alert-badge">4</span></div>
<div class="nav-item" onclick="nav(this,'education')"><span class="ni">📚</span>健康教育</div>
<div class="nav-item" onclick="nav(this,'reports')"><span class="ni">📤</span>报表导出</div>
<div class="nav-item" onclick="nav(this,'settings')"><span class="ni">⚙️</span>系统设置</div>
</div>
</nav>
<div class="sidebar-user">
<div class="su-avatar"></div>
<div>
<div class="su-name">王医生</div>
<div class="su-role">主治医师 · 心内科</div>
</div>
</div>
</aside>
<!-- ══════════════ MAIN ══════════════ -->
<div class="main">
<div class="topbar">
<div>
<div class="topbar-title" id="tb-title">健康总览</div>
<div class="topbar-sub" id="tb-sub">今日共有 8 项待处理事项</div>
</div>
<div class="topbar-right">
<div class="live-indicator">
<div class="live-dot"></div><span>实时监控中</span>
</div>
<button class="btn btn-ghost btn-sm" onclick="openModal('modal-addpt')">+ 新增患者</button>
<button class="btn btn-primary btn-sm" onclick="openModal('modal-addvital')">📥 录入指标</button>
</div>
</div>
<div class="content">
<!-- ════ DASHBOARD ════ -->
<div class="page active" id="page-dashboard">
<div class="kpi-grid">
<div class="kpi green">
<div class="kpi-label">在管患者</div>
<div class="kpi-val">1,147<span></span></div>
<div class="kpi-sub">↑ 本月新增 31 人</div>
<div class="kpi-glyph">👥</div>
</div>
<div class="kpi amber">
<div class="kpi-label">今日提醒</div>
<div class="kpi-val">48<span></span></div>
<div class="kpi-sub">用药+随访提醒</div>
<div class="kpi-glyph">🔔</div>
</div>
<div class="kpi rose">
<div class="kpi-label">异常预警</div>
<div class="kpi-val">4<span></span></div>
<div class="kpi-sub">需立即关注</div>
<div class="kpi-glyph">⚠️</div>
</div>
<div class="kpi sky">
<div class="kpi-label">随访完成率</div>
<div class="kpi-val">89<span>%</span></div>
<div class="kpi-sub">本月 312 次</div>
<div class="kpi-glyph">📅</div>
</div>
</div>
<!-- Alerts -->
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">⚠️</span>今日异常预警</div>
<button class="btn btn-ghost btn-sm" onclick="nav(document.querySelector('[onclick*=alerts]'),'alerts')">查看全部</button>
</div>
<div class="alert alert-rose">
<div class="alert-icon">🔴</div>
<div class="alert-body">
<div class="alert-title">张伟民 – 血糖严重偏高</div>
<div class="alert-desc">空腹血糖 14.2 mmol/L(正常范围 3.9–6.1),需紧急处置</div>
</div>
<button class="btn btn-danger btn-sm" onclick="openDrawer('P1001')">查看</button>
</div>
<div class="alert alert-rose">
<div class="alert-icon">🔴</div>
<div class="alert-body">
<div class="alert-title">李秀英 – 血压危急值</div>
<div class="alert-desc">收缩压 182 mmHg,超出安全阈值,请立即联系患者</div>
</div>
<button class="btn btn-danger btn-sm" onclick="openDrawer('P1002')">查看</button>
</div>
<div class="alert alert-amber">
<div class="alert-icon">🟡</div>
<div class="alert-body">
<div class="alert-title">王建国 – 用药依从性下降</div>
<div class="alert-desc">连续 3 天漏服降压药,本周依从率 57%</div>
</div>
<button class="btn btn-amber btn-sm" onclick="openDrawer('P1003')">提醒</button>
</div>
<div class="alert alert-amber">
<div class="alert-icon">🟡</div>
<div class="alert-body">
<div class="alert-title">刘 静 – 体重异常增加</div>
<div class="alert-desc">本月体重增加 4.2kg,心衰患者需警惕水肿</div>
</div>
<button class="btn btn-amber btn-sm">提醒</button>
</div>
</div>
<div style="display:grid;grid-template-columns:3fr 2fr;gap:16px;">
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">📈</span>血糖 / 血压趋势(近7天)</div>
<div style="display:flex;gap:8px;">
<span class="pill pill-green">血糖</span>
<span class="pill pill-sky">血压</span>
</div>
</div>
<div class="chart-box-lg"><canvas id="dashTrendChart"></canvas></div>
</div>
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">🥧</span>疾病类型分布</div>
</div>
<div class="chart-box-lg"><canvas id="diseaseChart"></canvas></div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">💊</span>今日用药提醒</div>
<span class="pill pill-amber">待服 3 人</span>
</div>
<div id="dash-meds"></div>
</div>
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">🕐</span>近期随访记录</div>
</div>
<div class="tl" id="dash-timeline"></div>
</div>
</div>
</div>
<!-- ════ PATIENTS ════ -->
<div class="page" id="page-patients">
<div class="card" style="padding:14px 18px;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<div class="search-wrap"><span>🔍</span><input id="pt-search" type="text" placeholder="搜索患者姓名、ID、诊断…" oninput="filterPt()"></div>
<select class="fi fi-select" style="width:130px;" id="pt-fs" onchange="filterPt()">
<option value="">全部疾病</option>
<option>糖尿病</option><option>高血压</option><option>冠心病</option><option>慢阻肺</option><option>慢性肾病</option>
</select>
<select class="fi fi-select" style="width:120px;" id="pt-fst" onchange="filterPt()">
<option value="">全部状态</option>
<option>管理中</option><option>待随访</option><option>病情稳定</option><option>病情恶化</option>
</select>
<button class="btn btn-primary btn-sm" onclick="openModal('modal-addpt')">+ 新增患者</button>
<button class="btn btn-ghost btn-sm" onclick="exportData('patients')">📤 导出</button>
</div>
</div>
<div class="card">
<div class="tbl-wrap">
<table>
<thead><tr><th>ID</th><th>姓名</th><th>年龄/性别</th><th>主要诊断</th><th>健康评分</th><th>用药状态</th><th>状态</th><th>最后随访</th><th>操作</th></tr></thead>
<tbody id="pt-tbody"></tbody>
</table>
<div class="empty" id="pt-empty" style="display:none;"><div class="empty-icon">👥</div><div class="empty-text">未找到匹配患者</div></div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:1px solid var(--border);">
<span style="font-size:12px;color:var(--text-light);" id="pt-count">共 0 名患者</span>
<div style="display:flex;gap:6px;">
<button class="btn btn-ghost btn-sm">« 上页</button>
<button class="btn btn-outline btn-sm">1</button>
<button class="btn btn-ghost btn-sm">2</button>
<button class="btn btn-ghost btn-sm">下页 »</button>
</div>
</div>
</div>
</div>
<!-- ════ VITALS ════ -->
<div class="page" id="page-vitals">
<div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;">
<button class="btn btn-primary btn-sm" onclick="openModal('modal-addvital')">+ 录入指标</button>
<select class="fi fi-select" style="width:180px;" id="vital-pt" onchange="loadPatientVitals()">
<option value="">选择患者查看</option>
</select>
<select class="fi fi-select" style="width:110px;" id="vital-days" onchange="renderVitalCharts()">
<option value="7">近7天</option>
<option value="14">近14天</option>
<option value="30">近30天</option>
</select>
</div>
<div class="vital-grid" style="margin-bottom:16px;" id="vital-summary"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">🩸</span>血糖变化趋势</div></div>
<div class="chart-box-lg"><canvas id="glucoseChart"></canvas></div>
</div>
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">❤️</span>血压变化趋势</div></div>
<div class="chart-box-lg"><canvas id="bpChart"></canvas></div>
</div>
</div>
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">📋</span>健康指标记录</div>
<button class="btn btn-ghost btn-sm" onclick="exportData('vitals')">📤 导出</button>
</div>
<div class="tbl-wrap">
<table>
<thead><tr><th>日期</th><th>患者</th><th>血糖(mmol/L)</th><th>收缩压(mmHg)</th><th>舒张压(mmHg)</th><th>心率(bpm)</th><th>体重(kg)</th><th>状态评估</th></tr></thead>
<tbody id="vital-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ════ MEDICINE ════ -->
<div class="page" id="page-medicine">
<div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="openModal('modal-addmed')">+ 添加处方</button>
<button class="btn btn-ghost" onclick="openModal('modal-schedule-all')">📅 批量提醒设置</button>
</div>
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon"></span>今日用药任务</div>
<span id="med-stats" class="pill pill-green">全部按时</span>
</div>
<div class="med-grid" id="med-grid"></div>
</div>
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">📋</span>患者处方列表</div>
<button class="btn btn-ghost btn-sm" onclick="exportData('medicine')">📤 导出</button>
</div>
<div class="tbl-wrap">
<table>
<thead><tr><th>患者</th><th>药品名称</th><th>规格</th><th>用法用量</th><th>服药时间</th><th>依从率</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="med-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ════ FOLLOWUP ════ -->
<div class="page" id="page-followup">
<div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="openModal('modal-addfu')">+ 安排随访</button>
<button class="btn btn-ghost" onclick="openModal('modal-sms')">📱 发送提醒</button>
<button class="btn btn-ghost btn-sm" onclick="exportData('followup')">📤 导出计划</button>
</div>
<div class="tabs">
<div class="tab active" onclick="fuTab(this,'all')">全部</div>
<div class="tab" onclick="fuTab(this,'today')">今日(3)</div>
<div class="tab" onclick="fuTab(this,'week')">本周(12)</div>
<div class="tab" onclick="fuTab(this,'done')">已完成</div>
<div class="tab" onclick="fuTab(this,'overdue')">逾期</div>
</div>
<div class="card">
<div class="tbl-wrap">
<table>
<thead><tr><th>随访ID</th><th>患者</th><th>疾病</th><th>随访方式</th><th>计划时间</th><th>状态</th><th>医生</th><th>操作</th></tr></thead>
<tbody id="fu-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ════ ALERTS ════ -->
<div class="page" id="page-alerts">
<div class="tabs">
<div class="tab active" onclick="alertTab(this,'all')">全部(4)</div>
<div class="tab" onclick="alertTab(this,'critical')">🔴 危急(2)</div>
<div class="tab" onclick="alertTab(this,'warning')">🟡 预警(2)</div>
<div class="tab" onclick="alertTab(this,'handled')">✅ 已处理</div>
</div>
<div id="alerts-list"></div>
</div>
<!-- ════ EDUCATION ════ -->
<div class="page" id="page-education">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap;">
<div class="search-wrap"><span>🔍</span><input type="text" placeholder="搜索健康知识…"></div>
<select class="fi fi-select" style="width:130px;">
<option>全部类别</option><option>糖尿病</option><option>高血压</option><option>心脏病</option><option>饮食营养</option><option>运动康复</option>
</select>
<button class="btn btn-primary btn-sm" onclick="openModal('modal-send-edu')">📤 推送给患者</button>
</div>
<div class="edu-grid" id="edu-grid"></div>
</div>
<!-- ════ REPORTS ════ -->
<div class="page" id="page-reports">
<div class="card">
<div class="card-hd">
<div class="card-title"><span class="ct-icon">📤</span>报表导出中心</div>
</div>
<p style="font-size:13px;color:var(--text-mid);margin-bottom:4px;">选择导出格式和数据范围,系统将自动生成报表文件</p>
<div class="export-grid">
<div class="exp-card" onclick="doExport('excel')">
<div class="exp-icon">📊</div>
<div class="exp-name">Excel 报表</div>
<div class="exp-desc">完整数据明细,适合二次分析</div>
</div>
<div class="exp-card" onclick="doExport('csv')">
<div class="exp-icon">📄</div>
<div class="exp-name">CSV 格式</div>
<div class="exp-desc">通用数据格式,兼容各系统</div>
</div>
<div class="exp-card" onclick="doExport('pdf')">
<div class="exp-icon">📑</div>
<div class="exp-name">PDF 报告</div>
<div class="exp-desc">格式化报告,可直接打印归档</div>
</div>
<div class="exp-card" onclick="doExport('json')">
<div class="exp-icon">🔗</div>
<div class="exp-name">JSON 接口</div>
<div class="exp-desc">对接HIS、EMR等医院信息系统</div>
</div>
</div>
</div>
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">⚙️</span>导出配置</div></div>
<div class="fg-grid c3">
<div class="fg"><label class="form-label">数据类型</label>
<select class="fi fi-select" id="exp-type"><option>全部数据</option><option>患者档案</option><option>健康指标</option><option>用药记录</option><option>随访记录</option></select>
</div>
<div class="fg"><label class="form-label">开始日期</label><input type="date" class="fi" id="exp-start"></div>
<div class="fg"><label class="form-label">结束日期</label><input type="date" class="fi" id="exp-end"></div>
<div class="fg"><label class="form-label">科室筛选</label>
<select class="fi fi-select"><option>全部科室</option><option>心内科</option><option>内分泌科</option><option>肾内科</option><option>呼吸科</option></select>
</div>
<div class="fg"><label class="form-label">疾病类型</label>
<select class="fi fi-select"><option>全部</option><option>糖尿病</option><option>高血压</option><option>冠心病</option></select>
</div>
<div class="fg" style="align-self:flex-end;">
<button class="btn btn-primary" style="width:100%;" onclick="doExport('excel')">🚀 生成并下载</button>
</div>
</div>
</div>
<!-- Monthly Stats Chart -->
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">📊</span>月度健康管理统计</div></div>
<div class="chart-box-lg"><canvas id="reportChart"></canvas></div>
</div>
</div>
<!-- ════ SETTINGS ════ -->
<div class="page" id="page-settings">
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">🔔</span>提醒规则设置</div></div>
<div class="fg-grid">
<div class="fg"><label>血糖高值预警阈值(mmol/L)</label><input type="number" class="fi" value="7.0" step="0.1"></div>
<div class="fg"><label>血糖低值预警阈值(mmol/L)</label><input type="number" class="fi" value="3.9" step="0.1"></div>
<div class="fg"><label>血压高值预警(收缩压 mmHg)</label><input type="number" class="fi" value="140"></div>
<div class="fg"><label>提前提醒时间</label>
<select class="fi fi-select"><option>提前30分钟</option><option>提前1小时</option><option>提前2小时</option></select>
</div>
</div>
</div>
<div class="card">
<div class="card-hd"><div class="card-title"><span class="ct-icon">📱</span>短信提醒模板</div></div>
<div class="fg-grid">
<div class="fg full"><label>用药提醒模板</label>
<textarea class="fi fi-ta">尊敬的{患者姓名},您好!现在是{时间},请按时服用{药品名称} {剂量},保持健康生活!如有不适请及时联系{医生姓名}:{电话}。</textarea>
</div>
<div class="fg full"><label>随访提醒模板</label>
<textarea class="fi fi-ta">尊敬的{患者姓名},您的定期随访时间为{随访时间},请提前准备好近期健康数据。如需改期请提前告知。</textarea>
</div>
</div>
<div class="form-actions">
<button class="btn btn-ghost">恢复默认</button>
<button class="btn btn-primary" onclick="toast('设置已保存','success')">保存设置</button>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
</div><!-- /app -->
<!-- ══════ MODALS ══════ -->
<!-- Add Patient -->
<div class="overlay" id="modal-addpt">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">🆕 新增患者档案</div>
<button class="modal-close" onclick="closeModal('modal-addpt')"></button>
</div>
<div class="fg-grid">
<div class="fg"><label>姓名 *</label><input type="text" class="fi" id="np-name" placeholder="患者姓名"></div>
<div class="fg"><label>年龄</label><input type="number" class="fi" id="np-age" placeholder="岁"></div>
<div class="fg"><label>性别</label><select class="fi fi-select" id="np-sex"><option></option><option></option></select></div>
<div class="fg"><label>联系电话 *</label><input type="tel" class="fi" id="np-phone" placeholder="手机号码"></div>
<div class="fg"><label>主要诊断 *</label>
<select class="fi fi-select" id="np-disease"><option>糖尿病</option><option>高血压</option><option>冠心病</option><option>慢阻肺</option><option>慢性肾病</option></select>
</div>
<div class="fg"><label>病情严重程度</label>
<select class="fi fi-select" id="np-severity"><option>轻度</option><option>中度</option><option>重度</option></select>
</div>
<div class="fg"><label>责任医生</label><input type="text" class="fi" id="np-doctor" placeholder="主治医师姓名"></div>
<div class="fg"><label>科室</label>
<select class="fi fi-select" id="np-dept"><option>心内科</option><option>内分泌科</option><option>肾内科</option><option>呼吸科</option></select>
</div>
<div class="fg full"><label>治疗方案摘要</label><textarea class="fi fi-ta" id="np-plan" placeholder="当前治疗方案、用药情况等…"></textarea></div>
<div class="fg full"><label>备注</label><textarea class="fi fi-ta" id="np-note" placeholder="过敏史、特殊注意事项…"></textarea></div>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-addpt')">取消</button>
<button class="btn btn-primary" onclick="submitPatient()">保存档案</button>
</div>
</div>
</div>
<!-- Add Vital -->
<div class="overlay" id="modal-addvital">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">📥 录入健康指标</div>
<button class="modal-close" onclick="closeModal('modal-addvital')"></button>
</div>
<div class="fg-grid c3">
<div class="fg"><label>选择患者 *</label><select class="fi fi-select" id="vt-pt"></select></div>
<div class="fg"><label>记录日期</label><input type="date" class="fi" id="vt-date"></div>
<div class="fg"><label>记录时间</label><input type="time" class="fi" id="vt-time"></div>
<div class="fg"><label>血糖(mmol/L)</label><input type="number" class="fi" id="vt-glucose" placeholder="如:6.2" step="0.1"></div>
<div class="fg"><label>收缩压(mmHg)</label><input type="number" class="fi" id="vt-sbp" placeholder="如:120"></div>
<div class="fg"><label>舒张压(mmHg)</label><input type="number" class="fi" id="vt-dbp" placeholder="如:80"></div>
<div class="fg"><label>心率(bpm)</label><input type="number" class="fi" id="vt-hr" placeholder="如:72"></div>
<div class="fg"><label>体重(kg)</label><input type="number" class="fi" id="vt-weight" placeholder="如:65.5" step="0.1"></div>
<div class="fg"><label>血氧饱和度(%)</label><input type="number" class="fi" id="vt-spo2" placeholder="如:98"></div>
<div class="fg full"><label>症状描述</label><textarea class="fi fi-ta" id="vt-note" placeholder="当前症状、自觉不适…" style="min-height:56px;"></textarea></div>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-addvital')">取消</button>
<button class="btn btn-primary" onclick="submitVital()">保存指标</button>
</div>
</div>
</div>
<!-- Add Medicine -->
<div class="overlay" id="modal-addmed">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">💊 添加处方药物</div>
<button class="modal-close" onclick="closeModal('modal-addmed')"></button>
</div>
<div class="fg-grid">
<div class="fg"><label>选择患者 *</label><select class="fi fi-select" id="med-pt"></select></div>
<div class="fg"><label>药品名称 *</label><input type="text" class="fi" id="med-name" placeholder="药品通用名称"></div>
<div class="fg"><label>规格</label><input type="text" class="fi" id="med-spec" placeholder="如:500mg/片"></div>
<div class="fg"><label>用法用量 *</label><input type="text" class="fi" id="med-dose" placeholder="如:每日2次,每次1片"></div>
<div class="fg"><label>服药时间</label>
<select class="fi fi-select" id="med-time"><option>早餐前</option><option>早餐后</option><option>午餐后</option><option>晚餐前</option><option>晚餐后</option><option>睡前</option></select>
</div>
<div class="fg"><label>疗程天数</label><input type="number" class="fi" id="med-days" placeholder="天" value="30"></div>
<div class="fg full"><label>注意事项</label><textarea class="fi fi-ta" id="med-note" placeholder="服药注意、禁忌事项…" style="min-height:56px;"></textarea></div>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-addmed')">取消</button>
<button class="btn btn-primary" onclick="submitMed()">保存处方</button>
</div>
</div>
</div>
<!-- Add Followup -->
<div class="overlay" id="modal-addfu">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">📅 安排随访</div>
<button class="modal-close" onclick="closeModal('modal-addfu')"></button>
</div>
<div class="fg-grid">
<div class="fg"><label>选择患者 *</label><select class="fi fi-select" id="fu-pt"></select></div>
<div class="fg"><label>随访方式</label>
<select class="fi fi-select" id="fu-type"><option>电话随访</option><option>视频随访</option><option>门诊随访</option><option>上门随访</option></select>
</div>
<div class="fg"><label>计划日期 *</label><input type="date" class="fi" id="fu-date"></div>
<div class="fg"><label>计划时间</label><input type="time" class="fi" id="fu-time" value="10:00"></div>
<div class="fg"><label>随访医生</label><input type="text" class="fi" id="fu-doctor" placeholder="负责医生"></div>
<div class="fg"><label>随访主题</label>
<select class="fi fi-select" id="fu-topic"><option>常规复查</option><option>药物调整</option><option>并发症筛查</option><option>康复指导</option></select>
</div>
<div class="fg full"><label>随访重点</label><textarea class="fi fi-ta" id="fu-note" placeholder="本次随访关注的重点问题…" style="min-height:56px;"></textarea></div>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-addfu')">取消</button>
<button class="btn btn-primary" onclick="submitFollowup()">确认安排</button>
</div>
</div>
</div>
<!-- SMS -->
<div class="overlay" id="modal-sms">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">📱 发送健康提醒</div>
<button class="modal-close" onclick="closeModal('modal-sms')"></button>
</div>
<div class="fg-grid">
<div class="fg full"><label>发送对象</label>
<select class="fi fi-select"><option>本周随访患者(12人)</option><option>今日用药提醒患者(48人)</option><option>血糖异常患者(4人)</option><option>全部在管患者</option></select>
</div>
<div class="fg full"><label>消息内容</label>
<textarea class="fi fi-ta" style="min-height:100px;">尊敬的患者,您好!您的定期随访时间临近,请保持按时用药,记录每日健康指标。如有不适请及时就医。祝您身体健康!</textarea>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin:10px 0;">
<span style="font-size:12px;color:var(--text-mid);">预计发送:<strong style="color:var(--sage-mid);">12 条</strong></span>
<span style="font-size:12px;color:var(--text-mid);">预计费用:<strong style="color:var(--amber);">¥ 1.20</strong></span>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-sms')">取消</button>
<button class="btn btn-primary" onclick="sendSMS()">📤 确认发送</button>
</div>
</div>
</div>
<!-- Edu Modal -->
<div class="overlay" id="modal-send-edu">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">📤 推送健康教育内容</div>
<button class="modal-close" onclick="closeModal('modal-send-edu')"></button>
</div>
<div class="fg-grid">
<div class="fg full"><label>推送对象</label>
<select class="fi fi-select"><option>糖尿病患者(342人)</option><option>高血压患者(289人)</option><option>全部在管患者</option></select>
</div>
<div class="fg full"><label>推送方式</label>
<select class="fi fi-select"><option>系统消息 + 短信</option><option>仅系统消息</option><option>仅短信</option></select>
</div>
</div>
<div class="form-actions">
<button class="btn btn-ghost" onclick="closeModal('modal-send-edu')">取消</button>
<button class="btn btn-primary" onclick="closeModal('modal-send-edu');toast('健康教育内容已推送成功','success')">确认推送</button>
</div>
</div>
</div>
<!-- Patient Drawer -->
<div class="drawer" id="patient-drawer">
<div class="drawer-hd">
<div style="font-family:'Lora',serif;font-size:17px;font-weight:600;" id="dw-name">患者详情</div>
<button class="modal-close" onclick="closeDrawer()"></button>
</div>
<div class="drawer-body" id="dw-body"></div>
<div class="drawer-footer">
<button class="btn btn-primary btn-sm" onclick="openModal('modal-addvital')">录入指标</button>
<button class="btn btn-outline btn-sm" onclick="openModal('modal-addfu')">安排随访</button>
<button class="btn btn-ghost btn-sm" onclick="openModal('modal-sms')">发提醒</button>
</div>
</div>
<!-- Toast -->
<div class="toast-wrap" id="toast-wrap"></div>
<script>
// ══════ DATA ══════
const pick=a=>a[Math.floor(Math.random()*a.length)];
const rnd=(a,b)=>Math.floor(Math.random()*(b-a+1))+a;
const fmtDate=d=>d.toISOString().slice(0,10);
const today=fmtDate(new Date());
const DISEASES=['糖尿病','高血压','冠心病','慢阻肺','慢性肾病'];
const DOCTORS=['李明医生','张华医生','陈静医生','刘伟医生','王芳医生'];
const DEPTS=['心内科','内分泌科','肾内科','呼吸科'];
const FU_TYPES=['电话随访','视频随访','门诊随访','上门随访'];
const SN=['','','','','','','','','',''];
const GN=['','','','','','','','','',''];
function genName(){return pick(SN)+pick(GN)+(Math.random()>.6?pick(GN):'');}
function rndDate(d){const dt=new Date();dt.setDate(dt.getDate()+d);return fmtDate(dt);}
// Patients
let patients=Array.from({length:20},(_,i)=>({
id:`P${String(1001+i).padStart(4,'0')}`,
name:genName(), age:rnd(38,80), sex:Math.random()>.5?'':'',
phone:`13${rnd(100000000,999999999)}`,
disease:pick(DISEASES), severity:pick(['轻度','中度','重度']),
doctor:pick(DOCTORS), dept:pick(DEPTS),
status:pick(['管理中','待随访','病情稳定','病情恶化']),
lastVisit:rndDate(-rnd(1,30)), nextVisit:rndDate(rnd(1,21)),
score:rnd(55,95), compliance:rnd(60,100),
plan:'', note:'',
history:[
{date:rndDate(-60),type:pick(FU_TYPES),result:'血糖/血压控制良好'},
{date:rndDate(-30),type:pick(FU_TYPES),result:'调整用药方案'},
]
}));
// Vitals
let vitals=Array.from({length:30},(_,i)=>{
const p=pick(patients);
const d=new Date(); d.setDate(d.getDate()-rnd(0,30));
return {
date:fmtDate(d), patient:p.name, patientId:p.id,
glucose:parseFloat((rnd(40,160)/10).toFixed(1)),
sbp:rnd(100,185), dbp:rnd(60,115),
hr:rnd(58,105), weight:parseFloat((rnd(480,900)/10).toFixed(1)),
spo2:rnd(92,100), note:''
};
}).sort((a,b)=>b.date.localeCompare(a.date));
// Medicines
const DRUGS=['二甲双胍','格列吡嗪','阿卡波糖','胰岛素','氨氯地平','厄贝沙坦','美托洛尔','阿托伐他汀','阿司匹林','沙丁胺醇'];
let medicines=patients.slice(0,12).flatMap((p,i)=>[{
patient:p.name, patientId:p.id, drug:pick(DRUGS),
spec:'500mg/片', dose:'每日2次,每次1片',
time:pick(['早餐前','早餐后','晚餐后','睡前']),
days:rnd(30,90), compliance:rnd(65,100),
status:pick(['按时服用','偶尔漏服','经常漏服']),
note:''
}]);
// Followups
let followups=Array.from({length:16},(_,i)=>{
const p=pick(patients);
return {
id:`FU${String(2001+i).padStart(4,'0')}`,
patient:p.name, patientId:p.id, disease:p.disease,
type:pick(FU_TYPES), doctor:p.doctor,
date:rndDate(rnd(-3,14)),
status:pick(['待执行','已完成','逾期','已取消']),
topic:pick(['常规复查','药物调整','并发症筛查','康复指导']),
note:''
};
});
// ══════ NAVIGATION ══════
const PAGE_META={
dashboard:['健康总览','今日共有 8 项待处理事项'],
patients:['患者档案','管理所有慢病患者健康档案'],
vitals:['健康监测','实时监控患者健康指标数据'],
medicine:['用药管理','处方管理与服药提醒跟踪'],
followup:['远程随访','安排与跟踪患者定期随访'],
alerts:['异常提醒','查看并处理所有健康异常预警'],
education:['健康教育','向患者推送个性化健康知识'],
reports:['报表导出','生成与下载健康管理报表'],
settings:['系统设置','配置提醒规则与系统参数'],
};
function nav(el,name){
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.getElementById('page-'+name).classList.add('active');
const[t,s]=PAGE_META[name]||[name,''];
document.getElementById('tb-title').textContent=t;
document.getElementById('tb-sub').textContent=s;
if(name==='patients')renderPatients();
if(name==='vitals'){renderVitals();renderVitalCharts();}
if(name==='medicine'){renderMedicine();}
if(name==='followup')renderFollowups();
if(name==='alerts')renderAlerts('all');
if(name==='education')renderEducation();
if(name==='reports')renderReportChart();
}
// ══════ DASHBOARD ══════
function renderDashboard(){
// Trend chart
const labels=['周一','周二','周三','周四','周五','周六','周日'];
const glucoseData=[7.2,6.8,8.1,7.5,6.9,7.8,7.1];
const bpData=[135,128,142,138,131,145,133];
const ctx=document.getElementById('dashTrendChart').getContext('2d');
new Chart(ctx,{type:'line',data:{labels,datasets:[
{label:'平均血糖(mmol/L)',data:glucoseData,borderColor:'#40916c',backgroundColor:'rgba(64,145,108,.08)',fill:true,tension:.4,yAxisID:'y',pointRadius:4},
{label:'平均收缩压(mmHg)',data:bpData,borderColor:'#1565c0',backgroundColor:'rgba(21,101,192,.06)',fill:true,tension:.4,yAxisID:'y1',pointRadius:4},
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{font:{size:11},color:'#5c7a6e'}}},scales:{
x:{ticks:{color:'#7a9e94'}},
y:{type:'linear',position:'left',ticks:{color:'#40916c'},title:{display:true,text:'血糖',color:'#40916c',font:{size:10}}},
y1:{type:'linear',position:'right',ticks:{color:'#1565c0'},title:{display:true,text:'血压',color:'#1565c0',font:{size:10}},grid:{drawOnChartArea:false}}
}}});
const ctx2=document.getElementById('diseaseChart').getContext('2d');
new Chart(ctx2,{type:'doughnut',data:{
labels:DISEASES,
datasets:[{data:[30,25,18,12,15],backgroundColor:['#40916c','#1565c0','#e76f00','#c0392b','#b8860b'],borderWidth:0}]
},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11},color:'#4a6b60',padding:8}}}}});
// Today meds
const todayPts=patients.slice(0,5);
document.getElementById('dash-meds').innerHTML=todayPts.map((p,i)=>`
<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border);${i===todayPts.length-1?'border:none;':''}">
<div style="width:34px;height:34px;border-radius:50%;background:var(--sage-pale);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--sage);flex-shrink:0;">${p.name[0]}</div>
<div style="flex:1;"><div style="font-size:13px;font-weight:500;">${p.name}</div><div style="font-size:11px;color:var(--text-light);">${pick(DRUGS)} · ${pick(['早餐前','晚餐后','睡前'])}</div></div>
<span class="pill ${i<2?'pill-amber':'pill-green'}">${i<2?'待服药':'已服药'}</span>
</div>
`).join('');
// Timeline
const tls=[
{time:'今日 14:22',t:'完成随访 – 张伟民',d:'电话随访,血糖控制良好'},
{time:'今日 10:05',t:'指标录入 – 李秀英',d:'血压 182/98 mmHg,已预警'},
{time:'昨日 16:30',t:'调整方案 – 王建国',d:'增加降压药剂量'},
{time:'昨日 09:00',t:'用药提醒发送',d:'共发送 48 条服药提醒'},
];
document.getElementById('dash-timeline').innerHTML=tls.map(t=>`
<div class="tl-item"><div class="tl-time">${t.time}</div><div class="tl-title">${t.t}</div><div class="tl-desc">${t.d}</div></div>
`).join('');
}
// ══════ PATIENTS ══════
function statusPill(s){const m={管理中:'pill-green',待随访:'pill-amber',病情稳定:'pill-sky',病情恶化:'pill-rose'};return `<span class="pill ${m[s]||'pill-gray'}"><span class="pill-dot"></span>${s}</span>`;}
function compliancePill(c){return c>=80?'pill-green':c>=60?'pill-amber':'pill-rose';}
function renderPatients(list){
const data=list||patients;
const tb=document.getElementById('pt-tbody');
document.getElementById('pt-count').textContent=`共 ${data.length} 名患者`;
if(!data.length){tb.innerHTML='';document.getElementById('pt-empty').style.display='block';return;}
document.getElementById('pt-empty').style.display='none';
tb.innerHTML=data.map(p=>`<tr>
<td style="font-family:monospace;color:var(--sage);font-size:12px;">${p.id}</td>
<td style="font-weight:600;">${p.name}</td>
<td>${p.age}岁 / ${p.sex}</td>
<td><span class="pill pill-sky">${p.disease}</span></td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div class="prog" style="width:60px;"><div class="prog-bar ${p.score>=80?'pb-green':p.score>=60?'pb-amber':'pb-rose'}" style="width:${p.score}%"></div></div>
<span style="font-size:12px;font-weight:600;">${p.score}</span>
</div>
</td>
<td><span class="pill ${compliancePill(p.compliance)}">${p.compliance}%</span></td>
<td>${statusPill(p.status)}</td>
<td style="font-size:12px;color:var(--text-light);">${p.lastVisit}</td>
<td style="display:flex;gap:5px;">
<button class="btn btn-ghost btn-xs" onclick="openDrawer('${p.id}')">档案</button>
<button class="btn btn-outline btn-xs" onclick="openModal('modal-addvital')">录指标</button>
</td>
</tr>`).join('');
}
function filterPt(){
const q=document.getElementById('pt-search').value.toLowerCase();
const ds=document.getElementById('pt-fs').value;
const st=document.getElementById('pt-fst').value;
renderPatients(patients.filter(p=>
(!q||(p.name+p.id+p.disease).toLowerCase().includes(q))&&
(!ds||p.disease===ds)&&(!st||p.status===st)
));
}
// ══════ VITALS ══════
let glucoseChartInst=null, bpChartInst=null;
function populateVitalPtSelect(){
const sel=document.getElementById('vital-pt');
sel.innerHTML='<option value="">选择患者查看</option>'+patients.map(p=>`<option value="${p.id}">${p.name}${p.id})</option>`).join('');
}
function renderVitals(){
populateVitalPtSelect();
const latest=vitals[0]||{glucose:7.1,sbp:128,dbp:82,hr:72,weight:65,spo2:98};
document.getElementById('vital-summary').innerHTML=`
<div class="vital-card"><div class="vital-label">最新血糖</div>
<div class="vital-val">${latest.glucose}<span class="vital-unit">mmol/L</span></div>
<div class="vital-status ${latest.glucose>7?'vital-high':latest.glucose<3.9?'vital-low':'vital-normal'}">${latest.glucose>7?'↑ 偏高':latest.glucose<3.9?'↓ 偏低':'✓ 正常'}</div>
</div>
<div class="vital-card"><div class="vital-label">最新血压</div>
<div class="vital-val">${latest.sbp}/${latest.dbp}<span class="vital-unit">mmHg</span></div>
<div class="vital-status ${latest.sbp>140?'vital-high':'vital-normal'}">${latest.sbp>140?'↑ 偏高':'✓ 正常'}</div>
</div>
<div class="vital-card"><div class="vital-label">心率</div>
<div class="vital-val">${latest.hr}<span class="vital-unit">bpm</span></div>
<div class="vital-status ${latest.hr>100?'vital-high':latest.hr<60?'vital-low':'vital-normal'}">${latest.hr>100?'↑ 偏快':latest.hr<60?'↓ 偏慢':'✓ 正常'}</div>
</div>
<div class="vital-card"><div class="vital-label">血氧饱和度</div>
<div class="vital-val">${latest.spo2}<span class="vital-unit">%</span></div>
<div class="vital-status ${latest.spo2<95?'vital-low':'vital-normal'}">${latest.spo2<95?'↓ 偏低':'✓ 正常'}</div>
</div>`;
// Vitals table
document.getElementById('vital-tbody').innerHTML=vitals.slice(0,20).map(v=>{
const gst=v.glucose>7?'pill-rose':v.glucose<3.9?'pill-amber':'pill-green';
const bst=v.sbp>140?'pill-rose':v.sbp<100?'pill-amber':'pill-green';
return `<tr>
<td style="font-size:12px;">${v.date}</td>
<td style="font-weight:500;">${v.patient}</td>
<td><span class="pill ${gst}">${v.glucose}</span></td>
<td>${v.sbp}</td><td>${v.dbp}</td><td>${v.hr}</td><td>${v.weight}</td>
<td>${v.sbp>140||v.glucose>7?'<span class="pill pill-rose">异常</span>':'<span class="pill pill-green">正常</span>'}</td>
</tr>`;
}).join('');
}
function renderVitalCharts(){
const days=parseInt(document.getElementById('vital-days').value)||7;
const labels=Array.from({length:days},(_,i)=>{const d=new Date();d.setDate(d.getDate()-days+i+1);return `${d.getMonth()+1}/${d.getDate()}`;});
const glucoseData=labels.map(()=>parseFloat((rnd(55,140)/10).toFixed(1)));
const sbpData=labels.map(()=>rnd(110,165));
const dbpData=sbpData.map(v=>v-rnd(35,55));
const opt={responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{font:{size:11},color:'#5c7a6e'}}},scales:{x:{ticks:{color:'#7a9e94',font:{size:11}}},y:{ticks:{color:'#7a9e94'},grid:{color:'rgba(0,0,0,.04)'}}}};
if(glucoseChartInst)glucoseChartInst.destroy();
glucoseChartInst=new Chart(document.getElementById('glucoseChart').getContext('2d'),{type:'line',data:{labels,datasets:[
{label:'空腹血糖',data:glucoseData,borderColor:'#40916c',backgroundColor:'rgba(64,145,108,.1)',fill:true,tension:.4,pointRadius:4},
{label:'安全上限(7.0)',data:labels.map(()=>7.0),borderColor:'#e76f00',borderDash:[4,4],borderWidth:1.5,pointRadius:0,fill:false},
]},options:opt});
if(bpChartInst)bpChartInst.destroy();
bpChartInst=new Chart(document.getElementById('bpChart').getContext('2d'),{type:'line',data:{labels,datasets:[
{label:'收缩压',data:sbpData,borderColor:'#1565c0',backgroundColor:'rgba(21,101,192,.08)',fill:true,tension:.4,pointRadius:4},
{label:'舒张压',data:dbpData,borderColor:'#74c69d',backgroundColor:'rgba(116,198,157,.08)',fill:true,tension:.4,pointRadius:4},
]},options:opt});
}
function loadPatientVitals(){renderVitalCharts();}
// ══════ MEDICINE ══════
function renderMedicine(){
const missed=medicines.filter(m=>m.status==='经常漏服').length;
document.getElementById('med-stats').innerHTML=missed>0?`<span class="pill pill-rose">${missed}人漏服</span>`:'<span class="pill pill-green">全部按时</span>';
// Med grid (today reminders)
const todayMeds=medicines.slice(0,6);
document.getElementById('med-grid').innerHTML=todayMeds.map((m,i)=>{
const taken=i>2;const missed=i===1;
return `<div class="med-card ${taken?'taken':missed?'missed':''}">
<div class="med-name">💊 ${m.drug}</div>
<div class="med-dose">${m.dose}</div>
<div class="med-time">⏰ ${m.time} · ${m.patient}</div>
<div class="med-actions">
${taken?`<span class="pill pill-green">✓ 已服药</span>`:
`<button class="btn btn-primary btn-xs" onclick="markTaken(this)">✓ 已服药</button>
<button class="btn btn-ghost btn-xs" onclick="toast('已记录漏服并发送提醒','warning')">漏服</button>`}
</div>
</div>`;
}).join('');
// Medicine table
document.getElementById('med-tbody').innerHTML=medicines.map(m=>`<tr>
<td style="font-weight:500;">${m.patient}</td>
<td>${m.drug}</td>
<td style="color:var(--text-light);font-size:12px;">${m.spec}</td>
<td style="font-size:12px;">${m.dose}</td>
<td>${m.time}</td>
<td><span class="pill ${compliancePill(m.compliance)}">${m.compliance}%</span></td>
<td><span class="pill ${m.status==='按时服用'?'pill-green':m.status==='偶尔漏服'?'pill-amber':'pill-rose'}">${m.status}</span></td>
<td style="display:flex;gap:5px;">
<button class="btn btn-ghost btn-xs" onclick="toast('提醒已发送给患者','success')">提醒</button>
<button class="btn btn-danger btn-xs" onclick="toast('处方已停用','info')">停用</button>
</td>
</tr>`).join('');
}
function markTaken(btn){
const card=btn.closest('.med-card');
card.classList.add('taken');
card.querySelector('.med-actions').innerHTML='<span class="pill pill-green">✓ 已服药</span>';
toast('服药记录已更新','success');
}
// ══════ FOLLOWUP ══════
const fuClass={待执行:'pill-amber',已完成:'pill-green',逾期:'pill-rose',已取消:'pill-gray'};
function renderFollowups(list){
const data=list||followups;
document.getElementById('fu-tbody').innerHTML=data.map(f=>`<tr>
<td style="font-family:monospace;color:var(--sage);font-size:12px;">${f.id}</td>
<td style="font-weight:500;">${f.patient}</td>
<td><span class="pill pill-sky">${f.disease}</span></td>
<td>${f.type}</td>
<td>${f.date}</td>
<td><span class="pill ${fuClass[f.status]||'pill-gray'}"><span class="pill-dot"></span>${f.status}</span></td>
<td style="font-size:12px;color:var(--text-light);">${f.doctor}</td>
<td style="display:flex;gap:5px;">
<button class="btn btn-ghost btn-xs" onclick="completeFU('${f.id}')">完成</button>
<button class="btn btn-outline btn-xs" onclick="openModal('modal-sms')">提醒</button>
</td>
</tr>`).join('');
}
function fuTab(el,filter){
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');
const map={all:followups,today:followups.filter(f=>f.date===today),week:followups.slice(0,10),done:followups.filter(f=>f.status==='已完成'),overdue:followups.filter(f=>f.status==='逾期')};
renderFollowups(map[filter]);
}
function completeFU(id){const f=followups.find(x=>x.id===id);if(f){f.status='已完成';renderFollowups();toast('随访已标记完成','success');}}
// ══════ ALERTS ══════
const ALERTS=[
{type:'critical',title:'张伟民 – 血糖严重偏高',desc:'空腹血糖 14.2 mmol/L,超出危急值(>13.9),需立即处置',time:'今日 08:32',pid:'P1001'},
{type:'critical',title:'李秀英 – 血压危急值',desc:'收缩压 182 mmHg,高血压危象风险,请立即联系患者',time:'今日 09:15',pid:'P1002'},
{type:'warning',title:'王建国 – 连续漏服用药',desc:'连续3天未服用降压药,依从率降至57%,需干预',time:'今日 10:00',pid:'P1003'},
{type:'warning',title:'刘 静 – 体重异常增加',desc:'1个月内体重增加4.2kg,心力衰竭患者需警惕水钠潴留',time:'今日 11:20',pid:'P1004'},
];
function renderAlerts(filter){
const data=filter==='all'?ALERTS:filter==='handled'?[]:ALERTS.filter(a=>a.type===filter);
document.getElementById('alerts-list').innerHTML=data.length?data.map(a=>`
<div class="alert ${a.type==='critical'?'alert-rose':'alert-amber'}" style="margin-bottom:10px;">
<div class="alert-icon">${a.type==='critical'?'🔴':'🟡'}</div>
<div class="alert-body">
<div class="alert-title">${a.title}</div>
<div class="alert-desc">${a.desc}</div>
<div style="font-size:11px;color:var(--text-faint);margin-top:4px;">⏱ ${a.time}</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<button class="btn ${a.type==='critical'?'btn-danger':'btn-amber'} btn-sm" onclick="openDrawer('${a.pid}')">查看档案</button>
<button class="btn btn-ghost btn-sm" onclick="this.closest('.alert').remove();toast('已标记为已处理','success')">已处理</button>
</div>
</div>
`).join(''):
filter==='handled'?'<div class="empty"><div class="empty-icon">✅</div><div class="empty-text">所有预警均已处理</div></div>':
'<div class="empty"><div class="empty-icon">🎉</div><div class="empty-text">当前无此类预警</div></div>';
}
function alertTab(el,filter){document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');renderAlerts(filter);}
// ══════ EDUCATION ══════
const EDU_DATA=[
{icon:'🩸',color:'#d8f3dc',tag:'糖尿病',tagColor:'pill-green',title:'血糖自我监测指南',desc:'教您如何正确使用血糖仪,理解血糖读数,制定合理的监测计划。',reads:'1.2k'},
{icon:'❤️',color:'#e3f2fd',tag:'高血压',tagColor:'pill-sky',title:'低盐饮食实践方案',desc:'每天6g食盐的实操方法,教您识别隐藏盐分,轻松降低血压。',reads:'987'},
{icon:'🚶',color:'#fff3e0',tag:'运动康复',tagColor:'pill-amber',title:'慢病患者安全运动指南',desc:'针对慢性病患者设计的安全运动方案,包括强度、频率和注意事项。',reads:'756'},
{icon:'💊',color:'#fdecea',tag:'用药管理',tagColor:'pill-rose',title:'如何提高服药依从性',desc:'8个实用技巧帮助您养成按时服药的习惯,避免漏服和忘服。',reads:'1.5k'},
{icon:'🥗',color:'#d8f3dc',tag:'饮食营养',tagColor:'pill-green',title:'糖尿病饮食交换份法',desc:'用食物交换份轻松规划一日三餐,平衡营养又控制血糖。',reads:'634'},
{icon:'😴',color:'#e8eaf6',tag:'生活方式',tagColor:'pill-gold',title:'睡眠质量与慢病管理',desc:'充足优质睡眠对血糖、血压控制的重要性及改善睡眠的实用方法。',reads:'521'},
];
function renderEducation(){
document.getElementById('edu-grid').innerHTML=EDU_DATA.map(e=>`
<div class="edu-card">
<div class="edu-banner" style="background:${e.color};">${e.icon}</div>
<div class="edu-body">
<div class="edu-tag"><span class="pill ${e.tagColor}">${e.tag}</span></div>
<div class="edu-title">${e.title}</div>
<div class="edu-desc">${e.desc}</div>
</div>
<div class="edu-footer">
<span style="font-size:11px;color:var(--text-faint);">👁 ${e.reads} 次阅读</span>
<button class="btn btn-outline btn-xs" onclick="openModal('modal-send-edu')">推送</button>
</div>
</div>
`).join('');
}
// ══════ REPORT CHART ══════
let reportChartInst=null;
function renderReportChart(){
if(reportChartInst)return;
const labels=['8月','9月','10月','11月','12月','1月'];
reportChartInst=new Chart(document.getElementById('reportChart').getContext('2d'),{type:'bar',data:{labels,datasets:[
{label:'新增患者',data:[28,35,31,42,38,31],backgroundColor:'rgba(64,145,108,.5)',borderColor:'#40916c',borderWidth:1.5},
{label:'完成随访',data:[245,278,262,310,298,278],backgroundColor:'rgba(21,101,192,.4)',borderColor:'#1565c0',borderWidth:1.5},
{label:'异常预警',data:[12,8,15,9,11,4],backgroundColor:'rgba(192,57,43,.4)',borderColor:'#c0392b',borderWidth:1.5},
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{font:{size:11},color:'#5c7a6e'}}},scales:{x:{ticks:{color:'#7a9e94'}},y:{ticks:{color:'#7a9e94'},grid:{color:'rgba(0,0,0,.04)'}}}}});
}
// ══════ DRAWER ══════
function openDrawer(pid){
const p=patients.find(x=>x.id===pid)||patients[0];
document.getElementById('dw-name').textContent=`${p.name}${p.id})`;
document.getElementById('dw-body').innerHTML=`
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;">
${[['年龄/性别',p.age+'岁 / '+p.sex],['联系电话',p.phone],['主要诊断',p.disease],['病情程度',p.severity],['责任医生',p.doctor],['科室',p.dept]].map(([k,v])=>`
<div style="background:var(--off);border:1px solid var(--border);border-radius:8px;padding:9px 11px;">
<div style="font-size:10px;color:var(--text-faint);text-transform:uppercase;letter-spacing:.5px;">${k}</div>
<div style="font-size:13px;font-weight:500;margin-top:2px;">${v}</div>
</div>`).join('')}
</div>
<div style="margin-bottom:14px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
<span style="font-size:12px;color:var(--text-mid);">健康评分</span>
<span style="font-size:13px;font-weight:700;color:${p.score>=80?'var(--sage-mid)':p.score>=60?'var(--amber)':'var(--rose)'};">${p.score}/100</span>
</div>
<div class="prog"><div class="prog-bar ${p.score>=80?'pb-green':p.score>=60?'pb-amber':'pb-rose'}" style="width:${p.score}%"></div></div>
</div>
<div style="margin-bottom:14px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
<span style="font-size:12px;color:var(--text-mid);">用药依从率</span>
<span style="font-size:13px;font-weight:700;color:${p.compliance>=80?'var(--sage-mid)':p.compliance>=60?'var(--amber)':'var(--rose)'};">${p.compliance}%</span>
</div>
<div class="prog"><div class="prog-bar ${compliancePill(p.compliance).replace('pill-','pb-')}" style="width:${p.compliance}%"></div></div>
</div>
<div style="font-size:12px;color:var(--text-light);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">随访记录</div>
<div class="tl">
${p.history.map(h=>`<div class="tl-item"><div class="tl-time">${h.date}</div><div class="tl-title">${h.type}</div><div class="tl-desc">${h.result}</div></div>`).join('')}
</div>
<div style="margin-top:14px;background:var(--off);border:1px solid var(--border);border-radius:8px;padding:11px;">
<div style="font-size:11px;color:var(--text-faint);margin-bottom:4px;">下次随访</div>
<div style="font-size:15px;font-weight:600;color:${p.nextVisit<today?'var(--rose)':'var(--sage-mid)'};">${p.nextVisit}</div>
</div>
`;
document.getElementById('patient-drawer').classList.add('open');
}
function closeDrawer(){document.getElementById('patient-drawer').classList.remove('open');}
// ══════ MODALS ══════
function openModal(id){document.getElementById(id).classList.add('show');}
function closeModal(id){document.getElementById(id).classList.remove('show');}
function populateSelects(){
const ptOpts=patients.map(p=>`<option value="${p.id}">${p.name}${p.id})</option>`).join('');
['vt-pt','med-pt','fu-pt'].forEach(id=>{const el=document.getElementById(id);if(el)el.innerHTML=ptOpts;});
const vitPt=document.getElementById('vital-pt');
if(vitPt)vitPt.innerHTML='<option value="">选择患者查看</option>'+ptOpts;
}
function submitPatient(){
const name=document.getElementById('np-name').value.trim();
const phone=document.getElementById('np-phone').value.trim();
if(!name||!phone){toast('请填写姓名和联系电话','error');return;}
const p={
id:`P${String(1001+patients.length).padStart(4,'0')}`,
name,age:parseInt(document.getElementById('np-age').value)||50,
sex:document.getElementById('np-sex').value,phone,
disease:document.getElementById('np-disease').value,
severity:document.getElementById('np-severity').value,
doctor:document.getElementById('np-doctor').value||pick(DOCTORS),
dept:document.getElementById('np-dept').value,
status:'待随访',lastVisit:today,nextVisit:rndDate(14),
score:rnd(60,90),compliance:rnd(70,95),
plan:document.getElementById('np-plan').value,
note:document.getElementById('np-note').value,history:[]
};
patients.unshift(p);
populateSelects();
closeModal('modal-addpt');
renderPatients();
toast(`✅ 患者 ${name} 档案已建立`,'success');
['np-name','np-phone','np-age','np-plan','np-note'].forEach(id=>document.getElementById(id).value='');
}
function submitVital(){
const ptId=document.getElementById('vt-pt').value;
const glucose=parseFloat(document.getElementById('vt-glucose').value);
const sbp=parseInt(document.getElementById('vt-sbp').value);
if(!ptId||isNaN(glucose)){toast('请选择患者并填写血糖值','error');return;}
const p=patients.find(x=>x.id===ptId);
vitals.unshift({
date:document.getElementById('vt-date').value||today,
patient:p?p.name:'未知',patientId:ptId,glucose,
sbp:sbp||0,dbp:parseInt(document.getElementById('vt-dbp').value)||0,
hr:parseInt(document.getElementById('vt-hr').value)||0,
weight:parseFloat(document.getElementById('vt-weight').value)||0,
spo2:parseInt(document.getElementById('vt-spo2').value)||0,
note:document.getElementById('vt-note').value
});
closeModal('modal-addvital');
if(glucose>13.9||sbp>180){toast('⚠️ 检测到危急值,已触发预警','error');}
else if(glucose>7||sbp>140){toast('⚠️ 指标偏高,已记录并预警','warning');}
else{toast('✅ 健康指标已保存','success');}
renderVitals();
}
function submitMed(){
const ptId=document.getElementById('med-pt').value;
const name=document.getElementById('med-name').value.trim();
if(!ptId||!name){toast('请选择患者并填写药品名称','error');return;}
const p=patients.find(x=>x.id===ptId);
medicines.unshift({
patient:p?p.name:'未知',patientId:ptId,drug:name,
spec:document.getElementById('med-spec').value||'按需',
dose:document.getElementById('med-dose').value||'遵医嘱',
time:document.getElementById('med-time').value,
days:parseInt(document.getElementById('med-days').value)||30,
compliance:100,status:'按时服用',note:document.getElementById('med-note').value
});
closeModal('modal-addmed');
renderMedicine();
toast(`✅ 已为 ${p?p.name:'患者'} 添加处方:${name}`,'success');
}
function submitFollowup(){
const ptId=document.getElementById('fu-pt').value;
const date=document.getElementById('fu-date').value;
if(!ptId||!date){toast('请选择患者和随访日期','error');return;}
const p=patients.find(x=>x.id===ptId);
followups.unshift({
id:`FU${String(2001+followups.length).padStart(4,'0')}`,
patient:p?p.name:'未知',patientId:ptId,disease:p?p.disease:'',
type:document.getElementById('fu-type').value,
doctor:document.getElementById('fu-doctor').value||pick(DOCTORS),
date,status:'待执行',
topic:document.getElementById('fu-topic').value,
note:document.getElementById('fu-note').value
});
closeModal('modal-addfu');
renderFollowups();
toast(`✅ 随访已安排:${p?p.name:''} · ${date}`,'success');
}
function sendSMS(){closeModal('modal-sms');toast('📱 短信已成功发送给 12 名患者','success');}
// ══════ EXPORT ══════
function exportData(type){
const headers={
patients:['患者ID','姓名','年龄','性别','电话','诊断','状态','健康评分','最后随访'],
vitals:['日期','患者','血糖','收缩压','舒张压','心率','体重'],
medicine:['患者','药品','规格','用法','服药时间','依从率','状态'],
followup:['随访ID','患者','疾病','方式','日期','状态','医生'],
};
const rows={
patients:patients.map(p=>[p.id,p.name,p.age,p.sex,p.phone,p.disease,p.status,p.score,p.lastVisit]),
vitals:vitals.map(v=>[v.date,v.patient,v.glucose,v.sbp,v.dbp,v.hr,v.weight]),
medicine:medicines.map(m=>[m.patient,m.drug,m.spec,m.dose,m.time,m.compliance+'%',m.status]),
followup:followups.map(f=>[f.id,f.patient,f.disease,f.type,f.date,f.status,f.doctor]),
};
const h=headers[type]||headers.patients;
const r=rows[type]||rows.patients;
const csv=[h,...r].map(row=>row.map(v=>`"${v||''}"`).join(',')).join('\n');
const blob=new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url;a.download=`慢病管理_${type}_${today}.csv`;a.click();
URL.revokeObjectURL(url);
toast('📥 报表已下载','success');
}
function doExport(fmt){
if(fmt==='csv'||fmt==='excel')exportData('patients');
else toast(`${fmt.toUpperCase()} 格式报告(完整版功能)`,'info');
}
// ══════ TOAST ══════
function toast(msg,type='info'){
const icons={success:'',error:'',warning:'⚠️',info:'ℹ️'};
const el=document.createElement('div');
el.className=`toast ${type}`;
el.innerHTML=`<span>${icons[type]||'ℹ️'}</span><span>${msg}</span>`;
document.getElementById('toast-wrap').appendChild(el);
setTimeout(()=>{el.style.opacity='0';el.style.transition='opacity .3s';},3200);
setTimeout(()=>el.remove(),3600);
}
// ══════ INIT ══════
(function init(){
// Set default dates
const d=new Date();
document.getElementById('exp-end').value=today;
const s=new Date();s.setMonth(s.getMonth()-1);
document.getElementById('exp-start').value=fmtDate(s);
document.getElementById('vt-date').value=today;
document.getElementById('vt-time').value=new Date().toTimeString().slice(0,5);
document.getElementById('fu-date').value=rndDate(7);
populateSelects();
renderDashboard();
renderPatients();
renderMedicine();
// Close drawer on outside click
document.addEventListener('click',e=>{
const dw=document.getElementById('patient-drawer');
if(dw.classList.contains('open')&&!dw.contains(e.target)&&!e.target.closest('[onclick*=openDrawer]'))closeDrawer();
});
// Reminder check every minute
setInterval(()=>{
const now=new Date();
if(now.getSeconds()<15&&now.getMinutes()%15===0){
toast('💊 用药提醒:有患者需要服药','warning');
}
},15000);
})();
</script>
</body>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment