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