From 9e5e77755e4b7ba17381ce909fdb1fb1b99f53d7 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Sun, 23 Aug 2026 22:05:28 +0800 Subject: [PATCH] =?UTF-8?q?[=E9=98=B6=E6=AE=B57.2]=20=E9=9A=90=E8=97=8F=20?= =?UTF-8?q?AI=20=E6=A8=A1=E5=9D=97=EF=BC=9B=E5=89=A9=E4=BD=99=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E8=BF=81=E7=A7=BB=E5=88=B0=E8=AE=BE=E8=AE=A1=E4=BB=A4?= =?UTF-8?q?=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI 模块暂时下线(待重新设计),加 FEATURES 开关集中控制: - 导航不显示"建议",路由也不注册 —— 只隐藏入口而保留可访问的 URL 等于没隐藏 - 设置页的模型清单区块一并隐藏,且不再请求 /analysis/models - 页面与后端端点都保留,重新打开是一行改动 登录/同步/建议/设置四个页面此前仍在用旧样式(共 101 处硬编码颜色), 深色模式下全部失效。已改写为设计令牌。 fix(a11y): 主按钮白字对比度不足 - 白字在 --accent 上实测 4.42:1(浅)/ 3.64:1(深),均低于正文 所需的 4.5:1 - 新增 --accent-solid 专用于填充面(按钮、选中标签),取同一蓝色阶 更深的一步:5.39:1 / 6.63:1;--accent 继续用于文字、边框和标记 fix(viz): 一年档柱形图不可读 - 365 根柱子在 607px 宽度下每根仅 1.66px,既看不清也远低于 ~24px 的悬停命中下限 - 超过 90 个点时柱形自动改为面积图,并在副标题里说明原因。 折线/面积的十字准星吸附最近点,本就不依赖逐标记命中区 其他: - 统计数值精度按量级取(11,013.99 既是虚假精度,又长到把单位挤到 第二行) - 设置页隐私说明更正:改用 OAuth 令牌后同步已不需要每次输密码 Co-Authored-By: Claude Haiku 4.5 --- client/src/App.tsx | 5 +- client/src/components/Layout.tsx | 3 +- client/src/features.ts | 12 ++ client/src/pages/DataSync.css | 291 +++++++++++---------------- client/src/pages/Login.css | 138 ++++--------- client/src/pages/Pages.css | 6 +- client/src/pages/Recommendations.css | 143 +++++++------ client/src/pages/Settings.css | 114 ++++++----- client/src/pages/Settings.tsx | 14 +- client/src/pages/Trends.tsx | 29 ++- client/src/theme.css | 7 + 11 files changed, 358 insertions(+), 404 deletions(-) create mode 100644 client/src/features.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 79de743..ccfa350 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,6 +9,7 @@ import Recommendations from './pages/Recommendations'; import Settings from './pages/Settings'; import Sleep from './pages/Sleep'; import Achievements from './pages/Achievements'; +import { FEATURES } from './features'; function App() { return ( @@ -27,7 +28,9 @@ function App() { } /> } /> } /> - } /> + {FEATURES.ai && ( + } /> + )} } /> diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 7e24575..e12a0a9 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; +import { FEATURES } from '../features'; import './Layout.css'; interface LayoutProps { @@ -11,7 +12,7 @@ const NAV = [ { path: '/trends', label: '趋势' }, { path: '/sleep', label: '睡眠' }, { path: '/achievements', label: '成就' }, - { path: '/recommendations', label: '建议' }, + ...(FEATURES.ai ? [{ path: '/recommendations', label: '建议' }] : []), { path: '/sync', label: '同步' }, { path: '/settings', label: '设置' }, ]; diff --git a/client/src/features.ts b/client/src/features.ts new file mode 100644 index 0000000..c6ddf8a --- /dev/null +++ b/client/src/features.ts @@ -0,0 +1,12 @@ +/** + * Feature switches. + * + * `ai` is off while the recommendation module is being reworked: the pages and + * the backend endpoints still exist, so turning it back on is a one-line + * change rather than a rebuild. Nothing links to the route while it is off, + * and the route itself is not registered — a hidden nav entry with a live URL + * would still be reachable by typing it. + */ +export const FEATURES = { + ai: false, +}; diff --git a/client/src/pages/DataSync.css b/client/src/pages/DataSync.css index 4704760..3edc7f5 100644 --- a/client/src/pages/DataSync.css +++ b/client/src/pages/DataSync.css @@ -1,203 +1,76 @@ .sync-container { display: grid; - gap: 2rem; - max-width: 600px; - margin: 0 auto; -} - -.subtitle { - color: #666; - font-size: 1rem; - margin-top: -0.5rem; - margin-bottom: 1.5rem; + gap: 1.25rem; + max-width: 620px; } .status-card { - background: white; - border: 1px solid #eee; - border-radius: 8px; - padding: 1.5rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + box-shadow: var(--shadow); } .status-card h3 { - margin-top: 0; - margin-bottom: 1rem; - color: #333; - font-size: 1.1rem; + margin: 0 0 0.9rem; + font-size: 0.95rem; + font-weight: 650; + color: var(--text-primary); } .status-info { display: flex; flex-direction: column; - gap: 1rem; + gap: 0.5rem; } .status-item { display: flex; justify-content: space-between; - align-items: center; - padding: 0.75rem; - background: #f9f9f9; - border-radius: 6px; - font-size: 0.95rem; -} - -.status-item.error { - background: #fee; - border: 1px solid #fcc; + align-items: baseline; + gap: 1rem; + padding: 0.55rem 0.7rem; + background: var(--surface-0); + border-radius: 7px; + font-size: 0.88rem; } .status-item .label { - font-weight: 600; - color: #666; + color: var(--text-muted); + font-size: 0.82rem; } .status-item .value { - color: #333; - font-weight: 500; + color: var(--text-primary); + font-weight: 550; + text-align: right; +} + +/* Colour reinforces the status word already present in the markup; it never + carries the meaning alone — the light-surface status steps are below 3:1. */ +.value.status-idle { color: var(--status-good); } +.value.status-syncing { color: var(--accent); } +.value.status-error { color: var(--status-critical); } + +.status-item.error { + background: color-mix(in srgb, var(--status-critical) 8%, var(--surface-0)); + border: 1px solid color-mix(in srgb, var(--status-critical) 25%, transparent); +} + +.status-item.error .value { + color: var(--status-critical); + font-weight: 400; + font-size: 0.8rem; + word-break: break-word; } .sync-actions { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; -} - -.btn-large { - width: 100%; - max-width: 300px; - padding: 1rem 2rem; - font-size: 1.1rem; -} - -.help-text { - text-align: center; - color: #999; - font-size: 0.9rem; - max-width: 400px; - line-height: 1.5; -} - -.error-message, -.success-message { - padding: 1rem; - border-radius: 8px; - font-size: 0.95rem; - animation: slideIn 0.3s ease-out; -} - -.error-message { - background: #fee; - border: 1px solid #fcc; - color: #c33; -} - -.success-message { - background: #efe; - border: 1px solid #cfc; - color: #333; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.info-box { - background: #f0f7ff; - border: 1px solid #d0e8ff; - border-radius: 8px; - padding: 1.5rem; -} - -.info-box h4 { - margin-top: 0; - margin-bottom: 1rem; - color: #333; - font-size: 1rem; -} - -.info-box ul { - margin: 0; - padding-left: 1.5rem; - list-style: disc; - color: #666; - line-height: 1.8; -} - -.info-box li { - margin-bottom: 0.5rem; - font-size: 0.9rem; -} - -@media (max-width: 768px) { - .sync-container { - gap: 1.5rem; - } - - .status-item { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .btn-large { - max-width: 100%; - } -} - -.cmd { - background: #2d2d33; - color: #e6e6e6; - padding: 0.9rem 1rem; - border-radius: 6px; - font-size: 0.82rem; - line-height: 1.7; - overflow-x: auto; - margin: 0; - font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; -} - -.mfa-card { - border-color: #667eea; - background: #f7f8ff; -} - -.code-input { - font-size: 1.5rem; - letter-spacing: 0.35em; - text-align: center; - font-family: Menlo, Monaco, Consolas, monospace; -} - -.mfa-buttons { - display: flex; - gap: 0.75rem; - align-items: center; -} - -.btn-plain { - background: none; - border: 1px solid #ddd; - color: #777; - padding: 0.6rem 1.1rem; - border-radius: 6px; - cursor: pointer; - font-family: inherit; - font-size: 0.95rem; -} - -.btn-plain:hover { - border-color: #bbb; - color: #555; + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + box-shadow: var(--shadow); } .sync-choices { @@ -206,6 +79,24 @@ flex-wrap: wrap; } +.mfa-card { + border-color: var(--accent); + background: var(--accent-soft); +} + +.code-input { + font-size: 1.4rem; + letter-spacing: 0.32em; + text-align: center; + font-family: Menlo, Monaco, Consolas, monospace; +} + +.mfa-buttons { + display: flex; + gap: 0.7rem; + align-items: center; +} + .progress-block { display: flex; flex-direction: column; @@ -216,7 +107,7 @@ display: flex; justify-content: space-between; align-items: baseline; - font-size: 0.88rem; + font-size: 0.9rem; color: var(--text-primary); } @@ -239,3 +130,55 @@ border-radius: 999px; transition: width 0.4s ease; } + +.info-box { + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; +} + +.info-box h4 { + margin: 0 0 0.7rem; + font-size: 0.9rem; + font-weight: 650; + color: var(--text-primary); +} + +.info-box ul { + margin: 0; + padding-left: 1.2rem; + color: var(--text-secondary); + line-height: 1.9; + font-size: 0.85rem; +} + +.info-box code { + background: var(--surface-0); + padding: 0.1rem 0.35rem; + border-radius: 4px; + font-size: 0.85em; +} + +.cmd { + background: var(--surface-0); + border: 1px solid var(--border); + color: var(--text-primary); + padding: 0.85rem 1rem; + border-radius: 7px; + font-size: 0.8rem; + line-height: 1.8; + overflow-x: auto; + margin: 0; + font-family: Menlo, Monaco, Consolas, monospace; +} + +@media (max-width: 600px) { + .status-item { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + .status-item .value { text-align: left; } + .sync-choices .btn { flex: 1; } +} diff --git a/client/src/pages/Login.css b/client/src/pages/Login.css index 91c7e16..bf0068a 100644 --- a/client/src/pages/Login.css +++ b/client/src/pages/Login.css @@ -3,154 +3,98 @@ align-items: center; justify-content: center; min-height: 100vh; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - padding: 1rem; + background: var(--surface-0); + padding: 1.5rem; } .login-card { - background: white; - border-radius: 12px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: var(--shadow); width: 100%; - max-width: 400px; - padding: 2rem; - animation: slideUp 0.3s ease-out; + max-width: 380px; + padding: 2rem 1.85rem; + animation: rise 0.25s ease-out; } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } +@keyframes rise { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } .login-header { text-align: center; - margin-bottom: 2rem; + margin-bottom: 1.75rem; } .login-header h1 { - font-size: 1.8rem; - margin: 0 0 0.5rem 0; - color: #333; + font-size: 1.25rem; + font-weight: 680; + margin: 0 0 0.35rem; + color: var(--text-primary); } .login-header p { - color: #999; - font-size: 0.9rem; + color: var(--text-muted); + font-size: 0.84rem; margin: 0; } .login-tabs { display: flex; - gap: 0; margin-bottom: 1.5rem; - border-bottom: 2px solid #f0f0f0; + border-bottom: 1px solid var(--border); } .tab-button { flex: 1; - padding: 0.75rem; + padding: 0.6rem; border: none; background: none; - color: #999; - font-size: 1rem; + color: var(--text-muted); + font-size: 0.92rem; cursor: pointer; - border-bottom: 3px solid transparent; - transition: all 0.3s ease; - margin-bottom: -2px; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 0.15s ease, border-color 0.15s ease; + font-family: inherit; } .tab-button:hover { - color: #667eea; + color: var(--text-primary); } .tab-button.active { - color: #667eea; - border-bottom-color: #667eea; -} - -.error-message { - background-color: #fee; - border: 1px solid #fcc; - color: #c33; - padding: 0.75rem; - border-radius: 6px; - margin-bottom: 1rem; - font-size: 0.9rem; + color: var(--accent); + border-bottom-color: var(--accent); + font-weight: 600; } .login-form { display: flex; flex-direction: column; - gap: 1rem; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.form-group label { - font-weight: 600; - color: #333; - font-size: 0.9rem; -} - -.form-group input { - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 1rem; - transition: all 0.3s ease; - font-family: inherit; -} - -.form-group input:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -.form-group input:disabled { - background-color: #f5f5f5; - cursor: not-allowed; } .submit-button { - padding: 0.75rem 1rem; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; + padding: 0.7rem 1rem; + background: var(--accent-solid); + color: #fff; border: none; - border-radius: 6px; - font-size: 1rem; + border-radius: 8px; + font-size: 0.95rem; font-weight: 600; cursor: pointer; - transition: all 0.3s ease; - margin-top: 0.5rem; + transition: filter 0.15s ease; + margin-top: 0.35rem; + font-family: inherit; } .submit-button:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); + filter: brightness(1.08); } .submit-button:disabled { - opacity: 0.7; + opacity: 0.55; cursor: not-allowed; } - -@media (max-width: 480px) { - .login-card { - padding: 1.5rem; - } - - .login-header h1 { - font-size: 1.5rem; - } -} diff --git a/client/src/pages/Pages.css b/client/src/pages/Pages.css index f56e75d..6fac603 100644 --- a/client/src/pages/Pages.css +++ b/client/src/pages/Pages.css @@ -104,8 +104,8 @@ .range-tab.active, .metric-tab.active { - background: var(--accent); - border-color: var(--accent); + background: var(--accent-solid); + border-color: var(--accent-solid); color: #fff; } @@ -186,7 +186,7 @@ } .btn-primary { - background: var(--accent); + background: var(--accent-solid); color: #fff; } diff --git a/client/src/pages/Recommendations.css b/client/src/pages/Recommendations.css index 9190c04..9711942 100644 --- a/client/src/pages/Recommendations.css +++ b/client/src/pages/Recommendations.css @@ -1,104 +1,99 @@ .model-bar { display: flex; align-items: center; - gap: 0.75rem; + gap: 0.7rem; flex-wrap: wrap; - padding: 1rem; - background: #f9f9fb; - border: 1px solid #eee; - border-radius: 8px; + padding: 0.9rem 1rem; + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); margin-bottom: 1rem; } .model-bar label { font-weight: 600; - color: #555; - font-size: 0.9rem; + color: var(--text-secondary); + font-size: 0.85rem; } .model-bar select { flex: 1; min-width: 220px; - padding: 0.5rem 0.75rem; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 0.95rem; + padding: 0.45rem 0.7rem; + border: 1px solid var(--border-strong); + border-radius: 7px; + font-size: 0.9rem; font-family: inherit; - background: white; + background: var(--surface-2); + color: var(--text-primary); } .model-bar select:disabled { - background: #f0f0f0; - color: #999; + background: var(--surface-0); + color: var(--text-muted); } .meta-bar { - padding: 0.75rem 1rem; - border-radius: 6px; - font-size: 0.9rem; + padding: 0.7rem 1rem; + border-radius: 8px; + font-size: 0.85rem; margin-bottom: 1.25rem; + border: 1px solid var(--border); } .meta-bar.ai { - background: #eef2ff; - border: 1px solid #d6dfff; - color: #3b4a94; + background: var(--accent-soft); + border-color: color-mix(in srgb, var(--accent) 30%, transparent); + color: var(--text-primary); } .meta-bar.rules { - background: #fff8e6; - border: 1px solid #ffe4a3; - color: #7a5a10; + background: color-mix(in srgb, var(--status-warning) 12%, var(--surface-1)); + border-color: color-mix(in srgb, var(--status-warning) 35%, transparent); + color: var(--text-primary); } .meta-bar .fallback { - opacity: 0.8; + color: var(--text-secondary); margin-left: 0.4rem; } .notice { - padding: 1rem; - background: #fff8e6; - border: 1px solid #ffe4a3; + padding: 0.9rem 1rem; + background: color-mix(in srgb, var(--status-warning) 12%, var(--surface-1)); + border: 1px solid color-mix(in srgb, var(--status-warning) 35%, transparent); border-radius: 8px; - color: #7a5a10; - font-size: 0.9rem; - line-height: 1.7; + color: var(--text-secondary); + font-size: 0.85rem; + line-height: 1.8; margin-bottom: 1rem; } .notice code { - background: rgba(0, 0, 0, 0.06); + background: var(--surface-0); padding: 0.1rem 0.35rem; - border-radius: 3px; - font-size: 0.85em; + border-radius: 4px; + font-size: 0.88em; } .rec-list { display: grid; - gap: 1rem; + gap: 0.85rem; } .rec-card { - background: white; - border: 1px solid #eee; - border-left: 4px solid #ccc; - border-radius: 8px; - padding: 1.1rem 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + background: var(--surface-1); + border: 1px solid var(--border); + border-left: 3px solid var(--border-strong); + border-radius: var(--radius); + padding: 1rem 1.15rem; + box-shadow: var(--shadow); } -.rec-card.priority-high { - border-left-color: #e05252; -} - -.rec-card.priority-medium { - border-left-color: #e0a052; -} - -.rec-card.priority-low { - border-left-color: #6bbf7f; -} +/* Priority is spelled out in the badge text; the stripe is reinforcement. */ +.rec-card.priority-high { border-left-color: var(--status-critical); } +.rec-card.priority-medium { border-left-color: var(--status-serious); } +.rec-card.priority-low { border-left-color: var(--status-good); } .rec-header { display: flex; @@ -109,52 +104,57 @@ } .rec-category { - font-weight: 700; - color: #333; - font-size: 1rem; + font-weight: 650; + color: var(--text-primary); + font-size: 0.95rem; } .rec-priority { - font-size: 0.75rem; - font-weight: 700; - padding: 0.15rem 0.6rem; + font-size: 0.72rem; + font-weight: 650; + padding: 0.15rem 0.55rem; border-radius: 999px; white-space: nowrap; + border: 1px solid transparent; } .rec-priority.priority-high { - background: #fdecec; - color: #b03030; + background: color-mix(in srgb, var(--status-critical) 12%, transparent); + color: var(--status-critical); + border-color: color-mix(in srgb, var(--status-critical) 30%, transparent); } .rec-priority.priority-medium { - background: #fdf3e6; - color: #9a6412; + background: color-mix(in srgb, var(--status-serious) 14%, transparent); + color: var(--status-serious); + border-color: color-mix(in srgb, var(--status-serious) 35%, transparent); } .rec-priority.priority-low { - background: #eaf6ed; - color: #35733f; + background: color-mix(in srgb, var(--status-good) 12%, transparent); + color: var(--status-good); + border-color: color-mix(in srgb, var(--status-good) 30%, transparent); } .rec-body { - color: #444; - line-height: 1.75; + color: var(--text-secondary); + line-height: 1.85; margin: 0; + font-size: 0.9rem; } .rec-based-on { - margin-top: 0.7rem; - font-size: 0.8rem; - color: #999; + margin-top: 0.65rem; + font-size: 0.76rem; + color: var(--text-muted); } .disclaimer { margin-top: 2rem; padding-top: 1rem; - border-top: 1px solid #eee; - font-size: 0.82rem; - color: #999; + border-top: 1px solid var(--border); + font-size: 0.78rem; + color: var(--text-muted); text-align: center; } @@ -163,7 +163,6 @@ flex-direction: column; align-items: stretch; } - .model-bar select, .model-bar .btn { width: 100%; diff --git a/client/src/pages/Settings.css b/client/src/pages/Settings.css index df5dc6d..9341cd7 100644 --- a/client/src/pages/Settings.css +++ b/client/src/pages/Settings.css @@ -1,7 +1,7 @@ .settings-section { - margin-bottom: 2.5rem; + margin-bottom: 2.25rem; padding-bottom: 1.5rem; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid var(--border); } .settings-section:last-child { @@ -9,115 +9,129 @@ } .settings-section h3 { - font-size: 1.1rem; - color: #333; - margin: 0 0 0.6rem 0; + font-size: 1rem; + font-weight: 650; + color: var(--text-primary); + margin: 0 0 0.55rem; } .settings-hint { - color: #777; - font-size: 0.9rem; - line-height: 1.7; + color: var(--text-muted); + font-size: 0.86rem; + line-height: 1.8; margin-bottom: 1rem; } .settings-hint code, .model-table code { - background: #f2f2f5; + background: var(--surface-0); padding: 0.1rem 0.35rem; - border-radius: 3px; - font-size: 0.85em; + border-radius: 4px; + font-size: 0.88em; + color: var(--text-secondary); } .model-table { width: 100%; border-collapse: collapse; - font-size: 0.9rem; + font-size: 0.86rem; + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; } .model-table th { text-align: left; - padding: 0.6rem 0.75rem; - border-bottom: 2px solid #eee; - color: #888; - font-size: 0.8rem; - font-weight: 600; + padding: 0.6rem 0.8rem; + background: var(--surface-2); + border-bottom: 1px solid var(--border); + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 650; text-transform: uppercase; - letter-spacing: 0.03em; + letter-spacing: 0.04em; } .model-table td { - padding: 0.7rem 0.75rem; - border-bottom: 1px solid #f4f4f4; - color: #444; + padding: 0.65rem 0.8rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); +} + +.model-table tbody tr:last-child td { + border-bottom: none; } .model-name { - color: #777; - font-size: 0.85rem; + color: var(--text-muted); + font-size: 0.82rem; } +/* State is spelled out in the badge text; colour only reinforces it. */ .badge { display: inline-block; - padding: 0.15rem 0.6rem; + padding: 0.15rem 0.55rem; border-radius: 999px; - font-size: 0.75rem; - font-weight: 600; + font-size: 0.74rem; + font-weight: 650; white-space: nowrap; + border: 1px solid transparent; } .badge.ok { - background: #eaf6ed; - color: #35733f; + background: color-mix(in srgb, var(--status-good) 12%, transparent); + color: var(--status-good); + border-color: color-mix(in srgb, var(--status-good) 30%, transparent); } .badge.off { - background: #f2f2f2; - color: #999; + background: var(--surface-0); + color: var(--text-muted); + border-color: var(--border); } .badge-default { margin-left: 0.4rem; padding: 0.05rem 0.4rem; - background: #eef2ff; - color: #4a5aa8; - border-radius: 3px; + background: var(--accent-soft); + color: var(--accent); + border-radius: 4px; font-size: 0.7rem; - font-weight: 600; + font-weight: 650; } .settings-list { margin: 0; - padding-left: 1.3rem; - color: #555; - line-height: 1.9; - font-size: 0.9rem; + padding-left: 1.25rem; + color: var(--text-secondary); + line-height: 1.95; + font-size: 0.88rem; } .btn-danger { - background: #fff; - color: #c04747; - border: 1px solid #e0b4b4; - padding: 0.6rem 1.2rem; - border-radius: 6px; - font-size: 0.95rem; + background: var(--surface-1); + color: var(--status-critical); + border: 1px solid color-mix(in srgb, var(--status-critical) 40%, transparent); + padding: 0.55rem 1.1rem; + border-radius: 8px; + font-size: 0.92rem; cursor: pointer; font-family: inherit; - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease; } .btn-danger:hover { - background: #fdecec; - border-color: #c04747; + background: color-mix(in srgb, var(--status-critical) 8%, var(--surface-1)); + border-color: var(--status-critical); } @media (max-width: 600px) { .model-table { - font-size: 0.8rem; + font-size: 0.78rem; } - .model-table th, .model-table td { - padding: 0.5rem 0.4rem; + padding: 0.45rem 0.5rem; } } diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index d96a990..e05eea6 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { apiClient, ModelInfo } from '../services/api'; +import { FEATURES } from '../features'; import './Settings.css'; function Settings() { @@ -9,6 +10,10 @@ function Settings() { const [loading, setLoading] = useState(true); useEffect(() => { + if (!FEATURES.ai) { + setLoading(false); + return; + } apiClient .getModels() .then(setModels) @@ -25,6 +30,7 @@ function Settings() {

设置

+ {FEATURES.ai && (

AI 模型

@@ -67,16 +73,18 @@ function Settings() { )}

+ )}

数据与隐私

  • 健康数据保存在自建数据库中,不上传第三方服务。
  • -
  • Garmin 密码只以哈希形式存储,无法还原,每次同步需重新输入。
  • - 使用 AI 建议时,你的每日指标会以匿名 CSV 形式发送给所选模型厂商; - 其中不含姓名、邮箱或设备标识。 + Garmin 账号通过 OAuth 令牌授权,密码不会被保存;令牌约一年后过期, + 届时在「同步」页重新绑定一次即可。
  • +
  • 本站登录密码以 PBKDF2 加盐哈希存储,无法还原。
  • +
diff --git a/client/src/pages/Trends.tsx b/client/src/pages/Trends.tsx index 373f9f4..99964e5 100644 --- a/client/src/pages/Trends.tsx +++ b/client/src/pages/Trends.tsx @@ -164,6 +164,16 @@ function Trends() { const group = GROUPS.find((g) => g.id === active) ?? GROUPS[0]; + /* Bars stop working long before a year fits: 365 of them across a typical + chart leaves ~1.7px each, well under a readable mark or a usable hover + target. Past this many points the same data is drawn as an area instead — + a line handles density natively because its crosshair snaps to the + nearest x rather than needing per-mark hit areas. */ + const DENSE_ABOVE = 90; + const dense = days.length > DENSE_ABOVE; + const renderType = + dense && (group.type === 'bar') ? 'area' as const : group.type; + const rows = useMemo( () => days.map((d) => { @@ -180,8 +190,16 @@ function Trends() { const primary = group.series[0]; const summary = stats(rows.map((r) => r[primary.key])); - const fmt = (v: number) => - (Math.round(v * 100) / 100).toLocaleString(undefined, { maximumFractionDigits: 2 }); + /* Precision by magnitude: "11,013.99 步" is both false precision and long + enough to wrap its unit onto a second line. */ + const fmt = (v: number) => { + const abs = Math.abs(v); + const decimals = abs >= 100 ? 0 : abs >= 10 ? 1 : 2; + return v.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }); + }; return (
@@ -241,8 +259,13 @@ function Trends() {