chore: restore clean mobile baseline as requested by user

This commit is contained in:
fuenmao
2026-09-21 16:02:49 +08:00
parent 93e9bb604d
commit e4070f207c
4 changed files with 192 additions and 1060 deletions
+97 -711
View File
@@ -1,724 +1,110 @@
:root {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #17322a;
background-color: #edebe4;
/* Brand Theme Tokens */
--primary-deep: #09372d;
--primary: #0c4d3f;
--primary-light: #156655;
--primary-subtle: #e8f2ef;
--accent-gold: #c89547;
--accent-light: #fdf6ea;
--accent-warm: #e3b063;
/* Neutrals */
--bg-app: #f6f5f0;
--card-bg: #ffffff;
--line: rgba(12, 77, 63, 0.08);
--line-light: rgba(12, 77, 63, 0.04);
--text-main: #142e27;
--text-sub: #576d65;
--text-muted: #8d9e97;
/* Shadows */
--shadow-card: 0 4px 16px -2px rgba(9, 55, 45, 0.05), 0 2px 6px -1px rgba(9, 55, 45, 0.03);
--shadow-float: 0 12px 32px -4px rgba(9, 55, 45, 0.12);
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
color: #173a34;
background: #f2f0e9;
font-synthesis: none;
--green: #0c5045;
--deep: #123f37;
--cream: #f8f4e8;
--gold: #d6a657;
--line: rgba(18, 63, 55, 0.12);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; background: #f2f0e9; }
button, input, textarea { font: inherit; }
button { cursor: pointer; }
body {
min-height: 100vh;
background-color: #edebe4;
display: flex;
justify-content: center;
}
#app {
width: 100%;
max-width: 460px;
min-height: 100vh;
background: var(--bg-app);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.06);
position: relative;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
button, input, textarea {
font: inherit;
border: none;
outline: none;
}
button {
cursor: pointer;
}
/* Common Typography */
.eyebrow {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0 0 6px;
color: var(--accent-gold);
margin: 0 0 8px;
color: #b68b48;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
/* Page Shell Layout */
.page-shell {
min-height: 100vh;
padding: 16px 16px calc(90px + env(safe-area-inset-bottom));
background: var(--bg-app);
display: flex;
flex-direction: column;
}
/* Card Common */
.section-card {
background: var(--card-bg);
border-radius: 20px;
padding: 18px 16px;
margin-bottom: 14px;
border: 1px solid var(--line);
box-shadow: var(--shadow-card);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.section-title > span {
font-size: 16px;
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.01em;
}
.section-title small {
color: var(--text-muted);
font-size: 12px;
}
.section-title button {
background: transparent;
color: var(--primary);
font-size: 13px;
font-weight: 600;
padding: 4px 8px;
border-radius: 6px;
}
/* Auth Page */
.auth-page {
min-height: 100vh;
background: var(--bg-app);
display: flex;
flex-direction: column;
letter-spacing: 0.16em;
}
.auth-page { min-height: 100vh; background: var(--cream); }
.brand-panel {
padding: calc(36px + env(safe-area-inset-top)) 24px 38px;
min-height: 42vh;
padding: 54px 28px 42px;
color: #fff;
background:
radial-gradient(circle at 85% 15%, rgba(227, 176, 99, 0.24), transparent 45%),
radial-gradient(circle at 10% 90%, rgba(21, 102, 85, 0.4), transparent 50%),
linear-gradient(155deg, var(--primary-deep) 0%, var(--primary) 65%, #13463b 100%);
border-radius: 0 0 32px 32px;
position: relative;
radial-gradient(circle at 82% 12%, rgba(236, 202, 137, .28), transparent 28%),
linear-gradient(145deg, #0b5548, #123d37 74%);
border-radius: 0 0 42px 42px;
}
.brand-mark {
display: grid; place-items: center; width: 48px; height: 48px; margin-bottom: 28px;
border: 1px solid rgba(255,255,255,.42); border-radius: 50%; font-family: serif; font-size: 24px;
}
.brand-panel h1 { max-width: 560px; margin: 0; font-family: "Songti SC", serif; font-size: clamp(32px, 8vw, 54px); line-height: 1.2; font-weight: 500; }
.brand-copy { max-width: 480px; margin: 18px 0 24px; color: rgba(255,255,255,.72); line-height: 1.7; }
.feature-row { display: flex; gap: 10px; flex-wrap: wrap; }
.feature-row span { padding: 7px 12px; background: rgba(255,255,255,.09); border: 1px solid rgba(255,255,255,.14); border-radius: 20px; font-size: 12px; }
.auth-card { position: relative; width: min(520px, calc(100% - 32px)); margin: -20px auto 0; padding: 28px 22px; background: #fff; border-radius: 24px; box-shadow: 0 18px 50px rgba(24,59,52,.12); }
.auth-heading h2 { margin: 0; font-family: "Songti SC", serif; font-size: 28px; }
.auth-heading > p:last-child { margin: 8px 0 24px; color: #70817d; font-size: 14px; }
.auth-card .van-field { margin-bottom: 12px; padding: 14px 12px; background: #f7f7f3; border-radius: 12px; }
.auth-card .van-button { margin-top: 20px; height: 48px; }
.text-button { display: block; margin: 18px auto 0; border: 0; background: transparent; color: var(--green); font-size: 14px; }
.privacy-note { margin: 20px 0 0; color: #9a9f9c; font-size: 11px; line-height: 1.6; text-align: center; }
.page-shell { min-height: 100vh; padding: 28px 16px 110px; background: #f3f1ea; }
.page-header, .dashboard-header { max-width: 760px; margin: 0 auto 24px; padding: 12px 4px; }
.page-header h1, .dashboard-header h1 { margin: 0; font-family: "Songti SC", serif; font-size: 30px; font-weight: 600; }
.page-header > p:last-child, .dashboard-header > div > p:last-child { color: #73817d; line-height: 1.6; }
.section-card, .hero-card, .metric-strip { max-width: 760px; margin: 0 auto 16px; padding: 20px; background: #fff; border: 1px solid rgba(18,63,55,.06); border-radius: 20px; box-shadow: 0 8px 24px rgba(31,66,58,.05); }
.section-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.section-title > span { font-size: 18px; font-weight: 700; }
.section-title small { color: #82908c; }
.section-title button { border: 0; background: transparent; color: var(--green); }
.module-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.module-card { min-height: 160px; padding: 16px; text-align: left; border: 1px solid var(--line); border-radius: 16px; background: #f8f8f4; color: var(--deep); transition: .2s; }
.module-card.selected { color: #fff; background: var(--green); transform: translateY(-2px); box-shadow: 0 12px 24px rgba(12,80,69,.18); }
.module-icon { display: grid; place-items: center; width: 38px; height: 38px; margin-bottom: 18px; border: 1px solid currentColor; border-radius: 50%; font-size: 20px; }
.module-card strong, .module-card small { display: block; }
.module-card small { min-height: 36px; margin-top: 7px; opacity: .7; line-height: 1.5; }
.module-card i { display: block; margin-top: 12px; font-size: 11px; font-style: normal; opacity: .75; }
.form-card .van-field { padding: 14px 0; border-bottom: 1px solid #eee; }
.two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; }
.sticky-action { position: fixed; z-index: 10; left: 0; right: 0; bottom: 0; padding: 14px 20px max(14px, env(safe-area-inset-bottom)); background: rgba(248,246,240,.94); backdrop-filter: blur(14px); border-top: 1px solid var(--line); }
.sticky-action .van-button { max-width: 720px; height: 50px; margin: 0 auto; }
.dashboard-header { display: flex; align-items: center; justify-content: space-between; }
.avatar-button { width: 46px; height: 46px; color: #fff; border: 0; border-radius: 50%; background: var(--green); }
.hero-card { padding: 26px; color: #fff; background: linear-gradient(140deg, #0c5549, #163f38); }
.hero-card h2 { margin: 6px 0 12px; font-family: "Songti SC", serif; font-size: 28px; }
.hero-card > p:not(.eyebrow) { color: rgba(255,255,255,.72); line-height: 1.7; }
.tag-row { display: flex; gap: 8px; margin: 18px 0; }
.tag-row span { padding: 6px 12px; border-radius: 20px; background: rgba(255,255,255,.12); font-size: 12px; }
.metric-strip { display: grid; grid-template-columns: repeat(3, 1fr); padding: 16px 8px; }
.metric-strip div { padding: 4px 14px; border-right: 1px solid var(--line); }
.metric-strip div:last-child { border-right: 0; }
.metric-strip small, .metric-strip strong { display: block; }
.metric-strip small { color: #80908b; font-size: 11px; }
.metric-strip strong { margin-top: 7px; font-size: 21px; }
.metric-strip em { margin-left: 3px; font-size: 11px; font-style: normal; font-weight: 500; }
.plan-item { display: grid; grid-template-columns: 42px 1fr; gap: 12px; padding: 16px 0; border-top: 1px solid #edf0ec; }
.plan-item:first-of-type { border-top: 0; }
.plan-index { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; color: var(--green); background: #e9f1ed; }
.plan-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.plan-heading span { color: #b1813f; font-size: 12px; text-align: right; }
.plan-item p, .scene-row p { margin: 7px 0 0; color: #75847f; font-size: 13px; line-height: 1.6; }
.status-dot { color: #a27639 !important; }
.scene-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 15px; border-radius: 14px; background: #f6f7f3; }
.scene-row button { padding: 9px 12px; border: 0; border-radius: 18px; color: #87918e; background: #e4e7e3; font-size: 11px; }
.medical-note { max-width: 760px; margin: 24px auto; color: #969e9b; font-size: 11px; text-align: center; }
.loading-card { color: #73817d; text-align: center; }
@media (max-width: 520px) {
.module-grid { grid-template-columns: 1fr; }
.module-card { min-height: auto; }
.two-column { grid-template-columns: 1fr; }
.brand-panel { min-height: 390px; }
.auth-card { margin-top: -54px; }
.plan-heading { display: block; }
.plan-heading span { display: block; margin-top: 5px; text-align: left; }
}
.brand-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px 5px 6px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 24px;
font-size: 12px;
margin-bottom: 20px;
backdrop-filter: blur(8px);
}
.brand-badge-icon {
width: 24px;
height: 24px;
background: var(--accent-gold);
color: #fff;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 13px;
}
.brand-panel h1 {
font-size: 28px;
font-weight: 700;
line-height: 1.25;
margin-bottom: 10px;
letter-spacing: -0.02em;
}
.brand-copy {
font-size: 13px;
color: rgba(255, 255, 255, 0.76);
line-height: 1.6;
margin-bottom: 18px;
}
.feature-pill-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.feature-pill-row span {
padding: 5px 11px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 16px;
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.auth-card {
width: calc(100% - 32px);
margin: -24px auto 24px;
padding: 22px 20px 24px;
background: #fff;
border-radius: 24px;
box-shadow: var(--shadow-float);
border: 1px solid var(--line);
position: relative;
z-index: 2;
}
.auth-tab-switch {
display: flex;
background: #f1efea;
border-radius: 12px;
padding: 3px;
margin-bottom: 20px;
}
.auth-tab-switch button {
flex: 1;
padding: 9px 0;
font-size: 14px;
font-weight: 600;
border-radius: 9px;
color: var(--text-sub);
background: transparent;
transition: all 0.2s ease;
}
.auth-tab-switch button.active {
background: #ffffff;
color: var(--primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.auth-card .van-field {
margin-bottom: 12px;
padding: 12px 14px;
background: #f8f7f4;
border-radius: 14px;
border: 1px solid transparent;
transition: border-color 0.2s;
}
.auth-card .van-field:focus-within {
border-color: var(--primary);
background: #ffffff;
}
.auth-card .van-button {
margin-top: 14px;
height: 48px;
font-weight: 600;
font-size: 15px;
box-shadow: 0 8px 20px -2px rgba(12, 77, 63, 0.28);
}
.privacy-note {
margin-top: 18px;
font-size: 11px;
color: var(--text-muted);
text-align: center;
line-height: 1.5;
}
/* Onboarding Header */
.page-header {
margin-bottom: 18px;
padding: 6px 4px 0;
}
.page-header h1 {
font-size: 24px;
font-weight: 700;
color: var(--text-main);
margin: 4px 0 8px;
letter-spacing: -0.02em;
}
.page-header p {
color: var(--text-sub);
font-size: 13px;
line-height: 1.5;
}
/* Module Selection Grid (3 Columns) */
.module-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.module-card {
padding: 14px 10px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border: 1.5px solid var(--line);
border-radius: 16px;
background: #faf9f6;
color: var(--text-main);
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
}
.module-card.selected {
border-color: var(--primary);
background: var(--primary-subtle);
color: var(--primary);
}
.module-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: #ffffff;
display: grid;
place-items: center;
font-size: 18px;
margin-bottom: 8px;
border: 1px solid var(--line);
box-shadow: var(--shadow-card);
}
.module-card.selected .module-icon {
background: var(--primary);
color: #fff;
border-color: transparent;
}
.module-card strong {
font-size: 13px;
font-weight: 700;
margin-bottom: 4px;
}
.module-card small {
font-size: 10px;
color: var(--text-muted);
line-height: 1.3;
}
.module-card.selected small {
color: var(--primary-light);
}
.module-check {
margin-top: 8px;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
background: #eee;
color: var(--text-muted);
}
.module-card.selected .module-check {
background: var(--primary);
color: #fff;
}
/* Profile Form Elements */
.form-group-title {
font-size: 12px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 12px 0 8px;
}
.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.custom-field-box {
background: #f8f7f3;
border-radius: 14px;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid transparent;
transition: all 0.2s;
}
.custom-field-box:focus-within {
background: #fff;
border-color: var(--primary);
}
.custom-field-box .van-field {
padding: 0;
background: transparent;
}
/* Sticky Bottom Action */
.sticky-action {
position: fixed;
z-index: 50;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 460px;
bottom: 0;
padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
background: rgba(246, 245, 240, 0.92);
backdrop-filter: blur(16px);
border-top: 1px solid var(--line);
}
.sticky-action .van-button {
height: 48px;
font-size: 15px;
font-weight: 600;
box-shadow: 0 8px 24px -2px rgba(12, 77, 63, 0.25);
}
/* Dashboard Header */
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding: 8px 2px 0;
}
.header-meta h1 {
font-size: 22px;
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.02em;
line-height: 1.3;
}
.date-badge {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
color: var(--text-sub);
font-size: 12px;
}
.user-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 12px 5px 6px;
background: #fff;
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: var(--shadow-card);
transition: transform 0.15s;
}
.user-chip:active {
transform: scale(0.97);
}
.avatar-badge {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary);
color: #fff;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 700;
}
.user-chip-name {
font-size: 12px;
font-weight: 600;
color: var(--text-main);
}
/* Hero Health Card */
.hero-card {
padding: 22px 20px;
color: #fff;
border-radius: 24px;
background:
radial-gradient(circle at 90% 10%, rgba(227, 176, 99, 0.28), transparent 45%),
linear-gradient(145deg, var(--primary-deep) 0%, var(--primary) 70%, #155749 100%);
box-shadow: var(--shadow-float);
border: 1px solid rgba(255, 255, 255, 0.12);
margin-bottom: 14px;
}
.hero-top-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.hero-card h2 {
font-size: 22px;
font-weight: 700;
line-height: 1.3;
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.hero-summary {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.65;
margin-bottom: 16px;
}
.tag-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.tag-pill {
padding: 4px 10px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 12px;
font-size: 11px;
color: #fff;
}
.hero-action-row {
display: flex;
gap: 10px;
align-items: center;
margin-top: 8px;
}
/* Metrics Strip (3 Cards) */
.metric-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 14px;
}
.metric-card {
background: #fff;
border-radius: 16px;
padding: 12px 10px;
text-align: center;
border: 1px solid var(--line);
box-shadow: var(--shadow-card);
}
.metric-label {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
}
.metric-card strong {
display: block;
font-size: 19px;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
}
.metric-card em {
font-size: 11px;
font-style: normal;
font-weight: 500;
color: var(--text-sub);
margin-left: 2px;
}
.metric-desc {
font-size: 10px;
color: var(--accent-gold);
margin-top: 2px;
}
/* Actionable Plan Items */
.plan-item {
display: flex;
gap: 12px;
padding: 14px 0;
border-top: 1px solid var(--line);
}
.plan-item:first-of-type {
border-top: none;
padding-top: 2px;
}
.plan-item-icon {
width: 36px;
height: 36px;
border-radius: 12px;
background: var(--primary-subtle);
color: var(--primary);
display: grid;
place-items: center;
font-size: 16px;
flex-shrink: 0;
}
.plan-item-content {
flex: 1;
}
.plan-item-header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 4px;
}
.plan-item-header strong {
font-size: 14px;
font-weight: 700;
color: var(--text-main);
}
.plan-item-target {
font-size: 11px;
color: var(--accent-gold);
font-weight: 600;
background: var(--accent-light);
padding: 2px 7px;
border-radius: 6px;
}
.plan-item-advice {
font-size: 12px;
color: var(--text-sub);
line-height: 1.6;
}
/* Smart Space / IoT Card */
.smart-space-card {
background: linear-gradient(135deg, #ffffff 0%, #f7faf9 100%);
border: 1px solid rgba(12, 77, 63, 0.12);
}
.smart-scene-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
}
.scene-icon-title {
display: flex;
align-items: center;
gap: 10px;
}
.scene-icon {
width: 34px;
height: 34px;
border-radius: 10px;
background: #f2f5f4;
color: var(--primary);
display: grid;
place-items: center;
font-size: 16px;
}
.scene-text strong {
display: block;
font-size: 13px;
font-weight: 700;
color: var(--text-main);
}
.scene-text p {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.smart-badge {
font-size: 10px;
padding: 4px 8px;
border-radius: 8px;
background: #f1efea;
color: var(--text-sub);
font-weight: 600;
}
.medical-disclaimer {
text-align: center;
font-size: 11px;
color: var(--text-muted);
line-height: 1.6;
margin: 18px 8px 10px;
}
/* Mobile Touch Optimization */
@media (max-width: 360px) {
.module-grid {
gap: 6px;
}
.module-card {
padding: 10px 6px;
}
.module-card strong {
font-size: 12px;
}
.metric-card strong {
font-size: 16px;
}
}
+21 -51
View File
@@ -9,7 +9,7 @@ const router = useRouter()
const mode = ref<'login' | 'register'>('login')
const loading = ref(false)
const form = reactive({ phone: '', password: '', displayName: '' })
const title = computed(() => mode.value === 'login' ? '欢迎回来' : '开启专属健康档案')
const title = computed(() => mode.value === 'login' ? '欢迎回来' : '创建健康档案')
async function submit() {
loading.value = true
@@ -31,84 +31,54 @@ async function submit() {
<template>
<main class="auth-page">
<section class="brand-panel">
<div class="brand-badge">
<span class="brand-badge-icon">常</span>
<span>一人一方 · 康养管理</span>
</div>
<div class="brand-mark">常</div>
<p class="eyebrow">EVERGREEN HEALTH</p>
<h1>一人一方<br />健康与空间协同</h1>
<p class="brand-copy">以睡眠为切入点,融合五感空间与动态健康处方,开启治未病日常管理。</p>
<div class="feature-pill-row">
<span>🌿 六维处方</span>
<span>🌙 五感微环境</span>
<span>📊 闭环调理</span>
<h1>一人一方<br />健康与空间协同管理</h1>
<p class="brand-copy">从睡眠、运动与饮食开始,让环境与健康方案每天同步。</p>
<div class="feature-row">
<span>个性方案</span><span>智能场景</span><span>持续反馈</span>
</div>
</section>
<section class="auth-card">
<div class="auth-tab-switch">
<button
type="button"
:class="{ active: mode === 'login' }"
@click="mode = 'login'"
>
账号登录
</button>
<button
type="button"
:class="{ active: mode === 'register' }"
@click="mode = 'register'"
>
注册建档
</button>
</div>
<div class="auth-heading">
<h2 style="font-size: 20px; font-weight: 700; color: #142e27; margin-bottom: 4px;">{{ title }}</h2>
<p style="color: #72847d; font-size: 13px; margin-bottom: 16px;">
{{ mode === 'login' ? '登录查看今日健康处方与微环境状态' : '第一步:填写手机号快速创建健康账户' }}
</p>
<p class="eyebrow">常青树健康管理</p>
<h2>{{ title }}</h2>
<p>{{ mode === 'login' ? '登录后查看今日健康方案' : '第一步,建立您的专属健康账户' }}</p>
</div>
<van-form @submit="submit">
<van-field
v-if="mode === 'register'"
v-model="form.displayName"
label="您的姓名"
placeholder="请输入您的真实姓名或称呼"
label="姓名"
placeholder="请输入姓名或称呼"
:rules="[{ required: true, message: '请输入姓名' }]"
/>
<van-field
v-model="form.phone"
type="tel"
label="手机号码"
label="手机号"
maxlength="11"
placeholder="请输入 11 位手机号"
placeholder="请输入手机号"
:rules="[{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }]"
/>
<van-field
v-model="form.password"
type="password"
label="账户密码"
placeholder="请输入至少 8 位密码"
label="密码"
placeholder="至少 8 位"
:rules="[{ required: true, message: '请输入至少 8 位密码' }]"
/>
<van-button
block
round
color="#0c4d3f"
native-type="submit"
:loading="loading"
>
{{ mode === 'login' ? '登 录' : '创建档案并下一步' }}
<van-button block round color="#0c5045" native-type="submit" :loading="loading">
{{ mode === 'login' ? '登录' : '注册并继续' }}
</van-button>
</van-form>
<p class="privacy-note">
🔒 数据安全说明:所有身体体征与处方数据均遵循隐私保护规范,仅用于为您生成生活方式干预建议。
</p>
<button class="text-button" type="button" @click="mode = mode === 'login' ? 'register' : 'login'">
{{ mode === 'login' ? '还没有账户?立即注册' : '已有账户?返回登录' }}
</button>
<p class="privacy-note">继续即表示您同意仅将健康信息用于生成个人健康管理建议。</p>
</section>
</main>
</template>
+54 -188
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button as VanButton, showConfirmDialog, showToast } from 'vant'
import { Button as VanButton, showToast } from 'vant'
import { clearTokens } from '../services/auth'
import { healthApi, type Dashboard } from '../services/health'
@@ -9,26 +9,16 @@ const router = useRouter()
const loading = ref(true)
const generating = ref(false)
const dashboard = ref<Dashboard>()
const moduleNames: Record<string, { name: string; icon: string }> = {
sleep: { name: '睡眠', icon: '🌙' },
exercise: { name: '运动', icon: '🏃' },
diet: { name: '饮食', icon: '🥗' }
const moduleNames: Record<string, string> = {
sleep: '睡眠',
exercise: '运动',
diet: '饮食'
}
const greetingInfo = computed(() => {
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 11) return { text: '早安', icon: '🌅' }
if (hour < 14) return { text: '午安', icon: '☀️' }
if (hour < 19) return { text: '下午好', icon: '🍵' }
return { text: '晚上好', icon: '🌙' }
})
const todayDateText = computed(() => {
return new Intl.DateTimeFormat('zh-CN', {
month: 'long',
day: 'numeric',
weekday: 'short'
}).format(new Date())
if (hour < 11) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
})
async function load() {
@@ -47,7 +37,7 @@ async function generate() {
try {
await healthApi.generatePlan()
await load()
showToast({ type: 'success', message: '方案已生成并更新' })
showToast({ type: 'success', message: '方案已更新' })
} catch (error) {
showToast(error instanceof Error ? error.message : '生成失败')
} finally {
@@ -55,17 +45,9 @@ async function generate() {
}
}
function handleUserClick() {
showConfirmDialog({
title: '账户设置',
message: `当前登录:${dashboard.value?.user.displayName || '健康用户'}(${dashboard.value?.user.phone || ''}),确定要退出登录吗?`,
confirmButtonText: '退出登录',
cancelButtonText: '取消',
confirmButtonColor: '#c0392b'
}).then(() => {
clearTokens()
router.replace('/auth')
}).catch(() => {})
function logout() {
clearTokens()
router.replace('/auth')
}
onMounted(load)
@@ -73,184 +55,68 @@ onMounted(load)
<template>
<main class="page-shell dashboard-page">
<!-- 顶部状态栏与用户头像 -->
<header class="dashboard-header">
<div class="header-meta">
<span class="eyebrow">EVERGREEN · TODAY</span>
<h1>{{ greetingInfo.icon }} {{ greetingInfo.text }},{{ dashboard?.user.displayName || '您好' }}</h1>
<div class="date-badge">
<span>📅 {{ todayDateText }}</span>
<span>· 一人一方每日协同</span>
</div>
<div>
<p class="eyebrow">EVERGREEN · TODAY</p>
<h1>{{ greeting }},{{ dashboard?.user.displayName || '您好' }}</h1>
<p>{{ new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }).format(new Date()) }}</p>
</div>
<button class="user-chip" type="button" @click="handleUserClick">
<div class="avatar-badge">
{{ dashboard?.user.displayName?.slice(0, 1) || '常' }}
</div>
<span class="user-chip-name">我的账户</span>
<button class="avatar-button" type="button" @click="logout">
{{ dashboard?.user.displayName?.slice(0, 1) || '常' }}
</button>
</header>
<!-- 加载态骨架 -->
<section v-if="loading" class="section-card loading-card" style="padding: 40px 20px;">
<div style="font-size: 24px; margin-bottom: 10px;">🌿</div>
<p style="color: var(--text-sub); font-size: 14px;">正在同步您的今日健康方案与微环境参数…</p>
</section>
<section v-if="loading" class="section-card loading-card">正在读取今日健康方案…</section>
<template v-else>
<!-- 今日方案主卡片 (Hero Card) -->
<section class="hero-card">
<div class="hero-top-row">
<span class="eyebrow" style="color: #f1c37a;">今日专属干预方案</span>
<span v-if="dashboard?.currentPlan" style="font-size: 11px; padding: 2px 8px; border-radius: 10px; background: rgba(255,255,255,0.16); color: #fff;">
V{{ dashboard.currentPlan.version }} · 方案生效中
</span>
</div>
<p class="eyebrow">今日健康建议</p>
<h2>{{ dashboard?.currentPlan?.title || '尚未生成健康方案' }}</h2>
<p class="hero-summary">
{{ dashboard?.currentPlan?.summary || '完成基础板块选择与身体数据录入后,系统将为您量身定制每日干预方案。' }}
</p>
<!-- 启用的健康板块标签 -->
<p>{{ dashboard?.currentPlan?.summary || '完成健康板块选择后,即可生成第一版个人参数。' }}</p>
<div class="tag-row">
<span v-for="code in dashboard?.selectedModules" :key="code" class="tag-pill">
{{ moduleNames[code]?.icon }} {{ moduleNames[code]?.name || code }}
</span>
</div>
<!-- 操作按钮行 -->
<div class="hero-action-row">
<van-button
v-if="!dashboard?.currentPlan"
round
color="#f1c37a"
text-color="#183b34"
style="font-weight: 700;"
:loading="generating"
@click="generate"
>
✨ 立即生成专属方案
</van-button>
<van-button
v-else
round
size="small"
color="rgba(255,255,255,0.18)"
style="border: 1px solid rgba(255,255,255,0.3); color: #fff;"
:loading="generating"
@click="generate"
>
↺ 重新评估生成
</van-button>
<span v-for="code in dashboard?.selectedModules" :key="code">{{ moduleNames[code] || code }}</span>
</div>
<van-button
v-if="!dashboard?.currentPlan"
round
color="#f1c37a"
text-color="#183b34"
:loading="generating"
@click="generate"
>
立即生成
</van-button>
</section>
<!-- 核心指标状态栏 (3 列式卡片) -->
<section class="metric-strip">
<div class="metric-card">
<span class="metric-label">🌙 睡眠目标</span>
<strong>7–8<em>h</em></strong>
<span class="metric-desc">建议 23:00 前</span>
</div>
<div class="metric-card">
<span class="metric-label">🏃 活动目标</span>
<strong>30<em>min</em></strong>
<span class="metric-desc">中等强度</span>
</div>
<div class="metric-card">
<span class="metric-label">🔖 方案版本</span>
<strong>V{{ dashboard?.currentPlan?.version || 0 }}<em>版</em></strong>
<span class="metric-desc">动态演进</span>
</div>
<div><small>睡眠目标</small><strong>7–8<em>h</em></strong></div>
<div><small>活动目标</small><strong>30<em>min</em></strong></div>
<div><small>方案版本</small><strong>V{{ dashboard?.currentPlan?.version || 0 }}</strong></div>
</section>
<!-- 今日六维行动建议 -->
<section class="section-card">
<div class="section-title">
<span>今日执行清单</span>
<button type="button" @click="router.push('/onboarding')">
调整偏好 ›
</button>
</div>
<div v-if="dashboard?.currentPlan?.items?.length">
<article
v-for="item in dashboard.currentPlan.items"
:key="item.moduleCode"
class="plan-item"
>
<div class="plan-item-icon">
{{ item.moduleCode === 'sleep' ? '🌙' : item.moduleCode === 'exercise' ? '🏃' : '🥗' }}
</div>
<div class="plan-item-content">
<div class="plan-item-header">
<strong>{{ item.title }}</strong>
<span class="plan-item-target">{{ item.target }}</span>
</div>
<p class="plan-item-advice">{{ item.advice }}</p>
</div>
</article>
</div>
<div v-else style="text-align: center; padding: 20px; color: var(--text-muted); font-size: 13px;">
暂无执行建议,点击上方按钮即可一键生成方案。
</div>
</section>
<!-- 五感微环境空间联动卡片 -->
<section class="section-card smart-space-card">
<div class="section-title">
<span>五感舒睡空间</span>
<small class="smart-badge">⚡ Home Assistant 待接入</small>
</div>
<div class="smart-scene-box">
<div class="scene-icon-title">
<div class="scene-icon">🛏️</div>
<div class="scene-text">
<strong>深度舒睡模式</strong>
<p>预冷20℃ · 琥珀微光 · 新风正压 · 草本扩香</p>
</div>
<div class="section-title"><span>我的健康板块</span><button type="button" @click="router.push('/onboarding')">调整</button></div>
<article v-for="item in dashboard?.currentPlan?.items" :key="item.moduleCode" class="plan-item">
<div class="plan-index">{{ item.moduleCode === 'sleep' ? '☾' : item.moduleCode === 'exercise' ? '↗' : '◒' }}</div>
<div>
<div class="plan-heading"><strong>{{ item.title }}</strong><span>{{ item.target }}</span></div>
<p>{{ item.advice }}</p>
</div>
<button
type="button"
disabled
style="padding: 6px 10px; border-radius: 12px; font-size: 11px; background: #e8eae6; color: #8e9a95;"
>
准备就绪
</button>
</article>
</section>
<section class="section-card smart-card">
<div class="section-title"><span>健康空间</span><small class="status-dot">Home Assistant 待接入</small></div>
<div class="scene-row">
<div>
<strong>舒睡模式</strong>
<p>灯光 · 空调 · 香氛 · 除湿机</p>
</div>
<button type="button" disabled>下一阶段启用</button>
</div>
</section>
<!-- 免责与合规提示 -->
<p class="medical-disclaimer">
⚠️ 本建议由一人一方生活方式决策模型生成,仅用于日常健康管理与微环境优化,不构成医疗诊断或临床处方。
</p>
<p class="medical-note">健康建议仅用于日常管理演示,不构成医疗诊断或治疗意见。</p>
</template>
<!-- 底部固定快捷导航栏 -->
<nav class="sticky-action" style="display: flex; gap: 8px; justify-content: space-between;">
<van-button
block
round
color="#0c4d3f"
style="flex: 2; height: 46px; font-size: 14px;"
@click="generate"
:loading="generating"
>
✨ 刷新今日建议
</van-button>
<van-button
round
plain
color="#0c4d3f"
style="flex: 1; height: 46px; font-size: 14px;"
@click="router.push('/onboarding')"
>
调整偏好
</van-button>
</nav>
</main>
</template>
+20 -110
View File
@@ -8,9 +8,9 @@ const router = useRouter()
const loading = ref(false)
const selected = ref<string[]>(['sleep', 'exercise', 'diet'])
const modules = [
{ code: 'sleep', icon: '🌙', title: '睡眠质量', copy: '环境、节律与深睡' },
{ code: 'exercise', icon: '🏃', title: '运动习惯', copy: '日常活动与活力' },
{ code: 'diet', icon: '🥗', title: '饮食营养', copy: '三餐热量与偏好' }
{ code: 'sleep', icon: '☾', title: '睡眠质量', copy: '睡眠时长、环境与恢复情况' },
{ code: 'exercise', icon: '↗', title: '运动情况', copy: '日常活动、训练与久坐提醒' },
{ code: 'diet', icon: '◒', title: '饮食情况', copy: '三餐、饮水与营养偏好' }
]
const profile = reactive({
gender: '',
@@ -53,20 +53,13 @@ async function submit() {
<template>
<main class="page-shell">
<header class="page-header">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
<span class="eyebrow">STEP 01 · 基础建档</span>
<span style="font-size: 11px; color: var(--accent-gold); font-weight: 600;">一人一方 · 智能生成</span>
</div>
<h1>今天,您最想关注什么?</h1>
<p>选择核心关注板块并补充身体数据,系统将依此推导并生成第一版个人健康方案。</p>
<p class="eyebrow">PERSONAL SETUP · 01</p>
<h1>今天,想先关注什么?</h1>
<p>选择健康板块,系统将结合您的基础信息生成第一版个人参数。</p>
</header>
<!-- 1. 核心健康板块选择 -->
<section class="section-card">
<div class="section-title">
<span>重点关注板块</span>
<small>可多选,已选 {{ selected.length }}/3</small>
</div>
<div class="section-title"><span>健康板块</span><small>可多选</small></div>
<div class="module-grid">
<button
v-for="item in modules"
@@ -76,114 +69,31 @@ async function submit() {
type="button"
@click="toggle(item.code)"
>
<div class="module-icon">{{ item.icon }}</div>
<span class="module-icon">{{ item.icon }}</span>
<strong>{{ item.title }}</strong>
<small>{{ item.copy }}</small>
<span class="module-check">{{ selected.includes(item.code) ? '✓ 已选' : '+ 选择' }}</span>
<i>{{ selected.includes(item.code) ? '已选择' : '选择' }}</i>
</button>
</div>
</section>
<!-- 2. 基础体征与体能 -->
<section class="section-card">
<div class="section-title">
<span>身体基线数据</span>
<small>用于核算 BMI 与基础消耗</small>
</div>
<section class="section-card form-card">
<div class="section-title"><span>基础情况</span><small>用于生成参数</small></div>
<div class="two-column">
<div class="custom-field-box">
<van-field
v-model="profile.gender"
label="性别"
placeholder="男 / 女"
/>
</div>
<div class="custom-field-box">
<van-field
v-model.number="profile.heightCm"
type="number"
label="身高"
placeholder="例: 175"
>
<template #extra><span style="font-size: 12px; color: #8d9e97;">cm</span></template>
</van-field>
</div>
</div>
<div class="two-column">
<div class="custom-field-box">
<van-field
v-model.number="profile.weightKg"
type="number"
label="体重"
placeholder="例: 68"
>
<template #extra><span style="font-size: 12px; color: #8d9e97;">kg</span></template>
</van-field>
</div>
<div class="custom-field-box">
<van-field
v-model.number="profile.averageSleepHours"
type="number"
label="平日睡眠"
placeholder="例: 6.5"
>
<template #extra><span style="font-size: 12px; color: #8d9e97;">h</span></template>
</van-field>
</div>
</div>
<div class="custom-field-box">
<van-field
v-model.number="profile.dailyExerciseMinutes"
type="number"
label="日均活动"
placeholder="平均每天中等活动时长"
>
<template #extra><span style="font-size: 12px; color: #8d9e97;">分钟/天</span></template>
</van-field>
<van-field v-model="profile.gender" label="性别" placeholder="选填" />
<van-field v-model.number="profile.heightCm" type="number" label="身高" placeholder="cm" />
<van-field v-model.number="profile.weightKg" type="number" label="体重" placeholder="kg" />
<van-field v-model.number="profile.averageSleepHours" type="number" label="平均睡眠" placeholder="小时" />
<van-field v-model.number="profile.dailyExerciseMinutes" type="number" label="每日运动" placeholder="分钟" />
</div>
<van-field v-model="profile.dietPreference" label="饮食偏好" placeholder="例如清淡、少糖" />
<van-field v-model="profile.healthGoal" label="健康目标" type="textarea" rows="2" placeholder="例如改善睡眠、增加日常活动" />
</section>
<!-- 3. 饮食偏好与健康诉求 -->
<section class="section-card">
<div class="section-title">
<span>饮食习惯与预期目标</span>
<small>个性化调配依据</small>
</div>
<div class="custom-field-box">
<van-field
v-model="profile.dietPreference"
label="饮食偏好"
placeholder="例如:少油清淡、偏面食、少糖"
/>
</div>
<div class="custom-field-box">
<van-field
v-model="profile.healthGoal"
label="近期诉求"
type="textarea"
rows="2"
placeholder="例如:希望改善晚上入睡困难、减缓下午疲乏"
/>
</div>
</section>
<!-- 底部固定操作按钮 -->
<div class="sticky-action">
<van-button
block
round
color="#0c4d3f"
:loading="loading"
@click="submit"
>
✨ 立即生成我的专属健康方案
<van-button block round color="#0c5045" :loading="loading" @click="submit">
生成我的健康方案
</van-button>
</div>
</main>
</template>