[阶段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 Settings from './pages/Settings';
|
||||||
import Sleep from './pages/Sleep';
|
import Sleep from './pages/Sleep';
|
||||||
import Achievements from './pages/Achievements';
|
import Achievements from './pages/Achievements';
|
||||||
|
import { FEATURES } from './features';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +28,9 @@ function App() {
|
|||||||
<Route path="/trends" element={<Trends />} />
|
<Route path="/trends" element={<Trends />} />
|
||||||
<Route path="/sleep" element={<Sleep />} />
|
<Route path="/sleep" element={<Sleep />} />
|
||||||
<Route path="/achievements" element={<Achievements />} />
|
<Route path="/achievements" element={<Achievements />} />
|
||||||
|
{FEATURES.ai && (
|
||||||
<Route path="/recommendations" element={<Recommendations />} />
|
<Route path="/recommendations" element={<Recommendations />} />
|
||||||
|
)}
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { FEATURES } from '../features';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -11,7 +12,7 @@ const NAV = [
|
|||||||
{ path: '/trends', label: '趋势' },
|
{ path: '/trends', label: '趋势' },
|
||||||
{ path: '/sleep', label: '睡眠' },
|
{ path: '/sleep', label: '睡眠' },
|
||||||
{ path: '/achievements', label: '成就' },
|
{ path: '/achievements', label: '成就' },
|
||||||
{ path: '/recommendations', label: '建议' },
|
...(FEATURES.ai ? [{ path: '/recommendations', label: '建议' }] : []),
|
||||||
{ path: '/sync', label: '同步' },
|
{ path: '/sync', label: '同步' },
|
||||||
{ path: '/settings', 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 {
|
.sync-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2rem;
|
gap: 1.25rem;
|
||||||
max-width: 600px;
|
max-width: 620px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: #666;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-top: -0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
background: white;
|
background: var(--surface-1);
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card h3 {
|
.status-card h3 {
|
||||||
margin-top: 0;
|
margin: 0 0 0.9rem;
|
||||||
margin-bottom: 1rem;
|
font-size: 0.95rem;
|
||||||
color: #333;
|
font-weight: 650;
|
||||||
font-size: 1.1rem;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-info {
|
.status-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item {
|
.status-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
padding: 0.75rem;
|
gap: 1rem;
|
||||||
background: #f9f9f9;
|
padding: 0.55rem 0.7rem;
|
||||||
border-radius: 6px;
|
background: var(--surface-0);
|
||||||
font-size: 0.95rem;
|
border-radius: 7px;
|
||||||
}
|
font-size: 0.88rem;
|
||||||
|
|
||||||
.status-item.error {
|
|
||||||
background: #fee;
|
|
||||||
border: 1px solid #fcc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item .label {
|
.status-item .label {
|
||||||
font-weight: 600;
|
color: var(--text-muted);
|
||||||
color: #666;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item .value {
|
.status-item .value {
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
font-weight: 500;
|
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 {
|
.sync-actions {
|
||||||
display: flex;
|
background: var(--surface-1);
|
||||||
flex-direction: column;
|
border: 1px solid var(--border);
|
||||||
gap: 1rem;
|
border-radius: var(--radius);
|
||||||
align-items: center;
|
padding: 1.25rem;
|
||||||
}
|
box-shadow: var(--shadow);
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-choices {
|
.sync-choices {
|
||||||
@@ -206,6 +79,24 @@
|
|||||||
flex-wrap: wrap;
|
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 {
|
.progress-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -216,7 +107,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
font-size: 0.88rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,3 +130,55 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
transition: width 0.4s ease;
|
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;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--surface-0);
|
||||||
padding: 1rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
background: white;
|
background: var(--surface-1);
|
||||||
border-radius: 12px;
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
border-radius: 14px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 380px;
|
||||||
padding: 2rem;
|
padding: 2rem 1.85rem;
|
||||||
animation: slideUp 0.3s ease-out;
|
animation: rise 0.25s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes rise {
|
||||||
from {
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
opacity: 0;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header h1 {
|
.login-header h1 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.25rem;
|
||||||
margin: 0 0 0.5rem 0;
|
font-weight: 680;
|
||||||
color: #333;
|
margin: 0 0 0.35rem;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header p {
|
.login-header p {
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.84rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-tabs {
|
.login-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
border-bottom: 2px solid #f0f0f0;
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
padding: 0.6rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
font-size: 1rem;
|
font-size: 0.92rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
transition: all 0.3s ease;
|
margin-bottom: -1px;
|
||||||
margin-bottom: -2px;
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button:hover {
|
.tab-button:hover {
|
||||||
color: #667eea;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button.active {
|
.tab-button.active {
|
||||||
color: #667eea;
|
color: var(--accent);
|
||||||
border-bottom-color: #667eea;
|
border-bottom-color: var(--accent);
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background-color: #fee;
|
|
||||||
border: 1px solid #fcc;
|
|
||||||
color: #c33;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.submit-button {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--accent-solid);
|
||||||
color: white;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: filter 0.15s ease;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.35rem;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button:hover:not(:disabled) {
|
.submit-button:hover:not(:disabled) {
|
||||||
transform: translateY(-2px);
|
filter: brightness(1.08);
|
||||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button:disabled {
|
.submit-button:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
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,
|
.range-tab.active,
|
||||||
.metric-tab.active {
|
.metric-tab.active {
|
||||||
background: var(--accent);
|
background: var(--accent-solid);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent-solid);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--accent);
|
background: var(--accent-solid);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,99 @@
|
|||||||
.model-bar {
|
.model-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.7rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 1rem;
|
padding: 0.9rem 1rem;
|
||||||
background: #f9f9fb;
|
background: var(--surface-1);
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-bar label {
|
.model-bar label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #555;
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-bar select {
|
.model-bar select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.45rem 0.7rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: white;
|
background: var(--surface-2);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-bar select:disabled {
|
.model-bar select:disabled {
|
||||||
background: #f0f0f0;
|
background: var(--surface-0);
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-bar {
|
.meta-bar {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-bar.ai {
|
.meta-bar.ai {
|
||||||
background: #eef2ff;
|
background: var(--accent-soft);
|
||||||
border: 1px solid #d6dfff;
|
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
color: #3b4a94;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-bar.rules {
|
.meta-bar.rules {
|
||||||
background: #fff8e6;
|
background: color-mix(in srgb, var(--status-warning) 12%, var(--surface-1));
|
||||||
border: 1px solid #ffe4a3;
|
border-color: color-mix(in srgb, var(--status-warning) 35%, transparent);
|
||||||
color: #7a5a10;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-bar .fallback {
|
.meta-bar .fallback {
|
||||||
opacity: 0.8;
|
color: var(--text-secondary);
|
||||||
margin-left: 0.4rem;
|
margin-left: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
padding: 1rem;
|
padding: 0.9rem 1rem;
|
||||||
background: #fff8e6;
|
background: color-mix(in srgb, var(--status-warning) 12%, var(--surface-1));
|
||||||
border: 1px solid #ffe4a3;
|
border: 1px solid color-mix(in srgb, var(--status-warning) 35%, transparent);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #7a5a10;
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.7;
|
line-height: 1.8;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice code {
|
.notice code {
|
||||||
background: rgba(0, 0, 0, 0.06);
|
background: var(--surface-0);
|
||||||
padding: 0.1rem 0.35rem;
|
padding: 0.1rem 0.35rem;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 0.88em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-list {
|
.rec-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-card {
|
.rec-card {
|
||||||
background: white;
|
background: var(--surface-1);
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--border);
|
||||||
border-left: 4px solid #ccc;
|
border-left: 3px solid var(--border-strong);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 1.1rem 1.25rem;
|
padding: 1rem 1.15rem;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-card.priority-high {
|
/* Priority is spelled out in the badge text; the stripe is reinforcement. */
|
||||||
border-left-color: #e05252;
|
.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-card.priority-medium {
|
|
||||||
border-left-color: #e0a052;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rec-card.priority-low {
|
|
||||||
border-left-color: #6bbf7f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rec-header {
|
.rec-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -109,52 +104,57 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rec-category {
|
.rec-category {
|
||||||
font-weight: 700;
|
font-weight: 650;
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-priority {
|
.rec-priority {
|
||||||
font-size: 0.75rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 650;
|
||||||
padding: 0.15rem 0.6rem;
|
padding: 0.15rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-priority.priority-high {
|
.rec-priority.priority-high {
|
||||||
background: #fdecec;
|
background: color-mix(in srgb, var(--status-critical) 12%, transparent);
|
||||||
color: #b03030;
|
color: var(--status-critical);
|
||||||
|
border-color: color-mix(in srgb, var(--status-critical) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-priority.priority-medium {
|
.rec-priority.priority-medium {
|
||||||
background: #fdf3e6;
|
background: color-mix(in srgb, var(--status-serious) 14%, transparent);
|
||||||
color: #9a6412;
|
color: var(--status-serious);
|
||||||
|
border-color: color-mix(in srgb, var(--status-serious) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-priority.priority-low {
|
.rec-priority.priority-low {
|
||||||
background: #eaf6ed;
|
background: color-mix(in srgb, var(--status-good) 12%, transparent);
|
||||||
color: #35733f;
|
color: var(--status-good);
|
||||||
|
border-color: color-mix(in srgb, var(--status-good) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-body {
|
.rec-body {
|
||||||
color: #444;
|
color: var(--text-secondary);
|
||||||
line-height: 1.75;
|
line-height: 1.85;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rec-based-on {
|
.rec-based-on {
|
||||||
margin-top: 0.7rem;
|
margin-top: 0.65rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.76rem;
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disclaimer {
|
.disclaimer {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid var(--border);
|
||||||
font-size: 0.82rem;
|
font-size: 0.78rem;
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-bar select,
|
.model-bar select,
|
||||||
.model-bar .btn {
|
.model-bar .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.settings-section {
|
.settings-section {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.25rem;
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section:last-child {
|
.settings-section:last-child {
|
||||||
@@ -9,115 +9,129 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-section h3 {
|
.settings-section h3 {
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
color: #333;
|
font-weight: 650;
|
||||||
margin: 0 0 0.6rem 0;
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-hint {
|
.settings-hint {
|
||||||
color: #777;
|
color: var(--text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.86rem;
|
||||||
line-height: 1.7;
|
line-height: 1.8;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-hint code,
|
.settings-hint code,
|
||||||
.model-table code {
|
.model-table code {
|
||||||
background: #f2f2f5;
|
background: var(--surface-0);
|
||||||
padding: 0.1rem 0.35rem;
|
padding: 0.1rem 0.35rem;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 0.88em;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table {
|
.model-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
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 {
|
.model-table th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.6rem 0.75rem;
|
padding: 0.6rem 0.8rem;
|
||||||
border-bottom: 2px solid #eee;
|
background: var(--surface-2);
|
||||||
color: #888;
|
border-bottom: 1px solid var(--border);
|
||||||
font-size: 0.8rem;
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-size: 0.75rem;
|
||||||
|
font-weight: 650;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table td {
|
.model-table td {
|
||||||
padding: 0.7rem 0.75rem;
|
padding: 0.65rem 0.8rem;
|
||||||
border-bottom: 1px solid #f4f4f4;
|
border-bottom: 1px solid var(--border);
|
||||||
color: #444;
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-name {
|
.model-name {
|
||||||
color: #777;
|
color: var(--text-muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* State is spelled out in the badge text; colour only reinforces it. */
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.15rem 0.6rem;
|
padding: 0.15rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 600;
|
font-weight: 650;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.ok {
|
.badge.ok {
|
||||||
background: #eaf6ed;
|
background: color-mix(in srgb, var(--status-good) 12%, transparent);
|
||||||
color: #35733f;
|
color: var(--status-good);
|
||||||
|
border-color: color-mix(in srgb, var(--status-good) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.off {
|
.badge.off {
|
||||||
background: #f2f2f2;
|
background: var(--surface-0);
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-default {
|
.badge-default {
|
||||||
margin-left: 0.4rem;
|
margin-left: 0.4rem;
|
||||||
padding: 0.05rem 0.4rem;
|
padding: 0.05rem 0.4rem;
|
||||||
background: #eef2ff;
|
background: var(--accent-soft);
|
||||||
color: #4a5aa8;
|
color: var(--accent);
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-list {
|
.settings-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.3rem;
|
padding-left: 1.25rem;
|
||||||
color: #555;
|
color: var(--text-secondary);
|
||||||
line-height: 1.9;
|
line-height: 1.95;
|
||||||
font-size: 0.9rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: #fff;
|
background: var(--surface-1);
|
||||||
color: #c04747;
|
color: var(--status-critical);
|
||||||
border: 1px solid #e0b4b4;
|
border: 1px solid color-mix(in srgb, var(--status-critical) 40%, transparent);
|
||||||
padding: 0.6rem 1.2rem;
|
padding: 0.55rem 1.1rem;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.92rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: all 0.2s ease;
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
background: #fdecec;
|
background: color-mix(in srgb, var(--status-critical) 8%, var(--surface-1));
|
||||||
border-color: #c04747;
|
border-color: var(--status-critical);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.model-table {
|
.model-table {
|
||||||
font-size: 0.8rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table th,
|
.model-table th,
|
||||||
.model-table td {
|
.model-table td {
|
||||||
padding: 0.5rem 0.4rem;
|
padding: 0.45rem 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { apiClient, ModelInfo } from '../services/api';
|
import { apiClient, ModelInfo } from '../services/api';
|
||||||
|
import { FEATURES } from '../features';
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
@@ -9,6 +10,10 @@ function Settings() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!FEATURES.ai) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
apiClient
|
apiClient
|
||||||
.getModels()
|
.getModels()
|
||||||
.then(setModels)
|
.then(setModels)
|
||||||
@@ -25,6 +30,7 @@ function Settings() {
|
|||||||
<div className="page">
|
<div className="page">
|
||||||
<h2>设置</h2>
|
<h2>设置</h2>
|
||||||
|
|
||||||
|
{FEATURES.ai && (
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3>AI 模型</h3>
|
<h3>AI 模型</h3>
|
||||||
<p className="settings-hint">
|
<p className="settings-hint">
|
||||||
@@ -67,16 +73,18 @@ function Settings() {
|
|||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3>数据与隐私</h3>
|
<h3>数据与隐私</h3>
|
||||||
<ul className="settings-list">
|
<ul className="settings-list">
|
||||||
<li>健康数据保存在自建数据库中,不上传第三方服务。</li>
|
<li>健康数据保存在自建数据库中,不上传第三方服务。</li>
|
||||||
<li>Garmin 密码只以哈希形式存储,无法还原,每次同步需重新输入。</li>
|
|
||||||
<li>
|
<li>
|
||||||
使用 AI 建议时,你的每日指标会以匿名 CSV 形式发送给所选模型厂商;
|
Garmin 账号通过 OAuth 令牌授权,密码不会被保存;令牌约一年后过期,
|
||||||
其中不含姓名、邮箱或设备标识。
|
届时在「同步」页重新绑定一次即可。
|
||||||
</li>
|
</li>
|
||||||
|
<li>本站登录密码以 PBKDF2 加盐哈希存储,无法还原。</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ function Trends() {
|
|||||||
|
|
||||||
const group = GROUPS.find((g) => g.id === active) ?? GROUPS[0];
|
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(
|
const rows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
days.map((d) => {
|
days.map((d) => {
|
||||||
@@ -180,8 +190,16 @@ function Trends() {
|
|||||||
|
|
||||||
const primary = group.series[0];
|
const primary = group.series[0];
|
||||||
const summary = stats(rows.map((r) => r[primary.key]));
|
const summary = stats(rows.map((r) => r[primary.key]));
|
||||||
const fmt = (v: number) =>
|
/* Precision by magnitude: "11,013.99 步" is both false precision and long
|
||||||
(Math.round(v * 100) / 100).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
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 (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
@@ -241,8 +259,13 @@ function Trends() {
|
|||||||
<Chart
|
<Chart
|
||||||
title={group.label}
|
title={group.label}
|
||||||
unit={group.unit}
|
unit={group.unit}
|
||||||
|
subtitle={
|
||||||
|
dense && renderType !== group.type
|
||||||
|
? `${days.length} 天数据:柱形在此密度下不可读,已改用面积图`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
data={rows}
|
data={rows}
|
||||||
type={group.type}
|
type={renderType}
|
||||||
series={group.series}
|
series={group.series}
|
||||||
height={320}
|
height={320}
|
||||||
footer={group.note}
|
footer={group.note}
|
||||||
|
|||||||
@@ -39,7 +39,12 @@
|
|||||||
--status-critical: #d03b3b;
|
--status-critical: #d03b3b;
|
||||||
|
|
||||||
--grid: #eceae5;
|
--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: #2a78d6;
|
||||||
|
--accent-solid: #256abf;
|
||||||
--accent-soft: #eef4fd;
|
--accent-soft: #eef4fd;
|
||||||
|
|
||||||
--radius: 10px;
|
--radius: 10px;
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
|
|
||||||
--grid: #2c2c28;
|
--grid: #2c2c28;
|
||||||
--accent: #3987e5;
|
--accent: #3987e5;
|
||||||
|
--accent-solid: #1c5cab;
|
||||||
--accent-soft: #1d2938;
|
--accent-soft: #1d2938;
|
||||||
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||||
@@ -96,6 +102,7 @@
|
|||||||
|
|
||||||
--grid: #2c2c28;
|
--grid: #2c2c28;
|
||||||
--accent: #3987e5;
|
--accent: #3987e5;
|
||||||
|
--accent-solid: #1c5cab;
|
||||||
--accent-soft: #1d2938;
|
--accent-soft: #1d2938;
|
||||||
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||||
|
|||||||
Reference in New Issue
Block a user