feat: extract secrets to .env, remove hardcoded credentials

- Backend: .env file for DB/JWT/Redis/MinIO config, appsettings.json cleared
- Backend: Program.cs loads .env at startup (no extra NuGet packages)
- Frontend: .env files for VITE_API_URL, api-clients use import.meta.env
- Added vite-env.d.ts type declarations for both frontends
- All hardcoded localhost:5000 replaced with env variable
- Added .env.example template for onboarding
This commit is contained in:
MingNian
2026-05-22 22:02:08 +08:00
parent 722ee76d93
commit d6a432aec4
27 changed files with 1616 additions and 472 deletions

15
backend/.env.example Normal file
View File

@@ -0,0 +1,15 @@
# PostgreSQL
ConnectionStrings__Default=Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=your_password
# JWT
Jwt__Secret=your-jwt-secret-change-me
Jwt__Issuer=HealthManager
Jwt__Audience=HealthManagerApp
# Redis (reserved)
Redis__Connection=localhost:6379
# MinIO (reserved)
MinIO__Endpoint=localhost:9000
MinIO__AccessKey=minioadmin
MinIO__SecretKey=minioadmin

View File

@@ -9,6 +9,24 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Swashbuckle.AspNetCore.SwaggerGen;
// Load .env file into environment variables
var envPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", ".env");
if (File.Exists(envPath))
{
foreach (var line in File.ReadAllLines(envPath))
{
var trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue;
var eq = trimmed.IndexOf('=');
if (eq > 0)
{
var key = trimmed[..eq].Trim();
var value = trimmed[(eq + 1)..].Trim();
Environment.SetEnvironmentVariable(key, value);
}
}
}
var builder = WebApplication.CreateBuilder(args);
// Database

View File

@@ -7,19 +7,19 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=postgres123"
"Default": ""
},
"Jwt": {
"Secret": "health-manager-jwt-secret-key-2026-super-secure-long-enough!",
"Secret": "",
"Issuer": "HealthManager",
"Audience": "HealthManagerApp"
},
"Redis": {
"Connection": "localhost:6379"
"Connection": ""
},
"MinIO": {
"Endpoint": "localhost:9000",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin"
"Endpoint": "",
"AccessKey": "",
"SecretKey": ""
}
}

View File

@@ -1,4 +1,20 @@
@import './variables.css';
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; }
a { color: inherit; }
button { cursor: pointer; }
body {
font-family: var(--font-family);
color: var(--color-text-primary);
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: inherit; text-decoration: none; }
button { cursor: pointer; font-family: inherit; }
input, select, textarea { font-family: inherit; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@@ -0,0 +1,51 @@
:root {
--color-primary: #4F6EF7;
--color-primary-hover: #3D56D6;
--color-primary-bg: #EDF0FD;
--color-primary-gradient: linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%);
--color-danger: #EF4444;
--color-danger-bg: #FEE9E9;
--color-success: #20C997;
--color-success-bg: #E6F9F2;
--color-warning: #F59E0B;
--color-warning-bg: #FFF8E6;
--color-info: #339AF0;
--color-info-bg: #EFF6FF;
--color-purple: #845EF7;
--color-purple-bg: #F3E8FF;
--color-pink: #F06595;
--color-pink-bg: #FFF0F5;
--color-white: #FFFFFF;
--color-bg: #F5F7FB;
--color-bg-secondary: #EDF0F7;
--color-text-primary: #1A1D28;
--color-text-secondary: #5A6072;
--color-text-tertiary: #9BA0B4;
--color-text-inverse: #FFFFFF;
--color-border: #E1E5ED;
--color-divider: #EEF0F5;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.08);
--shadow-primary: 0 4px 16px rgba(79, 110, 247, 0.25);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-full: 9999px;
--font-size-xs: 11px;
--font-size-sm: 13px;
--font-size-base: 14px;
--font-size-md: 15px;
--font-size-lg: 17px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 30px;
--font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
}

View File

