Files
qlib/frontend/web/app/globals.css
T
Simon 9aaca12751 fix(web): 控件对齐与输入友好整改(控件高度令牌化 + 对齐自检脚本)
问题不是"不好看",而是**可测量的错位**:同一行里原生 date 输入 38.8px、数字输入
36.8px、按钮 34px;16px 的勾选框与 36.8px 的下拉同排;因子行的下拉与权重框没有
可见标签;列表里的勾选框点不中;参数非法时「运行回测」直接置灰且不说原因。

根因:控件高度靠「上下 padding + 行高」拼出来,而 input / select / button 的原生行高
各不相同(Chrome 的 date 还会多 2px),必然参差;加上各处内联像素宽度
(style={{width:220}}、flex:1)与自搓布局,列自然对不齐。

改动:
- 新增控件高度令牌 --ctl-h-sm/md/lg(28/34/38px)与 --ctl-px,.input/select/.btn/
  .icon-btn/date 统一显式 height(不再拼 padding);原生 checkbox/radio 统一 16px,
  点击热区交给外层 label(.check/.radio-row/.check--cell),表格整格可点
- 因子行、选股条件行改为「表头 + CSS 栅格」成列对齐,列宽由样式决定,
  去掉内联像素宽度与 flex 拉伸,配 aria-label 供读屏分辨重复行
- 表单友好化:错误提示改为**失焦或提交后**才出现(清空重填的瞬间不再标红);
  提交被拦下时一次展开全部行内错误 + 自动聚焦并滚动到第一个问题字段;
  运行/保存按钮不再因参数非法而置灰(灰按钮不说原因 = 看起来不可点却无响应),
  改为可点击并讲清原因;补齐 topN/costs 两处「产生了却没人显示」的行内错误落点
- 数值字段补 inputMode/step/min/max 与单位、取值范围提示;工具条检索/筛选用
  .input--search/.input--filter/.input--picker 类,不再写内联宽度
- /experiments 筛选无结果的空态与「暂无实验」区分开(原文案会让人以为归档丢了)
- 同一页面可能挂两份表单:radio name 与 label/for 加表单实例前缀(useId),
  否则两边单选互相取消、label 指错控件
- 新增 scripts/verify_ui_alignment.py:系统 Chrome + 原生 CDP(仅标准库,
  独占随机端口与临时 profile),按 7 个页面 × 1500/375px 检查同排等高、
  高度取值归一、点击目标、标签与无障碍名、字号圆角一致、尺寸匹配内容、
  横向溢出、提示裁切;本次基线 108/32 → 现 140/140

验证:pytest 388 passed、ruff 全绿(顺带清掉 qlib_verify.py 一处死代码)、
tsc 0 错误、图表单测 7 passed、next build 成功、契约自检 59/59、
对齐自检 140/140(含 375px 小屏)。
2026-09-27 09:01:55 +08:00

