[阶段7.2] 隐藏 AI 模块;剩余页面迁移到设计令牌
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
<Route path="/trends" element={<Trends />} />
|
||||
<Route path="/sleep" element={<Sleep />} />
|
||||
<Route path="/achievements" element={<Achievements />} />
|
||||
{FEATURES.ai && (
|
||||
<Route path="/recommendations" element={<Recommendations />} />
|
||||
)}
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
||||
@@ -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: '设置' },
|
||||
];
|
||||
|
||||
12
client/src/features.ts
Normal file
12
client/src/features.ts
Normal file
@@ -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,
|
||||
};
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<div className="page">
|
||||
<h2>设置</h2>
|
||||
|
||||
{FEATURES.ai && (
|
||||
<section className="settings-section">
|
||||
<h3>AI 模型</h3>
|
||||
<p className="settings-hint">
|
||||
@@ -67,16 +73,18 @@ function Settings() {
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>数据与隐私</h3>
|
||||
<ul className="settings-list">
|
||||
<li>健康数据保存在自建数据库中,不上传第三方服务。</li>
|
||||
<li>Garmin 密码只以哈希形式存储,无法还原,每次同步需重新输入。</li>
|
||||
<li>
|
||||
使用 AI 建议时,你的每日指标会以匿名 CSV 形式发送给所选模型厂商;
|
||||
其中不含姓名、邮箱或设备标识。
|
||||
Garmin 账号通过 OAuth 令牌授权,密码不会被保存;令牌约一年后过期,
|
||||
届时在「同步」页重新绑定一次即可。
|
||||
</li>
|
||||
<li>本站登录密码以 PBKDF2 加盐哈希存储,无法还原。</li>
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="page">
|
||||
@@ -241,8 +259,13 @@ function Trends() {
|
||||
<Chart
|
||||
title={group.label}
|
||||
unit={group.unit}
|
||||
subtitle={
|
||||
dense && renderType !== group.type
|
||||
? `${days.length} 天数据:柱形在此密度下不可读,已改用面积图`
|
||||
: undefined
|
||||
}
|
||||
data={rows}
|
||||
type={group.type}
|
||||
type={renderType}
|
||||
series={group.series}
|
||||
height={320}
|
||||
footer={group.note}
|
||||
|
||||
@@ -39,7 +39,12 @@
|
||||
--status-critical: #d03b3b;
|
||||
|
||||
--grid: #eceae5;
|
||||
/* Separate token for filled surfaces (buttons, active tabs). White text on
|
||||
--accent measures only 4.42:1 light / 3.64:1 dark — under the 4.5:1 body
|
||||
floor — so fills use a darker step from the same blue ramp: 5.39:1 light,
|
||||
6.63:1 dark. --accent stays for text, borders and marks. */
|
||||
--accent: #2a78d6;
|
||||
--accent-solid: #256abf;
|
||||
--accent-soft: #eef4fd;
|
||||
|
||||
--radius: 10px;
|
||||
@@ -70,6 +75,7 @@
|
||||
|
||||
--grid: #2c2c28;
|
||||
--accent: #3987e5;
|
||||
--accent-solid: #1c5cab;
|
||||
--accent-soft: #1d2938;
|
||||
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
@@ -96,6 +102,7 @@
|
||||
|
||||
--grid: #2c2c28;
|
||||
--accent: #3987e5;
|
||||
--accent-solid: #1c5cab;
|
||||
--accent-soft: #1d2938;
|
||||
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
|
||||
Reference in New Issue
Block a user