Files
Simon a395db892d 修复实验页右侧内容溢出:宽表固定两端 + 提示;对比表首列固定;月度表收紧内边距
- 实验列表 11 列在 1366px 及更窄窗口放不下,「操作」列被推到卡片外;macOS 覆盖式
  滚动条不滚动不显示,看起来就是「右侧内容溢出/被切掉」。现在左右两端固定
  (选择列、ID、操作列),列宽按实测单行内容宽度定,只有真溢出时才提示可横向滚动。
- 对比视图(指标对比 / 参数差异)列数随实验个数增长,首列改为固定,同样按需提示。
- 详情图层里的月度收益表 13 列超出 38px:收紧内边距并固定「年份」列。
- 可排序表头命中区 19.2px → 34px(与同排复选框等高),修掉 UI 规范门禁的 7 项失败。
- 顺手把对比区两处会原样显示的字面 ** 改成「」。

门禁:ruff 通过;pytest 524 passed;tsc 0 error;npm run build OK;test:charts 7 passed;
verify_ui_alignment.py 160 项通过 / 0 失败。
2026-10-01 19:33:33 +08:00

2482 lines
51 KiB
CSS
Raw Permalink 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;
}
/* 数值/日期字段:在已限宽(.params-form ~1100px)的栅格里直接填满单元格,
与同行的下拉/文本框等宽 → 左右边缘严格对齐。早先的 max-width:180px 在表单
铺满整屏时有意义,限宽后反而让数字框比相邻下拉窄一截(截图实测),故移除。 */
.form-grid .field > input[type="number"].input,
.form-grid .field > input[type="date"].input {
width: 100%;
}
.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%);
}
/* ============================================================
参数表单布局:把 sprawling 的多列表单收进一个可读的最大宽度。
------------------------------------------------------------
为什么需要:表单直接铺满 ~1900px 的内容区时,5 列各 ~360px,控件拉得太开、
数字输入和下拉宽度还不一致(截图实测)。约束到 ~1100px 后每列 ~210px,
所有控件按 1fr 等宽 → 同行左右边缘严格对齐,数字也不会宽得离谱。
============================================================ */
.params-form {
max-width: 1100px;
}
/* 策略名 + 一句话说明:定宽名 + 占满剩余宽度的多行说明 */
.params-meta {
display: flex;
gap: var(--sp-4);
align-items: flex-start;
margin-bottom: var(--sp-4);
}
.params-meta__name {
flex: 0 0 340px;
min-width: 0;
}
.params-meta__desc {
flex: 1 1 auto;
min-width: 0;
}
.params-meta__desc textarea.input {
min-height: 64px;
line-height: 1.6;
resize: vertical;
}
.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;
/* 因子名列封顶 460px:再宽就是「momentum_60」配 1500px 下拉的失调(截图实测)。
权重列固定,操作列按内容。整行也限宽,避免在宽屏上拉成一条长线。
start 对齐:因子列比别列高(下拉 + 口径/方向/简介两行说明),
居中会让权重框浮在说明旁边显得没对齐。 */
grid-template-columns: minmax(150px, 460px) 118px auto;
align-items: start;
gap: var(--sp-2);
max-width: 760px;
}
.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);
max-width: 940px;
}
.cond-rows__head,
.cond-row {
display: grid;
grid-template-columns: minmax(150px, 260px) 84px 110px minmax(120px, 190px) auto;
align-items: start;
gap: var(--sp-2);
}
/* 单元格:下拉 + 该选项的口径说明(因子行与条件行共用 —— 选了就能看到含义,
不用切到「字段库」/「因子研究」页去查) */
.cell-stack {
display: grid;
gap: 2px;
align-content: center;
}
.cell-hint {
font-size: var(--fs-xs);
line-height: 1.35;
display: block;
}
/* 取值单元格:输入框 + 右侧的**界面单位**后缀。存储值一律是基准单位,这里显示的是
字段库选的界面单位(如「亿元」),提交时按系数换算 —— 所以后缀必须始终可见,
否则用户会把「5 亿元」当成「5 万元」来读。 */
.unit-input {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 4px;
}
.unit-suffix {
font-size: var(--fs-xs);
color: var(--text-3);
white-space: nowrap;
}
/* 因子 vs 条件 的关系说明卡(表单内固定展示) */
.relation-note {
margin-top: 14px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: var(--r-2);
background: var(--surface-2, transparent);
display: grid;
gap: 8px;
}
.relation-note__title {
font-weight: 600;
font-size: var(--fs-sm);
}
.relation-note__flow {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.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);
}
/* 月度收益表:13 列,默认内边距(9px 12px)下要 1070px,装不进 1032px 的详情图层,
右侧会被切掉 38px(就是「右侧内容溢出」的样子)。收到 8px 6px 后 13 列省下约 150px。 */
.tbl--monthly th,
.tbl--monthly td {
padding: 8px 6px;
}
/* ---------- 对比表(指标对比 / 参数差异):首列固定 ----------
两张表都是「一行一个指标/参数 + 每个实验一列」,列数随对比个数增长,窄窗口必然横向滚动。
首列(指标名、参数名)不固定的话,滚到右边看到一串数字却不知道对应哪一项。 */
.tbl--pin-first th:first-child,
.tbl--pin-first td:first-child {
position: sticky;
left: 0;
z-index: 2;
background: var(--surface-1);
}
.tbl--pin-first thead th:first-child {
z-index: 4;
background: #131c2e;
}
.tbl--pin-first tbody tr:hover td:first-child {
background: #17202f;
}
/* ---------- 宽表(实验列表):固定两端 + 中间横向滚动 ----------
实验列表有 11 列(选择 / 对比 / ID / 类型 / 发起时间 / 因子 / 区间 / 摘要 / 版本 / 体积 / 操作)。
1366px 及更窄的窗口一屏放不下:不给右端「操作」列做固定,它会被推到卡片外 ——
用户看到的是「右侧内容溢出了」,勾了「删除」却找不到「详情 / 复跑 / 打开归档」。
所以:左端两列选择框与 ID、右端操作列**固定**,中间列横向滚动;
同时用固定列宽 + 收紧内边距,让 1366px 以上仍然一屏放得下(不出现没必要的滚动条)。
固定列必须有不透明底色,否则中间列会从下面透出来(卡片是渐变,这里取近似纯色)。 */
.table-wrap--wide {
overflow-x: auto;
}
.tbl--wide {
table-layout: fixed;
}
.tbl--wide th,
.tbl--wide td {
padding: 8px 9px;
}
.tbl--wide th:nth-child(1),
.tbl--wide td:nth-child(1) {
width: 42px;
}
.tbl--wide th:nth-child(2),
.tbl--wide td:nth-child(2) {
width: 42px;
}
.tbl--wide th:nth-child(3),
.tbl--wide td:nth-child(3) {
width: 117px;
}
.tbl--wide th:nth-child(4),
.tbl--wide td:nth-child(4) {
width: 62px;
}
.tbl--wide th:nth-child(5),
.tbl--wide td:nth-child(5) {
width: 104px;
}
.tbl--wide th:nth-child(6),
.tbl--wide td:nth-child(6) {
width: 117px;
}
.tbl--wide th:nth-child(7),
.tbl--wide td:nth-child(7) {
width: 104px;
}
.tbl--wide th:nth-child(8),
.tbl--wide td:nth-child(8) {
width: 140px;
}
.tbl--wide th:nth-child(9),
.tbl--wide td:nth-child(9) {
width: 137px;
}
/* 这个表头文字比列宽长(「版本(代码 / 数据)」),允许它折两行;
其它 th 保持 nowrap,否则表头会挤成多行、列宽失控 */
.tbl--wide th:nth-child(9) {
white-space: normal;
line-height: 1.3;
}
.tbl--wide th:nth-child(10),
.tbl--wide td:nth-child(10) {
width: 71px;
}
.tbl--wide th:nth-child(11),
.tbl--wide td:nth-child(11) {
width: 186px;
text-align: right;
}
/* 版本列的数据指纹 chip 很长(d20260904;n≈7688k;a≈7922k;b≈7718k):
不设上限就会横向顶进「操作」列(看起来就是「右侧内容溢出」)。这里在单元格内省略,
完整值仍在 chip 的 title 里(悬停可见),不靠截断字符串假装完整。 */
.tbl--wide td:nth-child(9) .row {
min-width: 0;
flex-wrap: wrap;
}
.tbl--wide td:nth-child(9) .tag,
.tbl--wide td:nth-child(9) .hint {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
}
/* ID:等宽字符串不能折行(折成 "EXP-"/"8EA2819B" 就没法扫读),宽度不够时省略号 +
title 给完整值。发起时间与区间**允许折行**:它们按「日期 / 时刻」「起 / 止」自然断行,
比折在日期中间("2020-01-")可读,也让这两列不必占满一行的宽度。 */
.tbl--wide td:nth-child(3) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 左端固定:选择(删除)/ 对比 / ID。left 偏移 = 前面各固定列的宽度之和,
所以上面的列宽改动必须同步改这里的 left(table-layout: fixed 下宽度是确定的)。 */
.tbl--wide th:nth-child(-n + 3),
.tbl--wide td:nth-child(-n + 3) {
position: sticky;
z-index: 2;
background: var(--surface-1);
}
.tbl--wide th:nth-child(1),
.tbl--wide td:nth-child(1) {
left: 0;
}
.tbl--wide th:nth-child(2),
.tbl--wide td:nth-child(2) {
left: 38px;
}
.tbl--wide th:nth-child(3),
.tbl--wide td:nth-child(3) {
left: 76px;
}
/* 右端固定:操作列。加一点内阴影,滚动时能看出「这一列是浮在上面的」 */
.tbl--wide th:nth-child(11),
.tbl--wide td:nth-child(11) {
position: sticky;
right: 0;
z-index: 2;
background: var(--surface-1);
box-shadow: -10px 0 12px -12px rgba(0, 0, 0, 0.9);
}
.tbl--wide thead th {
z-index: 4;
background: #131c2e;
}
/* 行 hover / 选中:固定列的底色也要跟着变,否则整行高亮时两端是「断开」的 */
.tbl--wide tbody tr:hover td:nth-child(-n + 3),
.tbl--wide tbody tr:hover td:nth-child(11) {
background: #17202f;
}
.tbl--wide tbody tr.row-active td:nth-child(-n + 3),
.tbl--wide tbody tr.row-active td:nth-child(11) {
background: #17253d;
}
/* 扩展行 / 详情内嵌 */
.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;
}
/* 窄屏:策略名与说明上下堆叠,名占满整行 */
.params-meta {
flex-direction: column;
gap: var(--sp-3);
}
.params-meta__name {
flex: 1 1 auto;
width: 100%;
}
.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);
}
/* ---------- 回测组合:选股策略多选卡片 ---------- */
.strategy-pick {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--sp-2);
}
.strategy-pick__item {
display: grid;
grid-template-columns: auto 1fr;
grid-template-areas:
"check name"
"check meta"
"desc desc"
"factors factors";
gap: 2px var(--sp-2);
align-items: center;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
background: var(--surface-2);
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.strategy-pick__item:hover {
border-color: rgba(150, 165, 195, 0.42);
}
.strategy-pick__item.is-on {
border-color: var(--accent);
background: var(--accent-soft);
}
.strategy-pick__item input[type="checkbox"] {
grid-area: check;
width: 16px;
height: 16px;
}
.strategy-pick__name {
grid-area: name;
font-weight: 600;
color: var(--text-1);
font-size: var(--fs-sm);
}
.strategy-pick__meta {
grid-area: meta;
color: var(--text-3);
font-size: var(--fs-xs);
}
.strategy-pick__desc {
grid-area: desc;
color: var(--text-2);
font-size: var(--fs-xs);
line-height: 1.5;
margin-top: 4px;
}
.strategy-pick__factors {
grid-area: factors;
color: var(--violet);
font-size: var(--fs-xs);
margin-top: 2px;
}
/* ---------- 回测组合:已保存组合库 ---------- */
/* 单列纵向列表(每行「摘要 + 操作」),与选股策略卡的网格区分:
组合条目信息更密(参数 chip + 说明 + 三个动作),横排比网格更好扫读。 */
.combo-list {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.combo-item {
display: flex;
flex-wrap: wrap;
gap: var(--sp-3);
align-items: center;
justify-content: space-between;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
background: var(--surface-2);
transition: border-color 0.15s ease, background 0.15s ease;
}
.combo-item:hover {
border-color: rgba(150, 165, 195, 0.42);
}
/* 当前载入的组合:与 .strategy-pick__item.is-on 同一视觉语言(强调色描边) */
.combo-item.is-on {
border-color: var(--accent);
background: var(--accent-soft);
}
.combo-item__main {
min-width: 0;
flex: 1 1 320px;
}
/* ---------- 作业反馈条(JobProgress):点「运行」后必须立刻看得见 ---------- */
.job-progress {
margin: 12px 0;
padding: 12px 14px;
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: var(--r-md);
background: var(--surface-2);
}
.job-progress--run {
border-left-color: var(--accent);
background: var(--accent-soft);
}
.job-progress--ok {
border-left-color: var(--pos);
background: rgba(61, 220, 151, 0.08);
}
.job-progress--bad {
border-left-color: var(--neg);
background: rgba(255, 106, 118, 0.08);
}
.job-progress__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-2);
}
.job-progress__title {
font-weight: 600;
font-size: var(--fs-sm);
}
.job-progress__meta {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-xs);
color: var(--text-2);
font-variant-numeric: tabular-nums;
}
.job-progress__actions {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-left: auto;
}
.job-progress__err {
margin-top: 8px;
padding: 8px 10px;
border-radius: var(--r-sm);
background: var(--surface-1);
color: var(--neg);
font-size: var(--fs-xs);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* ---------- 买卖说明:分段筛选 + 理由单元格 ---------- */
.seg {
display: inline-flex;
border: 1px solid var(--line);
border-radius: var(--r-sm);
overflow: hidden;
}
.seg__btn {
appearance: none;
border: 0;
background: var(--surface-2);
color: var(--text-2);
font: inherit;
font-size: var(--fs-xs);
padding: 5px 10px;
cursor: pointer;
}
.seg__btn + .seg__btn {
border-left: 1px solid var(--line);
}
.seg__btn.is-on {
background: var(--accent-soft);
color: var(--accent-strong);
font-weight: 600;
}
/* 可排序表头:本体必须和同一行里的控件(如「全选本页」复选框,命中区 34px)等高,
否则 19.2px 高的点击目标既难点中,也会被 UI 规范门禁判为「同排控件不等高」。
行高不因此变化:表头行本来就由 34px 的复选框决定。 */
.th-sort {
appearance: none;
border: 0;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
padding: 0 6px;
min-height: var(--ctl-h-md);
border-radius: var(--r-sm);
}
.th-sort:hover {
background: rgba(150, 165, 195, 0.075);
}
.reason-cell {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 260px;
}
.reason-cell__head {
display: flex;
align-items: flex-start;
gap: 6px;
flex-wrap: wrap;
}
.reason-cell__text {
font-size: var(--fs-xs);
color: var(--text-1);
overflow-wrap: anywhere;
}
.reason-cell__facts {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.reason-brief {
font-size: var(--fs-xs);
color: var(--text-2);
max-width: 160px;
overflow-wrap: anywhere;
}
.nowrap {
white-space: nowrap;
}