@@ -1,17 +1,68 @@
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../stores/auth.store';
const SIDEBAR_ICONS: Record<string, React.ReactNode> = {
dashboard: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
),
patients: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
),
consultations: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
<line x1="9" y1="10" x2="15" y2="10" />
<line x1="12" y1="7" x2="12" y2="13" />
</svg>
),
reports: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
),
followups: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
),
};
const navItems = [
{ to: '/dashboard', label: '工作台', icon: '📊' },
{ to: '/patients', label: '患者管理', icon: '👥' },
{ to: '/consultations', label: '在线问诊', icon: '💬' },
{ to: '/reports', label: '报告审核', icon: '📋' },
{ to: '/follow-ups', label: '复查管理', icon: '📅' },
{ to: '/dashboard', label: '工作台', ikey: 'dashboard' },
{ to: '/patients', label: '患者管理', ikey: 'patients' },
{ to: '/consultations', label: '在线问诊', ikey: 'consultations' },
{ to: '/reports', label: '报告审核', ikey: 'reports' },
{ to: '/follow-ups', label: '复查管理', ikey: 'followups' },
];
const sidebarBg = '#0F1D3D';
const accentColor = '#4D8FFF';
const textMuted = '#8E9DB5';
const sidebarStyles = {
bg: '#FFFFFF',
cardBg: 'linear-gradient(145deg, #4F6EF7 0%, #6988FF 100%)',
accentColor: '#4F6EF7',
textMuted: '#9BA0B4',
textPrimary: '#1A1D28',
borderColor: '#EEF0F5',
hoverBg: '#F5F7FB',
activeBg: '#EDF0FD',
};
const { accentColor, textMuted, textPrimary } = sidebarStyles;
export function DoctorLayout() {
const { user, logout } = useAuthStore();
@@ -24,60 +75,85 @@ export function DoctorLayout() {
};
return (
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif' }}>
{/* Sidebar */}
<div style={{ display: 'flex', minHeight: '100vh' }}>
<aside style={{
width: 220, background: sidebarBg, color: '#fff',
width: 224, background: sidebarStyles.bg, color: textPrimary,
display: 'flex', flexDirection: 'column', flexShrink: 0,
boxShadow: '2px 0 24px rgba(0,0,0,0.04)',
borderRight: '1px solid #F0F2F5',
}}>
<div style={{ padding: '24px 20px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h1 style={{ fontSize: 17, margin: 0, fontWeight: 600, color: '#fff', letterSpacing: 1 }}>
<span style={{ color: accentColor }}></span>
</h1>
<p style={{ fontSize: 12, margin: '6px 0 0', color: textMuted }}></p>
<div style={{ padding: '24px 20px 20px', borderBottom: `1px solid ${sidebarStyles.borderColor}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 38, height: 38, borderRadius: 12,
background: sidebarStyles.cardBg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#fff" stroke="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
</div>
<div>
<h1 style={{ fontSize: 17, margin: 0, fontWeight: 700, color: textPrimary, letterSpacing: 0.5 }}></h1>
<p style={{ fontSize: 11, margin: '4px 0 0', color: textMuted }}></p>
</div>
</div>
</div>
<nav style={{ flex: 1, padding: '12px 0' }}>
<nav style={{ flex: 1, padding: '8px 0' }}>
{navItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
style={({ isActive }) => ({
display: 'flex', alignItems: 'center', gap: 10,
padding: '11px 20px', margin: '2px 8px',
borderRadius: 8,
color: isActive ? '#fff' : textMuted,
background: isActive ? accentColor : 'transparent',
padding: '11px 16px', margin: '2px 10px',
borderRadius: 10,
color: isActive ? accentColor : textMuted,
background: isActive ? sidebarStyles.activeBg : 'transparent',
textDecoration: 'none', fontSize: 14,
fontWeight: isActive ? 500 : 400,
transition: 'all 0.15s',
fontWeight: isActive ? 600 : 400,
transition: 'all 0.2s',
})}
>
<span style={{ fontSize: 16 }}>{item.icon}</span>
{SIDEBAR_ICONS[item.ikey]}
<span>{item.label}</span>
</NavLink>
))}
</nav>
<div style={{ padding: '16px 20px', borderTop: '1px solid rgba(255,255,255,0.08)' }}>
<div style={{ fontSize: 13, color: '#fff', fontWeight: 500 }}>{user?.name}</div>
<div style={{ fontSize: 11, color: textMuted, marginTop: 2 }}>{user?.department} · {user?.title}</div>
<div style={{ padding: '16px 16px', borderTop: `1px solid ${sidebarStyles.borderColor}`, background: '#FAFBFD' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
<div style={{
width: 38, height: 38, borderRadius: 12,
background: sidebarStyles.cardBg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 700, color: '#fff',
}}>
{user?.name?.charAt(0) || 'D'}
</div>
<div>
<div style={{ fontSize: 13, color: textPrimary, fontWeight: 600 }}>{user?.name}</div>
<div style={{ fontSize: 11, color: textMuted, marginTop: 1 }}>{user?.department} · {user?.title}</div>
</div>
</div>
<button onClick={handleLogout}
style={{
marginTop: 10, padding: '6px 14px', fontSize: 12,
background: 'transparent', color: textMuted, border: '1px solid rgba(255,255,255,0.15)',
borderRadius: 6, cursor: 'pointer', transition: 'all 0.15s',
width: '100%', padding: '8px 0', fontSize: 12,
background: 'transparent', color: '#EF4444',
border: '1px solid #FEE9E9', borderRadius: 8,
cursor: 'pointer', transition: 'all 0.2s',
fontWeight: 500,
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderColor = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = textMuted; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.15)'; }}>
onMouseEnter={(e) => { e.currentTarget.style.background = '#FEF2F2'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
退
</button>
</div>
</aside>
{/* Main content */}
<main style={{ flex: 1, background: '#F2F5FA', overflow: 'auto' }}>
<div key={location.pathname} style={{ animation: 'fadeIn 0.2s ease-out' }}>
<div key={location.pathname} style={{ animation: 'fadeIn 0.25s ease-out' }}>
<Outlet />
</div>
</main>

View File

@@ -4,108 +4,26 @@
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--accent: #4F6EF7;
--accent-bg: rgba(79, 110, 247, 0.1);
--accent-border: rgba(79, 110, 247, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
width: 100%;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
margin: 0;
text-align: left;
min-height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
body { margin: 0; }

View File

@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
import './assets/styles/global.css';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>

View File

@@ -27,37 +27,48 @@ export function LoginPage() {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
minHeight: '100vh', background: '#f0f2f5',
minHeight: '100vh', background: 'linear-gradient(135deg, #EBF0FD 0%, #F5F7FB 50%, #EDF0FD 100%)',
}}>
<form onSubmit={handleLogin} style={{
width: 400, padding: 40, background: '#fff', borderRadius: 8,
boxShadow: '0 2px 12px rgba(0,0,0,0.1)',
width: 400, padding: 40, background: '#fff', borderRadius: 20,
boxShadow: '0 8px 30px rgba(0,0,0,0.08)',
}}>
<h2 style={{ textAlign: 'center', marginBottom: 24 }}></h2>
<div style={{ textAlign: 'center', marginBottom: 28 }}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="#4F6EF7" stroke="none" style={{ marginBottom: 12 }}>
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
<h2 style={{ margin: 0, fontSize: 22, fontWeight: 700, color: '#1A1D28' }}></h2>
<p style={{ margin: '6px 0 0', fontSize: 13, color: '#9BA0B4' }}> · </p>
</div>
{error && <div style={{ color: '#f44336', marginBottom: 12, fontSize: 13 }}>{error}</div>}
{error && <div style={{ color: '#EF4444', marginBottom: 12, fontSize: 13, background: '#FEE9E9', padding: '8px 12px', borderRadius: 8 }}>{error}</div>}
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 13 }}></label>
<label style={{ display: 'block', marginBottom: 4, fontSize: 13, fontWeight: 500, color: '#5A6072' }}></label>
<input value={phone} onChange={(e) => setPhone(e.target.value)}
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 4, fontSize: 14 }} />
style={{ width: '100%', padding: '12px 14px', border: '1.5px solid #E1E5ED', borderRadius: 10, fontSize: 14, outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.2s' }}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 13 }}> ()</label>
<label style={{ display: 'block', marginBottom: 4, fontSize: 13, fontWeight: 500, color: '#5A6072' }}> ()</label>
<input value={code} onChange={(e) => setCode(e.target.value)}
placeholder="输入任意验证码"
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 4, fontSize: 14 }} />
style={{ width: '100%', padding: '12px 14px', border: '1.5px solid #E1E5ED', borderRadius: 10, fontSize: 14, outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.2s' }}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<button type="submit" disabled={loading} style={{
width: '100%', padding: '12px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 4, fontSize: 15, opacity: loading ? 0.7 : 1,
width: '100%', padding: '13px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
border: 'none', borderRadius: 10, fontSize: 15, fontWeight: 600,
opacity: loading ? 0.7 : 1, boxShadow: '0 4px 16px rgba(79,110,247,0.3)',
}}>
{loading ? '登录中...' : '登录'}
</button>
<p style={{ marginTop: 16, fontSize: 12, color: '#999', textAlign: 'center' }}>
<p style={{ marginTop: 16, fontSize: 12, color: '#9BA0B4', textAlign: 'center' }}>
13700137000 ( )
</p>
</form>

View File

@@ -25,7 +25,6 @@ export function ChatPage() {
const bottomRef = useRef<HTMLDivElement>(null);
const connRef = useRef<HubConnection | null>(null);
// Load initial messages via HTTP
useEffect(() => {
if (!id) return;
api.get<Message[]>(`/api/consultations/${id}/messages`)
@@ -33,12 +32,11 @@ export function ChatPage() {
.catch(() => {});
}, [id]);
// Set up SignalR connection
useEffect(() => {
if (!id) return;
const conn = new HubConnectionBuilder()
.withUrl('http://localhost:5000/hubs/chat', {
.withUrl(`${import.meta.env.VITE_API_URL}/hubs/chat`, {
accessTokenFactory: () => getToken(),
})
.withAutomaticReconnect()
@@ -46,7 +44,6 @@ export function ChatPage() {
conn.on('ReceiveMessage', (msg: Message) => {
setMessages((prev) => {
// Dedup — guard against reconnection replay
if (prev.some((m) => m.id === msg.id)) return prev;
return [...prev, msg];
});
@@ -73,7 +70,6 @@ export function ChatPage() {
};
}, [id]);
// Auto-scroll on new messages
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
@@ -89,31 +85,37 @@ export function ChatPage() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 0px)' }}>
<div style={{ padding: '14px 20px', background: '#fff', borderBottom: '1px solid #eee', fontSize: 15, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
padding: '15px 24px', background: '#fff', borderBottom: '1px solid #F0F2F5',
fontSize: 15, fontWeight: 600, color: '#1A1D28', display: 'flex', alignItems: 'center', gap: 8,
boxShadow: '0 1px 4px rgba(0,0,0,0.03)',
}}>
线
<span style={{
width: 8, height: 8, borderRadius: '50%',
background: connected ? '#4caf50' : '#ccc',
background: connected ? '#20C997' : '#C0C5D2',
display: 'inline-block',
}} />
</div>
<div style={{ flex: 1, overflow: 'auto', padding: 20, background: '#fafafa' }}>
<div style={{ flex: 1, overflow: 'auto', padding: 24, background: '#F5F7FB' }}>
{messages.map((msg) => (
<div key={msg.id} style={{
display: 'flex', justifyContent: msg.senderRole === 'doctor' ? 'flex-end' : 'flex-start',
marginBottom: 12,
marginBottom: 14,
}}>
<div style={{
maxWidth: '70%', padding: '10px 14px', borderRadius: 12, fontSize: 14,
background: msg.senderRole === 'doctor' ? '#1976d2' : '#fff',
color: msg.senderRole === 'doctor' ? '#fff' : '#333',
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
maxWidth: '70%', padding: '12px 16px', borderRadius: 14, fontSize: 14,
background: msg.senderRole === 'doctor'
? 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)'
: '#fff',
color: msg.senderRole === 'doctor' ? '#fff' : '#1A1D28',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
}}>
<div>{msg.content}</div>
<div style={{
fontSize: 10, marginTop: 4, textAlign: 'right',
opacity: 0.7,
fontSize: 10, marginTop: 6, textAlign: 'right',
opacity: 0.65,
}}>
{msg.createdAt?.split('T')[1]?.slice(0, 5)}
</div>
@@ -123,14 +125,23 @@ export function ChatPage() {
<div ref={bottomRef} />
</div>
<div style={{ padding: '12px 20px', background: '#fff', borderTop: '1px solid #eee', display: 'flex', gap: 12 }}>
<div style={{
padding: '14px 24px', background: '#fff', borderTop: '1px solid #F0F2F5',
display: 'flex', gap: 12, boxShadow: '0 -1px 4px rgba(0,0,0,0.03)',
}}>
<input value={input} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder="输入回复..."
style={{ flex: 1, padding: '10px 14px', border: '1px solid #ddd', borderRadius: 20, fontSize: 14 }} />
style={{
flex: 1, padding: '11px 16px', border: '1.5px solid #E1E5ED', borderRadius: 24,
fontSize: 14, outline: 'none',
}}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
<button onClick={handleSend} style={{
padding: '10px 24px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 20, fontSize: 14,
padding: '11px 24px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
border: 'none', borderRadius: 24, fontSize: 14, fontWeight: 600,
boxShadow: '0 4px 14px rgba(79,110,247,0.3)',
}}>
</button>

View File

@@ -2,64 +2,54 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../services/api-client';
interface ConsultationItem {
id: string; patientId: string; patientName: string; subject: string;
status: string; startedAt: string;
}
interface RawConsultation {
id: string; patientId: string; patientName?: string; subject?: string;
status: string; startedAt: string;
}
export function ConsultationListPage() {
const [consultations, setConsultations] = useState<ConsultationItem[]>([]);
const [consultations, setConsultations] = useState<RawConsultation[]>([]);
useEffect(() => {
api.get<RawConsultation[]>('/api/consultations').then((r) => {
const mapped = r.data.map((c) => ({
id: c.id,
patientId: c.patientId,
patientName: c.patientName || 'unknown',
subject: c.subject || 'online consult',
status: c.status,
startedAt: c.startedAt,
}));
setConsultations(mapped);
setConsultations(r.data);
}).catch(() => {});
}, []);
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 16 }}>线</h2>
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 6, fontSize: 20, fontWeight: 700, color: '#1A1D28' }}>线</h2>
<p style={{ marginBottom: 18, fontSize: 13, color: '#9BA0B4' }}> {consultations.length} </p>
<div style={{ background: '#fff', borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)', overflow: 'hidden' }}>
{consultations.map((c) => (
<Link key={c.id} to={`/consultations/${c.id}`} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '14px 20px', borderBottom: '1px solid #f5f5f5',
textDecoration: 'none', color: 'inherit',
}}>
padding: '16px 22px', borderBottom: '1px solid #F5F6F9',
textDecoration: 'none', color: 'inherit', transition: 'background 0.15s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = '#F9FAFC'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = ''; }}>
<div>
<div style={{ fontSize: 14, fontWeight: 500 }}>{c.patientName}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 2 }}>{c.subject}</div>
<div style={{ fontSize: 14, fontWeight: 600, color: '#1A1D28' }}>{c.patientName || '未知'}</div>
<div style={{ fontSize: 12, color: '#9BA0B4', marginTop: 3 }}>{c.subject || '在线问诊'}</div>
</div>
<div style={{ textAlign: 'right' }}>
<span style={{
padding: '2px 8px', borderRadius: 10, fontSize: 11,
background: c.status === 'active' ? '#e8f5e9' : '#f5f5f5',
color: c.status === 'active' ? '#2e7d32' : '#999',
padding: '4px 12px', borderRadius: 10, fontSize: 11, fontWeight: 500,
background: c.status === 'active' ? '#E6F9F2' : '#F5F6F9',
color: c.status === 'active' ? '#20C997' : '#9BA0B4',
}}>
{c.status === 'active' ? '进行中' : '已结束'}
</span>
<div style={{ fontSize: 11, color: '#bbb', marginTop: 4 }}>
<div style={{ fontSize: 11, color: '#C0C5D2', marginTop: 4 }}>
{c.startedAt?.split('T')[0]}
</div>
</div>
</Link>
))}
{consultations.length === 0 && (
<div style={{ padding: 40, textAlign: 'center', color: '#999' }}></div>
<div style={{ padding: 40, textAlign: 'center', color: '#9BA0B4', fontSize: 13 }}></div>
)}
</div>
</div>

View File

@@ -9,6 +9,41 @@ interface RawConsultation { id: string; status: string; patientName: string; sub
interface RawFollowUp { id: string; scheduledAt: string; title: string; status: string; }
interface RawReport { id: string; title: string; status: string; }
const statCardStyle: React.CSSProperties = {
background: '#fff', padding: 22, borderRadius: 16,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
position: 'relative', overflow: 'hidden',
};
const statColorBar = (color: string): React.CSSProperties => ({
position: 'absolute', top: 0, left: 0, width: 4, height: '100%',
background: color, borderRadius: '4px 0 0 4px',
});
const todoIcons: Record<string, React.ReactNode> = {
reports: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
),
consultations: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
),
followups: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#845EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
),
};
export function DashboardPage() {
const user = useAuthStore((s) => s.user);
const [stats, setStats] = useState<DashboardStats>({
@@ -41,58 +76,69 @@ export function DashboardPage() {
loadStats();
}, []);
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 20 }}>{user?.name}</h2>
const statItems = [
{ label: '患者总数', value: stats.totalPatients, color: '#4F6EF7', bg: '#EDF0FD' },
{ label: '进行中问诊', value: stats.activeConsultations, color: '#20C997', bg: '#E6F9F2' },
{ label: '待审核报告', value: stats.pendingReports, color: '#F59E0B', bg: '#FFF8E6' },
{ label: '今日随访', value: stats.todayFollowUps, color: '#845EF7', bg: '#F3E8FF' },
];
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 32 }}>
{[
{ label: '患者总数', value: stats.totalPatients, color: '#1976d2' },
{ label: '进行中问诊', value: stats.activeConsultations, color: '#388e3c' },
{ label: '待审核报告', value: stats.pendingReports, color: '#f57c00' },
{ label: '今日随访', value: stats.todayFollowUps, color: '#7b1fa2' },
].map((item) => (
<div key={item.label} style={{
background: '#fff', padding: 20, borderRadius: 8,
borderLeft: `4px solid ${item.color}`, boxShadow: '0 1px 4px rgba(0,0,0,0.08)',
}}>
<div style={{ fontSize: 28, fontWeight: 700, color: item.color }}>{item.value}</div>
<div style={{ fontSize: 13, color: '#888', marginTop: 4 }}>{item.label}</div>
const quickActions = [
{ label: '患者列表', href: '/patients', color: '#4F6EF7', bg: '#EDF0FD' },
{ label: '在线问诊', href: '/consultations', color: '#20C997', bg: '#E6F9F2' },
{ label: '报告审核', href: '/reports', color: '#F59E0B', bg: '#FFF8E6' },
{ label: '随访管理', href: '/follow-ups', color: '#845EF7', bg: '#F3E8FF' },
];
return (
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 4, fontSize: 22, fontWeight: 700, color: '#1A1D28' }}>{user?.name}</h2>
<p style={{ marginBottom: 24, fontSize: 13, color: '#9BA0B4' }}>{user?.department} · {user?.title}</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 28 }}>
{statItems.map((item) => (
<div key={item.label} style={statCardStyle}>
<div style={statColorBar(item.color)} />
<div style={{ paddingLeft: 8 }}>
<div style={{ fontSize: 30, fontWeight: 800, color: item.color, lineHeight: 1.1 }}>{item.value}</div>
<div style={{ fontSize: 13, color: '#5A6072', marginTop: 6, fontWeight: 500 }}>{item.label}</div>
</div>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<div style={{ background: '#fff', padding: 20, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<h3 style={{ marginBottom: 12, fontSize: 15 }}></h3>
<div style={{ background: '#fff', padding: 22, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<h3 style={{ marginBottom: 16, fontSize: 16, fontWeight: 600, color: '#1A1D28' }}></h3>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{[
{ label: '患者列表', href: '/patients' },
{ label: '在线问诊', href: '/consultations' },
{ label: '报告审核', href: '/reports' },
{ label: '随访管理', href: '/follow-ups' },
].map((action) => (
{quickActions.map((action) => (
<Link key={action.label} to={action.href} style={{
padding: '8px 16px', background: '#f0f2f5', borderRadius: 4,
textDecoration: 'none', color: '#1976d2', fontSize: 13,
}}>
padding: '10px 18px', background: action.bg, borderRadius: 10,
textDecoration: 'none', color: action.color, fontSize: 13,
fontWeight: 600, transition: 'all 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; }}>
{action.label}
</Link>
))}
</div>
</div>
<div style={{ background: '#fff', padding: 20, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<h3 style={{ marginBottom: 12, fontSize: 15 }}></h3>
<ul style={{ fontSize: 13, color: '#666', listStyle: 'none', padding: 0 }}>
<li style={{ padding: '6px 0', borderBottom: '1px solid #f0f0f0' }}>
📋 : {stats.pendingReports}
<div style={{ background: '#fff', padding: 22, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<h3 style={{ marginBottom: 14, fontSize: 16, fontWeight: 600, color: '#1A1D28' }}></h3>
<ul style={{ fontSize: 13, color: '#5A6072', listStyle: 'none', padding: 0 }}>
<li style={{ padding: '10px 0', borderBottom: '1px solid #F0F2F5', display: 'flex', alignItems: 'center', gap: 8 }}>
{todoIcons.reports}
: <strong style={{ color: '#F59E0B' }}>{stats.pendingReports}</strong>
</li>
<li style={{ padding: '6px 0', borderBottom: '1px solid #f0f0f0' }}>
💬 : {stats.activeConsultations}
<li style={{ padding: '10px 0', borderBottom: '1px solid #F0F2F5', display: 'flex', alignItems: 'center', gap: 8 }}>
{todoIcons.consultations}
: <strong style={{ color: '#4F6EF7' }}>{stats.activeConsultations}</strong>
</li>
<li style={{ padding: '6px 0' }}>
📅 访: {stats.todayFollowUps}
<li style={{ padding: '10px 0', display: 'flex', alignItems: 'center', gap: 8 }}>
{todoIcons.followups}
访: <strong style={{ color: '#845EF7' }}>{stats.todayFollowUps}</strong>
</li>
</ul>
</div>

View File

@@ -29,30 +29,35 @@ export function FollowUpEditPage() {
e.preventDefault();
const body = { title, patientId, scheduledAt, notes };
try {
if (isNew) {
await api.post('/api/follow-ups', body);
} else {
await api.put(`/api/follow-ups/${id}`, body);
}
if (isNew) { await api.post('/api/follow-ups', body); }
else { await api.put(`/api/follow-ups/${id}`, body); }
navigate('/follow-ups');
} catch { alert('操作失败'); }
};
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 16 }}>{isNew ? '新建随访' : '编辑随访'}</h2>
const inputStyle: React.CSSProperties = {
width: '100%', padding: '10px 14px', border: '1.5px solid #E1E5ED',
borderRadius: 10, fontSize: 13, outline: 'none', boxSizing: 'border-box',
};
const labelStyle: React.CSSProperties = {
display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5,
};
<form onSubmit={handleSubmit} style={{ background: '#fff', padding: 24, borderRadius: 8, maxWidth: 500 }}>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<input value={title} onChange={(e) => setTitle(e.target.value)} required
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} />
return (
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 20, fontSize: 20, fontWeight: 700, color: '#1A1D28' }}>{isNew ? '新建随访' : '编辑随访'}</h2>
<form onSubmit={handleSubmit} style={{ background: '#fff', padding: 28, borderRadius: 16, maxWidth: 520, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input value={title} onChange={(e) => setTitle(e.target.value)} required style={inputStyle}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<select value={patientId} onChange={(e) => setPatientId(e.target.value)} required
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }}>
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<select value={patientId} onChange={(e) => setPatientId(e.target.value)} required style={inputStyle}>
<option value=""></option>
{patients.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
@@ -60,21 +65,23 @@ export function FollowUpEditPage() {
</select>
</div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<input type="datetime-local" value={scheduledAt} onChange={(e) => setScheduledAt(e.target.value)} required
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} />
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input type="datetime-local" value={scheduledAt} onChange={(e) => setScheduledAt(e.target.value)} required style={inputStyle}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 18 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<div style={{ marginBottom: 20 }}>
<label style={labelStyle}></label>
<textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={3}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4, resize: 'vertical' }} />
style={{ ...inputStyle, resize: 'vertical', fontFamily: 'inherit' }} />
</div>
<button type="submit" style={{
padding: '10px 24px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 4, fontSize: 14,
padding: '11px 28px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
border: 'none', borderRadius: 10, fontSize: 14, fontWeight: 600,
boxShadow: '0 4px 16px rgba(79,110,247,0.25)',
}}>
{isNew ? '创建' : '保存'}
</button>

View File

@@ -2,79 +2,69 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../services/api-client';
interface FollowUpItem {
id: string; patientId: string; patientName: string;
title: string; scheduledAt: string; status: string;
}
interface RawFollowUpItem {
id: string; patientId: string; patientName?: string;
title: string; scheduledAt: string; status: string;
}
export function FollowUpListPage() {
const [followUps, setFollowUps] = useState<FollowUpItem[]>([]);
const [followUps, setFollowUps] = useState<RawFollowUpItem[]>([]);
useEffect(() => {
api.get<RawFollowUpItem[]>('/api/follow-ups').then((r) => {
const mapped = r.data.map((f) => ({
id: f.id,
patientId: f.patientId,
patientName: f.patientName || 'unknown',
title: f.title,
scheduledAt: f.scheduledAt,
status: f.status,
}));
setFollowUps(mapped);
}).catch(() => {});
api.get<RawFollowUpItem[]>('/api/follow-ups').then((r) => setFollowUps(r.data)).catch(() => {});
}, []);
const statusLabel = (s: string) => {
switch (s) {
case 'pending': return { text: '待随访', color: '#f57c00', bg: '#fff3e0' };
case 'completed': return { text: '已完成', color: '#2e7d32', bg: '#e8f5e9' };
case 'missed': return { text: '已错过', color: '#c62828', bg: '#ffebee' };
default: return { text: s, color: '#666', bg: '#f5f5f5' };
case 'pending': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' };
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
case 'missed': return { text: '已错过', color: '#EF4444', bg: '#FEE9E9' };
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
}
};
return (
<div style={{ padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h2>访</h2>
<div style={{ padding: 28 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1A1D28', margin: 0 }}>访</h2>
<Link to="/follow-ups/new/edit" style={{
padding: '8px 16px', background: '#1976d2', color: '#fff',
borderRadius: 4, textDecoration: 'none', fontSize: 13,
padding: '10px 20px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
borderRadius: 10, textDecoration: 'none', fontSize: 13, fontWeight: 600,
boxShadow: '0 4px 16px rgba(79,110,247,0.25)',
}}>
+ 访
访
</Link>
</div>
<p style={{ marginBottom: 18, fontSize: 13, color: '#9BA0B4' }}> {followUps.length} 访</p>
<div style={{ background: '#fff', borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)', overflow: 'hidden' }}>
{followUps.map((f) => {
const s = statusLabel(f.status);
return (
<div key={f.id} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '14px 20px', borderBottom: '1px solid #f5f5f5',
padding: '16px 22px', borderBottom: '1px solid #F5F6F9',
}}>
<div>
<div style={{ fontSize: 14, fontWeight: 500 }}>{f.title}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 2 }}>
{f.patientName} · {f.scheduledAt?.split('T')[0]}
<div style={{ fontSize: 14, fontWeight: 600, color: '#1A1D28' }}>{f.title}</div>
<div style={{ fontSize: 12, color: '#9BA0B4', marginTop: 3 }}>
{f.patientName || '未知'} · {f.scheduledAt?.split('T')[0]}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ padding: '2px 8px', borderRadius: 10, fontSize: 11, background: s.bg, color: s.color }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<span style={{ padding: '4px 12px', borderRadius: 10, fontSize: 11, fontWeight: 500, background: s.bg, color: s.color }}>
{s.text}
</span>
<Link to={`/follow-ups/${f.id}/edit`} style={{ color: '#1976d2', fontSize: 13 }}></Link>
<Link to={`/follow-ups/${f.id}/edit`} style={{
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
padding: '4px 12px', background: '#EDF0FD', borderRadius: 6,
}}></Link>
</div>
</div>
);
})}
{followUps.length === 0 && (
<div style={{ padding: 40, textAlign: 'center', color: '#999' }}>访</div>
<div style={{ padding: 40, textAlign: 'center', color: '#9BA0B4', fontSize: 13 }}>访</div>
)}
</div>
</div>

View File

@@ -12,6 +12,18 @@ interface HealthRecord {
id: string; type: string; value: string; unit: string; recordedAt: string;
}
const typeLabels: Record<string, string> = {
blood_pressure: '血压', heart_rate: '心率', blood_sugar: '血糖', spo2: '血氧',
};
const typeColors: Record<string, string> = {
blood_pressure: '#EF4444', heart_rate: '#F59E0B', blood_sugar: '#4F6EF7', spo2: '#20C997',
};
const typeBgs: Record<string, string> = {
blood_pressure: '#FEE9E9', heart_rate: '#FFF8E6', blood_sugar: '#EDF0FD', spo2: '#E6F9F2',
};
export function PatientDetailPage() {
const { id } = useParams<{ id: string }>();
const [patient, setPatient] = useState<PatientDetail | null>(null);
@@ -19,14 +31,13 @@ export function PatientDetailPage() {
useEffect(() => {
if (!id) return;
// Fetch patient detail directly by ID + health records
api.get<PatientDetail>(`/api/patients/${id}`).then((r) => {
if (r.data) setPatient(r.data);
}).catch(() => {});
api.get<HealthRecord[]>(`/api/health-records?patientId=${id}&days=30`).then((r) => setRecords(r.data));
}, [id]);
if (!patient) return <div style={{ padding: 24 }}>...</div>;
if (!patient) return <div style={{ padding: 28, color: '#9BA0B4' }}>...</div>;
const latestByType: Record<string, HealthRecord> = {};
records.forEach((r) => {
@@ -44,34 +55,79 @@ export function PatientDetailPage() {
};
return (
<div style={{ padding: 24 }}>
<Link to="/patients" style={{ fontSize: 13, color: '#1976d2' }}> </Link>
<div style={{ padding: 28 }}>
<Link to="/patients" style={{ fontSize: 13, color: '#4F6EF7', fontWeight: 500 }}> </Link>
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<h2>{patient.name}</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 24px', marginTop: 12, fontSize: 14 }}>
<div>{patient.phone}</div>
<div>{patient.gender || '-'}</div>
<div>{patient.birthday || '-'}</div>
<div>{patient.heightCm}cm / {patient.weightKg}kg</div>
<div>{(patient.medicalHistory || []).join('、') || '-'}</div>
<div>{patient.stentDate || '-'}</div>
<div>{patient.stentType || '-'}</div>
<div style={{ background: '#fff', marginTop: 16, padding: 28, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 20 }}>
<div style={{
width: 52, height: 52, borderRadius: 16,
background: 'linear-gradient(135deg, #4F6EF7, #6C8AFF)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20, fontWeight: 700, color: '#fff',
}}>
{patient.name?.charAt(0) || '?'}
</div>
<div>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{patient.name}</h2>
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#9BA0B4' }}>{patient.phone}</p>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 32px', fontSize: 13 }}>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.phone}</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.gender || '-'}</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.birthday || '-'}</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}>/</span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.heightCm}cm / {patient.weightKg}kg</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{(patient.medicalHistory || []).join('、') || '-'}</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.stentDate || '-'}</span>
</div>
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
<span style={{ color: '#9BA0B4' }}></span>
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.stentType || '-'}</span>
</div>
</div>
</div>
<h3 style={{ marginTop: 24, marginBottom: 12 }}></h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
<h3 style={{ marginTop: 28, marginBottom: 14, fontSize: 17, fontWeight: 700, color: '#1A1D28' }}></h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 14 }}>
{Object.entries(latestByType).map(([type, record]) => (
<div key={type} style={{ background: '#fff', padding: 16, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<div style={{ fontSize: 12, color: '#888' }}>
{type === 'blood_pressure' ? '血压' : type === 'heart_rate' ? '心率' : type}
</div>
<div style={{ fontSize: 20, fontWeight: 600, marginTop: 4 }}>
{parseValueDisplay(record)} {record.unit}
</div>
<div style={{ fontSize: 11, color: '#bbb', marginTop: 4 }}>
{record.recordedAt?.split('T')[0]}
<div key={type} style={{
background: '#fff', padding: 20, borderRadius: 16,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', position: 'relative',
}}>
<div style={{ position: 'absolute', top: 0, left: 0, width: 4, height: '100%', background: typeColors[type] || '#4F6EF7', borderRadius: '4px 0 0 4px' }} />
<div style={{ paddingLeft: 8 }}>
<div style={{
fontSize: 11, fontWeight: 600, color: typeColors[type] || '#4F6EF7',
background: typeBgs[type] || '#EDF0FD', display: 'inline-block',
padding: '3px 10px', borderRadius: 6, marginBottom: 10,
}}>
{typeLabels[type] || type}
</div>
<div style={{ fontSize: 22, fontWeight: 800, color: '#1A1D28' }}>
{parseValueDisplay(record)} <span style={{ fontSize: 13, fontWeight: 500, color: '#9BA0B4' }}>{record.unit}</span>
</div>
<div style={{ fontSize: 11, color: '#C0C5D2', marginTop: 6 }}>
{record.recordedAt?.split('T')[0]}
</div>
</div>
</div>
))}

View File

@@ -24,40 +24,50 @@ export function PatientListPage() {
);
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 16 }}></h2>
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 6, fontSize: 20, fontWeight: 700, color: '#1A1D28' }}></h2>
<p style={{ marginBottom: 18, fontSize: 13, color: '#9BA0B4' }}> {patients.length} </p>
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="搜索姓名或手机号..."
style={{ width: 300, padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4, marginBottom: 16 }} />
style={{
width: 280, padding: '10px 14px', border: '1.5px solid #E1E5ED', borderRadius: 10,
fontSize: 13, marginBottom: 18, outline: 'none', boxSizing: 'border-box',
transition: 'border-color 0.2s',
}}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
{loading ? <div>...</div> : (
<div style={{ background: '#fff', borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
{loading ? <div style={{ color: '#9BA0B4' }}>...</div> : (
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr style={{ borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<tr style={{ borderBottom: '2px solid #F0F2F5', textAlign: 'left', background: '#F9FAFC' }}>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
</tr>
</thead>
<tbody>
{filtered.map((p) => (
<tr key={p.id} style={{ borderBottom: '1px solid #f5f5f5' }}>
<td style={{ padding: '10px 16px' }}>{p.name}</td>
<td style={{ padding: '10px 16px', color: '#888' }}>{p.phone}</td>
<td style={{ padding: '10px 16px' }}>{p.gender || '-'}</td>
<td style={{ padding: '10px 16px' }}>{(p.medicalHistory || []).slice(0, 3).join('、') || '-'}</td>
<td style={{ padding: '10px 16px' }}>{p.stentDate || '-'}</td>
<td style={{ padding: '10px 16px' }}>
<Link to={`/patients/${p.id}`} style={{ color: '#1976d2', fontSize: 13 }}></Link>
<tr key={p.id} style={{ borderBottom: '1px solid #F5F6F9' }}>
<td style={{ padding: '12px 20px', fontWeight: 500 }}>{p.name}</td>
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{p.phone}</td>
<td style={{ padding: '12px 20px' }}>{p.gender || '-'}</td>
<td style={{ padding: '12px 20px', color: '#5A6072' }}>{(p.medicalHistory || []).slice(0, 3).join('、') || '-'}</td>
<td style={{ padding: '12px 20px', color: '#5A6072' }}>{p.stentDate || '-'}</td>
<td style={{ padding: '12px 20px' }}>
<Link to={`/patients/${p.id}`} style={{
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
padding: '4px 12px', background: '#EDF0FD', borderRadius: 6,
}}></Link>
</td>
</tr>
))}
{filtered.length === 0 && (
<tr><td colSpan={6} style={{ padding: 24, textAlign: 'center', color: '#999' }}></td></tr>
<tr><td colSpan={6} style={{ padding: 32, textAlign: 'center', color: '#9BA0B4' }}></td></tr>
)}
</tbody>
</table>

View File

@@ -56,50 +56,54 @@ export function ReportDetailPage() {
finally { setSubmitting(false); }
};
if (!report) return <div style={{ padding: 24 }}>...</div>;
if (!report) return <div style={{ padding: 28, color: '#9BA0B4' }}>...</div>;
const isCompleted = report.status === 'completed';
const riskMap: Record<string, { text: string; color: string }> = {
normal: { text: '正常', color: '#2e7d32' },
attention: { text: '关注', color: '#f57c00' },
abnormal: { text: '异常', color: '#c62828' },
normal: { text: '正常', color: '#20C997' },
attention: { text: '关注', color: '#F59E0B' },
abnormal: { text: '异常', color: '#EF4444' },
};
const inputStyle: React.CSSProperties = {
width: '100%', padding: '10px 14px', border: '1.5px solid #E1E5ED',
borderRadius: 10, fontSize: 13, outline: 'none', boxSizing: 'border-box', fontFamily: 'inherit',
};
return (
<div style={{ padding: 24 }}>
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}> </Link>
<div style={{ padding: 28 }}>
<Link to="/reports" style={{ fontSize: 13, color: '#4F6EF7', fontWeight: 500 }}> </Link>
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<div style={{ background: '#fff', marginTop: 16, padding: 28, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ margin: 0 }}>{report.title}</h2>
<div style={{ marginTop: 8, fontSize: 13, color: '#888' }}>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{report.title}</h2>
<div style={{ marginTop: 8, fontSize: 13, color: '#9BA0B4' }}>
{report.patientName || '未知'} &nbsp;|&nbsp;
{categoryMap[report.category] || report.category} &nbsp;|&nbsp;
{report.createdAt?.split('T')[0]}
</div>
</div>
<span style={{
padding: '4px 12px', borderRadius: 12, fontSize: 12, fontWeight: 500,
background: isCompleted ? '#e8f5e9' : '#fff3e0',
color: isCompleted ? '#2e7d32' : '#f57c00',
padding: '6px 14px', borderRadius: 12, fontSize: 12, fontWeight: 600,
background: isCompleted ? '#E6F9F2' : '#FFF8E6',
color: isCompleted ? '#20C997' : '#F59E0B',
}}>
{isCompleted ? '已完成' : '待审核'}
</span>
</div>
{/* 图片 */}
{report.imageUrls && report.imageUrls.length > 0 && (
<div style={{ marginTop: 20 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}>{report.imageUrls.length}</h4>
<div style={{ marginTop: 24 }}>
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 10, color: '#5A6072' }}>{report.imageUrls.length}</h4>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{report.imageUrls.map((url, i) => (
<div key={i} onClick={() => setLightbox(url)} style={{
width: 120, height: 120, borderRadius: 8, overflow: 'hidden',
cursor: 'pointer', border: '2px solid #eee', background: '#f5f5f5',
width: 120, height: 120, borderRadius: 12, overflow: 'hidden',
cursor: 'pointer', border: '2px solid #F0F2F5', background: '#F9FAFC',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<img src={`http://localhost:5000${url}`} alt={`图片${i}`}
<img src={`${import.meta.env.VITE_API_URL}${url}`} alt={`图片${i}`}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
</div>
@@ -108,37 +112,35 @@ export function ReportDetailPage() {
</div>
)}
{/* 灯箱 */}
{lightbox && (
<div onClick={() => setLightbox(null)} style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999, cursor: 'pointer',
}}>
<img src={`http://localhost:5000${lightbox}`} alt="预览" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} />
<img src={`${import.meta.env.VITE_API_URL}${lightbox}`} alt="预览" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 12 }} />
</div>
)}
{/* 已完成解读 */}
{isCompleted && (
<div style={{ marginTop: 20, padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}></h4>
<div style={{ fontSize: 13 }}>
<p><strong></strong>
<div style={{ marginTop: 24, padding: 20, background: '#E6F9F2', borderRadius: 12 }}>
<h4 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: '#20C997' }}></h4>
<div style={{ fontSize: 13, color: '#5A6072' }}>
<p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}></strong>
<span style={{ color: riskMap[report.riskLevel || '']?.color, fontWeight: 600 }}>
{riskMap[report.riskLevel || '']?.text || report.riskLevel || '-'}
</span>
</p>
<p><strong></strong>{report.summary || '-'}</p>
{report.suggestions && <p><strong></strong>{report.suggestions}</p>}
<p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}></strong>{report.summary || '-'}</p>
{report.suggestions && <p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}></strong>{report.suggestions}</p>}
</div>
{report.items && report.items.length > 0 && (
<table style={{ width: '100%', marginTop: 12, borderCollapse: 'collapse', fontSize: 12 }}>
<table style={{ width: '100%', marginTop: 14, borderCollapse: 'collapse', fontSize: 12 }}>
<thead><tr style={{ textAlign: 'left', borderBottom: '2px solid #c8e6c9' }}>
<th style={{ padding: '6px 8px' }}></th>
<th style={{ padding: '6px 8px' }}></th>
<th style={{ padding: '6px 8px' }}></th>
<th style={{ padding: '6px 8px' }}></th>
<th style={{ padding: '6px 8px', color: '#5A6072' }}></th>
<th style={{ padding: '6px 8px', color: '#5A6072' }}></th>
<th style={{ padding: '6px 8px', color: '#5A6072' }}></th>
<th style={{ padding: '6px 8px', color: '#5A6072' }}></th>
</tr></thead>
<tbody>
{report.items.map((item) => (
@@ -146,7 +148,7 @@ export function ReportDetailPage() {
<td style={{ padding: '6px 8px' }}>{item.itemName}</td>
<td style={{ padding: '6px 8px' }}>{item.resultValue} {item.unit || ''}</td>
<td style={{ padding: '6px 8px' }}>{item.referenceRange || '-'}</td>
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#c62828' : '#2e7d32', fontWeight: 500 }}>
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#EF4444' : '#20C997', fontWeight: 600 }}>
{item.isAbnormal ? '是' : '否'}
</td>
</tr>
@@ -157,68 +159,66 @@ export function ReportDetailPage() {
</div>
)}
{/* 解读表单 */}
{!isCompleted && (
<div style={{ marginTop: 24, borderTop: '1px solid #eee', paddingTop: 20 }}>
<h3 style={{ fontSize: 15, marginBottom: 16 }}></h3>
<div style={{ marginTop: 28, borderTop: '1px solid #F0F2F5', paddingTop: 24 }}>
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 18, color: '#1A1D28' }}></h3>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}></label>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}></label>
<textarea value={summary} onChange={(e) => setSummary(e.target.value)}
placeholder="请输入您的专业解读总结..."
rows={4}
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }} />
style={{ ...inputStyle, resize: 'vertical' }} />
</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 12 }}>
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}></label>
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit' }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}></label>
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)} style={inputStyle}>
<option value="normal"></option>
<option value="attention"></option>
<option value="abnormal"></option>
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}></label>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}></label>
<input value={suggestions} onChange={(e) => setSuggestions(e.target.value)}
placeholder="如:继续当前用药方案"
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit', boxSizing: 'border-box' }} />
style={inputStyle} />
</div>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 8 }}></label>
{items.map((item, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 6, alignItems: 'center' }}>
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<input placeholder="项目名称" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
style={{ flex: 2, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
style={{ flex: 2, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
<input placeholder="结果" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
style={{ flex: 1, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
<input placeholder="单位" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
style={{ width: 70, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
style={{ width: 70, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
<input placeholder="参考范围" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 3 }}>
style={{ flex: 1, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, color: '#5A6072' }}>
<input type="checkbox" checked={item.isAbnormal} onChange={(e) => updateItem(i, 'isAbnormal', e.target.checked)} />
</label>
<button onClick={() => removeItem(i)}
style={{ background: 'none', border: 'none', color: '#c62828', cursor: 'pointer', fontSize: 16 }}
disabled={items.length <= 1}></button>
style={{ background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer', fontSize: 18, fontWeight: 700 }}
disabled={items.length <= 1}>×</button>
</div>
))}
<button onClick={addItem} style={{
padding: '4px 12px', border: '1px dashed #1976d2', borderRadius: 4,
background: 'none', color: '#1976d2', cursor: 'pointer', fontSize: 12,
padding: '6px 14px', border: '1.5px dashed #4F6EF7', borderRadius: 8,
background: 'none', color: '#4F6EF7', cursor: 'pointer', fontSize: 12, fontWeight: 500,
}}>+ </button>
</div>
<button onClick={handleInterpret} disabled={submitting} style={{
padding: '10px 28px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 6, fontSize: 14, cursor: 'pointer',
opacity: submitting ? 0.7 : 1, marginTop: 8,
padding: '11px 32px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
border: 'none', borderRadius: 10, fontSize: 14, cursor: 'pointer', fontWeight: 600,
opacity: submitting ? 0.7 : 1, marginTop: 8, boxShadow: '0 4px 16px rgba(79,110,247,0.25)',
}}>
{submitting ? '提交中...' : '提交解读'}
</button>

View File

@@ -2,80 +2,68 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../services/api-client';
interface ReportItem {
id: string; patientId: string; patientName: string;
title: string; category: string; status: string; createdAt: string;
}
interface RawReportItem {
id: string; patientId: string; patientName?: string;
title: string; category: string; status: string; createdAt: string;
}
export function ReportListPage() {
const [reports, setReports] = useState<ReportItem[]>([]);
const [reports, setReports] = useState<RawReportItem[]>([]);
useEffect(() => {
api.get<RawReportItem[]>('/api/reports').then((r) => {
const mapped = r.data.map((rp) => ({
id: rp.id,
patientId: rp.patientId,
patientName: rp.patientName || 'unknown',
title: rp.title,
category: rp.category,
status: rp.status,
createdAt: rp.createdAt,
}));
setReports(mapped);
}).catch(() => {});
api.get<RawReportItem[]>('/api/reports').then((r) => setReports(r.data)).catch(() => {});
}, []);
const statusLabel = (s: string) => {
switch (s) {
case 'pending': return { text: '待审核', color: '#f57c00', bg: '#fff3e0' };
case 'completed': return { text: '已完成', color: '#2e7d32', bg: '#e8f5e9' };
default: return { text: s, color: '#666', bg: '#f5f5f5' };
case 'pending': return { text: '待审核', color: '#F59E0B', bg: '#FFF8E6' };
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
}
};
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 16 }}></h2>
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 6, fontSize: 20, fontWeight: 700, color: '#1A1D28' }}></h2>
<p style={{ marginBottom: 18, fontSize: 13, color: '#9BA0B4' }}> {reports.length} </p>
<div style={{ background: '#fff', borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr style={{ borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<th style={{ padding: '12px 16px' }}></th>
<tr style={{ borderBottom: '2px solid #F0F2F5', textAlign: 'left', background: '#F9FAFC' }}>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
<th style={{ padding: '13px 20px', fontWeight: 600, color: '#5A6072' }}></th>
</tr>
</thead>
<tbody>
{reports.map((r) => {
const s = statusLabel(r.status);
return (
<tr key={r.id} style={{ borderBottom: '1px solid #f5f5f5' }}>
<td style={{ padding: '10px 16px' }}>{r.patientName}</td>
<td style={{ padding: '10px 16px' }}>{r.title}</td>
<td style={{ padding: '10px 16px', color: '#888' }}>{r.category}</td>
<td style={{ padding: '10px 16px' }}>
<span style={{ padding: '2px 8px', borderRadius: 10, fontSize: 11, background: s.bg, color: s.color }}>
<tr key={r.id} style={{ borderBottom: '1px solid #F5F6F9' }}>
<td style={{ padding: '12px 20px', fontWeight: 500 }}>{r.patientName || '未知'}</td>
<td style={{ padding: '12px 20px' }}>{r.title}</td>
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.category}</td>
<td style={{ padding: '12px 20px' }}>
<span style={{ padding: '4px 12px', borderRadius: 10, fontSize: 11, fontWeight: 500, background: s.bg, color: s.color }}>
{s.text}
</span>
</td>
<td style={{ padding: '10px 16px', color: '#888' }}>{r.createdAt?.split('T')[0]}</td>
<td style={{ padding: '10px 16px' }}>
<Link to={`/reports/${r.id}`} style={{ color: '#1976d2', fontSize: 13 }}></Link>
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.createdAt?.split('T')[0]}</td>
<td style={{ padding: '12px 20px' }}>
<Link to={`/reports/${r.id}`} style={{
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
padding: '4px 12px', background: '#EDF0FD', borderRadius: 6,
}}></Link>
</td>
</tr>
);
})}
{reports.length === 0 && (
<tr><td colSpan={6} style={{ padding: 24, textAlign: 'center', color: '#999' }}></td></tr>
<tr><td colSpan={6} style={{ padding: 32, textAlign: 'center', color: '#9BA0B4' }}></td></tr>
)}
</tbody>
</table>

View File

@@ -24,10 +24,8 @@ export function ProfilePage() {
e.preventDefault();
try {
await api.put('/api/auth/me', {
name: form.name,
department: form.department,
title: form.title,
introduction: form.introduction,
name: form.name, department: form.department,
title: form.title, introduction: form.introduction,
});
updateProfile(form);
alert('保存成功');
@@ -36,44 +34,71 @@ export function ProfilePage() {
if (!user) return null;
const inputStyle: React.CSSProperties = {
width: '100%', padding: '10px 14px', border: '1.5px solid #E1E5ED',
borderRadius: 10, fontSize: 13, outline: 'none', boxSizing: 'border-box',
};
const labelStyle: React.CSSProperties = {
display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5,
};
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: 16 }}></h2>
<div style={{ padding: 28 }}>
<h2 style={{ marginBottom: 20, fontSize: 20, fontWeight: 700, color: '#1A1D28' }}></h2>
<form onSubmit={handleSubmit} style={{ background: '#fff', padding: 24, borderRadius: 8, maxWidth: 500 }}>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} />
<form onSubmit={handleSubmit} style={{ background: '#fff', padding: 28, borderRadius: 16, maxWidth: 520, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 24, paddingBottom: 20, borderBottom: '1px solid #F0F2F5' }}>
<div style={{
width: 56, height: 56, borderRadius: 18,
background: 'linear-gradient(135deg, #4F6EF7, #6C8AFF)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, fontWeight: 700, color: '#fff',
}}>
{user.name?.charAt(0) || 'D'}
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#1A1D28' }}>{user.name}</div>
<div style={{ fontSize: 12, color: '#9BA0B4', marginTop: 2 }}>{user.department} · {user.title}</div>
</div>
</div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} style={inputStyle}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input value={form.phone} disabled
style={{ width: '100%', padding: '8px 12px', border: '1px solid #eee', borderRadius: 4, background: '#f9f9f9' }} />
style={{ ...inputStyle, background: '#F5F7FB', color: '#9BA0B4', border: '1.5px solid #EEF0F5' }} />
</div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<input value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} />
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })} style={inputStyle}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} />
<div style={{ marginBottom: 16 }}>
<label style={labelStyle}></label>
<input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} style={inputStyle}
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
</div>
<div style={{ marginBottom: 18 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}></label>
<div style={{ marginBottom: 20 }}>
<label style={labelStyle}></label>
<textarea value={form.introduction} onChange={(e) => setForm({ ...form, introduction: e.target.value })} rows={4}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4, resize: 'vertical' }} />
style={{ ...inputStyle, resize: 'vertical', fontFamily: 'inherit' }} />
</div>
<button type="submit" style={{
padding: '10px 24px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 4, fontSize: 14,
padding: '11px 28px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
border: 'none', borderRadius: 10, fontSize: 14, fontWeight: 600,
boxShadow: '0 4px 16px rgba(79,110,247,0.25)',
}}>
</button>

