refactor: patient frontend UI overhaul

- Reworked design system (variables, global styles, component CSS)
- Updated TabBar with icon-based navigation
- Redesigned HomePage, HealthHub, ServicesHub layouts
- Improved Exercise/Diet, Medication, Profile pages styling
- Simplified constants (removed emoji icons, streamlined data)
- Fixed launch.json cwd paths for frontend projects
This commit is contained in:
MingNian
2026-05-22 17:48:18 +08:00
parent 94da24572e
commit 722ee76d93
44 changed files with 854 additions and 393 deletions

View File

@@ -2,10 +2,18 @@
"version": "0.0.1", "version": "0.0.1",
"configurations": [ "configurations": [
{ {
"name": "健康管家 Web Demo", "name": "健康管家-患者端",
"runtimeExecutable": "cmd.exe", "runtimeExecutable": "cmd.exe",
"runtimeArgs": ["/c", "D:\\nodejs\\npm.cmd", "run", "dev"], "runtimeArgs": ["/c", "D:\\nodejs\\npm.cmd", "run", "dev"],
"port": 5175 "cwd": "D:\\APP\\frontend-patient",
"port": 5173
},
{
"name": "健康管家-医生端",
"runtimeExecutable": "cmd.exe",
"runtimeArgs": ["/c", "D:\\nodejs\\npm.cmd", "run", "dev"],
"cwd": "D:\\APP\\frontend-doctor",
"port": 5174
} }
] ]
} }

View File

