Files
qlib/frontend/web/app/globals.css
T
Simon 48a97c2a12 feat(backtest): 买卖点理由(用数据说话)+ 因子曲线 + 曲线新页面放大
用户要求:「所有买卖点详细说明买卖理由,用数据说话」「回测图上增加因子相关曲线
(买卖依据是股息率,就加股息率曲线)」「所有曲线能弹出新页面放大」。

一、买卖理由(后端产出结构化数据,前端只展示)
- 新增 `quant/trade_reasons.py`:封闭词表 + 文案构造器,组合引擎与单策略引擎共用,
  避免两个引擎对同一件事写出两种说法。理由里带**引擎当时的真实数字**:
  综合分名次/候选数/综合分/各因子原始值/持有交易日/预算与最低佣金/涨停比值等。
- 买入:按名次建仓、顺延成交、涨停未买、停牌未买、现金不足、不足最低佣金;
  卖出:跌出 TopN(含第几名掉出)、被股票池过滤(与「跌出 TopN」分开写)、
  超 Tmax 强制了结、Tmin 保护暂留、停牌/跌停顺延。
- `ActionRecord.reason` 覆盖**成交与未成交**全部买卖点(原 `reject_reason` 保留不动,
  老归档仍可读);`Trade.entry_reason / exit_reason` 跟着成交记录走。
- 名次来自调仓日完整排名(新增 `_ranked_by_day`),拿不到名次时如实写「未给出名次」,
  绝不编造一个名次填进去。
- 未成交明细不再只写执行层原因:把「为什么选中它、当时各因子多少」一并给出。

二、因子曲线
- `FactorCurve`:每个策略因子一条曲线,值为**当日持仓按市值加权平均的原始值**
  (不做 z-score、不按方向取反,空仓日不落点、不插值、不用 0 填充),并带
  label/direction/unit 供界面说明口径;`FactorDef/FactorTemplate` 新增 `unit`
  (股息率 %、量比/接近新高 倍数、动量等 小数),11 个内置因子实例已逐一核对。
- 归档体积预算照旧按整包计量,无需改迁移。

三、界面
- 结果页新增「买卖说明」区块:全部买卖点 + 理由 + 数字标签,支持方向/成交状态/关键字
  筛选与日期排序;成交明细表加「为什么买 / 为什么卖」两列;新增「因子曲线」区块,
  每条曲线标出组合成交日,直接对照「买卖发生在什么水平」。
- 「新页面放大」:每条曲线(净值/回撤/因子/个股/月度)都能开 `/charts/{归档id}?s=...`
  整页看大图;放大页是 Server Component,数据从归档直出,URL 可分享且与归档一致。
  未归档的结果如实说明「未归档,无法放大」,不给坏链接。
- 数字格式与后端 `f"{v:.4f}"` 同规则(四舍六入五成双):修掉 0.03125 在理由原文里
  显示 0.0312、旁边标签显示 0.0313 的不一致(17 组边界值与 Python 逐一比对一致)。
- `/factors/compose` 结果区改用同一个 `BacktestResultView`,两处口径不会再漂移。

验证:
- 新增 `tests/test_trade_reasons.py` 8 条(买入数字、跌出 TopN 名次、不在候选池、
  Tmax、Tmin 暂留、涨停未成交、因子曲线加权值、空仓不落点);后端 510 条全过,ruff clean。
- 真实数据端到端:`/api/combos/run` 6 个月高股息组合(EXP-8EA2819B)13 个买卖点
  100% 带理由与数字,因子曲线 dividend_yield 117 点、单位 %;
  `scripts/verify_backtest_page_contract.py`(4 年、301 个买卖点、140 笔成交)扩展断言
  理由词表/名次/因子值/曲线单调性后通过。
- 浏览器实测:归档详情页与放大页 `/charts/...?s=factor:dividend_yield` 等 5 种曲线
  全部 200 渲染,截图确认表格与曲线数值正确。
2026-10-01 17:57:00 +08:00

2312 lines
46 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;
}
/* 数值/日期字段:在已限宽(.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);
}
/* 扩展行 / 详情内嵌 */
.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;
}
.th-sort {
appearance: none;
border: 0;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
padding: 0;
}
.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;
}