feat(miniprogram): 温润东方风全面 UI 重设计
73 文件变更,覆盖全部 40 个页面 SCSS + TabBar 图标 + 组件样式。 统一赤陶主色 #C4623A + 暖米背景 + 衬线标题字体 + 12px 圆角体系。
This commit is contained in:
@@ -1,54 +1,122 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.dm-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 60px;
|
||||
padding: 0 0 60px;
|
||||
}
|
||||
|
||||
.dm-section {
|
||||
/* ── hero ── */
|
||||
.dm-hero {
|
||||
padding: 48px 32px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dm-hero-icon {
|
||||
@include flex-center;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: $r-lg;
|
||||
background: $pri-l;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dm-hero-icon-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.dm-hero-title {
|
||||
@include section-title;
|
||||
font-size: 36px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dm-hero-sub {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ── card ── */
|
||||
.dm-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: $shadow-md;
|
||||
padding: 28px;
|
||||
margin: 0 24px 20px;
|
||||
}
|
||||
|
||||
.dm-section-title {
|
||||
.dm-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dm-card-serial {
|
||||
@include flex-center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: $r-sm;
|
||||
background: $pri-l;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dm-card-serial-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.dm-card-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid $pri;
|
||||
}
|
||||
|
||||
.dm-date-picker {
|
||||
.dm-card-badge {
|
||||
@include tag($acc-l, $acc);
|
||||
font-size: 20px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── date picker ── */
|
||||
.dm-date-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.dm-date-value {
|
||||
font-size: 28px;
|
||||
color: $pri;
|
||||
@include serif-number;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dm-date-arrow {
|
||||
font-size: 28px;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
transform: rotate(180deg);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dm-bp-row {
|
||||
/* ── blood pressure group ── */
|
||||
.dm-bp-group {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dm-bp-field {
|
||||
@@ -56,16 +124,30 @@
|
||||
}
|
||||
|
||||
.dm-field-label {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dm-bp-sep {
|
||||
font-size: 40px;
|
||||
.dm-bp-divider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dm-bp-line {
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: $bd;
|
||||
}
|
||||
|
||||
.dm-bp-slash {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 36px;
|
||||
color: $tx3;
|
||||
padding-bottom: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@@ -73,29 +155,36 @@
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── single row with unit ── */
|
||||
.dm-single-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dm-field-unit-inline {
|
||||
.dm-input-flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dm-unit-inline {
|
||||
font-size: 26px;
|
||||
color: $tx3;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dm-input {
|
||||
/* ── input field ── */
|
||||
.dm-input-box {
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
width: 100%;
|
||||
@include serif-number;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -103,13 +192,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── submit ── */
|
||||
.dm-submit {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 24px;
|
||||
border-radius: $r;
|
||||
padding: 26px;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
box-shadow: 0 4px 12px rgba(8, 145, 178, 0.3);
|
||||
margin: 40px 24px 0;
|
||||
box-shadow: $shadow-md;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
@@ -118,7 +208,7 @@
|
||||
}
|
||||
|
||||
.dm-submit-disabled {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -126,12 +216,14 @@
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ── reset ── */
|
||||
.dm-reset {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin-top: 16px;
|
||||
padding: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.dm-reset-text {
|
||||
|
||||
@@ -87,7 +87,6 @@ export default function DailyMonitoring() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Zod 验证数值范围
|
||||
const parseNum = (v: string) => v ? parseFloat(v) : undefined;
|
||||
const fields = {
|
||||
morningSystolic: parseNum(morningSystolic),
|
||||
@@ -163,44 +162,72 @@ export default function DailyMonitoring() {
|
||||
}
|
||||
};
|
||||
|
||||
const isToday = recordDate === today;
|
||||
|
||||
return (
|
||||
<View className='dm-page'>
|
||||
{/* 页面标题 */}
|
||||
<View className='dm-hero'>
|
||||
<View className='dm-hero-icon'>
|
||||
<Text className='dm-hero-icon-text'>记</Text>
|
||||
</View>
|
||||
<Text className='dm-hero-title'>日常监测</Text>
|
||||
<Text className='dm-hero-sub'>每日健康数据上报</Text>
|
||||
</View>
|
||||
|
||||
{/* 日期选择 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>记录日期</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>1</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>记录日期</Text>
|
||||
{isToday && (
|
||||
<Text className='dm-card-badge'>今日</Text>
|
||||
)}
|
||||
</View>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={dateList}
|
||||
value={dateIdx}
|
||||
onChange={(e) => setDateIdx(Number(e.detail.value))}
|
||||
>
|
||||
<View className='dm-date-picker'>
|
||||
<View className='dm-date-row'>
|
||||
<Text className='dm-date-value'>{recordDate}</Text>
|
||||
<Text className='dm-date-arrow'>▾</Text>
|
||||
<Text className='dm-date-arrow'>V</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
{/* 晨起血压 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>晨起血压</Text>
|
||||
<View className='dm-bp-row'>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>2</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>晨起血压</Text>
|
||||
</View>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box'
|
||||
placeholder='如 120'
|
||||
value={morningSystolic}
|
||||
onInput={(e) => setMorningSystolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<Text className='dm-bp-sep'>/</Text>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box'
|
||||
placeholder='如 80'
|
||||
value={morningDiastolic}
|
||||
onInput={(e) => setMorningDiastolic(e.detail.value)}
|
||||
@@ -211,25 +238,34 @@ export default function DailyMonitoring() {
|
||||
</View>
|
||||
|
||||
{/* 晚间血压 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>晚间血压</Text>
|
||||
<View className='dm-bp-row'>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>3</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>晚间血压</Text>
|
||||
</View>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box'
|
||||
placeholder='如 120'
|
||||
value={eveningSystolic}
|
||||
onInput={(e) => setEveningSystolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<Text className='dm-bp-sep'>/</Text>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box'
|
||||
placeholder='如 80'
|
||||
value={eveningDiastolic}
|
||||
onInput={(e) => setEveningDiastolic(e.detail.value)}
|
||||
@@ -240,70 +276,95 @@ export default function DailyMonitoring() {
|
||||
</View>
|
||||
|
||||
{/* 体重 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>体重</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>4</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>体重</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 65.0'
|
||||
value={weight}
|
||||
onInput={(e) => setWeight(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-field-unit-inline'>kg</Text>
|
||||
<Text className='dm-unit-inline'>kg</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 血糖 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>血糖</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>5</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>血糖</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 5.6'
|
||||
value={bloodSugar}
|
||||
onInput={(e) => setBloodSugar(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-field-unit-inline'>mmol/L</Text>
|
||||
<Text className='dm-unit-inline'>mmol/L</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 饮水量 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>饮水量</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>6</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>饮水量</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 2000'
|
||||
value={fluidIntake}
|
||||
onInput={(e) => setFluidIntake(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-field-unit-inline'>ml</Text>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 尿量 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>尿量</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>7</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>尿量</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 1500'
|
||||
value={urineOutput}
|
||||
onInput={(e) => setUrineOutput(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-field-unit-inline'>ml</Text>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 备注 */}
|
||||
<View className='dm-section'>
|
||||
<Text className='dm-section-title'>备注</Text>
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>8</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>备注</Text>
|
||||
</View>
|
||||
<Input
|
||||
className='dm-input dm-input-full'
|
||||
className='dm-input-box dm-input-full'
|
||||
placeholder='如:头晕、乏力等(可选)'
|
||||
value={notes}
|
||||
onInput={(e) => setNotes(e.detail.value)}
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/mixins.scss';
|
||||
|
||||
.health-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding-bottom: 40px;
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* ─── 页头 ─── */
|
||||
.health-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px 32px;
|
||||
padding: 24px 32px 8px;
|
||||
}
|
||||
|
||||
.health-header-title {
|
||||
.health-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.health-header-btn {
|
||||
.health-add-btn {
|
||||
background: $pri;
|
||||
padding: 12px 28px;
|
||||
padding: 10px 28px;
|
||||
border-radius: $r-sm;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.health-header-btn-text {
|
||||
.health-add-text {
|
||||
font-size: 26px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ---- Quick Actions (快捷操作) ----
|
||||
|
||||
.quick-actions {
|
||||
/* ─── 快捷操作 ─── */
|
||||
.health-actions-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.quick-action-item {
|
||||
.action-item {
|
||||
flex: 1;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
@@ -48,60 +53,54 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.15s;
|
||||
gap: 10px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-action-icon-wrap {
|
||||
.action-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
@include flex-center;
|
||||
|
||||
&.icon-primary { background: $pri-l; }
|
||||
&.icon-accent { background: $acc-l; }
|
||||
&.icon-warn { background: $wrn-l; }
|
||||
}
|
||||
|
||||
.quick-action-icon-primary {
|
||||
background: $pri-l;
|
||||
.action-char {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
|
||||
.icon-accent & { color: $acc; }
|
||||
.icon-warn & { color: $wrn; }
|
||||
}
|
||||
|
||||
.quick-action-icon-green {
|
||||
background: $acc-l;
|
||||
}
|
||||
|
||||
.quick-action-icon-orange {
|
||||
background: $wrn-l;
|
||||
}
|
||||
|
||||
.quick-action-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.quick-action-label {
|
||||
.action-label {
|
||||
font-size: 24px;
|
||||
color: $tx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// ---- Checkin Status (打卡状态) ----
|
||||
|
||||
/* ─── 打卡卡片 ─── */
|
||||
.checkin-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
padding: 24px 28px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.checkin-left {
|
||||
.checkin-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -110,7 +109,7 @@
|
||||
.checkin-done {
|
||||
font-size: 28px;
|
||||
color: $acc;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checkin-streak {
|
||||
@@ -124,164 +123,178 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkin-go-btn {
|
||||
.checkin-go {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 12px 24px;
|
||||
padding: 12px 28px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-go-text {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ---- Health Grid (体征概览) ----
|
||||
/* ─── 通用 section ─── */
|
||||
.health-section {
|
||||
margin: 0 24px 28px;
|
||||
}
|
||||
|
||||
.health-grid {
|
||||
/* ─── 体征概览 ─── */
|
||||
.vitals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.health-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border-left: 6px solid $bd;
|
||||
transition: border-left-color 0.2s;
|
||||
|
||||
&.status-normal { border-left-color: $acc; }
|
||||
&.status-high { border-left-color: $dan; }
|
||||
&.status-low { border-left-color: $dan; }
|
||||
}
|
||||
|
||||
.health-card-label {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.health-card-value {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.health-card-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.health-card-unit {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.health-card-status {
|
||||
font-size: 22px;
|
||||
|
||||
&.status-normal { color: $acc; }
|
||||
&.status-high, &.status-low { color: $dan; font-weight: bold; }
|
||||
}
|
||||
|
||||
.health-card-ref {
|
||||
font-size: 20px;
|
||||
color: $tx3;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// ---- Trend Actions (趋势快捷入口) ----
|
||||
|
||||
.health-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
flex: 1;
|
||||
.vital-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 24px 20px;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 24px;
|
||||
.vital-label {
|
||||
font-size: 22px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
// ---- Recent Daily Monitoring (最近日常监测) ----
|
||||
|
||||
.recent-section {
|
||||
padding: 0 24px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.recent-section-title {
|
||||
font-size: 28px;
|
||||
.vital-value {
|
||||
@include serif-number;
|
||||
font-size: 44px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid $pri;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.recent-record {
|
||||
.vital-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vital-unit {
|
||||
font-size: 20px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.vital-tag {
|
||||
@include tag($acc-l, $acc);
|
||||
|
||||
&.tag-warn {
|
||||
@include tag($wrn-l, $wrn);
|
||||
}
|
||||
}
|
||||
|
||||
.vital-ref {
|
||||
font-size: 20px;
|
||||
color: $tx3;
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ─── 趋势入口 ─── */
|
||||
.trend-row {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.trend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $bd-l;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: $r-sm;
|
||||
background: $pri-l;
|
||||
@include flex-center;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.trend-char {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.trend-label {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trend-arrow {
|
||||
font-size: 32px;
|
||||
color: $tx3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── 最近监测 ─── */
|
||||
.record-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.recent-record-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.record-date {
|
||||
font-size: 24px;
|
||||
color: $pri;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.recent-record-date {
|
||||
font-size: 26px;
|
||||
color: $pri;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.recent-record-data {
|
||||
.record-data {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.recent-data-item {
|
||||
.record-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.recent-data-label {
|
||||
.record-item-label {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.recent-data-value {
|
||||
.record-item-value {
|
||||
@include serif-number;
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -9,11 +9,11 @@ import { trackEvent } from '../../services/analytics';
|
||||
import Loading from '../../components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
function getStatusStyle(status?: string) {
|
||||
if (status === 'high') return { cls: 'status-high', label: '偏高 ▲' };
|
||||
if (status === 'low') return { cls: 'status-low', label: '偏低 ▼' };
|
||||
if (status === 'normal') return { cls: 'status-normal', label: '正常 ─' };
|
||||
return { cls: '', label: '' };
|
||||
function getStatusTag(status?: string) {
|
||||
if (status === 'high') return { label: '偏高', cls: 'tag-warn' };
|
||||
if (status === 'low') return { label: '偏低', cls: 'tag-warn' };
|
||||
if (status === 'normal') return { label: '正常', cls: 'tag-ok' };
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Health() {
|
||||
@@ -32,7 +32,7 @@ export default function Health() {
|
||||
const status = await getCheckinStatus();
|
||||
setCheckinStatus(status);
|
||||
} catch {
|
||||
// ignore — points API may not be available
|
||||
// points API 可能不可用
|
||||
}
|
||||
|
||||
if (currentPatient) {
|
||||
@@ -40,7 +40,7 @@ export default function Health() {
|
||||
const resp = await listDailyMonitoring(currentPatient.id, { page: 1, page_size: 3 });
|
||||
setRecentRecords(resp.data || []);
|
||||
} catch {
|
||||
// ignore — daily monitoring API may not be available yet
|
||||
// daily monitoring API 可能不可用
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -57,10 +57,6 @@ export default function Health() {
|
||||
Taro.navigateTo({ url: `/pages/health/trend/index?indicator=${indicator}` });
|
||||
};
|
||||
|
||||
const goToTrendPage = () => {
|
||||
Taro.navigateTo({ url: '/pages/health/trend/index?indicator=blood_pressure_systolic' });
|
||||
};
|
||||
|
||||
const goToMall = () => {
|
||||
Taro.switchTab({ url: '/pages/mall/index' });
|
||||
};
|
||||
@@ -73,6 +69,18 @@ export default function Health() {
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '--', unit: 'kg', indicator: 'weight', status: summary.weight?.status, ref: summary.weight?.reference_range },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ label: '日常上报', char: '日', bg: 'icon-primary', action: goToDailyMonitoring },
|
||||
{ label: '体征录入', char: '录', bg: 'icon-accent', action: goToInput },
|
||||
{ label: '查看趋势', char: '势', bg: 'icon-warn', action: () => goToTrend('blood_pressure_systolic') },
|
||||
];
|
||||
|
||||
const trendLinks = [
|
||||
{ label: '血压趋势', indicator: 'blood_pressure_systolic', char: '压' },
|
||||
{ label: '心率趋势', indicator: 'heart_rate', char: '率' },
|
||||
{ label: '血糖趋势', indicator: 'blood_sugar_fasting', char: '糖' },
|
||||
];
|
||||
|
||||
const formatBp = (record: DailyMonitoring) => {
|
||||
const parts: string[] = [];
|
||||
if (record.morning_bp_systolic && record.morning_bp_diastolic) {
|
||||
@@ -86,42 +94,33 @@ export default function Health() {
|
||||
|
||||
return (
|
||||
<View className='health-page'>
|
||||
{/* 页头 */}
|
||||
<View className='health-header'>
|
||||
<Text className='health-header-title'>健康数据</Text>
|
||||
<View className='health-header-btn' onClick={goToInput}>
|
||||
<Text className='health-header-btn-text'>+ 录入</Text>
|
||||
<Text className='health-title'>健康数据</Text>
|
||||
<View className='health-add-btn' onClick={goToInput}>
|
||||
<Text className='health-add-text'>录入</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<View className='quick-actions'>
|
||||
<View className='quick-action-item' onClick={goToDailyMonitoring}>
|
||||
<View className='quick-action-icon-wrap quick-action-icon-primary'>
|
||||
<Text className='quick-action-icon'>📋</Text>
|
||||
<View className='health-actions-row'>
|
||||
{quickActions.map((a) => (
|
||||
<View className='action-item' key={a.label} onClick={a.action}>
|
||||
<View className={`action-icon ${a.bg}`}>
|
||||
<Text className='action-char'>{a.char}</Text>
|
||||
</View>
|
||||
<Text className='action-label'>{a.label}</Text>
|
||||
</View>
|
||||
<Text className='quick-action-label'>日常上报</Text>
|
||||
</View>
|
||||
<View className='quick-action-item' onClick={goToInput}>
|
||||
<View className='quick-action-icon-wrap quick-action-icon-green'>
|
||||
<Text className='quick-action-icon'>💉</Text>
|
||||
</View>
|
||||
<Text className='quick-action-label'>体征录入</Text>
|
||||
</View>
|
||||
<View className='quick-action-item' onClick={goToTrendPage}>
|
||||
<View className='quick-action-icon-wrap quick-action-icon-orange'>
|
||||
<Text className='quick-action-icon'>📈</Text>
|
||||
</View>
|
||||
<Text className='quick-action-label'>查看趋势</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 打卡状态 */}
|
||||
{checkinStatus && (
|
||||
<View className='checkin-card'>
|
||||
<View className='checkin-left'>
|
||||
<View className='checkin-info'>
|
||||
{checkinStatus.checked_in_today ? (
|
||||
<>
|
||||
<Text className='checkin-done'>今日已打卡 ✓</Text>
|
||||
<Text className='checkin-done'>今日已打卡</Text>
|
||||
{checkinStatus.consecutive_days > 0 && (
|
||||
<Text className='checkin-streak'>连续 {checkinStatus.consecutive_days} 天</Text>
|
||||
)}
|
||||
@@ -131,7 +130,7 @@ export default function Health() {
|
||||
)}
|
||||
</View>
|
||||
{!checkinStatus.checked_in_today && (
|
||||
<View className='checkin-go-btn' onClick={goToMall}>
|
||||
<View className='checkin-go' onClick={goToMall}>
|
||||
<Text className='checkin-go-text'>去打卡</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -139,67 +138,68 @@ export default function Health() {
|
||||
)}
|
||||
|
||||
{/* 今日体征概览 */}
|
||||
{loading && !todaySummary ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<View className='health-grid'>
|
||||
{items.map((item) => {
|
||||
const style = getStatusStyle(item.status);
|
||||
return (
|
||||
<View className={`health-card ${style.cls}`} key={item.label} onClick={() => goToTrend(item.indicator)}>
|
||||
<Text className='health-card-label'>{item.label}</Text>
|
||||
<Text className='health-card-value'>{item.value}</Text>
|
||||
<View className='health-card-bottom'>
|
||||
<Text className='health-card-unit'>{item.unit}</Text>
|
||||
{style.label && <Text className={`health-card-status ${style.cls}`}>{style.label}</Text>}
|
||||
<View className='health-section'>
|
||||
<Text className='section-title'>今日体征</Text>
|
||||
{loading && !todaySummary ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<View className='vitals-grid'>
|
||||
{items.map((item) => {
|
||||
const tag = getStatusTag(item.status);
|
||||
return (
|
||||
<View className='vital-card' key={item.label} onClick={() => goToTrend(item.indicator)}>
|
||||
<Text className='vital-label'>{item.label}</Text>
|
||||
<Text className='vital-value'>{item.value}</Text>
|
||||
<View className='vital-bottom'>
|
||||
<Text className='vital-unit'>{item.unit}</Text>
|
||||
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</Text>}
|
||||
</View>
|
||||
{item.ref && <Text className='vital-ref'>参考 {item.ref}</Text>}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 趋势快捷入口 */}
|
||||
<View className='health-section'>
|
||||
<Text className='section-title'>健康趋势</Text>
|
||||
<View className='trend-row'>
|
||||
{trendLinks.map((t) => (
|
||||
<View className='trend-item' key={t.label} onClick={() => goToTrend(t.indicator)}>
|
||||
<View className='trend-icon'>
|
||||
<Text className='trend-char'>{t.char}</Text>
|
||||
</View>
|
||||
{item.ref && <Text className='health-card-ref'>参考: {item.ref}</Text>}
|
||||
<Text className='trend-label'>{t.label}</Text>
|
||||
<Text className='trend-arrow'>›</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 原有趋势快捷入口 */}
|
||||
<View className='health-actions'>
|
||||
<View className='action-card' onClick={() => goToTrend('blood_pressure_systolic')}>
|
||||
<Text className='action-icon'>📈</Text>
|
||||
<Text className='action-label'>血压趋势</Text>
|
||||
</View>
|
||||
<View className='action-card' onClick={() => goToTrend('heart_rate')}>
|
||||
<Text className='action-icon'>❤️</Text>
|
||||
<Text className='action-label'>心率趋势</Text>
|
||||
</View>
|
||||
<View className='action-card' onClick={() => goToTrend('blood_sugar_fasting')}>
|
||||
<Text className='action-icon'>🩸</Text>
|
||||
<Text className='action-label'>血糖趋势</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 最近日常监测记录 */}
|
||||
{/* 最近监测记录 */}
|
||||
{recentRecords.length > 0 && (
|
||||
<View className='recent-section'>
|
||||
<Text className='recent-section-title'>最近监测记录</Text>
|
||||
<View className='health-section'>
|
||||
<Text className='section-title'>最近监测</Text>
|
||||
{recentRecords.map((record) => (
|
||||
<View className='recent-record' key={record.id}>
|
||||
<View className='recent-record-header'>
|
||||
<Text className='recent-record-date'>{record.record_date}</Text>
|
||||
</View>
|
||||
<View className='recent-record-data'>
|
||||
<View className='recent-data-item'>
|
||||
<Text className='recent-data-label'>血压</Text>
|
||||
<Text className='recent-data-value'>{formatBp(record)}</Text>
|
||||
<View className='record-card' key={record.id}>
|
||||
<Text className='record-date'>{record.record_date}</Text>
|
||||
<View className='record-data'>
|
||||
<View className='record-item'>
|
||||
<Text className='record-item-label'>血压</Text>
|
||||
<Text className='record-item-value'>{formatBp(record)}</Text>
|
||||
</View>
|
||||
{record.weight != null && (
|
||||
<View className='recent-data-item'>
|
||||
<Text className='recent-data-label'>体重</Text>
|
||||
<Text className='recent-data-value'>{record.weight} kg</Text>
|
||||
<View className='record-item'>
|
||||
<Text className='record-item-label'>体重</Text>
|
||||
<Text className='record-item-value'>{record.weight} kg</Text>
|
||||
</View>
|
||||
)}
|
||||
{record.blood_sugar != null && (
|
||||
<View className='recent-data-item'>
|
||||
<Text className='recent-data-label'>血糖</Text>
|
||||
<Text className='recent-data-value'>{record.blood_sugar} mmol/L</Text>
|
||||
<View className='record-item'>
|
||||
<Text className='record-item-label'>血糖</Text>
|
||||
<Text className='record-item-value'>{record.blood_sugar} mmol/L</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,59 +1,204 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.input-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding: 0 0 60px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 32px;
|
||||
/* ── hero ── */
|
||||
.input-hero {
|
||||
padding: 48px 32px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
.input-hero-icon {
|
||||
@include flex-center;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: $r-lg;
|
||||
background: $pri-l;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-hero-icon-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.input-picker {
|
||||
.input-hero-title {
|
||||
@include section-title;
|
||||
font-size: 36px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-hero-sub {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ── card ── */
|
||||
.input-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: $shadow-md;
|
||||
padding: 28px;
|
||||
margin: 0 24px 20px;
|
||||
}
|
||||
|
||||
.input-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-card-indicator {
|
||||
@include flex-center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
background: $acc-l;
|
||||
}
|
||||
|
||||
.input-card-indicator-char {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
.input-card-label {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
/* ── picker ── */
|
||||
.input-picker-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.input-picker-value {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
.input-picker-arrow {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
font-size: 28px;
|
||||
transform: rotate(180deg);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
background: $card;
|
||||
/* ── section title ── */
|
||||
.input-section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── blood pressure group ── */
|
||||
.input-bp-group {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-bp-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-field-label {
|
||||
font-size: 22px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-bp-divider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-bp-line {
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: $bd;
|
||||
}
|
||||
|
||||
.input-bp-slash {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 36px;
|
||||
color: $tx3;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ── input field ── */
|
||||
.input-field-box {
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
width: 100%;
|
||||
@include serif-number;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-submit {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
margin-top: 48px;
|
||||
.input-field-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-text {
|
||||
.input-field-unit {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── submit ── */
|
||||
.input-submit {
|
||||
background: $pri;
|
||||
border-radius: $r;
|
||||
padding: 26px;
|
||||
text-align: center;
|
||||
margin: 48px 24px 0;
|
||||
box-shadow: $shadow-md;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.input-submit-disabled {
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-submit-text {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
@@ -99,71 +99,106 @@ export default function HealthInput() {
|
||||
}
|
||||
};
|
||||
|
||||
const indicatorInitial = INDICATORS[indicatorIdx].label.charAt(0);
|
||||
|
||||
return (
|
||||
<View className='input-page'>
|
||||
<View className='input-section'>
|
||||
<Text className='input-label'>指标类型</Text>
|
||||
{/* 页面标题 */}
|
||||
<View className='input-hero'>
|
||||
<View className='input-hero-icon'>
|
||||
<Text className='input-hero-icon-text'>录</Text>
|
||||
</View>
|
||||
<Text className='input-hero-title'>体征录入</Text>
|
||||
<Text className='input-hero-sub'>记录今日健康数据</Text>
|
||||
</View>
|
||||
|
||||
{/* 指标类型选择 */}
|
||||
<View className='input-card'>
|
||||
<View className='input-card-header'>
|
||||
<View className='input-card-indicator'>
|
||||
<Text className='input-card-indicator-char'>{indicatorInitial}</Text>
|
||||
</View>
|
||||
<Text className='input-card-label'>指标类型</Text>
|
||||
</View>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={INDICATORS.map((i) => i.label)}
|
||||
value={indicatorIdx}
|
||||
onChange={(e) => setIndicatorIdx(Number(e.detail.value))}
|
||||
>
|
||||
<View className='input-picker'>
|
||||
<Text>{INDICATORS[indicatorIdx].label}</Text>
|
||||
<Text className='picker-arrow'>▾</Text>
|
||||
<View className='input-picker-row'>
|
||||
<Text className='input-picker-value'>{INDICATORS[indicatorIdx].label}</Text>
|
||||
<Text className='input-picker-arrow'>V</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
{/* 数值输入 */}
|
||||
{INDICATORS[indicatorIdx].value === 'blood_pressure' ? (
|
||||
<>
|
||||
<View className='input-section'>
|
||||
<Text className='input-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field'
|
||||
placeholder='如 120'
|
||||
value={systolic}
|
||||
onInput={(e) => setSystolic(e.detail.value)}
|
||||
/>
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>血压数值</Text>
|
||||
<View className='input-bp-group'>
|
||||
<View className='input-bp-field'>
|
||||
<Text className='input-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field-box'
|
||||
placeholder='如 120'
|
||||
value={systolic}
|
||||
onInput={(e) => setSystolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<View className='input-bp-divider'>
|
||||
<View className='input-bp-line' />
|
||||
<Text className='input-bp-slash'>/</Text>
|
||||
<View className='input-bp-line' />
|
||||
</View>
|
||||
<View className='input-bp-field'>
|
||||
<Text className='input-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field-box'
|
||||
placeholder='如 80'
|
||||
value={diastolic}
|
||||
onInput={(e) => setDiastolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className='input-section'>
|
||||
<Text className='input-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field'
|
||||
placeholder='如 80'
|
||||
value={diastolic}
|
||||
onInput={(e) => setDiastolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
<Text className='input-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='input-section'>
|
||||
<Text className='input-label'>数值</Text>
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>检测数值</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field'
|
||||
className='input-field-box input-field-full'
|
||||
placeholder='请输入数值'
|
||||
value={value}
|
||||
onInput={(e) => setValue(e.detail.value)}
|
||||
/>
|
||||
<Text className='input-field-unit'>
|
||||
{INDICATORS[indicatorIdx].label.match(/\((.+)\)/)?.[1] || ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='input-section'>
|
||||
<Text className='input-label'>备注(可选)</Text>
|
||||
{/* 备注 */}
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>备注</Text>
|
||||
<Input
|
||||
className='input-field'
|
||||
placeholder='如:饭后2小时'
|
||||
className='input-field-box input-field-full'
|
||||
placeholder='如:饭后2小时(可选)'
|
||||
value={note}
|
||||
onInput={(e) => setNote(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='input-submit' onClick={submitting ? undefined : handleSubmit}>
|
||||
<Text className='submit-text'>{submitting ? '提交中...' : '提交'}</Text>
|
||||
{/* 提交 */}
|
||||
<View
|
||||
className={`input-submit ${submitting ? 'input-submit-disabled' : ''}`}
|
||||
onClick={submitting ? undefined : handleSubmit}
|
||||
>
|
||||
<Text className='input-submit-text'>{submitting ? '提交中...' : '提交录入'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,113 +1,148 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.trend-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.trend-header {
|
||||
padding: 24px 32px;
|
||||
/* ── hero ── */
|
||||
.trend-hero {
|
||||
padding: 48px 32px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-title {
|
||||
font-size: 34px;
|
||||
.trend-hero-icon {
|
||||
@include flex-center;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: $r-lg;
|
||||
background: $pri-l;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.trend-hero-icon-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.trend-hero-title {
|
||||
@include section-title;
|
||||
font-size: 36px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ── range tabs ── */
|
||||
.trange-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 0 32px 28px;
|
||||
}
|
||||
|
||||
.trange-tab {
|
||||
padding: 12px 32px;
|
||||
border-radius: $r-pill;
|
||||
background: $card;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.trange-tab-active {
|
||||
background: $pri;
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
.trange-tab-text {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trange-tab-text-active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── chart card ── */
|
||||
.trend-chart-card {
|
||||
margin: 0 24px 20px;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: $shadow-md;
|
||||
padding: 24px;
|
||||
min-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── reference card ── */
|
||||
.trend-ref-card {
|
||||
margin: 0 24px 20px;
|
||||
background: $acc-l;
|
||||
border-radius: $r;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.trend-ref-label {
|
||||
font-size: 24px;
|
||||
color: $acc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trend-ref-value {
|
||||
font-size: 26px;
|
||||
color: $acc;
|
||||
@include serif-number;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── list ── */
|
||||
.trend-list {
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.trend-list-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.trend-tabs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.trend-tab {
|
||||
padding: 10px 28px;
|
||||
border-radius: 20px;
|
||||
background: $card;
|
||||
}
|
||||
|
||||
.trend-tab.active {
|
||||
background: $pri;
|
||||
}
|
||||
|
||||
.trend-tab-text {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
.trend-tab.active .trend-tab-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
margin: 24px;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.trend-empty {
|
||||
font-size: 26px;
|
||||
color: $tx3;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.chart-bar-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
width: 100%;
|
||||
background: linear-gradient(to top, $pri, $pri-l);
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.chart-bar-date {
|
||||
font-size: 18px;
|
||||
color: $tx3;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.trend-list {
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.trend-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $card;
|
||||
padding: 20px 24px;
|
||||
padding: 22px 28px;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $r $r 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 $r $r;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-item:first-child {
|
||||
border-radius: $r $r 0 0;
|
||||
.trend-item-warn {
|
||||
background: $wrn-l;
|
||||
}
|
||||
|
||||
.trend-item:last-child {
|
||||
border-radius: 0 0 $r $r;
|
||||
border-bottom: none;
|
||||
.trend-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.trend-item-date {
|
||||
@@ -115,8 +150,21 @@
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
.trend-item-tag {
|
||||
@include tag($wrn-l, $wrn);
|
||||
}
|
||||
|
||||
.trend-item-warn .trend-item-tag {
|
||||
@include tag($dan-l, $dan);
|
||||
}
|
||||
|
||||
.trend-item-value {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
color: $pri;
|
||||
@include serif-number;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trend-item-value-warn {
|
||||
color: $dan;
|
||||
}
|
||||
|
||||
@@ -33,25 +33,39 @@ export default function Trend() {
|
||||
|
||||
const meta = INDICATOR_META[indicator] || { label: indicator, unit: '' };
|
||||
|
||||
const isOutOfRange = (val: number) => {
|
||||
if (meta.refMin !== undefined && val < meta.refMin) return true;
|
||||
if (meta.refMax !== undefined && val > meta.refMax) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='trend-page'>
|
||||
<View className='trend-header'>
|
||||
<Text className='trend-title'>{meta.label} 趋势</Text>
|
||||
<View className='trend-tabs'>
|
||||
{RANGE_OPTIONS.map((opt) => (
|
||||
<View
|
||||
key={opt.value}
|
||||
className={`trend-tab ${range === opt.value ? 'active' : ''}`}
|
||||
onClick={() => setRange(opt.value)}
|
||||
>
|
||||
<Text className='trend-tab-text'>{opt.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
{/* 页面标题 */}
|
||||
<View className='trend-hero'>
|
||||
<View className='trend-hero-icon'>
|
||||
<Text className='trend-hero-icon-text'>T</Text>
|
||||
</View>
|
||||
<Text className='trend-hero-title'>{meta.label}趋势</Text>
|
||||
</View>
|
||||
|
||||
{/* 时间范围切换 */}
|
||||
<View className='trange-wrap'>
|
||||
{RANGE_OPTIONS.map((opt) => (
|
||||
<View
|
||||
key={opt.value}
|
||||
className={`trange-tab ${range === opt.value ? 'trange-tab-active' : ''}`}
|
||||
onClick={() => setRange(opt.value)}
|
||||
>
|
||||
<Text className={`trange-tab-text ${range === opt.value ? 'trange-tab-text-active' : ''}`}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* ECharts 折线图 */}
|
||||
<View className='trend-chart-container'>
|
||||
<View className='trend-chart-card'>
|
||||
<TrendChart
|
||||
data={points}
|
||||
referenceMin={meta.refMin}
|
||||
@@ -60,15 +74,36 @@ export default function Trend() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 参考区间 */}
|
||||
{meta.refMin !== undefined && meta.refMax !== undefined && (
|
||||
<View className='trend-ref-card'>
|
||||
<Text className='trend-ref-label'>参考区间</Text>
|
||||
<Text className='trend-ref-value'>
|
||||
{meta.refMin} ~ {meta.refMax} {meta.unit}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 数据列表 */}
|
||||
{points.length > 0 && (
|
||||
<View className='trend-list'>
|
||||
{points.slice().reverse().map((p, i) => (
|
||||
<View className='trend-item' key={i}>
|
||||
<Text className='trend-item-date'>{p.date}</Text>
|
||||
<Text className='trend-item-value'>{p.value}{meta.unit ? ` ${meta.unit}` : ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
<Text className='trend-list-title'>历史记录</Text>
|
||||
{points.slice().reverse().map((p, i) => {
|
||||
const abnormal = isOutOfRange(p.value);
|
||||
return (
|
||||
<View className={`trend-item ${abnormal ? 'trend-item-warn' : ''}`} key={i}>
|
||||
<View className='trend-item-left'>
|
||||
<Text className='trend-item-date'>{p.date}</Text>
|
||||
{abnormal && (
|
||||
<Text className='trend-item-tag'>偏高</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className={`trend-item-value ${abnormal ? 'trend-item-value-warn' : ''}`}>
|
||||
{p.value}{meta.unit ? ` ${meta.unit}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user