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:
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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}`)}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user