1977 lines
38 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
qlib-platform 设计系统(ui-ux-pro-max · Financial Terminal Dark)
------------------------------------------------------------
语义令牌 → 组件类:深浅分层、高对比文本、金融语义色
(涨=绿 / 跌=红 / 警示=琥珀 / 主色=天蓝)、1px 低透明边框、
统一 4px 间距刻度、可见焦点环、reduced-motion 友好。
============================================================ */
/* 本地自托管字体(Noto Sans SC 可变 400–700,见 fonts.css / public/fonts)。
不再依赖 fonts.googleapis.com / fonts.gstatic.com 等外部网络。 */
@import "./fonts.css";
:root {
color-scheme: dark;
/* 表面分层(自底向上抬升) */
--bg-0: #060912;
--bg-1: #0a0f1c;
--bg-2: #101729;
--bg-3: #172035;
--surface-1: #101827;
--surface-2: #141d30;
--surface-3: #1b2540;
/* 文本 */
--text-1: #e9eef7;
--text-2: #a8b3c9;
--text-3: #8b98b2;
/* 边框 / 分隔 */
--line: rgba(150, 165, 195, 0.13);
--line-strong: rgba(150, 165, 195, 0.28);
/* 语义色 */
--accent: #3fb6ff;
--accent-strong: #0d99f2;
--accent-soft: rgba(63, 182, 255, 0.14);
--violet: #8b95ff;
--pos: #3ddc97;
--pos-strong: #1fb871;
--pos-soft: rgba(61, 220, 151, 0.13);
--neg: #ff7a7a;
--neg-soft: rgba(255, 122, 122, 0.13);
--warn: #f5b82e;
--warn-soft: rgba(245, 184, 46, 0.14);
--focus: rgba(63, 182, 255, 0.55);
/* 圆角 */
--r-xs: 6px;
--r-sm: 9px;
--r-md: 12px;
--r-lg: 16px;
/* 控件高度令牌 —— 对齐的根基。
为什么必须显式定高:input / select / button 的原生行高与内边距各不相同,
靠 padding 拼高度会得到 34 / 36.8 / 38.8 三种结果(Chrome 的 date 输入还会
多出 2px),同一行里就参差不齐。显式 height 是唯一能保证等高的做法。 */
--ctl-h-sm: 28px; /* 密集行的小按钮 / 图标按钮 */
--ctl-h-md: 34px; /* 标准控件:输入框 / 下拉 / 按钮 */
--ctl-h-lg: 38px; /* 主操作控件(大号输入框、登录式表单) */
/* 控件横向内边距:与高度同源,保证左右留白一致 */
--ctl-px: 12px;
--ctl-px-sm: 10px;
/* 间距(4px 节奏) */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--sp-7: 48px;
/* 字体 */
--font-sans: "Noto Sans SC", -apple-system, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code",
Consolas, "Noto Sans SC", monospace;
/* 阴影:克制的两层 */
--shadow-1: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 8px 24px rgba(0, 0, 0, 0.25);
--shadow-pop: 0 12px 40px rgba(0, 0, 0, 0.45);
/* 字号(px 由浏览器缩放) */
--fs-xs: 12px;
--fs-sm: 13px;
--fs-md: 14px;
--fs-lg: 16px;
--fs-xl: 20px;
--fs-2xl: 26px;
--nav-w: 236px;
}
/* ---------- 基础 ---------- */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg-0);
color: var(--text-1);
font-family: var(--font-sans);
font-size: var(--fs-md);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
::selection {
background: var(--accent-soft);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1,
h2,
h3,
p {
margin: 0;
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
}
:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
/* 视觉隐藏但保留给屏幕阅读器(表单控件 label 用) */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* ---------- 应用外壳(侧栏 + 内容) ---------- */
.shell {
display: grid;
grid-template-columns: var(--nav-w) minmax(0, 1fr);
min-height: 100vh;
}
/* 左侧导航 */
.rail {
position: sticky;
top: 0;
height: 100vh;
display: flex;
flex-direction: column;
gap: var(--sp-2);
background: linear-gradient(180deg, rgba(16, 23, 40, 0.96), rgba(10, 15, 28, 0.98));
border-right: 1px solid var(--line);
padding: var(--sp-4) var(--sp-3);
overflow-y: auto;
z-index: 30;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 2px var(--sp-2) var(--sp-3);
font-weight: 700;
letter-spacing: 0.2px;
}
.brand .logo {
display: inline-flex;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
border-radius: 9px;
color: #04121f;
background: linear-gradient(135deg, var(--accent), var(--violet));
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14) inset;
flex: none;
}
.brand small {
display: block;
color: var(--text-3);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.4px;
line-height: 1.2;
}
.nav-label {
margin: var(--sp-3) var(--sp-2) var(--sp-1);
color: var(--text-3);
font-size: var(--fs-xs);
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
}
.nav a.nav-link {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: var(--r-sm);
color: var(--text-2);
font-size: var(--fs-sm);
font-weight: 500;
text-decoration: none;
transition: background 0.15s ease, color 0.15s ease;
}
.nav a.nav-link:hover {
background: rgba(150, 165, 195, 0.09);
color: var(--text-1);
text-decoration: none;
}
.nav a.nav-link.active {
background: var(--accent-soft);
color: #dff1ff;
box-shadow: inset 0 0 0 1px rgba(63, 182, 255, 0.22);
}
.nav a.nav-link.active .nav-ic {
color: var(--accent);
}
.nav-ic {
display: inline-flex;
color: var(--text-3);
flex: none;
transition: color 0.15s ease;
}
.rail-foot {
margin-top: auto;
padding: var(--sp-3) var(--sp-2) 0;
border-top: 1px solid var(--line);
color: var(--text-3);
font-size: var(--fs-xs);
line-height: 1.5;
}
/* 顶栏(窄屏) */
.topbar {
display: none;
}
/* 抽屉与遮罩仅窄屏启用:桌面端隐藏,避免它们作为 shell 网格子元素
把 main 挤到第二行(导致主内容被压在侧栏下方)。 */
.drawer,
.scrim {
display: none;
}
.content {
min-width: 0;
padding: var(--sp-5) clamp(16px, 3vw, 40px) var(--sp-7);
}
.page {
max-width: 1180px;
margin: 0 auto;
}
/* 抽屉(窄屏) */
.scrim {
display: none;
}
/* ---------- 页头 ---------- */
.page-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--sp-4);
margin-bottom: var(--sp-5);
flex-wrap: wrap;
}
.page-title {
font-size: var(--fs-2xl);
font-weight: 700;
letter-spacing: -0.2px;
line-height: 1.25;
}
.page-sub {
margin-top: 4px;
color: var(--text-2);
font-size: var(--fs-sm);
max-width: 720px;
}
.page-actions {
display: flex;
align-items: center;
gap: var(--sp-2);
flex-wrap: wrap;
}
/* ---------- 卡片 ---------- */
.card {
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-1);
margin-bottom: var(--sp-4);
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--line);
}
.card__title {
font-size: var(--fs-md);
font-weight: 600;
display: flex;
align-items: center;
gap: var(--sp-2);
color: var(--text-1);
}
.card__title .ic {
color: var(--text-3);
}
.card__sub {
color: var(--text-3);
font-size: var(--fs-xs);
font-weight: 400;
margin-top: 1px;
}
.card__tools {
display: flex;
align-items: center;
gap: var(--sp-2);
flex: none;
}
.card__body {
padding: var(--sp-4);
}
.card__body--flush {
padding: 0;
}
.card--flat {
background: transparent;
box-shadow: none;
}
/* ---------- 按钮 ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
border: 1px solid var(--line-strong);
background: var(--surface-3);
color: var(--text-1);
border-radius: var(--r-sm);
/* 显式高度 + 横向内边距(不再用「上下 padding 拼高度」):
这样按钮与同排的输入框/下拉必然等高,换字体或换浏览器也不会错位。 */
height: var(--ctl-h-md);
padding: 0 var(--ctl-px);
font-size: var(--fs-sm);
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease, border-color 0.15s ease,
transform 0.05s ease, box-shadow 0.15s ease;
}
.btn:hover {
background: #243152;
border-color: rgba(150, 165, 195, 0.4);
text-decoration: none;
}
.btn:active {
transform: translateY(0.5px);
}
.btn:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
.btn--primary {
background: linear-gradient(180deg, #4dc0ff, var(--accent-strong));
color: #061423;
border-color: transparent;
font-weight: 600;
}
.btn--primary:hover {
filter: brightness(1.07);
background: linear-gradient(180deg, #4dc0ff, var(--accent-strong));
}
.btn--danger {
color: var(--neg);
border-color: rgba(255, 122, 122, 0.35);
}
.btn--danger:hover {
background: var(--neg-soft);
}
.btn--sm {
height: var(--ctl-h-sm);
padding: 0 var(--ctl-px-sm);
font-size: var(--fs-xs);
border-radius: var(--r-xs);
}
.btn .ic {
flex: none;
}
/* 图标按钮 */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--ctl-h-sm);
height: var(--ctl-h-sm);
border-radius: var(--r-sm);
color: var(--text-2);
background: transparent;
border: 1px solid transparent;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.icon-btn:hover {
background: rgba(150, 165, 195, 0.12);
color: var(--text-1);
}
/* 大号图标按钮:顶栏等需要更舒服点击目标的位置(仍属于 28/34/38 三级刻度) */
.icon-btn--lg {
width: var(--ctl-h-lg);
height: var(--ctl-h-lg);
}
/* ---------- 表单 ---------- */
.input,
select.input,
textarea.input {
width: 100%;
/* 与 .btn 同一个高度令牌:输入框与按钮同排必然等高。
Chrome 的原生 date 输入比其它控件高 2px,靠 height 一并压平。 */
height: var(--ctl-h-md);
background: #0d1424;
border: 1px solid var(--line-strong);
color: var(--text-1);
border-radius: var(--r-sm);
padding: 0 var(--ctl-px-sm);
font-size: var(--fs-sm);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
/* 多行输入不套用控件定高(内容行数决定高度) */
textarea.input {
height: auto;
min-height: 76px;
padding: 8px var(--ctl-px-sm);
line-height: 1.6;
resize: vertical;
}
.input:hover {
border-color: rgba(150, 165, 195, 0.42);
}
.input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.input::placeholder {
color: var(--text-3);
}
select.input {
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--text-2) 50%),
linear-gradient(135deg, var(--text-2) 50%, transparent 50%);
background-position: calc(100% - 16px) 55%, calc(100% - 11px) 55%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 28px;
}
input[type="date"].input {
padding-right: 8px;
}
/* 原生勾选控件:尺寸统一到 16px,并把「点击目标」交给外层 label(.check / .radio-row)——
16px 的方块本身永远达不到 28px 触控目标,靠 label 撑开热区才是正确做法。 */
input[type="checkbox"],
input[type="radio"] {
width: 16px;
height: 16px;
margin: 0;
flex: none;
accent-color: var(--accent);
cursor: pointer;
}
/* 单行勾选项(与相邻的下拉/输入同高,保证整行对齐一致) */
.check {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
min-height: var(--ctl-h-md);
color: var(--text-2);
font-size: var(--fs-sm);
cursor: pointer;
}
.check:hover {
color: var(--text-1);
}
/* 表格单元里的勾选框:把整个单元做成热区(16px 方块在表格里几乎点不中)。
用 label 包住 → 点文字/空白都能选中,行高即为常规控件高度。 */
.check--cell {
min-height: var(--ctl-h-md);
width: 100%;
justify-content: center;
padding: 0 var(--sp-1);
}
input[type="number"].input,
input[type="date"].input {
width: auto;
min-width: 0;
}
/* 数值/日期字段按内容给宽度:内容只有几位数字,拉满整行既难读也难与相邻控件对齐 */
.form-grid .field > input[type="number"].input {
max-width: 180px;
}
.field {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 0;
}
.field__label {
color: var(--text-2);
font-size: var(--fs-xs);
font-weight: 500;
letter-spacing: 0.2px;
/* 无标签时也占一行高度(栅格里标签行高不一致会让控件上下错位) */
min-height: 17px;
line-height: 17px;
}
.field__hint {
color: var(--text-3);
font-size: var(--fs-xs);
line-height: 1.5;
/* 提示不裁切:说明文字被 line-clamp 截断等于把信息藏起来。
跨行对齐由 .form-grid 的 align-items:end 保证,不靠裁切文案。 */
}
/* 工具条里的检索框 / 筛选器:宽度由类统一,避免每处写内联像素宽度 */
.input--search {
width: min(240px, 100%);
}
.input--filter {
width: 130px;
}
/* 选项文案较长的选择器(如「代码 名称(收益)」)给更宽的宽度 */
.input--picker {
width: min(320px, 100%);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
gap: var(--sp-3);
align-items: end;
}
/* 整行字段(长文本框、日期区间、条件编辑器) */
.field--wide {
grid-column: 1 / -1;
}
.form-grid .btn {
align-self: end;
height: var(--ctl-h-md);
}
/* 小节标题(表单内的分组标题,与 .card__title 区分:更小、不抢层级) */
.form-section__title {
font-size: var(--fs-sm);
font-weight: 600;
color: var(--text-1);
}
/* 因子行:表头 + 栅格行,靠栅格列宽保证「因子 / 权重 / 操作」三列严格对齐。
反面做法是在 JSX 里给每个控件写死像素宽度(style={{width:220}}),
一旦文案或字体变化就会错位。 */
.factor-rows {
display: grid;
gap: var(--sp-2);
}
.factor-rows__head,
.factor-row {
display: grid;
grid-template-columns: minmax(150px, 1fr) 118px auto;
align-items: center;
gap: var(--sp-2);
}
.factor-rows__head {
color: var(--text-3);
font-size: var(--fs-xs);
padding-bottom: 2px;
border-bottom: 1px solid var(--line);
}
.factor-row .btn {
justify-self: start;
}
/* 选股条件行:与因子行同一套栅格思路(表头 + 成列),
列宽由 CSS 决定,不在 JSX 里写内联像素宽度。 */
.cond-rows {
display: grid;
gap: var(--sp-2);
}
.cond-rows__head,
.cond-row {
display: grid;
grid-template-columns: minmax(150px, 1fr) 84px minmax(96px, 140px) auto;
align-items: center;
gap: var(--sp-2);
}
.cond-rows__head {
color: var(--text-3);
font-size: var(--fs-xs);
padding-bottom: 2px;
border-bottom: 1px solid var(--line);
}
.cond-row .btn {
justify-self: start;
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-2);
}
.between {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
flex-wrap: wrap;
}
.chart-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(430px, 100%), 1fr));
gap: var(--sp-4);
}
.chart-grid .card {
margin-bottom: 0;
}
.stack {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
/* 搜索框 */
.searchbox {
position: relative;
min-width: min(320px, 100%);
}
.searchbox .ic {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-3);
pointer-events: none;
}
.searchbox .input {
padding-left: 32px;
}
.searchbox .clear {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
}
/* ---------- 徽标 / 状态 ---------- */
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
border-radius: 999px;
padding: 2px 9px;
font-size: var(--fs-xs);
font-weight: 500;
line-height: 1.6;
border: 1px solid transparent;
white-space: nowrap;
}
.pill--default {
color: var(--text-2);
background: rgba(150, 165, 195, 0.12);
border-color: rgba(150, 165, 195, 0.18);
}
.pill--accent {
color: #bfe9ff;
background: var(--accent-soft);
border-color: rgba(63, 182, 255, 0.3);
}
.pill--pos {
color: #9ff0cb;
background: var(--pos-soft);
border-color: rgba(61, 220, 151, 0.3);
}
.pill--neg {
color: #ffb1b1;
background: var(--neg-soft);
border-color: rgba(255, 122, 122, 0.3);
}
.pill--warn {
color: #ffe3a3;
background: var(--warn-soft);
border-color: rgba(245, 184, 46, 0.32);
}
.pill--violet {
color: #cdd2ff;
background: rgba(139, 149, 255, 0.14);
border-color: rgba(139, 149, 255, 0.3);
}
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 7px;
border-radius: var(--r-xs);
background: rgba(150, 165, 195, 0.1);
color: var(--text-2);
font-size: var(--fs-xs);
border: 1px solid var(--line);
}
.tag--mono {
font-family: var(--font-mono);
font-size: var(--fs-xs);
}
/* ---------- 指标卡 ---------- */
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.metric {
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: var(--sp-3) var(--sp-4);
position: relative;
overflow: hidden;
}
.metric__label {
display: flex;
align-items: center;
gap: 7px;
color: var(--text-2);
font-size: var(--fs-xs);
font-weight: 500;
}
.metric__label .ic {
color: var(--text-3);
}
.metric__value {
margin-top: 6px;
font-size: 24px;
font-weight: 700;
letter-spacing: -0.3px;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.metric__value--sm {
font-size: var(--fs-lg);
}
.metric__sub {
margin-top: 4px;
color: var(--text-3);
font-size: var(--fs-xs);
font-variant-numeric: tabular-nums;
}
.metric__value.tone-pos,
.tone-pos {
color: var(--pos);
}
.metric__value.tone-neg,
.tone-neg {
color: var(--neg);
}
.metric__value.tone-warn,
.tone-warn {
color: var(--warn);
}
.metric__value.tone-accent,
.tone-accent {
color: var(--accent);
}
.metric__value.tone-plain,
.tone-plain {
color: var(--text-1);
}
/* ---------- 表格 ---------- */
.table-wrap {
overflow-x: auto;
}
table.tbl {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-sm);
font-variant-numeric: tabular-nums;
}
.tbl th,
.tbl td {
text-align: left;
padding: 9px 12px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.tbl th {
color: var(--text-3);
font-weight: 600;
font-size: var(--fs-xs);
letter-spacing: 0.4px;
background: rgba(150, 165, 195, 0.045);
white-space: nowrap;
}
.tbl tbody tr {
transition: background 0.12s ease;
}
.tbl tbody tr:hover {
background: rgba(150, 165, 195, 0.055);
}
.tbl tr:last-child td {
border-bottom: none;
}
.tbl tbody tr.row-active {
background: var(--accent-soft);
}
.cell-mono,
.mono {
font-family: var(--font-mono);
}
.cell-strong {
font-weight: 600;
color: var(--text-1);
}
.tbl .num,
.num {
font-variant-numeric: tabular-nums;
}
.tbl .tone-pos,
.tbl td.tone-pos {
color: var(--pos);
}
.tbl .tone-neg,
.tbl td.tone-neg {
color: var(--neg);
}
.tbl .tone-warn,
.tbl td.tone-warn {
color: var(--warn);
}
/* 扩展行 / 详情内嵌 */
.expand-cell {
background: rgba(10, 15, 28, 0.6);
}
.expand-cell > div {
border-left: 2px solid var(--accent-soft);
margin: 4px 12px 12px;
padding: 10px 12px;
border-radius: var(--r-sm);
background: var(--bg-1);
}
/* ---------- 进度 / 加载 / 空态 ---------- */
.progress {
height: 6px;
border-radius: 999px;
background: rgba(150, 165, 195, 0.16);
overflow: hidden;
margin: var(--sp-2) 0;
}
.progress__bar {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent-strong), var(--accent));
transition: width 0.3s ease;
}
.skeleton {
border-radius: var(--r-sm);
background: linear-gradient(90deg, rgba(150, 165, 195, 0.08), rgba(150, 165, 195, 0.16), rgba(150, 165, 195, 0.08));
background-size: 200% 100%;
animation: shimmer 1.4s infinite linear;
}
@keyframes shimmer {
to {
background-position: -200% 0;
}
}
.empty {
padding: var(--sp-6) var(--sp-4);
text-align: center;
color: var(--text-3);
}
.empty .ic {
color: var(--text-3);
opacity: 0.6;
margin-bottom: var(--sp-2);
}
.empty strong {
display: block;
color: var(--text-2);
margin-bottom: 2px;
}
/* 提示条 */
.banner {
display: flex;
align-items: flex-start;
gap: 9px;
padding: 10px 13px;
border-radius: var(--r-sm);
font-size: var(--fs-sm);
margin-bottom: var(--sp-3);
border: 1px solid;
line-height: 1.55;
overflow-wrap: anywhere;
}
.banner .ic {
flex: none;
margin-top: 2px;
}
.banner--error {
color: #ffc9c9;
background: var(--neg-soft);
border-color: rgba(255, 122, 122, 0.32);
}
.banner--warn {
color: #ffe6ae;
background: var(--warn-soft);
border-color: rgba(245, 184, 46, 0.32);
}
.banner--info {
color: #d6efff;
background: var(--accent-soft);
border-color: rgba(63, 182, 255, 0.3);
}
.banner--muted {
color: var(--text-2);
background: rgba(150, 165, 195, 0.08);
border-color: var(--line);
}
/* 说明文本 */
.hint {
color: var(--text-3);
font-size: var(--fs-xs);
line-height: 1.7;
}
/* 数值条(分层收益等) */
.vbars {
display: flex;
align-items: flex-end;
gap: 8px;
height: 96px;
padding: var(--sp-3) 0;
}
.vbars__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 0;
}
.vbars__bar {
width: min(46px, 70%);
min-height: 3px;
border-radius: 4px 4px 0 0;
transition: height 0.3s ease;
}
.vbars__bar.is-pos {
background: linear-gradient(180deg, var(--pos), var(--pos-strong));
}
.vbars__bar.is-neg {
background: linear-gradient(180deg, #ff9d9d, var(--neg));
}
.vbars__val {
font-size: var(--fs-xs);
color: var(--text-1);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.vbars__cap {
color: var(--text-3);
font-size: var(--fs-xs);
}
/* 选中清单 chips */
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
/* 历史记录 chip 是可点击控件:高度必须达到点击目标下限,
不能被 padding 压到 27px 这种「看得见点不准」的尺寸 */
min-height: var(--ctl-h-sm);
background: var(--surface-3);
border: 1px solid var(--line-strong);
border-radius: 999px;
padding: 3px 6px 3px 11px;
font-size: var(--fs-xs);
color: var(--text-2);
}
button.chip {
cursor: pointer;
}
button.chip:hover {
border-color: var(--accent);
color: var(--text-1);
}
.chip b {
color: var(--text-1);
font-weight: 600;
}
.chip__x {
display: inline-flex;
padding: 3px;
border-radius: 50%;
cursor: pointer;
color: var(--text-3);
background: transparent;
border: none;
}
.chip__x:hover {
color: var(--neg);
background: var(--neg-soft);
}
/* 月度收益着色单元格 */
.cell-heat {
font-variant-numeric: tabular-nums;
border-radius: var(--r-xs);
padding: 2px 7px;
display: inline-block;
min-width: 58px;
text-align: right;
}
.cell-heat.pos {
color: #c7ffe4;
background: var(--pos-soft);
}
.cell-heat.neg {
color: #ffd2d2;
background: var(--neg-soft);
}
.cell-heat.flat {
color: var(--text-2);
background: rgba(150, 165, 195, 0.1);
}
/* 旋转加载图标 */
.spin {
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.ic {
display: inline-block;
flex: none;
vertical-align: -2px;
}
.quick-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.quick-card {
display: flex;
gap: 13px;
align-items: flex-start;
padding: var(--sp-4);
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line);
border-radius: var(--r-md);
color: var(--text-1);
text-decoration: none;
transition: border-color 0.15s ease, transform 0.12s ease, background 0.15s ease;
}
.quick-card:hover {
border-color: rgba(63, 182, 255, 0.4);
text-decoration: none;
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
}
.quick-card .quick-ic {
display: inline-flex;
width: 38px;
height: 38px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 11px;
background: var(--accent-soft);
color: var(--accent);
}
.quick-card--violet .quick-ic {
background: rgba(139, 149, 255, 0.14);
color: var(--violet);
}
.quick-card--pos .quick-ic {
background: var(--pos-soft);
color: var(--pos);
}
.quick-card--warn .quick-ic {
background: var(--warn-soft);
color: var(--warn);
}
.quick-card h3 {
font-size: var(--fs-md);
font-weight: 600;
margin-bottom: 2px;
}
.quick-card p {
color: var(--text-2);
font-size: var(--fs-xs);
line-height: 1.55;
}
.quick-card .go {
margin-top: 8px;
font-size: var(--fs-xs);
font-weight: 600;
color: var(--accent);
display: inline-flex;
align-items: center;
gap: 4px;
}
/* 运行面板(回测/组合配置) */
.runpanel {
position: sticky;
bottom: 12px;
z-index: 20;
display: flex;
align-items: center;
gap: var(--sp-3);
flex-wrap: wrap;
background: rgba(13, 19, 35, 0.92);
backdrop-filter: blur(8px);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
padding: var(--sp-3) var(--sp-4);
box-shadow: var(--shadow-pop);
}
.runpanel__info {
color: var(--text-2);
font-size: var(--fs-xs);
flex: 1;
min-width: 200px;
}
.runpanel__info b {
color: var(--text-1);
}
/* 数值差箭头 */
.delta {
display: inline-flex;
align-items: center;
gap: 3px;
font-weight: 600;
}
/* 列表(说明/步骤) */
.steplist {
margin: 0;
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-2);
font-size: var(--fs-sm);
}
.steplist b {
color: var(--text-1);
}
/* ---------- 响应式 ---------- */
@media (max-width: 940px) {
:root {
--nav-w: 0px;
}
.shell {
grid-template-columns: 1fr;
}
.rail {
display: none;
}
.topbar {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
gap: var(--sp-2);
height: 52px;
padding: 0 var(--sp-3);
background: rgba(10, 15, 28, 0.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
}
.topbar .brand {
padding: 0;
}
.drawer {
position: fixed;
inset: 0 auto 0 0;
width: min(78vw, 290px);
background: var(--bg-1);
border-right: 1px solid var(--line-strong);
z-index: 60;
transform: translateX(-102%);
transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.3, 1);
display: flex;
flex-direction: column;
padding: var(--sp-3);
gap: var(--sp-1);
overflow-y: auto;
box-shadow: var(--shadow-pop);
}
.drawer.open {
transform: translateX(0);
}
.drawer .brand {
padding-bottom: var(--sp-3);
margin-bottom: var(--sp-1);
border-bottom: 1px solid var(--line);
}
.scrim {
display: block;
position: fixed;
inset: 0;
background: rgba(3, 6, 12, 0.6);
z-index: 50;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.scrim.show {
opacity: 1;
pointer-events: auto;
}
.content {
padding: var(--sp-4) 14px var(--sp-6);
}
.runpanel {
position: static;
border-radius: var(--r-md);
}
}
@media (max-width: 560px) {
.metric-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--sp-2);
}
.metric__value {
font-size: 20px;
}
.form-grid {
grid-template-columns: 1fr 1fr;
}
.page-head {
flex-direction: column;
}
}
/* ============================================================
图表(TradingView Lightweight Charts)—— 图例 / tooltip / 容器
============================================================ */
.lw-wrap {
display: flex;
flex-direction: column;
gap: var(--sp-2);
min-width: 0;
}
.lw-legend {
display: flex;
flex-wrap: wrap;
gap: var(--sp-1) var(--sp-2);
align-items: center;
}
.lw-legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-2);
color: var(--text-2);
font-size: var(--fs-xs);
cursor: pointer;
transition: opacity 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.lw-legend-item:hover {
border-color: var(--line-strong);
background: var(--surface-3);
}
.lw-legend-item:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
}
.lw-legend-item i {
width: 9px;
height: 9px;
border-radius: 2px;
flex: 0 0 auto;
}
.lw-legend-item b {
color: var(--text-1);
font-weight: 600;
}
.lw-legend-item.is-off {
opacity: 0.42;
text-decoration: line-through;
}
.lw-canvas {
position: relative;
width: 100%;
min-width: 0;
}
/* tooltip:LW 无内置实现,由 LwChart 订阅十字光标渲染 */
.lw-tip {
position: absolute;
z-index: 5;
pointer-events: none;
min-width: 132px;
padding: 7px 9px;
border: 1px solid rgba(150, 165, 195, 0.28);
border-radius: var(--r-sm);
background: rgba(20, 29, 48, 0.96);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
font-size: var(--fs-xs);
}
.lw-tip-time {
color: var(--text-3);
margin-bottom: 4px;
}
.lw-tip-row {
display: flex;
align-items: center;
gap: 6px;
line-height: 1.6;
}
.lw-tip-row i {
width: 8px;
height: 8px;
border-radius: 2px;
flex: 0 0 auto;
}
.lw-tip-label {
color: var(--text-2);
flex: 1 1 auto;
white-space: nowrap;
}
.lw-tip-row b {
color: var(--text-1);
}
/* ============================================================
股票代码 + 名称(任何出现代码的地方都必须成对出现且可点击)
============================================================ */
.symlink {
display: inline-flex;
align-items: baseline;
gap: 6px;
max-width: 100%;
padding: 1px 3px;
margin: -1px -3px;
border-radius: var(--r-xs);
color: inherit;
text-decoration: none;
transition: background 0.12s ease;
}
.symlink:hover {
background: var(--accent-soft);
text-decoration: none;
}
.symlink:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
}
.symlink .sym-code {
font-family: var(--font-mono);
font-size: 0.92em;
color: var(--text-2);
white-space: nowrap;
}
.symlink:hover .sym-code {
color: var(--accent);
}
.symlink .sym-name {
color: var(--text-1);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 9em;
}
.symlink .sym-name--missing {
color: var(--text-3);
font-weight: 400;
}
/* 名称列优先:窄屏时允许名称换行而不是把代码挤掉 */
.sym-cell {
min-width: 132px;
}
/* ============================================================
策略说明与计算公式
============================================================ */
.doc-block {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.doc-summary {
display: flex;
align-items: flex-start;
gap: var(--sp-2);
padding: 10px 12px;
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: var(--r-sm);
background: linear-gradient(90deg, rgba(63, 182, 255, 0.08), transparent 70%);
color: var(--text-1);
line-height: 1.65;
}
.doc-formula {
display: flex;
flex-direction: column;
gap: 6px;
}
.doc-formula pre {
margin: 0;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--bg-1);
color: var(--text-1);
font-family: var(--font-mono);
font-size: var(--fs-xs);
line-height: 1.75;
overflow-x: auto;
white-space: pre;
}
.doc-steps {
margin: 0;
padding-left: 18px;
color: var(--text-2);
font-size: var(--fs-sm);
line-height: 1.8;
}
.doc-steps li b {
color: var(--text-1);
}
/* 策略卡片 */
.strategy-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: var(--sp-4);
}
.strategy-desc {
color: var(--text-2);
line-height: 1.7;
font-size: var(--fs-sm);
}
.strategy-actions {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
}
/* ============================================================
实验对比
============================================================ */
.cmp-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-2);
}
.cmp-diff-add {
color: var(--pos);
}
.cmp-diff-del {
color: var(--neg);
}
.cmp-diff-same {
color: var(--text-3);
}
.cmp-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--fs-xs);
color: var(--text-3);
}
/* ============================================================
易用性:粘性操作条 / 结果锚点 / 键盘提示
============================================================ */
.sticky-bar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
padding: 10px 12px;
margin-bottom: var(--sp-3);
border: 1px solid var(--line);
border-radius: var(--r-md);
background: rgba(10, 15, 28, 0.92);
backdrop-filter: blur(8px);
}
.kbd {
display: inline-block;
padding: 1px 5px;
border: 1px solid var(--line-strong);
border-bottom-width: 2px;
border-radius: 4px;
background: var(--surface-2);
color: var(--text-2);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.5;
}
/* 表单校验:错误态 + 行内提示 */
.field--invalid input,
.field--invalid select {
border-color: var(--neg);
}
.field-err {
color: var(--neg);
font-size: var(--fs-xs);
margin-top: 4px;
}
/* 可点击行(表格) */
.tbl tbody tr.row-clickable {
cursor: pointer;
}
/* 进度阶段列表 */
.stages {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
align-items: center;
}
.stage {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 9px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-2);
color: var(--text-3);
font-size: var(--fs-xs);
}
.stage.is-done {
color: var(--pos);
border-color: rgba(61, 220, 151, 0.35);
}
.stage.is-active {
color: var(--accent);
border-color: rgba(63, 182, 255, 0.45);
background: var(--accent-soft);
}
/* 面包屑 / 返回 */
.crumb {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--text-3);
font-size: var(--fs-sm);
}
.crumb a {
color: var(--text-2);
}
.crumb a:hover {
color: var(--accent);
}
/* ---------- 补充:既有页面已在用但此前没有样式规则的类 ---------- */
/* .dim:次要信息(日期/计数),此前无规则导致与正文同色,层级不清 */
.dim {
color: var(--text-3);
}
/* .input--invalid:单个输入框的错误态(.field--invalid 是整字段态) */
.input--invalid,
.input--invalid:focus {
border-color: var(--neg);
box-shadow: 0 0 0 3px var(--neg-soft);
}
/* ---------- 单选组(买不进补位策略:三选一) ---------- */
.radio-col {
display: flex;
flex-direction: column;
gap: var(--sp-1);
}
.radio-row {
display: flex;
align-items: center;
gap: var(--sp-2);
/* 点击目标由整行承担(16px 的圆点本身太小),并与 .check 同一高度节奏 */
min-height: var(--ctl-h-sm);
padding: 2px 0;
font-size: var(--fs-sm);
color: var(--text-2);
cursor: pointer;
line-height: 1.45;
}
.radio-row input {
flex: none;
}
.radio-row:hover {
color: var(--text-1);
}
/* 单选行里的说明文字:与选项标题同一行时用弱化色,不抢视线 */
.radio-row .hint {
color: var(--text-3);
}
/* ---------- 归档详情:元数据条 / KV / 规格表 / 代码块 ---------- */
/* 归档元数据与完整度提示条:信息密度高但不抢图表注意力 */
.archive-bar {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 9px 12px;
margin: 0 0 10px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
}
.kv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 8px 18px;
}
.kv {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
min-width: 0;
}
.kv__k {
flex: none;
color: var(--text-3);
}
.kv__v {
color: var(--text-1);
word-break: break-all;
}
.kv__v--warn {
color: var(--warn);
}
/* 规格表:选股条件 / 交易执行依据 */
.spec-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 14px;
}
.spec-block {
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 12px;
background: var(--surface-1);
}
.spec-block__title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--text-2);
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px dashed var(--line);
}
.spec-row {
display: flex;
gap: 10px;
align-items: baseline;
font-size: 13px;
padding: 3px 0;
line-height: 1.5;
}
.spec-row__k {
flex: none;
min-width: 96px;
color: var(--text-3);
}
.spec-row__v {
color: var(--text-1);
min-width: 0;
word-break: break-word;
}
.codeblock {
max-height: 420px;
overflow: auto;
font-size: 12px;
line-height: 1.55;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--bg-1);
white-space: pre-wrap;
word-break: break-word;
}
/* 行内代码:后端说明文本用 `…` 标记字段/命令名 */
.inline-code {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 0.92em;
padding: 1px 5px;
border: 1px solid var(--line);
border-radius: var(--r-xs);
background: var(--bg-2);
color: var(--text-1);
}