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:
15
backend/.env.example
Normal file
15
backend/.env.example
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
51
frontend-doctor/src/assets/styles/variables.css
Normal file
51
frontend-doctor/src/assets/styles/variables.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,36 +55,81 @@ 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>
|
||||
|
||||
<h3 style={{ marginTop: 24, marginBottom: 12 }}>最近健康数据</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
|
||||
<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: 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 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: 20, fontWeight: 600, marginTop: 4 }}>
|
||||
{parseValueDisplay(record)} {record.unit}
|
||||
<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: '#bbb', marginTop: 4 }}>
|
||||
<div style={{ fontSize: 11, color: '#C0C5D2', marginTop: 6 }}>
|
||||
{record.recordedAt?.split('T')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || '未知'} |
|
||||
分类:{categoryMap[report.category] || report.category} |
|
||||
日期:{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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
9
frontend-doctor/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
9
frontend-patient/src/vite-env.d.ts
vendored
Normal 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
897
ui-mockup.html
Normal 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>
|
||||
已完成
|
||||
</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>
|
||||
待服用
|
||||
</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>
|
||||
Reference in New Issue
Block a user