View File

@@ -6,7 +6,7 @@ interface ApiResponse<T> {
message: string;
}
const BASE_URL = 'http://localhost:5000';
const BASE_URL = import.meta.env.VITE_API_URL;
// Endpoints that should NEVER include auth token
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];

9
frontend-doctor/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -62,7 +62,7 @@ export function ChatPage() {
// Set up SignalR connection
const conn = new HubConnectionBuilder()
.withUrl('http://localhost:5000/hubs/chat', {
.withUrl(`${import.meta.env.VITE_API_URL}/hubs/chat`, {
accessTokenFactory: () => getToken(),
})
.withAutomaticReconnect()

View File

@@ -53,7 +53,7 @@ export function ReportDetailPage() {
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{report.imageUrls.map((url, i) => (
<img key={i} src={`http://localhost:5000${url}`} alt="report"
<img key={i} src={`${import.meta.env.VITE_API_URL}${url}`} alt="report"
style={{ width: 80, height: 80, borderRadius: 8, objectFit: 'cover', border: '1px solid #eee' }} />
))}
</div>

View File

@@ -38,7 +38,7 @@ export function ReportUploadPage() {
const formData = new FormData();
formData.append('file', file);
const token = JSON.parse(localStorage.getItem('hrt_auth') || '{}')?.state?.token;
const res = await fetch('http://localhost:5000/api/files/upload', {
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/files/upload`, {
method: 'POST',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
body: formData,

View File

@@ -9,7 +9,7 @@ interface ApiResponse<T> {
message: string;
}
const BASE_URL = 'http://localhost:5000';
const BASE_URL = import.meta.env.VITE_API_URL;
// Endpoints that should NEVER include auth token
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];

9
frontend-patient/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

897
ui-mockup.html Normal file
View File

@@ -0,0 +1,897 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>健康管家 - UI 重设计预览</title>
<style>
:root {
--bg: #F0F4F8;
--card: #FFFFFF;
--primary: #4F6EF7;
--primary-light: #EEF1FE;
--primary-gradient: linear-gradient(135deg, #4F6EF7, #6C8CFF);
--accent-1: #FF6B6B;
--accent-2: #FFA94D;
--accent-3: #20C997;
--accent-4: #845EF7;
--accent-5: #339AF0;
--accent-6: #F06595;
--text-1: #1A1D28;
--text-2: #5A5F72;
--text-3: #9BA0B4;
--divider: #EDF0F5;
--radius-sm: 10px;
--radius: 16px;
--radius-lg: 20px;
--shadow-card: 0 2px 12px rgba(0,0,0,0.04);
--shadow-lg: 0 8px 30px rgba(0,0,0,0.08);
--tab-height: 64px;
--header-height: 50px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #E8ECF1;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 30px 0;
-webkit-font-smoothing: antialiased;
}
/* SVG 图标通用 */
.icon { display: block; flex-shrink: 0; }
.icon-white { stroke: #fff; fill: none; }
.icon-colored { stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; fill: none; }
/* ====== 主容器 ====== */
.phone-frame {
width: 390px; height: 844px;
background: var(--bg);
border-radius: 36px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.18), 0 0 0 3px #1a1a2e, 0 0 0 6px #222;
display: flex; flex-direction: column; position: relative;
}
.status-bar {
height: 48px; background: var(--card);
display: flex; justify-content: space-between; align-items: center;
padding: 0 24px; font-size: 12px; font-weight: 600; color: var(--text-1); flex-shrink: 0;
}
.status-icons { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-2); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-3); }
.page-header {
height: var(--header-height); background: var(--card);
display: flex; align-items: center; justify-content: center;
padding: 0 16px; font-size: 17px; font-weight: 600; color: var(--text-1);
flex-shrink: 0; position: relative; border-bottom: 1px solid var(--divider);
}
.page-header .back-btn, .page-header .header-right {
position: absolute; top: 50%; transform: translateY(-50%);
width: 34px; height: 34px; border-radius: 10px;
border: none; background: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s;
}
.page-header .back-btn { left: 12px; }
.page-header .back-btn:hover { background: var(--bg); }
.page-header .header-right { right: 12px; font-size: 13px; color: var(--primary); font-weight: 600; }
.scroll-area {
flex: 1; overflow-y: auto; padding: 16px 16px 0; scroll-behavior: smooth;
}
.scroll-area::-webkit-scrollbar { display: none; }
/* 底部导航 */
.tab-bar {
height: var(--tab-height); background: var(--card);
border-top: 1px solid var(--divider);
display: flex; align-items: center; padding: 0 8px; flex-shrink: 0;
}
.tab-item {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
border: none; background: none; cursor: pointer; padding: 6px 0;
transition: all 0.2s; position: relative;
}
.tab-item .tab-icon-wrap {
width: 44px; height: 44px; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
transition: all 0.25s; position: relative;
}
.tab-item.active .tab-icon-wrap {
background: var(--primary-light); transform: translateY(-6px);
box-shadow: 0 4px 12px rgba(79,110,247,0.25);
}
.tab-item.active .tab-icon-wrap::after {
content: ''; position: absolute; bottom: -2px;
width: 20px; height: 3px; border-radius: 3px; background: var(--primary);
}
.tab-item .tab-label { font-size: 10px; font-weight: 500; color: var(--text-3); transition: color 0.2s; }
.tab-item.active .tab-label { color: var(--primary); font-weight: 600; }
/* 通用 */
.card {
background: var(--card); border-radius: var(--radius-lg);
padding: 18px; box-shadow: var(--shadow-card); margin-bottom: 14px;
transition: transform 0.2s, box-shadow 0.2s;
}
.card:active { transform: scale(0.985); box-shadow: var(--shadow-lg); }
.section-title {
font-size: 16px; font-weight: 700; color: var(--text-1);
margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
}
.section-title .dot { width: 4px; height: 18px; border-radius: 2px; background: var(--primary); }
.tag {
display: inline-flex; align-items: center; padding: 3px 10px;
border-radius: 20px; font-size: 11px; font-weight: 600;
}
.tag-success { background: #E6F9F2; color: #0D8A5E; }
.tag-warning { background: #FFF4E5; color: #D67E0B; }
.tag-danger { background: #FEE9E9; color: #D53131; }
.tag-info { background: var(--primary-light); color: var(--primary); }
/* 问候 */
.greeting { display: flex; justify-content: space-between; align-items: center; padding: 4px 0 14px; }
.greeting-left .hi { font-size: 13px; color: var(--text-3); margin-bottom: 2px; display: flex; align-items: center; gap: 6px; }
.greeting-left .name { font-size: 22px; font-weight: 800; color: var(--text-1); }
.notify-btn {
width: 44px; height: 44px; border-radius: 14px;
background: var(--card); border: 1px solid var(--divider);
cursor: pointer; display: flex; align-items: center; justify-content: center;
position: relative; box-shadow: var(--shadow-card);
}
.notify-dot {
position: absolute; top: 8px; right: 8px;
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent-1); border: 2px solid #fff;
}
/* 健康概览 */
.overview-card {
background: linear-gradient(145deg, #3A54E8 0%, #5B74F7 30%, #7D9AFF 100%);
border-radius: var(--radius-lg); padding: 20px 16px; color: #fff;
margin-bottom: 16px; position: relative; overflow: hidden;
box-shadow: 0 8px 25px rgba(58,84,232,0.3);
}
.overview-card::before {
content: ''; position: absolute; width: 200px; height: 200px;
border-radius: 50%; background: rgba(255,255,255,0.06); top: -60px; right: -60px;
}
.overview-card::after {
content: ''; position: absolute; width: 120px; height: 120px;
border-radius: 50%; background: rgba(255,255,255,0.04); bottom: -40px; left: -30px;
}
.overview-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 18px; position: relative; z-index: 1;
}
.overview-header .title {
font-size: 15px; font-weight: 600; opacity: 0.95;
display: flex; align-items: center; gap: 8px;
}
.overview-header .heart-icon {
width: 28px; height: 28px; background: rgba(255,255,255,0.2);
border-radius: 8px; display: flex; align-items: center; justify-content: center;
}
.overview-header .time {
font-size: 11px; opacity: 0.65; background: rgba(255,255,255,0.15);
padding: 4px 10px; border-radius: 12px; display: flex; align-items: center; gap: 4px;
}
.overview-metrics {
display: flex; align-items: center; justify-content: space-around; position: relative; z-index: 1;
}
.metric-item { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.metric-icon-wrap {
width: 40px; height: 40px; border-radius: 12px;
background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px);
}
.metric-value { font-size: 26px; font-weight: 800; line-height: 1; letter-spacing: -0.5px; }
.metric-sub { font-size: 13px; font-weight: 500; opacity: 0.8; }
.metric-unit { font-size: 10px; opacity: 0.55; font-weight: 500; margin-top: 2px; }
.metric-divider { width: 1px; height: 90px; background: rgba(255,255,255,0.15); }
/* 快捷操作 */
.quick-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px; }
.quick-action {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 14px 4px; background: var(--card); border-radius: var(--radius);
border: none; cursor: pointer; box-shadow: var(--shadow-card);
transition: all 0.2s;
}
.quick-action:active { transform: scale(0.94); box-shadow: var(--shadow-lg); }
.quick-action .qa-icon {
width: 46px; height: 46px; border-radius: 14px;
display: flex; align-items: center; justify-content: center;
}
.qa-icon.bp { background: #FEE9E9; }
.qa-icon.med { background: #FFF4E5; }
.qa-icon.chat { background: #E6F0FF; }
.qa-icon.report { background: #F3E8FF; }
.qa-icon.calendar { background: #E6F9F2; }
.qa-icon.followup { background: #FFF0F5; }
.qa-icon.diet { background: #F0FDF4; }
.qa-icon.device { background: #EFF6FF; }
.quick-action .qa-label { font-size: 11px; color: var(--text-2); font-weight: 600; }
/* 小贴士 */
.tip-card {
background: linear-gradient(135deg, #FFFDF5, #FFF8EC);
border: 1px solid #FDE8B3; border-radius: var(--radius);
padding: 14px 16px; display: flex; align-items: flex-start; gap: 10px; margin-bottom: 16px;
}
.tip-card .tip-bulb { flex-shrink: 0; margin-top: 2px; }
.tip-card .tip-body { flex: 1; }
.tip-card .tip-label {
font-size: 11px; color: #B7791F; font-weight: 700; margin-bottom: 4px;
display: flex; align-items: center; gap: 8px;
}
.tip-card .tip-label .refresh { margin-left: auto; font-size: 11px; color: #D69E2E; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 3px; }
.tip-card .tip-text { font-size: 13px; color: #7B3F00; line-height: 1.6; }
/* 健康中心网格 */
.health-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; }
.health-card {
background: var(--card); border-radius: var(--radius); padding: 18px 12px;
border: none; cursor: pointer; box-shadow: var(--shadow-card);
display: flex; flex-direction: column; align-items: center; gap: 8px;
text-align: center; transition: all 0.2s; position: relative; overflow: hidden;
}
.health-card::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; opacity: 0.7; }
.health-card.bp::after { background: var(--accent-1); }
.health-card.hr::after { background: var(--accent-2); }
.health-card.sugar::after { background: var(--accent-4); }
.health-card.spo2::after { background: var(--accent-5); }
.health-card.weight::after { background: var(--accent-3); }
.health-card.steps::after { background: #6366F1; }
.health-card:active { transform: scale(0.95); }
.health-card .hc-icon { width: 50px; height: 50px; border-radius: 16px; display: flex; align-items: center; justify-content: center; }
.health-card .hc-title { font-size: 14px; font-weight: 700; color: var(--text-1); }
.health-card .hc-desc { font-size: 11px; color: var(--text-3); }
.health-card .hc-arrow {
width: 22px; height: 22px; border-radius: 50%; background: var(--bg);
display: flex; align-items: center; justify-content: center;
color: var(--text-3); margin-top: 2px;
}
.health-card .hc-arrow svg { width: 10px; height: 10px; }
/* 健康入口链接 */
.quick-links { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.quick-link {
display: flex; align-items: center; gap: 12px; padding: 14px 16px;
background: var(--card); border-radius: var(--radius); border: none;
cursor: pointer; box-shadow: var(--shadow-card); font-size: 14px;
font-weight: 600; color: var(--text-1); transition: all 0.15s;
}
.quick-link:active { background: #FAFBFC; transform: scale(0.99); }
.quick-link .ql-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.quick-link .ql-arrow { margin-left: auto; color: var(--text-3); }
/* 服务页 */
.services-grid { display: flex; flex-direction: column; gap: 12px; }
.service-card {
display: flex; align-items: center; gap: 16px; padding: 20px 18px;
background: var(--card); border-radius: var(--radius-lg); border: none;
cursor: pointer; box-shadow: var(--shadow-card); transition: all 0.2s; text-align: left;
}
.service-card:active { transform: scale(0.98); }
.service-card .sc-icon { width: 52px; height: 52px; border-radius: 16px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.service-card .sc-info { flex: 1; }
.service-card .sc-title { font-size: 16px; font-weight: 700; color: var(--text-1); margin-bottom: 4px; }
.service-card .sc-desc { font-size: 12px; color: var(--text-3); }
.service-card .sc-arrow {
width: 34px; height: 34px; border-radius: 50%; background: var(--bg);
display: flex; align-items: center; justify-content: center; color: var(--text-3); flex-shrink: 0;
}
/* 个人中心 */
.profile-header-card {
background: var(--primary-gradient); border-radius: var(--radius-lg);
padding: 20px; display: flex; align-items: center; gap: 14px;
margin-bottom: 16px; box-shadow: 0 6px 24px rgba(79,110,247,0.3); cursor: pointer;
}
.profile-avatar {
width: 56px; height: 56px; border-radius: 18px;
background: rgba(255,255,255,0.25); display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: 800; color: #fff; flex-shrink: 0; backdrop-filter: blur(4px);
}
.profile-info { flex: 1; }
.profile-info .pi-name { font-size: 18px; font-weight: 700; color: #fff; }
.profile-info .pi-phone { font-size: 12px; color: rgba(255,255,255,0.7); margin-top: 2px; }
.profile-header-card .pi-arrow { color: rgba(255,255,255,0.8); }
.stats-row {
display: flex; background: var(--card); border-radius: var(--radius);
box-shadow: var(--shadow-card); padding: 16px 0; margin-bottom: 16px;
}
.stat-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.stat-col .stat-value { font-size: 18px; font-weight: 800; color: var(--text-1); }
.stat-col .stat-label { font-size: 11px; color: var(--text-3); font-weight: 500; }
.stat-sep { width: 1px; background: var(--divider); margin: 8px 0; }
.menu-list { background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow-card); overflow: hidden; margin-bottom: 16px; }
.menu-item {
display: flex; align-items: center; padding: 15px 18px;
border: none; background: none; width: 100%; cursor: pointer;
font-size: 14px; font-weight: 500; color: var(--text-1);
transition: background 0.15s; text-align: left;
}
.menu-item:active { background: #FAFBFC; }
.menu-item+.menu-item { border-top: 1px solid var(--divider); }
.menu-item .mi-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin-right: 12px; flex-shrink: 0; }
.menu-item .mi-arrow { margin-left: auto; color: var(--text-3); }
.menu-item .mi-badge {
margin-left: 8px; background: var(--accent-1); color: #fff;
font-size: 10px; font-weight: 700; min-width: 20px; height: 20px;
border-radius: 10px; display: flex; align-items: center; justify-content: center; padding: 0 6px;
}
.logout-btn {
width: 100%; padding: 14px; border-radius: var(--radius);
border: 1.5px solid #FDD; background: #FFF; color: var(--accent-1);
font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.15s;
}
.logout-btn:active { background: #FFF5F5; }
/* 列表 */
.list-item {
display: flex; align-items: center; gap: 14px; padding: 16px;
background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow-card);
margin-bottom: 10px; border: none; width: 100%; cursor: pointer;
text-align: left; transition: all 0.15s;
}
.list-item:active { transform: scale(0.99); }
.list-item .li-icon { width: 44px; height: 44px; border-radius: 14px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.list-item .li-content { flex: 1; min-width: 0; }
.list-item .li-title { font-size: 15px; font-weight: 600; color: var(--text-1); margin-bottom: 3px; }
.list-item .li-sub { font-size: 12px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-item .li-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.list-item .li-time { font-size: 11px; color: var(--text-3); }
.list-item .li-value { font-size: 15px; font-weight: 700; color: var(--text-1); }
/* Tab 切换 */
.tab-content { display: none; }
.tab-content.active { display: block; }
/* 设备 */
.device-card {
display: flex; align-items: center; gap: 14px; padding: 16px;
background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow-card); margin-bottom: 10px;
}
.device-card .dc-icon { width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center; }
.device-card .dc-info { flex: 1; }
.device-card .dc-name { font-size: 15px; font-weight: 600; color: var(--text-1); margin-bottom: 3px; }
.device-card .dc-status { font-size: 12px; display: flex; align-items: center; gap: 4px; }
.connected-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-3); display: inline-block; }
/* 页码 */
.page-tag {
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
font-size: 12px; color: var(--text-3); font-weight: 500;
background: var(--card); padding: 4px 14px; border-radius: 12px;
box-shadow: var(--shadow-card); z-index: 5;
}
/* 动画 */
@keyframes fadeInUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.animate-in { animation: fadeInUp 0.35s ease-out; }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
.pulse { animation: pulse 2s ease-in-out infinite; }
/* 用药 */
.med-row { display: flex; align-items: center; justify-content: space-between; }
.med-row+.med-row { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--divider); }
.med-left { display: flex; align-items: center; gap: 10px; }
.med-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; }
.med-info-name { font-size: 14px; font-weight: 600; color: var(--text-1); }
.med-info-dose { font-size: 11px; color: var(--text-3); margin-top: 2px; }
/* 趋势柱状图 */
.trend-bars { display: flex; align-items: flex-end; gap: 8px; justify-content: center; height: 50px; }
.trend-bar { width: 20px; border-radius: 4px; background: rgba(255,255,255,0.3); }
.trend-bar.hi { background: rgba(255,255,255,0.45); }
</style>
</head>
<body>
<div class="phone-frame">
<div class="status-bar">
<span>9:41</span>
<div class="status-icons"><span>5G</span><span>▮▮▮▮</span><div class="status-dot"></div></div>
</div>
<!-- ==================== 首页 ==================== -->
<div class="tab-content active" id="page-home">
<div class="scroll-area animate-in">
<div class="greeting">
<div class="greeting-left">
<div class="hi">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
上午好
</div>
<div class="name">张建国</div>
</div>
<button class="notify-btn">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5A5F72" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notify-dot"></span>
</button>
</div>
<!-- 健康概览 -->
<div class="overview-card">
<div class="overview-header">
<span class="title">
<span class="heart-icon pulse">
<svg width="14" height="14" viewBox="0 0 24 24" fill="rgba(255,255,255,0.9)" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</span>
健康概览
</span>
<span class="time">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.7)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
今天 08:30
</span>
</div>
<div class="overview-metrics">
<div class="metric-item">
<div class="metric-icon-wrap">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg>
</div>
<div class="metric-value">128</div>
<div class="metric-sub">/82</div>
<div class="metric-unit">血压 mmHg</div>
</div>
<div class="metric-divider"></div>
<div class="metric-item">
<div class="metric-icon-wrap">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div>
<div class="metric-value">72</div>
<div class="metric-unit">心率 bpm</div>
</div>
<div class="metric-divider"></div>
<div class="metric-item">
<div class="metric-icon-wrap">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div class="metric-value">5.6</div>
<div class="metric-unit">血糖 mmol/L</div>
</div>
<div class="metric-divider"></div>
<div class="metric-item">
<div class="metric-icon-wrap">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
</div>
<div class="metric-value">98</div>
<div class="metric-unit">血氧 %</div>
</div>
</div>
</div>
<!-- 快捷功能 -->
<div class="section-title"><span class="dot"></span>快捷功能</div>
<div class="quick-actions">
<button class="quick-action" onclick="showPage('bp-list')">
<div class="qa-icon bp"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg></div>
<span class="qa-label">血压</span>
</button>
<button class="quick-action">
<div class="qa-icon med"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="4"/><path d="M10 9v6M14 9v6M8 12h8"/></svg></div>
<span class="qa-label">用药</span>
</button>
<button class="quick-action">
<div class="qa-icon chat"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="10" x2="15" y2="10"/><line x1="12" y1="7" x2="12" y2="13"/></svg></div>
<span class="qa-label">问诊</span>
</button>
<button class="quick-action">
<div class="qa-icon report"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></div>
<span class="qa-label">报告</span>
</button>
<button class="quick-action">
<div class="qa-icon calendar"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div>
<span class="qa-label">日历</span>
</button>
<button class="quick-action">
<div class="qa-icon followup"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F06595" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-2.5L14 13l-2-6-3 6.5L7 10H5"/><rect x="2" y="2" width="20" height="20" rx="3"/></svg></div>
<span class="qa-label">复查</span>
</button>
<button class="quick-action">
<div class="qa-icon diet"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<span class="qa-label">饮食</span>
</button>
<button class="quick-action">
<div class="qa-icon device"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#339AF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/><path d="M9 6h6"/></svg></div>
<span class="qa-label">设备</span>
</button>
</div>
<!-- 健康小贴士 -->
<div class="tip-card">
<div class="tip-bulb">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>
</div>
<div class="tip-body">
<div class="tip-label">
每日健康提醒
<span class="refresh">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#D69E2E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
换一条
</span>
</div>
<div class="tip-text">每天测量血压的最佳时间是早晨起床后1小时内、服药前。保持规律监测记录数据变化趋势。</div>
</div>
</div>
<!-- 今日用药 -->
<div class="card">
<div class="section-title" style="margin-bottom:8px;"><span class="dot"></span>今日用药</div>
<div class="med-row">
<div class="med-left">
<div class="med-icon" style="background:#FFF4E5;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="4"/><path d="M10 9v6M14 9v6M8 12h8"/></svg>
</div>
<div><div class="med-info-name">阿司匹林肠溶片</div><div class="med-info-dose">每日1次 · 100mg</div></div>
</div>
<span class="tag tag-success">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#0D8A5E" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
&nbsp;已完成
</span>
</div>
<div class="med-row">
<div class="med-left">
<div class="med-icon" style="background:#E6F0FF;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="4"/><path d="M10 9v6M14 9v6M8 12h8"/></svg>
</div>
<div><div class="med-info-name">氯吡格雷</div><div class="med-info-dose">每日1次 · 75mg</div></div>
</div>
<span class="tag tag-warning">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
&nbsp;待服用
</span>
</div>
</div>
</div>
</div>
<!-- ==================== 健康中心 ==================== -->
<div class="tab-content" id="page-health">
<div class="page-header">健康中心</div>
<div class="scroll-area animate-in">
<div class="section-title" style="margin-top:4px;"><span class="dot"></span>健康指标</div>
<div class="health-grid">
<button class="health-card bp" onclick="showPage('bp-list')">
<div class="hc-icon" style="background:#FEE9E9;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg>
</div>
<div class="hc-title">血压</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="health-card hr">
<div class="hc-icon" style="background:#FFF4E5;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div>
<div class="hc-title">心率</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="health-card sugar">
<div class="hc-icon" style="background:#F3E8FF;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div class="hc-title">血糖</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="health-card spo2">
<div class="hc-icon" style="background:#E6F0FF;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#339AF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
</div>
<div class="hc-title">血氧</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="health-card weight">
<div class="hc-icon" style="background:#E6F9F2;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>
</div>
<div class="hc-title">体重</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="health-card steps">
<div class="hc-icon" style="background:#EEF2FF;">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 4h3l3 7-4 2v7H9v-7l-4-2 3-7h3"/><circle cx="12" cy="4" r="2"/></svg>
</div>
<div class="hc-title">步数</div><div class="hc-desc">记录·趋势</div>
<div class="hc-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
</div>
<div class="section-title"><span class="dot"></span>健康工具</div>
<div class="quick-links">
<button class="quick-link">
<div class="ql-icon" style="background:#FFF0E0;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
健康日历
<span class="ql-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</span>
</button>
<button class="quick-link">
<div class="ql-icon" style="background:#FFF4E5;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="4"/><path d="M10 9v6M14 9v6M8 12h8"/></svg>
</div>
服药管理
<span class="ql-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</span>
</button>
<button class="quick-link">
<div class="ql-icon" style="background:#E6F9F2;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
运动饮食
<span class="ql-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</span>
</button>
</div>
</div>
</div>
<!-- ==================== 服务 ==================== -->
<div class="tab-content" id="page-services">
<div class="page-header">健康服务</div>
<div class="scroll-area animate-in">
<div class="services-grid" style="margin-top:4px;">
<button class="service-card">
<div class="sc-icon" style="background:#E6F0FF;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="sc-info"><div class="sc-title">在线问诊</div><div class="sc-desc">图文咨询专业医生,获得健康指导</div></div>
<div class="sc-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="service-card">
<div class="sc-icon" style="background:#F3E8FF;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
</div>
<div class="sc-info"><div class="sc-title">报告解读</div><div class="sc-desc">上传检查报告,权威医生解读</div></div>
<div class="sc-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
<button class="service-card">
<div class="sc-icon" style="background:#FEE9E9;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-2.5L14 13l-2-6-3 6.5L7 10H5"/><rect x="2" y="2" width="20" height="20" rx="3"/></svg>
</div>
<div class="sc-info"><div class="sc-title">复查管理</div><div class="sc-desc">智能规划复查计划,准时提醒</div></div>
<div class="sc-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></div>
</button>
</div>
<div class="section-title" style="margin-top:20px;"><span class="dot"></span>最近记录</div>
<button class="list-item">
<div class="li-icon" style="background:#E6F0FF;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="li-content"><div class="li-title">王医生 心血管内科</div><div class="li-sub">最近血压控制得怎么样?...</div></div>
<div class="li-right"><span class="li-time">今天 10:30</span><span class="tag tag-info">进行中</span></div>
</button>
<button class="list-item">
<div class="li-icon" style="background:#F3E8FF;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<div class="li-content"><div class="li-title">心电图报告</div><div class="li-sub">2024年5月15日 · 正常</div></div>
<div class="li-right"><span class="li-time">5月15日</span><span class="tag tag-success">已解读</span></div>
</button>
</div>
</div>
<!-- ==================== 我的 ==================== -->
<div class="tab-content" id="page-profile">
<div class="page-header">
我的
<button class="header-right" style="display:flex;align-items:center;gap:2px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
<div class="scroll-area animate-in">
<div class="profile-header-card">
<div class="profile-avatar"></div>
<div class="profile-info"><div class="pi-name">张建国</div><div class="pi-phone">138****8888</div></div>
<div class="pi-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.8)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
<div class="stats-row">
<div class="stat-col">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>
<div class="stat-value">172cm</div><div class="stat-label">身高</div>
</div>
<div class="stat-sep"></div>
<div class="stat-col">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="2"/><path d="M10 22V10l-4 4"/><path d="M14 22V10l4 4"/></svg>
<div class="stat-value">68kg</div><div class="stat-label">体重</div>
</div>
<div class="stat-sep"></div>
<div class="stat-col">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/></svg>
<div class="stat-value">冠心病</div><div class="stat-label">病史</div>
</div>
</div>
<div class="menu-list">
<button class="menu-item">
<div class="mi-icon" style="background:#FFF4E5;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="4"/><path d="M10 9v6M14 9v6M8 12h8"/></svg>
</div>
我的用药
<span class="mi-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
</button>
<button class="menu-item">
<div class="mi-icon" style="background:#EEF2FF;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
</div>
设备管理
<span class="mi-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
</button>
<button class="menu-item">
<div class="mi-icon" style="background:#F3E8FF;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</div>
设置
<span class="mi-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
</button>
<button class="menu-item">
<div class="mi-icon" style="background:#E6F9F2;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</div>
关于
<span class="mi-arrow"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
</button>
</div>
<button class="logout-btn">退出登录</button>
</div>
</div>
<!-- ==================== 设备绑定 ==================== -->
<div class="tab-content" id="page-device">
<div class="page-header">
<button class="back-btn" onclick="showPage('profile')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
设备管理
<button class="header-right">+ 添加</button>
</div>
<div class="scroll-area animate-in">
<div class="device-card">
<div class="dc-icon" style="background:#EEF2FF;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<div class="dc-info"><div class="dc-name">智能血压计 BP-200</div><div class="dc-status"><span class="connected-dot"></span> 已连接 · 电量 85%</div></div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9BA0B4" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="device-card">
<div class="dc-icon" style="background:#E6F9F2;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#20C997" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
</div>
<div class="dc-info"><div class="dc-name">心率监测手环 H3</div><div class="dc-status"><span class="connected-dot"></span> 已连接 · 电量 62%</div></div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9BA0B4" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="device-card" style="opacity:0.6;">
<div class="dc-icon" style="background:#F3E8FF;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#845EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div class="dc-info"><div class="dc-name">血糖仪 G-100</div><div class="dc-status" style="color:#9BA0B4;">未绑定</div></div>
<span style="color:#4F6EF7;font-size:13px;font-weight:600;">绑定</span>
</div>
</div>
</div>
<!-- ==================== 血压记录列表 ==================== -->
<div class="tab-content" id="page-bp-list">
<div class="page-header">
<button class="back-btn" onclick="showPage('home')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
血压记录
<button class="header-right">+ 记录</button>
</div>
<div class="scroll-area animate-in">
<div class="overview-card" style="background:linear-gradient(145deg,#4F6EF7,#6C8CFF);box-shadow:0 6px 20px rgba(79,110,247,0.25);">
<div style="display:flex;align-items:center;gap:12px;position:relative;z-index:1;">
<div style="text-align:center;">
<div style="font-size:11px;opacity:0.7;margin-bottom:4px;">本月平均</div>
<div style="font-size:28px;font-weight:800;">126/81</div>
<div style="font-size:10px;opacity:0.55;">mmHg</div>
</div>
<div style="width:1px;height:60px;background:rgba(255,255,255,0.2);"></div>
<div style="flex:1;text-align:center;">
<div style="font-size:11px;opacity:0.7;margin-bottom:8px;">近7天趋势</div>
<div class="trend-bars">
<div class="trend-bar" style="height:30px;"></div>
<div class="trend-bar" style="height:22px;"></div>
<div class="trend-bar hi" style="height:40px;"></div>
<div class="trend-bar" style="height:28px;"></div>
<div class="trend-bar" style="height:35px;"></div>
<div class="trend-bar" style="height:20px;"></div>
<div class="trend-bar" style="height:32px;"></div>
</div>
</div>
</div>
</div>
<div class="section-title"><span class="dot"></span>历史记录</div>
<button class="list-item">
<div class="li-icon" style="background:#FEE9E9;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg>
</div>
<div class="li-content"><div class="li-title">血压</div><div class="li-sub">早晨 · 服药前测量</div></div>
<div class="li-right"><span class="li-time">今天 08:30</span><span class="li-value">128/82</span><span class="tag tag-success">正常</span></div>
</button>
<button class="list-item">
<div class="li-icon" style="background:#FEE9E9;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg>
</div>
<div class="li-content"><div class="li-title">血压</div><div class="li-sub">晚上 · 服药后测量</div></div>
<div class="li-right"><span class="li-time">昨天 20:00</span><span class="li-value">135/85</span><span class="tag tag-warning">偏高</span></div>
</button>
<button class="list-item">
<div class="li-icon" style="background:#FEE9E9;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/><polyline points="12 7 12 13 15 15"/></svg>
</div>
<div class="li-content"><div class="li-title">血压</div><div class="li-sub">早晨 · 服药前测量</div></div>
<div class="li-right"><span class="li-time">5月20日</span><span class="li-value">125/79</span><span class="tag tag-success">正常</span></div>
</button>
</div>
</div>
<!-- 底部导航 -->
<div class="tab-bar">
<button class="tab-item active" onclick="showPage('home')">
<span class="tab-icon-wrap">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5A5F72" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</span>
<span class="tab-label">首页</span>
</button>
<button class="tab-item" onclick="showPage('health')">
<span class="tab-icon-wrap">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5A5F72" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</span>
<span class="tab-label">健康</span>
</button>
<button class="tab-item" onclick="showPage('services')">
<span class="tab-icon-wrap">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5A5F72" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span class="tab-label">服务</span>
</button>
<button class="tab-item" onclick="showPage('profile')">
<span class="tab-icon-wrap">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5A5F72" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span class="tab-label">我的</span>
</button>
</div>
<div class="page-tag">首页</div>
</div>
<script>
function showPage(pageName) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
var target = document.getElementById('page-' + pageName);
if (target) target.classList.add('active');
var tabMap = { home:0, health:1, services:2, profile:3 };
var idx = tabMap[pageName];
document.querySelectorAll('.tab-item').forEach(function(el, i) { el.classList.toggle('active', i===idx); });
var pageNames = { home:'首页', health:'健康中心', services:'健康服务', profile:'我的', 'bp-list':'血压记录', device:'设备管理' };
var tag = document.querySelector('.page-tag');
if (tag) tag.textContent = pageNames[pageName] || '';
var scrollArea = target ? target.querySelector('.scroll-area') : null;
if (scrollArea) scrollArea.scrollTop = 0;
}
document.querySelectorAll('.quick-action').forEach(function(btn, i) {
btn.addEventListener('click', function() { var a=['bp-list',null,null,null,null,null,null,'device']; if(a[i]) showPage(a[i]); });
});
</script>
</body>
</html>