@@ -36,7 +36,51 @@
white-space: nowrap; white-space: nowrap;
} }
/* Section Title */
.section-title {
font-size: var(--font-size-md);
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title::before {
content: '';
width: 4px;
height: 18px;
border-radius: 2px;
background: var(--color-primary);
}
/* Tag */
.tag {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
}
.tag-success { background: var(--color-success-bg); color: #0D8A5E; }
.tag-warning { background: var(--color-warning-bg); color: #D67E0B; }
.tag-danger { background: var(--color-danger-bg); color: #D53131; }
.tag-info { background: var(--color-primary-bg); color: var(--color-primary); }
.tag-primary { background: var(--color-primary-bg); color: var(--color-primary); }
/* Transitions */ /* Transitions */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.page-enter { .page-enter {
animation: slideInRight 0.3s ease-out; animation: slideInRight 0.3s ease-out;
} }

View File

@@ -1,30 +1,39 @@
:root { :root {
/* Primary - Rich Medical Blue */ /* Primary - Modern Indigo Blue */
--color-primary: #2563EB; --color-primary: #4F6EF7;
--color-primary-light: #3B82F6; --color-primary-light: #6C8CFF;
--color-primary-dark: #1D4ED8; --color-primary-dark: #3D56D4;
--color-primary-bg: #EFF6FF; --color-primary-bg: #EEF1FE;
--color-primary-gradient: linear-gradient(135deg, #2563EB, #4F8AF8); --color-primary-gradient: linear-gradient(135deg, #4F6EF7, #6C8CFF);
/* Accent */
--color-accent-red: #FF6B6B;
--color-accent-orange: #FFA94D;
--color-accent-green: #20C997;
--color-accent-purple: #845EF7;
--color-accent-sky: #339AF0;
--color-accent-pink: #F06595;
/* Status */ /* Status */
--color-success: #10B981; --color-success: #20C997;
--color-success-bg: #ECFDF5; --color-success-bg: #E6F9F2;
--color-warning: #F59E0B; --color-warning: #F59E0B;
--color-warning-bg: #FFFBEB; --color-warning-bg: #FFF4E5;
--color-danger: #EF4444; --color-danger: #FF6B6B;
--color-danger-bg: #FEF2F2; --color-danger-bg: #FEE9E9;
/* Neutral */ /* Neutral */
--color-white: #FFFFFF; --color-white: #FFFFFF;
--color-bg: #F4F6FA; --color-bg: #F0F4F8;
--color-bg-secondary: #EBEEF3; --color-bg-secondary: #E8ECF2;
--color-border: #E4E7EC; --color-border: #E4E8EE;
--color-border-light: #F0F1F4; --color-border-light: #EEF1F6;
--color-divider: #EDF0F5;
/* Text */ /* Text */
--color-text-primary: #1A1E2B; --color-text-primary: #1A1D28;
--color-text-secondary: #6B7280; --color-text-secondary: #5A5F72;
--color-text-tertiary: #9CA3AF; --color-text-tertiary: #9BA0B4;
--color-text-inverse: #FFFFFF; --color-text-inverse: #FFFFFF;
/* Spacing */ /* Spacing */
@@ -37,21 +46,21 @@
--spacing-3xl: 32px; --spacing-3xl: 32px;
/* Border radius */ /* Border radius */
--radius-sm: 8px; --radius-sm: 10px;
--radius-md: 12px; --radius-md: 14px;
--radius-lg: 16px; --radius-lg: 16px;
--radius-xl: 20px; --radius-xl: 20px;
--radius-2xl: 24px; --radius-2xl: 24px;
--radius-full: 9999px; --radius-full: 9999px;
/* Shadows */ /* Shadows */
--shadow-xs: 0 1px 2px rgba(0,0,0,0.04); --shadow-xs: 0 1px 3px rgba(0,0,0,0.03);
--shadow-sm: 0 2px 8px rgba(0,0,0,0.05); --shadow-sm: 0 2px 12px rgba(0,0,0,0.04);
--shadow-md: 0 4px 16px rgba(0,0,0,0.07); --shadow-md: 0 4px 20px rgba(0,0,0,0.06);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.09); --shadow-lg: 0 8px 30px rgba(0,0,0,0.08);
/* Font */ /* Font */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; --font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
--font-size-xs: 11px; --font-size-xs: 11px;
--font-size-sm: 12px; --font-size-sm: 12px;
--font-size-base: 14px; --font-size-base: 14px;
@@ -62,8 +71,8 @@
--font-size-3xl: 32px; --font-size-3xl: 32px;
/* Layout */ /* Layout */
--tab-bar-height: 56px; --tab-bar-height: 64px;
--header-height: 48px; --header-height: 50px;
--max-content-width: 414px; --max-content-width: 414px;
/* Z-index */ /* Z-index */

View File

@@ -2,14 +2,14 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 18px; min-width: 20px;
height: 18px; height: 20px;
padding: 0 5px; padding: 0 6px;
border-radius: 10px; border-radius: 10px;
background: var(--color-danger); background: var(--color-accent-red);
color: white; color: white;
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 700;
line-height: 1; line-height: 1;
} }
@@ -18,5 +18,5 @@
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--color-danger); background: var(--color-accent-red);
} }

View File

@@ -4,7 +4,7 @@
justify-content: center; justify-content: center;
gap: var(--spacing-sm); gap: var(--spacing-sm);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-weight: 500; font-weight: 600;
transition: all 0.2s; transition: all 0.2s;
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@@ -21,11 +21,13 @@
.lg { padding: 12px 24px; font-size: var(--font-size-md); } .lg { padding: 12px 24px; font-size: var(--font-size-md); }
.primary { .primary {
background: var(--color-primary); background: var(--color-primary-gradient);
color: var(--color-text-inverse); color: var(--color-text-inverse);
box-shadow: 0 4px 14px rgba(79,110,247,0.3);
} }
.primary:hover:not(:disabled) { .primary:hover:not(:disabled) {
background: var(--color-primary-dark); box-shadow: 0 6px 20px rgba(79,110,247,0.35);
transform: translateY(-1px);
} }
.secondary { .secondary {
@@ -48,24 +50,4 @@
.text { .text {
background: transparent; background: transparent;
color: var(--color-primary); color: var(--color-primary);
padding-left: 4px;
padding-right: 4px;
}
.text:hover:not(:disabled) {
opacity: 0.8;
}
.fullWidth { width: 100%; }
.spinner {
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
} }

View File

@@ -1,17 +1,17 @@
.card { .card {
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-lg); border-radius: var(--radius-xl);
padding: var(--spacing-lg); padding: 18px;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
} }
.clickable { .clickable {
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
transition: transform 0.15s, box-shadow 0.15s;
} }
.clickable:active { .clickable:active {
transform: scale(0.98); transform: scale(0.985);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }

View File

@@ -7,11 +7,24 @@
} }
.icon { .icon {
font-size: 48px; width: 64px;
margin-bottom: 12px; height: 64px;
border-radius: 20px;
background: var(--color-bg-secondary);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
}
.icon svg {
width: 32px;
height: 32px;
stroke: var(--color-text-tertiary);
} }
.message { .message {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
font-weight: 500;
} }

View File

@@ -1,14 +1,23 @@
import styles from './Empty.module.css'; import styles from './Empty.module.css';
interface EmptyProps { interface EmptyProps {
icon?: string; icon?: React.ReactNode;
message?: string; message?: string;
} }
export function Empty({ icon = '📭', message = '暂无数据' }: EmptyProps) { const DEFAULT_ICON = (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" 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>
);
export function Empty({ icon = DEFAULT_ICON, message = '暂无数据' }: EmptyProps) {
return ( return (
<div className={styles.empty}> <div className={styles.empty}>
<span className={styles.icon}>{icon}</span> <div className={styles.icon}>{icon}</div>
<p className={styles.message}>{message}</p> <p className={styles.message}>{message}</p>
</div> </div>
); );

View File

@@ -7,7 +7,7 @@
.label { .label {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 500; font-weight: 600;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
@@ -16,15 +16,16 @@
padding: 10px 14px; padding: 10px 14px;
background: var(--color-bg); background: var(--color-bg);
border: 1.5px solid var(--color-border); border: 1.5px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-sm);
font-size: var(--font-size-base); font-size: var(--font-size-base);
color: var(--color-text-primary); color: var(--color-text-primary);
transition: border-color 0.2s; transition: border-color 0.2s, box-shadow 0.2s;
} }
.input:focus { .input:focus {
border-color: var(--color-primary); border-color: var(--color-primary);
background: var(--color-white); background: var(--color-white);
box-shadow: 0 0 0 3px rgba(79,110,247,0.1);
} }
.input::placeholder { .input::placeholder {

View File

@@ -19,6 +19,7 @@
min-width: 160px; min-width: 160px;
text-align: center; text-align: center;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
font-weight: 500;
} }
.success { background: var(--color-success); } .success { background: var(--color-success); }

View File

@@ -1,5 +1,6 @@
.layout { .layout {
min-height: 100vh; min-height: 100vh;
background: var(--color-bg);
} }
.main { .main {

View File

@@ -7,7 +7,7 @@
max-width: var(--max-content-width); max-width: var(--max-content-width);
height: var(--header-height); height: var(--header-height);
background: var(--color-white); background: var(--color-white);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-divider);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -33,13 +33,13 @@
width: 36px; width: 36px;
height: 36px; height: 36px;
margin-left: -8px; margin-left: -8px;
border-radius: var(--radius-full); border-radius: var(--radius-sm);
color: var(--color-text-primary); color: var(--color-text-primary);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.title { .title {
font-size: var(--font-size-md); font-size: 17px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
flex: 1; flex: 1;

View File

@@ -7,11 +7,12 @@
max-width: var(--max-content-width); max-width: var(--max-content-width);
height: var(--tab-bar-height); height: var(--tab-bar-height);
background: var(--color-white); background: var(--color-white);
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-divider);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
z-index: var(--z-tab-bar); z-index: var(--z-tab-bar);
padding: 0 8px;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
@@ -21,9 +22,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 2px; gap: 2px;
padding: var(--spacing-xs) var(--spacing-md); padding: 6px 0;
min-width: 56px; min-width: 56px;
min-height: 44px;
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
transition: color 0.2s; transition: color 0.2s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@@ -34,33 +34,57 @@
} }
.tabIcon { .tabIcon {
font-size: 22px; width: 44px;
line-height: 1; height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.25s;
}
.tabActive .tabIcon {
background: var(--color-primary-bg);
transform: translateY(-6px);
box-shadow: 0 4px 12px rgba(79,110,247,0.25);
}
.tabActive .tabIcon::after {
content: '';
position: absolute;
bottom: -2px;
width: 20px;
height: 3px;
border-radius: 3px;
background: var(--color-primary);
} }
.tabLabel { .tabLabel {
font-size: var(--font-size-xs); font-size: 10px;
font-weight: 500; font-weight: 500;
transition: color 0.2s;
} }
.tabIcon { .tabActive .tabLabel {
position: relative; font-weight: 600;
} }
.badge { .badge {
position: absolute; position: absolute;
top: -6px; top: -4px;
right: -10px; right: -6px;
min-width: 16px; min-width: 18px;
height: 16px; height: 18px;
padding: 0 4px; padding: 0 5px;
background: #EF4444; background: var(--color-accent-red);
color: #fff; color: #fff;
border-radius: 10px; border-radius: 10px;
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
line-height: 1; line-height: 1;
border: 2px solid #fff;
} }

View File

@@ -1,7 +1,48 @@
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { NAV_ITEMS } from '@/utils/constants';
import styles from './TabBar.module.css'; import styles from './TabBar.module.css';
const NAV_ITEMS = [
{
path: '/home',
label: '首页',
svg: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
},
{
path: '/health',
label: '健康',
svg: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
},
{
path: '/services',
label: '服务',
svg: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
},
{
path: '/profile',
label: '我的',
svg: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
},
];
export function TabBar() { export function TabBar() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -16,7 +57,7 @@ export function TabBar() {
className={`${styles.tab} ${isActive ? styles.tabActive : ''}`} className={`${styles.tab} ${isActive ? styles.tabActive : ''}`}
onClick={() => navigate(item.path)} onClick={() => navigate(item.path)}
> >
<span className={styles.tabIcon}>{item.icon}</span> <span className={styles.tabIcon}>{item.svg}</span>
<span className={styles.tabLabel}>{item.label}</span> <span className={styles.tabLabel}>{item.label}</span>
</button> </button>
); );

View File

@@ -48,7 +48,11 @@ export function LoginPage() {
return ( return (
<div className={styles.page}> <div className={styles.page}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.logo}></div> <div className={styles.logo}>
<svg width="36" height="36" viewBox="0 0 24 24" fill="var(--color-primary)" 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>
<h1 className={styles.title}></h1> <h1 className={styles.title}></h1>
<p className={styles.subtitle}></p> <p className={styles.subtitle}></p>
</div> </div>

View File

@@ -1,17 +1,17 @@
.tabs { display: flex; gap: 8px; margin-bottom: 16px; } .tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); } .tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); font-weight: 500; }
.tabActive { background: var(--color-primary); color: var(--color-text-inverse); } .tabActive { background: var(--color-primary); color: var(--color-text-inverse); }
.sectionTitle { font-size: var(--font-size-base); font-weight: 600; margin: 16px 0 8px; } .sectionTitle { font-size: var(--font-size-base); font-weight: 700; margin: 16px 0 8px; }
.recCard { margin-bottom: 8px; } .recCard { margin-bottom: 8px; }
.recHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-weight: 600; font-size: var(--font-size-sm); } .recHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-weight: 600; font-size: var(--font-size-sm); }
.suitBadge { font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); } .suitBadge { font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); font-weight: 600; }
.suitYes { background: var(--color-success-bg); color: var(--color-success); } .suitYes { background: var(--color-success-bg); color: var(--color-success); }
.suitNo { background: var(--color-danger-bg); color: var(--color-danger); } .suitNo { background: var(--color-danger-bg); color: var(--color-danger); }
.notSuitable { opacity: 0.5; } .notSuitable { opacity: 0.5; }
.recMeta { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } .recMeta { font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.recDesc { font-size: var(--font-size-xs); color: var(--color-text-secondary); margin: 6px 0; } .recDesc { font-size: var(--font-size-xs); color: var(--color-text-secondary); margin: 6px 0; line-height: 1.5; }
.foodTags { display: flex; gap: 6px; flex-wrap: wrap; } .foodTags { display: flex; gap: 6px; flex-wrap: wrap; }
.foodTag { padding: 2px 8px; font-size: var(--font-size-xs); background: var(--color-primary-bg); color: var(--color-primary); border-radius: var(--radius-sm); } .foodTag { padding: 2px 8px; font-size: var(--font-size-xs); background: var(--color-primary-bg); color: var(--color-primary); border-radius: var(--radius-sm); font-weight: 500; }
.addCard { margin-bottom: 12px; display: flex; flex-direction: column; gap: 10px; } .addCard { margin-bottom: 12px; display: flex; flex-direction: column; gap: 10px; }
.addRow { display: flex; gap: 8px; align-items: center; } .addRow { display: flex; gap: 8px; align-items: center; }
.select { padding: 10px 12px; border: 1.5px solid var(--color-border); border-radius: var(--radius-md); font-size: var(--font-size-sm); background: var(--color-bg); outline: none; } .select { padding: 10px 12px; border: 1.5px solid var(--color-border); border-radius: var(--radius-md); font-size: var(--font-size-sm); background: var(--color-bg); outline: none; }

View File

@@ -72,7 +72,7 @@ export function ExerciseDietPage() {
{recommendations.map((r, i) => ( {recommendations.map((r, i) => (
<Card key={i} className={`${styles.recCard} ${!r.suitable ? styles.notSuitable : ''}`}> <Card key={i} className={`${styles.recCard} ${!r.suitable ? styles.notSuitable : ''}`}>
<div className={styles.recHeader}> <div className={styles.recHeader}>
<span>{r.icon} {r.name}</span> <span>{r.name}</span>
<span className={`${styles.suitBadge} ${r.suitable ? styles.suitYes : styles.suitNo}`}> <span className={`${styles.suitBadge} ${r.suitable ? styles.suitYes : styles.suitNo}`}>
{r.suitable ? '适合' : '不适合'} {r.suitable ? '适合' : '不适合'}
</span> </span>
@@ -83,7 +83,7 @@ export function ExerciseDietPage() {
<h3 className={styles.sectionTitle}></h3> <h3 className={styles.sectionTitle}></h3>
{dietRecommendations.slice(0, 3).map((d, i) => ( {dietRecommendations.slice(0, 3).map((d, i) => (
<Card key={i} className={styles.recCard}> <Card key={i} className={styles.recCard}>
<div className={styles.recHeader}><span>🍽 {d.title}</span></div> <div className={styles.recHeader}><span>{d.title}</span></div>
<p className={styles.recDesc}>{d.description}</p> <p className={styles.recDesc}>{d.description}</p>
<div className={styles.foodTags}> <div className={styles.foodTags}>
{d.recommendedFoods.slice(0, 3).map((f, j) => ( {d.recommendedFoods.slice(0, 3).map((f, j) => (
@@ -143,7 +143,7 @@ export function ExerciseDietPage() {
{diets.length === 0 ? <Empty message="暂无饮食记录" /> : diets.slice(0, 10).map((d) => ( {diets.length === 0 ? <Empty message="暂无饮食记录" /> : diets.slice(0, 10).map((d) => (
<Card key={d.id} className={styles.logCard}> <Card key={d.id} className={styles.logCard}>
<div>{d.mealType === 'breakfast' ? '🌅' : d.mealType === 'lunch' ? '🌞' : d.mealType === 'dinner' ? '🌙' : '🍪'} {d.foods.map((f) => f.name).join(', ')}</div> <div>{d.foods.map((f) => f.name).join(', ')}</div>
<div className={styles.logDate}>{d.totalCalories}kcal · {formatDate(d.date, 'MM-DD')}</div> <div className={styles.logDate}>{d.totalCalories}kcal · {formatDate(d.date, 'MM-DD')}</div>
</Card> </Card>
))} ))}

View File

@@ -1,7 +1,7 @@
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 10px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -9,20 +9,30 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
padding: 20px 12px; padding: 18px 12px;
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: transform 0.15s; transition: transform 0.2s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
position: relative;
overflow: hidden;
} }
.card:active { transform: scale(0.96); } .card:active { transform: scale(0.95); }
.cardIcon { font-size: 32px; line-height: 1; } .cardIcon {
.cardTitle { font-size: var(--font-size-base); font-weight: 600; } width: 50px;
.cardDesc { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } height: 50px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.cardTitle { font-size: var(--font-size-base); font-weight: 700; color: var(--color-text-primary); }
.cardDesc { font-size: 11px; color: var(--color-text-tertiary); }
.extraLinks { .extraLinks {
display: flex; display: flex;
@@ -36,10 +46,12 @@
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-md); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 600;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
transition: background 0.15s;
} }
.linkCard:active { background: var(--color-bg); } .linkCard:active { background: #FAFBFC; }

View File

@@ -1,8 +1,121 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { MEASUREMENT_TYPES } from '@/utils/constants';
import styles from './HealthHubPage.module.css'; import styles from './HealthHubPage.module.css';
const HEALTH_ITEMS = [
{
path: '/health/records?type=blood_pressure',
label: '血压',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
bg: '#FEE9E9',
},
{
path: '/health/records?type=heart_rate',
label: '心率',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
),
bg: '#FFF4E5',
},
{
path: '/health/records?type=blood_sugar',
label: '血糖',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#845EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
),
bg: '#F3E8FF',
},
{
path: '/health/records?type=spo2',
label: '血氧',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#339AF0" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
bg: '#E6F0FF',
},
{
path: '/health/records?type=weight',
label: '体重',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#20C997" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20V10" />
<path d="M18 20V4" />
<path d="M6 20v-4" />
</svg>
),
bg: '#E6F9F2',
},
{
path: '/health/records?type=steps',
label: '步数',
desc: '记录和趋势',
svg: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#6366F1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M13 4h3l3 7-4 2v7H9v-7l-4-2 3-7h3" />
<circle cx="12" cy="4" r="2" />
</svg>
),
bg: '#EEF2FF',
},
];
const QUICK_LINKS = [
{
label: '健康日历',
path: '/health/calendar',
svg: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" 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>
),
bg: '#FFF0E0',
},
{
label: '服药管理',
path: '/health/medications',
svg: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="5" width="16" height="14" rx="4" />
<path d="M10 9v6M14 9v6M8 12h8" />
</svg>
),
bg: '#FFF4E5',
},
{
label: '运动饮食',
path: '/health/exercise-diet',
svg: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#20C997" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
),
bg: '#E6F9F2',
},
];
export function HealthHubPage() { export function HealthHubPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -10,66 +123,24 @@ export function HealthHubPage() {
<div className="page"> <div className="page">
<PageHeader title="健康中心" showBack={false} /> <PageHeader title="健康中心" showBack={false} />
<div className={styles.grid}> <div className={styles.grid}>
<button {HEALTH_ITEMS.map((item) => (
className={styles.card} <button key={item.path} className={styles.card} onClick={() => navigate(item.path)}>
onClick={() => navigate('/health/records?type=blood_pressure')} <span className={styles.cardIcon} style={{ background: item.bg }}>{item.svg}</span>
> <span className={styles.cardTitle}>{item.label}</span>
<span className={styles.cardIcon}>💓</span> <span className={styles.cardDesc}>{item.desc}</span>
<span className={styles.cardTitle}></span> </button>
<span className={styles.cardDesc}></span> ))}
</button>
<button
className={styles.card}
onClick={() => navigate('/health/records?type=heart_rate')}
>
<span className={styles.cardIcon}></span>
<span className={styles.cardTitle}></span>
<span className={styles.cardDesc}></span>
</button>
<button
className={styles.card}
onClick={() => navigate('/health/records?type=blood_sugar')}
>
<span className={styles.cardIcon}>🩸</span>
<span className={styles.cardTitle}></span>
<span className={styles.cardDesc}></span>
</button>
<button
className={styles.card}
onClick={() => navigate('/health/records?type=spo2')}
>
<span className={styles.cardIcon}>🫁</span>
<span className={styles.cardTitle}></span>
<span className={styles.cardDesc}></span>
</button>
<button
className={styles.card}
onClick={() => navigate('/health/records?type=weight')}
>
<span className={styles.cardIcon}></span>
<span className={styles.cardTitle}></span>
<span className={styles.cardDesc}></span>
</button>
<button
className={styles.card}
onClick={() => navigate('/health/records?type=steps')}
>
<span className={styles.cardIcon}>🚶</span>
<span className={styles.cardTitle}></span>
<span className={styles.cardDesc}></span>
</button>
</div> </div>
<div className={styles.extraLinks}> <div className={styles.extraLinks}>
<button className={styles.linkCard} onClick={() => navigate('/health/calendar')}> {QUICK_LINKS.map((link) => (
📅 <button key={link.path} className={styles.linkCard} onClick={() => navigate(link.path)}>
</button> <span className={styles.cardIcon} style={{ background: link.bg, width: 40, height: 40, borderRadius: 12 }}>
<button className={styles.linkCard} onClick={() => navigate('/health/medications')}> {link.svg}
💊 </span>
</button> {link.label}
<button className={styles.linkCard} onClick={() => navigate('/health/exercise-diet')}> </button>
🏃 ))}
</button>
</div> </div>
</div> </div>
); );

View File

@@ -3,11 +3,12 @@
width: 100%; width: 100%;
padding: 12px; padding: 12px;
margin-bottom: 10px; margin-bottom: 10px;
background: var(--color-primary); background: var(--color-primary-gradient);
color: var(--color-text-inverse); color: var(--color-text-inverse);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 500; font-weight: 600;
box-shadow: 0 4px 14px rgba(79,110,247,0.3);
} }
.chartBtn { .chartBtn {
@@ -20,6 +21,7 @@
border: 1.5px solid var(--color-primary); border: 1.5px solid var(--color-primary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 600;
} }
.recordCard { .recordCard {
@@ -28,7 +30,7 @@
.recordValue { .recordValue {
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
font-weight: 700; font-weight: 800;
color: var(--color-text-primary); color: var(--color-text-primary);
margin-bottom: 6px; margin-bottom: 6px;
} }

View File

@@ -38,7 +38,7 @@ export function HealthRecordListPage() {
</button> </button>
{records.length === 0 ? ( {records.length === 0 ? (
<Empty icon={config.icon} message={`暂无${config.label}记录`} /> <Empty message={`暂无${config.label}记录`} />
) : ( ) : (
records.map((r) => ( records.map((r) => (
<Card key={r.id} className={styles.recordCard}> <Card key={r.id} className={styles.recordCard}>
@@ -51,7 +51,7 @@ export function HealthRecordListPage() {
<div className={styles.recordMeta}> <div className={styles.recordMeta}>
<span>{formatDate(r.recordedAt, 'MM-DD HH:mm')}</span> <span>{formatDate(r.recordedAt, 'MM-DD HH:mm')}</span>
<span className={styles.source}> <span className={styles.source}>
{r.source === 'device' ? '📡 设备' : '手动'} {r.source === 'device' ? '设备' : '手动'}
</span> </span>
</div> </div>
</Card> </Card>

View File

@@ -36,11 +36,11 @@ export function DeviceBindingPage() {
<div className="page--no-tab"> <div className="page--no-tab">
<PageHeader title="设备管理" /> <PageHeader title="设备管理" />
<Button fullWidth loading={scanning} onClick={handleScan} style={{ marginBottom: 16 }}> <Button fullWidth loading={scanning} onClick={handleScan} style={{ marginBottom: 16 }}>
{scanning ? '搜索中...' : '🔍 扫描附近设备'} {scanning ? '搜索中...' : '扫描附近设备'}
</Button> </Button>
{devices.length === 0 ? ( {devices.length === 0 ? (
<Empty icon="📡" message="暂无已绑定设备" /> <Empty message="暂无已绑定设备" />
) : ( ) : (
devices.map((d) => ( devices.map((d) => (
<Card key={d.id} className={styles.deviceCard}> <Card key={d.id} className={styles.deviceCard}>
@@ -51,7 +51,7 @@ export function DeviceBindingPage() {
<span className={`${styles.status} ${d.status === 'connected' ? styles.connected : styles.disconnected}`}> <span className={`${styles.status} ${d.status === 'connected' ? styles.connected : styles.disconnected}`}>
{d.status === 'connected' ? '已连接' : '未连接'} {d.status === 'connected' ? '已连接' : '未连接'}
</span> </span>
<span className={styles.battery}>🔋 {d.batteryLevel}%</span> <span className={styles.battery}> {d.batteryLevel}%</span>
</div> </div>
</div> </div>
<Button <Button

View File

@@ -7,132 +7,171 @@
.greetingText { .greetingText {
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 800;
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.notifyBtn { .notifyBtn {
position: relative; position: relative;
font-size: 20px; width: 44px;
padding: 4px; height: 44px;
min-width: 44px;
min-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--color-white); background: var(--color-white);
border-radius: 50%; border-radius: 14px;
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-sm);
border: 1px solid var(--color-divider);
} }
.notifyBadge { .notifyBadge {
position: absolute; position: absolute;
top: -2px; top: 6px;
right: -2px; right: 6px;
min-width: 18px; width: 8px;
height: 18px; height: 8px;
padding: 0 5px; border-radius: 50%;
border-radius: 9px; background: var(--color-accent-red);
background: #EF4444; border: 2px solid #fff;
color: #fff;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
} }
.overviewCard { .overviewCard {
margin-bottom: 16px; margin-bottom: 16px;
background: linear-gradient(135deg, #2563EB 0%, #3B82F6 40%, #5B9AFF 100%); background: linear-gradient(145deg, #3A54E8 0%, #5B74F7 30%, #7D9AFF 100%);
color: #fff; color: #fff;
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
padding: 16px 14px; padding: 20px 16px;
overflow: hidden; overflow: hidden;
position: relative;
box-shadow: 0 8px 25px rgba(58,84,232,0.3);
}
.overviewCard::before {
content: '';
position: absolute;
width: 160px;
height: 160px;
border-radius: 50%;
background: rgba(255,255,255,0.05);
top: -40px;
right: -40px;
}
.overviewCard::after {
content: '';
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(255,255,255,0.04);
bottom: -30px;
left: -30px;
} }
.overviewHeader { .overviewHeader {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 14px; margin-bottom: 18px;
position: relative;
z-index: 1;
} }
.overviewTitle { .overviewTitle {
font-size: var(--font-size-md); font-size: var(--font-size-md);
font-weight: 600; font-weight: 600;
opacity: 0.9; opacity: 0.95;
} }
.overviewTime { .overviewTime {
font-size: var(--font-size-xs); font-size: 11px;
opacity: 0.7; opacity: 0.65;
background: rgba(255,255,255,0.15);
padding: 4px 10px;
border-radius: 12px;
} }
.overviewData { .overviewData {
display: flex; display: flex;
align-items: center; align-items: stretch;
gap: 6px; position: relative;
z-index: 1;
} }
.bpSection, .dataCol {
.hrSection {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
gap: 2px; gap: 2px;
padding: 2px 0;
}
.dataCol:first-child {
flex: 1.35;
} }
.dataLabel { .dataLabel {
font-size: 10px; font-size: 10px;
opacity: 0.7; opacity: 0.65;
font-weight: 500;
letter-spacing: 0.5px;
} }
.bpValues { .bpValues {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 2px; gap: 1px;
} }
.bpNum { .bpNum {
font-size: 24px; font-size: 22px;
font-weight: 700; font-weight: 800;
line-height: 1.1; line-height: 1.15;
color: #fff; color: #fff;
white-space: nowrap; white-space: nowrap;
letter-spacing: -1px;
} }
.bpSep { .bpSep {
font-size: var(--font-size-md); font-size: 14px;
opacity: 0.5; opacity: 0.35;
margin: 0 -1px;
} }
.hrNum { .hrNum {
font-size: 24px; font-size: 22px;
font-weight: 700; font-weight: 800;
line-height: 1.1; line-height: 1.15;
color: #fff;
white-space: nowrap; white-space: nowrap;
letter-spacing: -0.5px;
} }
.unit { .unit {
font-size: 10px; font-size: 10px;
opacity: 0.6; opacity: 0.5;
font-weight: 500;
} }
.divider { .divider {
width: 1px; width: 1px;
height: 48px; background: rgba(255,255,255,0.18);
background: rgba(255,255,255,0.2);
flex-shrink: 0; flex-shrink: 0;
align-self: stretch;
margin: 6px 0;
}
.riskAbnormal {
color: #FF7171 !important;
} }
/* Quick Actions */ /* Quick Actions */
.quickActions { .quickActions {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 10px; gap: 10px;
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -142,35 +181,42 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 14px 8px; padding: 14px 4px;
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: all 0.15s; transition: all 0.2s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.quickAction:active { .quickAction:active {
transform: scale(0.96); transform: scale(0.94);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.quickIcon { .quickIcon {
font-size: 28px; width: 46px;
height: 46px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
line-height: 1; line-height: 1;
} }
.quickLabel { .quickLabel {
font-size: var(--font-size-xs); font-size: 11px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500; font-weight: 600;
} }
/* Health Tip */ /* Health Tip */
.tipCard { .tipCard {
margin-bottom: 16px; margin-bottom: 16px;
background: linear-gradient(135deg, #FFFBEB, #FFF7ED); background: linear-gradient(135deg, #FFFDF5, #FFF8EC);
border: 1px solid #FDE68A; border: 1px solid #FDE8B3;
border-radius: var(--radius-lg);
} }
.tipHeader { .tipHeader {
@@ -182,18 +228,19 @@
.tipTitle { .tipTitle {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 600; font-weight: 700;
color: #92400E; color: #B7791F;
} }
.tipHint { .tipHint {
margin-left: auto; margin-left: auto;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-text-tertiary); color: #D69E2E;
font-weight: 500;
} }
.tipContent { .tipContent {
font-size: var(--font-size-sm); font-size: 13px;
color: #78350F; color: #7B3F00;
line-height: 1.6; line-height: 1.6;
} }

View File

@@ -4,17 +4,112 @@ import { Card } from '@/components/common/Card';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useNotificationStore } from '@/stores/notification.store'; import { useNotificationStore } from '@/stores/notification.store';
import * as healthService from '@/services/health.service'; import * as healthService from '@/services/health.service';
import { getBPRiskLevel } from '@/utils/format';
import type { HealthStats } from '@/types'; import type { HealthStats } from '@/types';
import styles from './HomePage.module.css'; import styles from './HomePage.module.css';
const QUICK_ACTIONS = [ const QUICK_ACTIONS = [
{ key: 'bp', label: '血压', icon: '💓', path: '/health/records?type=blood_pressure' }, {
{ key: 'med', label: '用药', icon: '💊', path: '/health/medications' }, key: 'bp',
{ key: 'chat', label: '问诊', icon: '💬', path: '/services/consultation' }, label: '血压',
{ key: 'report', label: '报告', icon: '📋', path: '/services/reports' }, path: '/health/records?type=blood_pressure',
{ key: 'calendar', label: '日历', icon: '📅', path: '/health/calendar' }, svg: (
{ key: 'followup', label: '复查', icon: '🏥', path: '/services/follow-ups' }, <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
iconBg: '#FEE9E9',
},
{
key: 'med',
label: '用药',
path: '/health/medications',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="5" width="16" height="14" rx="4" />
<path d="M10 9v6M14 9v6M8 12h8" />
</svg>
),
iconBg: '#FFF4E5',
},
{
key: 'chat',
label: '问诊',
path: '/services/consultation',
svg: (
<svg width="24" height="24" 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" />
<line x1="9" y1="10" x2="15" y2="10" />
<line x1="12" y1="7" x2="12" y2="13" />
</svg>
),
iconBg: '#E6F0FF',
},
{
key: 'report',
label: '报告',
path: '/services/reports',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#845EF7" 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>
),
iconBg: '#F3E8FF',
},
{
key: 'calendar',
label: '日历',
path: '/health/calendar',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#20C997" 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>
),
iconBg: '#E6F9F2',
},
{
key: 'followup',
label: '复查',
path: '/services/follow-ups',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F06595" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
iconBg: '#FFF0F5',
},
{
key: 'diet',
label: '饮食',
path: '/health/exercise-diet',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#10B981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
),
iconBg: '#F0FDF4',
},
{
key: 'device',
label: '设备',
path: '/home/device-binding',
svg: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#339AF0" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
iconBg: '#EFF6FF',
},
]; ];
export function HomePage() { export function HomePage() {
@@ -36,65 +131,80 @@ export function HomePage() {
const bpValue = bpStats?.latest?.value; const bpValue = bpStats?.latest?.value;
const systolic = typeof bpValue === 'object' ? bpValue.systolic : null; const systolic = typeof bpValue === 'object' ? bpValue.systolic : null;
const diastolic = typeof bpValue === 'object' ? bpValue.diastolic : null; const diastolic = typeof bpValue === 'object' ? bpValue.diastolic : null;
const riskLevel = systolic && diastolic ? getBPRiskLevel(systolic, diastolic) : null;
const bpAbnormal = systolic !== null && diastolic !== null
&& (systolic >= 120 || diastolic >= 80);
const hrValue = hrStats?.latest ? Number(hrStats.latest.value) : null;
const hrAbnormal = hrValue !== null && (hrValue < 60 || hrValue > 100);
const sugarValue = sugarStats?.latest ? Number(sugarStats.latest.value) : null;
const sugarAbnormal = sugarValue !== null && (sugarValue < 3.9 || sugarValue > 6.1);
const spo2Value = spo2Stats?.latest ? Number(spo2Stats.latest.value) : null;
const spo2Abnormal = spo2Value !== null && spo2Value < 95;
return ( return (
<div className="page" style={{ paddingTop: 0 }}> <div className="page" style={{ paddingTop: 0 }}>
<div className={styles.greetingBar}> <div className={styles.greetingBar}>
<div className={styles.greetingText}>{user?.nickname || '用户'}</div> <div className={styles.greetingText}>{user?.nickname || '用户'}</div>
<button onClick={() => navigate('/notifications')} className={styles.notifyBtn}> <button onClick={() => navigate('/notifications')} className={styles.notifyBtn}>
🔔{unreadCount > 0 && <span className={styles.notifyBadge}>{unreadCount > 99 ? '99+' : unreadCount}</span>} <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
{unreadCount > 0 && <span className={styles.notifyBadge} />}
</button> </button>
</div> </div>
{/* Health Overview */}
<Card className={styles.overviewCard}> <Card className={styles.overviewCard}>
<div className={styles.overviewHeader}> <div className={styles.overviewHeader}>
<span className={styles.overviewTitle}></span> <span className={styles.overviewTitle}></span>
<span className={styles.overviewTime}></span> <span className={styles.overviewTime}></span>
</div> </div>
<div className={styles.overviewData}> <div className={styles.overviewData}>
<div className={styles.bpSection}> <div className={styles.dataCol}>
<span className={styles.dataLabel}></span> <span className={styles.dataLabel}></span>
{systolic ? ( {systolic ? (
<div className={styles.bpValues}> <div className={styles.bpValues}>
<span className={`${styles.bpNum} ${riskLevel === 'normal' ? '' : riskLevel === 'borderline' ? styles.riskBp : styles.riskAbnormal}`}> <span className={`${styles.bpNum} ${bpAbnormal ? styles.riskAbnormal : ''}`}>
{systolic} {systolic}
</span> </span>
<span className={styles.bpSep}>/</span> <span className={styles.bpSep}>/</span>
<span className={`${styles.bpNum} ${riskLevel === 'normal' ? '' : riskLevel === 'borderline' ? styles.riskBp : styles.riskAbnormal}`}> <span className={`${styles.bpNum} ${bpAbnormal ? styles.riskAbnormal : ''}`}>
{diastolic} {diastolic}
</span> </span>
</div> </div>
) : <span className={styles.bpNum} style={{ fontSize: 28, opacity: 0.4 }}>--/--</span>} ) : <span className={styles.bpNum} style={{ fontSize: 22, opacity: 0.4 }}>--/--</span>}
<span className={styles.unit}>mmHg</span> <span className={styles.unit}>mmHg</span>
</div> </div>
<div className={styles.divider} /> <div className={styles.divider} />
<div className={styles.hrSection}> <div className={styles.dataCol}>
<span className={styles.dataLabel}></span> <span className={styles.dataLabel}></span>
<span className={styles.hrNum}>{hrStats?.latest ? Number(hrStats.latest.value) : '--'}</span> <span className={`${styles.hrNum} ${hrAbnormal ? styles.riskAbnormal : ''}`}>{hrValue ?? '--'}</span>
<span className={styles.unit}>bpm</span> <span className={styles.unit}>bpm</span>
</div> </div>
<div className={styles.divider} /> <div className={styles.divider} />
<div className={styles.hrSection}> <div className={styles.dataCol}>
<span className={styles.dataLabel}></span> <span className={styles.dataLabel}></span>
<span className={styles.hrNum}>{sugarStats?.latest ? Number(sugarStats.latest.value) : '--'}</span> <span className={`${styles.hrNum} ${sugarAbnormal ? styles.riskAbnormal : ''}`}>{sugarValue ?? '--'}</span>
<span className={styles.unit}>mmol/L</span> <span className={styles.unit}>mmol/L</span>
</div> </div>
<div className={styles.divider} /> <div className={styles.divider} />
<div className={styles.hrSection}> <div className={styles.dataCol}>
<span className={styles.dataLabel}></span> <span className={styles.dataLabel}></span>
<span className={styles.hrNum}>{spo2Stats?.latest ? Number(spo2Stats.latest.value) : '--'}</span> <span className={`${styles.hrNum} ${spo2Abnormal ? styles.riskAbnormal : ''}`}>{spo2Value ?? '--'}</span>
<span className={styles.unit}>%</span> <span className={styles.unit}>%</span>
</div> </div>
</div> </div>
</Card> </Card>
{/* Quick Actions */}
<div className={styles.quickActions}> <div className={styles.quickActions}>
{QUICK_ACTIONS.map((action) => ( {QUICK_ACTIONS.map((action) => (
<button key={action.key} className={styles.quickAction} onClick={() => navigate(action.path)}> <button key={action.key} className={styles.quickAction} onClick={() => navigate(action.path)}>
<span className={styles.quickIcon}>{action.icon}</span> <span className={styles.quickIcon} style={{ background: action.iconBg }}>
{action.svg}
</span>
<span className={styles.quickLabel}>{action.label}</span> <span className={styles.quickLabel}>{action.label}</span>
</button> </button>
))} ))}

View File

@@ -7,11 +7,11 @@
justify-content: space-between; justify-content: space-between;
padding: 8px 0; padding: 8px 0;
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
border-bottom: 1px solid var(--color-border-light); border-bottom: 1px solid var(--color-divider);
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.activeBadge { color: var(--color-success); font-weight: 500; } .activeBadge { color: var(--color-success); font-weight: 600; }
.adherenceCard { text-align: center; } .adherenceCard { text-align: center; }
@@ -19,7 +19,7 @@
.adherenceRate { .adherenceRate {
font-size: var(--font-size-3xl); font-size: var(--font-size-3xl);
font-weight: 700; font-weight: 800;
color: var(--color-success); color: var(--color-success);
margin-bottom: 4px; margin-bottom: 4px;
} }

View File

@@ -122,7 +122,7 @@ export function MedicationDetailPage() {
})} })}
</div> </div>
<div style={{ display: 'flex', gap: 12, marginTop: 8, fontSize: 11, color: '#9CA3AF' }}> <div style={{ display: 'flex', gap: 12, marginTop: 8, fontSize: 11, color: '#9CA3AF' }}>
<span>🟢 </span><span>🟡 </span><span> </span> <span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#20C997',display:'inline-block'}}/> </span><span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#F59E0B',display:'inline-block'}}/> </span><span style={{display:'inline-flex',alignItems:'center',gap:4}}><span style={{width:10,height:10,borderRadius:'50%',background:'#E4E8EE',display:'inline-block'}}/> </span>
</div> </div>
</Card> </Card>
<ToastContainer /> <ToastContainer />

View File

@@ -6,7 +6,7 @@
.sectionLabel { .sectionLabel {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 500; font-weight: 600;
color: var(--color-text-secondary); color: var(--color-text-secondary);
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;

View File

@@ -70,7 +70,7 @@ export function MedicationEditPage() {
<input type="time" value={slot} onChange={(e) => updateTimeSlot(i, e.target.value)} <input type="time" value={slot} onChange={(e) => updateTimeSlot(i, e.target.value)}
style={{ flex: 1, padding: '8px 12px', border: '1px solid #ddd', borderRadius: 8, fontSize: 14, fontFamily: 'inherit' }} /> style={{ flex: 1, padding: '8px 12px', border: '1px solid #ddd', borderRadius: 8, fontSize: 14, fontFamily: 'inherit' }} />
<button onClick={() => removeTimeSlot(i)} disabled={timeSlots.length <= 1} <button onClick={() => removeTimeSlot(i)} disabled={timeSlots.length <= 1}
style={{ background: 'none', border: 'none', color: '#EF4444', fontSize: 18, cursor: 'pointer', padding: 4 }}></button> style={{ background: 'none', border: 'none', color: '#EF4444', fontSize: 18, cursor: 'pointer', padding: 4, fontWeight: 700 }}>×</button>
</div> </div>
))} ))}
<button onClick={addTimeSlot} style={{ padding: '6px 14px', border: '1px dashed #2563EB', borderRadius: 8, background: 'none', color: '#2563EB', fontSize: 13, cursor: 'pointer' }}> <button onClick={addTimeSlot} style={{ padding: '6px 14px', border: '1px dashed #2563EB', borderRadius: 8, background: 'none', color: '#2563EB', fontSize: 13, cursor: 'pointer' }}>

View File

@@ -10,6 +10,7 @@
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500;
} }
.tabActive { .tabActive {
@@ -35,10 +36,10 @@
bottom: 80px; bottom: 80px;
right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px)); right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px));
padding: 12px 20px; padding: 12px 20px;
background: var(--color-primary); background: var(--color-primary-gradient);
color: var(--color-text-inverse); color: var(--color-text-inverse);
border-radius: var(--radius-full); border-radius: var(--radius-full);
font-weight: 600; font-weight: 600;
box-shadow: var(--shadow-lg); box-shadow: 0 4px 16px rgba(79,110,247,0.35);
z-index: 50; z-index: 50;
} }

View File

@@ -1,83 +1,53 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { Card } from '@/components/common/Card'; import { Card } from '@/components/common/Card';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import * as medicationService from '@/services/medication.service'; import * as medicationService from '@/services/medication.service';
import type { Medication, MedicationRecord } from '@/types'; import type { Medication } from '@/types';
import styles from './MedicationListPage.module.css'; import styles from './MedicationListPage.module.css';
import { useNavigate } from 'react-router-dom';
export function MedicationListPage() { export function MedicationListPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [medications, setMedications] = useState<Medication[]>([]); const [medications, setMedications] = useState<Medication[]>([]);
const [takenMap, setTakenMap] = useState<Record<string, boolean>>({}); const [tab, setTab] = useState<'active' | 'ended'>('active');
const [tab, setTab] = useState<'active' | 'completed'>('active');
useEffect(() => { useEffect(() => {
medicationService.getMedications().then(async (meds) => { medicationService.getMedications().then(setMedications);
const today = new Date().toISOString().split('T')[0];
// Auto-expire: check endDate
const updated = meds.map((m) => {
if (m.status === 'active' && m.endDate && m.endDate < today) {
return { ...m, status: 'completed' as const };
}
return m;
});
setMedications(updated);
// Check which meds have all slots taken today
const map: Record<string, boolean> = {};
for (const med of updated) {
if (med.status !== 'active') continue;
try {
const records = await medicationService.getMedicationRecords(med.id);
const todayRecords = records.filter((r) => r.takenAt?.startsWith(today) && r.isTaken);
map[med.id] = todayRecords.length >= med.timeSlots.length;
} catch { map[med.id] = true; }
}
setTakenMap(map);
});
}, []); }, []);
const filtered = medications.filter((m) => const filtered = medications.filter((m) => tab === 'active' ? m.status === 'active' : m.status === 'ended');
tab === 'active' ? m.status === 'active' : m.status === 'completed',
); const allTaken = (med: Medication) => med.records?.every((r) => r.taken);
return ( return (
<div className="page--no-tab"> <div className="page--no-tab">
<PageHeader title="服药管理" /> <PageHeader title="我的用药" />
<div className={styles.tabs}> <div className={styles.tabs}>
<button className={`${styles.tab} ${tab === 'active' ? styles.tabActive : ''}`} onClick={() => setTab('active')}></button> <button className={`${styles.tab} ${tab === 'active' ? styles.tabActive : ''}`} onClick={() => setTab('active')}></button>
<button className={`${styles.tab} ${tab === 'completed' ? styles.tabActive : ''}`} onClick={() => setTab('completed')}></button> <button className={`${styles.tab} ${tab === 'ended' ? styles.tabActive : ''}`} onClick={() => setTab('ended')}></button>
</div> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Empty icon="💊" message={tab === 'active' ? '暂无进行中的用药' : '暂无已结束的用药'} /> <Empty message={tab === 'active' ? '暂无进行中的用药' : '暂无已结束的用药'} />
) : ( ) : (
filtered.map((med) => { filtered.map((med) => (
const allTaken = takenMap[med.id]; <Card key={med.id} className={styles.medCard} onClick={() => navigate(`/health/medications/${med.id}`)}>
return ( <div className={styles.medHeader}>
<Card key={med.id} className={styles.medCard} onClick={() => navigate(`/health/medications/${med.id}`)}> <span className={styles.medName}>{med.drugName}</span>
<div className={styles.medHeader}> {med.status === 'active' && allTaken(med) && (
<span className={styles.medName}>{med.drugName}</span> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10B981" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
{med.status === 'active' && !allTaken && <span className={styles.unreadDot} />} <polyline points="20 6 9 17 4 12" />
{med.status === 'active' && allTaken && <span style={{ fontSize: 11, color: '#10B981' }}></span>} </svg>
</div>
<div className={styles.medDosage}>{med.dosage} · {med.frequency} · {med.timeSlots.join(', ')}</div>
{med.notes && <div className={styles.medNote}>{med.notes}</div>}
{med.status === 'active' && med.endDate && (
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 4 }}>
{med.endDate}
</div>
)} )}
</Card> </div>
); <div className={styles.medDosage}>{med.dosage} · {med.frequency}</div>
}) {med.note && <div className={styles.medNote}>{med.note}</div>}
</Card>
))
)} )}
<button className={styles.fab} onClick={() => navigate('/health/medications/add')}> <button className={styles.fab} onClick={() => navigate('/health/medications/add')}>+ </button>
+
</button>
</div> </div>
); );
} }

View File

@@ -76,7 +76,7 @@ export function NotificationListPage() {
</div> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Empty icon="🔔" message="暂无通知" /> <Empty message="暂无通知" />
) : ( ) : (
filtered.map((n) => ( filtered.map((n) => (
<Card <Card

View File

@@ -3,38 +3,49 @@
align-items: center; align-items: center;
gap: 16px; gap: 16px;
margin-bottom: 12px; margin-bottom: 12px;
background: var(--color-primary-gradient);
color: #fff;
border-radius: var(--radius-xl);
padding: 20px;
box-shadow: 0 6px 24px rgba(79,110,247,0.3);
} }
.avatar { .avatar {
width: 56px; height: 56px; width: 56px;
border-radius: 50%; height: 56px;
background: var(--color-primary-bg); border-radius: 18px;
color: var(--color-primary); background: rgba(255,255,255,0.25);
color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
font-weight: 700; font-weight: 800;
backdrop-filter: blur(4px);
} }
.nickname { font-size: var(--font-size-lg); font-weight: 600; } .profileInfo { flex: 1; }
.phone { font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin-top: 2px; }
.nickname { font-size: var(--font-size-lg); font-weight: 700; }
.phone { font-size: 12px; opacity: 0.7; margin-top: 2px; }
.editHint { color: rgba(255,255,255,0.8); }
.statsCard { .statsCard {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px 0;
} }
.stat { text-align: center; } .stat { text-align: center; }
.statValue { font-size: var(--font-size-sm); font-weight: 600; display: block; } .statValue { font-size: 18px; font-weight: 800; display: block; color: var(--color-text-primary); }
.statLabel { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } .statLabel { font-size: 11px; color: var(--color-text-tertiary); font-weight: 500; }
.statDivider { width: 1px; height: 32px; background: var(--color-border); } .statDivider { width: 1px; height: 32px; background: var(--color-divider); }
.menuList { .menuList {
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-lg); border-radius: var(--radius-xl);
overflow: hidden; overflow: hidden;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
margin-bottom: 16px; margin-bottom: 16px;
@@ -44,14 +55,16 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 14px 16px; padding: 15px 18px;
width: 100%; width: 100%;
font-size: var(--font-size-base); font-size: var(--font-size-base);
border-bottom: 1px solid var(--color-border-light); font-weight: 500;
border-bottom: 1px solid var(--color-divider);
transition: background 0.15s;
} }
.menuItem:last-child { border-bottom: none; } .menuItem:last-child { border-bottom: none; }
.menuItem:active { background: var(--color-bg); } .menuItem:active { background: #FAFBFC; }
.menuRight { display: flex; align-items: center; gap: 8px; } .menuRight { display: flex; align-items: center; gap: 8px; }
@@ -61,7 +74,11 @@
padding: 14px; padding: 14px;
background: var(--color-white); background: var(--color-white);
color: var(--color-danger); color: var(--color-danger);
border-radius: var(--radius-lg); border-radius: var(--radius-xl);
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 600;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
border: 1.5px solid #FDD;
} }
.logoutBtn:active { background: #FFF5F5; }

View File

@@ -49,26 +49,57 @@ export function ProfilePage() {
<div className={styles.menuList}> <div className={styles.menuList}>
<button className={styles.menuItem} onClick={() => navigate('/health/medications')}> <button className={styles.menuItem} onClick={() => navigate('/health/medications')}>
<span>💊 </span> <span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#D67E0B" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: 'middle', marginRight: 10 }}>
<rect x="4" y="5" width="16" height="14" rx="4" />
<path d="M10 9v6M14 9v6M8 12h8" />
</svg>
</span>
<span></span> <span></span>
</button> </button>
<button className={styles.menuItem} onClick={() => navigate('/notifications')}> <button className={styles.menuItem} onClick={() => navigate('/notifications')}>
<span>🔔 </span> <span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: 'middle', marginRight: 10 }}>
<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>
<div className={styles.menuRight}> <div className={styles.menuRight}>
{unreadCount > 0 && <Badge count={unreadCount} />} {unreadCount > 0 && <Badge count={unreadCount} />}
<span></span> <span></span>
</div> </div>
</button> </button>
<button className={styles.menuItem} onClick={() => navigate('/home/device-binding')}> <button className={styles.menuItem} onClick={() => navigate('/home/device-binding')}>
<span>📡 </span> <span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6366F1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: 'middle', marginRight: 10 }}>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</svg>
</span>
<span></span> <span></span>
</button> </button>
<button className={styles.menuItem} onClick={() => navigate('/profile/settings')}> <button className={styles.menuItem} onClick={() => navigate('/profile/settings')}>
<span> </span> <span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#845EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: 'middle', marginRight: 10 }}>
<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>
</span>
<span></span> <span></span>
</button> </button>
<button className={styles.menuItem} onClick={() => navigate('/profile/settings/about')}> <button className={styles.menuItem} onClick={() => navigate('/profile/settings/about')}>
<span> </span> <span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#20C997" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: 'middle', marginRight: 10 }}>
<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>
</span>
<span></span> <span></span>
</button> </button>
</div> </div>

View File

@@ -46,7 +46,11 @@ export function AboutPage() {
<div className="page--no-tab"> <div className="page--no-tab">
<PageHeader title="关于" /> <PageHeader title="关于" />
<div style={{ textAlign: 'center', padding: '40px 20px' }}> <div style={{ textAlign: 'center', padding: '40px 20px' }}>
<div style={{ fontSize: 56 }}>💙</div> <div style={{ fontSize: 56 }}>
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="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>
</div>
<div style={{ fontSize: '18px', fontWeight: 700, marginTop: 12 }}> Demo</div> <div style={{ fontSize: '18px', fontWeight: 700, marginTop: 12 }}> Demo</div>
<div style={{ fontSize: '13px', color: '#9CA3AF', marginTop: 4 }}>v1.0.0-demo</div> <div style={{ fontSize: '13px', color: '#9CA3AF', marginTop: 4 }}>v1.0.0-demo</div>
<div style={{ fontSize: '13px', color: '#6B7280', marginTop: 16 }}> H5 Web Demo</div> <div style={{ fontSize: '13px', color: '#6B7280', marginTop: 16 }}> H5 Web Demo</div>

View File

@@ -17,24 +17,26 @@
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500;
} }
.active { background: var(--color-primary-bg); color: var(--color-primary); } .active { background: var(--color-primary-bg); color: var(--color-primary); font-weight: 600; }
.docCard { margin-bottom: 8px; } .docCard { margin-bottom: 8px; }
.docHeader { display: flex; gap: 12px; margin-bottom: 12px; } .docHeader { display: flex; gap: 12px; margin-bottom: 12px; }
.avatar { .avatar {
width: 48px; height: 48px; width: 48px;
border-radius: 50%; height: 48px;
border-radius: 16px;
background: var(--color-primary-bg); background: var(--color-primary-bg);
color: var(--color-primary); color: var(--color-primary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
font-weight: 600; font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -49,7 +51,8 @@
} }
.onlineDot { .onlineDot {
width: 8px; height: 8px; width: 8px;
height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--color-success); background: var(--color-success);
} }
@@ -63,7 +66,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid var(--color-border-light); border-top: 1px solid var(--color-divider);
} }
.fee { font-size: var(--font-size-sm); color: var(--color-danger); font-weight: 600; } .fee { font-size: var(--font-size-sm); color: var(--color-danger); font-weight: 600; }

View File

@@ -28,7 +28,7 @@ export function DoctorListPage() {
))} ))}
</div> </div>
{doctors.length === 0 ? ( {doctors.length === 0 ? (
<Empty icon="👨‍⚕️" message="暂无医生" /> <Empty message="暂无医生" />
) : ( ) : (
doctors.map((doc) => ( doctors.map((doc) => (
<Card key={doc.id} className={styles.docCard}> <Card key={doc.id} className={styles.docCard}>

View File

@@ -1,9 +1,9 @@
.tabs { display: flex; gap: 8px; margin-bottom: 14px; } .tabs { display: flex; gap: 8px; margin-bottom: 14px; }
.tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); } .tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); font-weight: 500; }
.tabActive { background: var(--color-primary); color: var(--color-text-inverse); } .tabActive { background: var(--color-primary); color: var(--color-text-inverse); }
.card { margin-bottom: 8px; } .card { margin-bottom: 8px; }
.cardHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .cardHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.title { font-size: var(--font-size-base); font-weight: 600; } .title { font-size: var(--font-size-base); font-weight: 600; }
.status { font-size: var(--font-size-xs); font-weight: 500; } .status { font-size: var(--font-size-xs); font-weight: 500; }
.meta { font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin-top: 2px; } .meta { font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin-top: 2px; }
.fab { position: fixed; bottom: 80px; right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px)); padding: 12px 20px; background: var(--color-primary); color: var(--color-text-inverse); border-radius: var(--radius-full); font-weight: 600; box-shadow: var(--shadow-lg); z-index: 50; } .fab { position: fixed; bottom: 80px; right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px)); padding: 12px 20px; background: var(--color-primary-gradient); color: var(--color-text-inverse); border-radius: var(--radius-full); font-weight: 600; box-shadow: 0 4px 16px rgba(79,110,247,0.35); z-index: 50; }

View File

@@ -33,7 +33,7 @@ export function FollowUpListPage() {
<button className={`${styles.tab} ${tab === 'completed' ? styles.tabActive : ''}`} onClick={() => setTab('completed')}></button> <button className={`${styles.tab} ${tab === 'completed' ? styles.tabActive : ''}`} onClick={() => setTab('completed')}></button>
</div> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Empty icon="🏥" message="暂无复查计划" /> <Empty message="暂无复查计划" />
) : ( ) : (
filtered.map((f) => ( filtered.map((f) => (
<Card key={f.id} className={styles.card}> <Card key={f.id} className={styles.card}>

View File

@@ -1,8 +1,8 @@
.tabs { display: flex; gap: 8px; margin-bottom: 14px; } .tabs { display: flex; gap: 8px; margin-bottom: 14px; }
.tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); } .tab { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); font-weight: 500; }
.tabActive { background: var(--color-primary); color: var(--color-text-inverse); } .tabActive { background: var(--color-primary); color: var(--color-text-inverse); }
.card { margin-bottom: 8px; } .card { margin-bottom: 8px; }
.cardHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .cardHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.cardTitle { font-size: var(--font-size-base); font-weight: 600; } .cardTitle { font-size: var(--font-size-base); font-weight: 600; }
.cardMeta { display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--color-text-tertiary); } .cardMeta { display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
.fab { position: fixed; bottom: 80px; right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px)); padding: 12px 20px; background: var(--color-primary); color: var(--color-text-inverse); border-radius: var(--radius-full); font-weight: 600; box-shadow: var(--shadow-lg); z-index: 50; } .fab { position: fixed; bottom: 80px; right: max(16px, calc((100vw - var(--max-content-width)) / 2 + 16px)); padding: 12px 20px; background: var(--color-primary-gradient); color: var(--color-text-inverse); border-radius: var(--radius-full); font-weight: 600; box-shadow: 0 4px 16px rgba(79,110,247,0.35); z-index: 50; }

View File

@@ -41,7 +41,7 @@ export function ReportListPage() {
))} ))}
</div> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Empty icon="📋" message="暂无报告" /> <Empty message="暂无报告" />
) : ( ) : (
filtered.map((r) => ( filtered.map((r) => (
<Card key={r.id} className={styles.card} onClick={() => navigate(`/services/reports/${r.id}`)}> <Card key={r.id} className={styles.card} onClick={() => navigate(`/services/reports/${r.id}`)}>

View File

@@ -74,7 +74,9 @@ export function ReportUploadPage() {
</div> </div>
<div className={styles.uploadArea} onClick={() => fileRef.current?.click()}> <div className={styles.uploadArea} onClick={() => fileRef.current?.click()}>
<span style={{ fontSize: 36 }}>📷</span> <div style={{ width: 72, height: 72, borderRadius: 18, background: 'var(--color-bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px' }}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#9BA0B4" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</div>
<span style={{ fontSize: 14, color: '#6B7280' }}></span> <span style={{ fontSize: 14, color: '#6B7280' }}></span>
<span style={{ fontSize: 11, color: '#9CA3AF' }}>{files.length > 0 ? `已选 ${files.length} 个文件` : '支持 jpg、png、pdf可多选'}</span> <span style={{ fontSize: 11, color: '#9CA3AF' }}>{files.length > 0 ? `已选 ${files.length} 个文件` : '支持 jpg、png、pdf可多选'}</span>
</div> </div>
@@ -85,9 +87,9 @@ export function ReportUploadPage() {
{files.map((file, i) => ( {files.map((file, i) => (
<div key={i} className={styles.fileItem}> <div key={i} className={styles.fileItem}>
<span className={styles.fileName}> <span className={styles.fileName}>
{file.type.startsWith('image/') ? '🖼' : '📄'} {file.name} ({(file.size / 1024).toFixed(0)}KB) {file.name} ({(file.size / 1024).toFixed(0)}KB)
</span> </span>
<button className={styles.fileRemove} onClick={() => removeFile(i)}></button> <button className={styles.fileRemove} onClick={() => removeFile(i)} style={{ fontWeight: 700 }}>×</button>
</div> </div>
))} ))}
</div> </div>

View File

@@ -6,18 +6,27 @@
.card { .card {
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: flex-start; gap: 16px;
gap: 4px; padding: 20px 18px;
padding: 20px;
background: var(--color-white); background: var(--color-white);
border-radius: var(--radius-lg); border-radius: var(--radius-xl);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: transform 0.15s; transition: transform 0.2s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.card:active { transform: scale(0.98); } .card:active { transform: scale(0.98); }
.icon { font-size: 36px; }
.label { font-size: var(--font-size-md); font-weight: 600; } .icon {
.desc { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } width: 52px;
height: 52px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.label { font-size: var(--font-size-md); font-weight: 700; color: var(--color-text-primary); margin-bottom: 4px; }
.desc { font-size: 12px; color: var(--color-text-tertiary); }

View File

@@ -3,9 +3,44 @@ import { PageHeader } from '@/components/layout/PageHeader';
import styles from './ServicesHubPage.module.css'; import styles from './ServicesHubPage.module.css';
const SERVICES = [ const SERVICES = [
{ label: '在线问诊', icon: '👨‍⚕️', desc: '图文咨询医生', path: '/services/consultation' }, {
{ label: '报告解读', icon: '📋', desc: '上传检查报告', path: '/services/reports' }, label: '在线问诊',
{ label: '复查管理', icon: '🏥', desc: '管理复查计划', path: '/services/follow-ups' }, desc: '图文咨询医生',
path: '/services/consultation',
svg: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#4F6EF7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
bg: '#E6F0FF',
},
{
label: '报告解读',
desc: '上传检查报告',
path: '/services/reports',
svg: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#845EF7" 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>
),
bg: '#F3E8FF',
},
{
label: '复查管理',
desc: '管理复查计划',
path: '/services/follow-ups',
svg: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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>
),
bg: '#FEE9E9',
},
]; ];
export function ServicesHubPage() { export function ServicesHubPage() {
@@ -17,9 +52,11 @@ export function ServicesHubPage() {
<div className={styles.grid}> <div className={styles.grid}>
{SERVICES.map((s) => ( {SERVICES.map((s) => (
<button key={s.label} className={styles.card} onClick={() => navigate(s.path)}> <button key={s.label} className={styles.card} onClick={() => navigate(s.path)}>
<span className={styles.icon}>{s.icon}</span> <span className={styles.icon} style={{ background: s.bg }}>{s.svg}</span>
<span className={styles.label}>{s.label}</span> <div>
<span className={styles.desc}>{s.desc}</span> <div className={styles.label}>{s.label}</div>
<div className={styles.desc}>{s.desc}</div>
</div>
</button> </button>
))} ))}
</div> </div>

View File

@@ -1,19 +1,19 @@
export const MEASUREMENT_TYPES = { export const MEASUREMENT_TYPES = {
blood_pressure: { label: '血压', unit: 'mmHg', icon: '💓', color: '#EF4444' }, blood_pressure: { label: '血压', unit: 'mmHg', color: '#EF4444' },
heart_rate: { label: '心率', unit: 'bpm', icon: '❤️', color: '#F59E0B' }, heart_rate: { label: '心率', unit: 'bpm', color: '#F59E0B' },
blood_sugar: { label: '血糖', unit: 'mmol/L', icon: '🩸', color: '#8B5CF6' }, blood_sugar: { label: '血糖', unit: 'mmol/L', color: '#8B5CF6' },
spo2: { label: '血氧', unit: '%', icon: '🫁', color: '#06B6D4' }, spo2: { label: '血氧', unit: '%', color: '#06B6D4' },
weight: { label: '体重', unit: 'kg', icon: '⚖️', color: '#10B981' }, weight: { label: '体重', unit: 'kg', color: '#10B981' },
steps: { label: '步数', unit: '步', icon: '🚶', color: '#6366F1' }, steps: { label: '步数', unit: '步', color: '#6366F1' },
} as const; } as const;
export type MeasurementType = keyof typeof MEASUREMENT_TYPES; export type MeasurementType = keyof typeof MEASUREMENT_TYPES;
export const NAV_ITEMS = [ export const NAV_ITEMS = [
{ path: '/home', label: '首页', icon: '🏠', activeIcon: '🏠' }, { path: '/home', label: '首页' },
{ path: '/health', label: '健康', icon: '💊', activeIcon: '💊' }, { path: '/health', label: '健康' },
{ path: '/services', label: '服务', icon: '👨‍⚕️', activeIcon: '👨‍⚕️' }, { path: '/services', label: '服务' },
{ path: '/profile', label: '我的', icon: '👤', activeIcon: '👤' }, { path: '/profile', label: '我的' },
] as const; ] as const;
export const DEPARTMENT_OPTIONS = [ export const DEPARTMENT_OPTIONS = [
@@ -64,14 +64,14 @@ export const HEALTH_TIPS = [
]; ];
export const EXERCISE_RECOMMENDATIONS = [ export const EXERCISE_RECOMMENDATIONS = [
{ name: '散步', duration: '30-45分钟', frequency: '每天', intensity: '低', suitable: true, caloriesPerHalfHour: 80, icon: '🚶' }, { name: '散步', duration: '30-45分钟', frequency: '每天', intensity: '低', suitable: true, caloriesPerHalfHour: 80 },
{ name: '太极拳', duration: '20-30分钟', frequency: '每周3-5次', intensity: '低', suitable: true, caloriesPerHalfHour: 120, icon: '🧘' }, { name: '太极拳', duration: '20-30分钟', frequency: '每周3-5次', intensity: '低', suitable: true, caloriesPerHalfHour: 120 },
{ name: '慢跑', duration: '20-30分钟', frequency: '每周3-4次', intensity: '中', suitable: true, caloriesPerHalfHour: 250, icon: '🏃' }, { name: '慢跑', duration: '20-30分钟', frequency: '每周3-4次', intensity: '中', suitable: true, caloriesPerHalfHour: 250 },
{ name: '游泳', duration: '30分钟', frequency: '每周2-3次', intensity: '中', suitable: true, caloriesPerHalfHour: 300, icon: '🏊' }, { name: '游泳', duration: '30分钟', frequency: '每周2-3次', intensity: '中', suitable: true, caloriesPerHalfHour: 300 },
{ name: '骑自行车', duration: '30-60分钟', frequency: '每周3-5次', intensity: '中低', suitable: true, caloriesPerHalfHour: 200, icon: '🚴' }, { name: '骑自行车', duration: '30-60分钟', frequency: '每周3-5次', intensity: '中低', suitable: true, caloriesPerHalfHour: 200 },
{ name: '八段锦', duration: '15-20分钟', frequency: '每天', intensity: '低', suitable: true, caloriesPerHalfHour: 90, icon: '🙆' }, { name: '八段锦', duration: '15-20分钟', frequency: '每天', intensity: '低', suitable: true, caloriesPerHalfHour: 90 },
{ name: '剧烈跑步', duration: '30分钟', frequency: '每周3次', intensity: '高', suitable: false, caloriesPerHalfHour: 400, icon: '🏃' }, { name: '剧烈跑步', duration: '30分钟', frequency: '每周3次', intensity: '高', suitable: false, caloriesPerHalfHour: 400 },
{ name: '举重训练', duration: '45分钟', frequency: '每周3次', intensity: '高', suitable: false, caloriesPerHalfHour: 350, icon: '🏋️' }, { name: '举重训练', duration: '45分钟', frequency: '每周3次', intensity: '高', suitable: false, caloriesPerHalfHour: 350 },
]; ];
export const DIET_RECOMMENDATIONS = [ export const DIET_RECOMMENDATIONS = [
@@ -100,11 +100,19 @@ export const DIET_RECOMMENDATIONS = [
suitable: true, suitable: true,
}, },
{ {
category: '地中海饮食', category: '优质蛋白',
title: '富含蔬果和优质脂肪的饮食模式', title: '优先选择优质蛋白来源',
description: '被多项研究证实对心血管健康有益,降低心血管事件风险。', description: '优质蛋白有助于维持肌肉力量和心血管修复每日蛋白摄入约1.0-1.2g/kg体重。',
recommendedFoods: ['橄榄油', '坚果', '深海鱼', '蔬菜', '水果', '全谷物'], recommendedFoods: ['鱼', '去皮禽肉', '豆制品', '蛋清'],
avoidFoods: ['红肉', '加工食品', '含糖饮料'], avoidFoods: ['红肉', '加工肉制品'],
suitable: true,
},
{
category: '限制饮酒',
title: '戒烟限酒,保护心血管',
description: '酒精会升高血压、增加心率加重心脏负担。PCI术后患者建议完全戒酒。',
recommendedFoods: ['白开水', '淡茶', '无糖饮品'],
avoidFoods: ['白酒', '啤酒', '红酒', '含酒精饮料'],
suitable: true, suitable: true,
}, },
]; ];