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 小屏)。
This commit is contained in:
@@ -223,8 +223,7 @@ export function BacktestResultView({
|
||||
tools={
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: 180 }}
|
||||
className="input input--search"
|
||||
placeholder="搜索代码或名称"
|
||||
value={curveQuery}
|
||||
onChange={(e) => setCurveQuery(e.target.value)}
|
||||
@@ -240,8 +239,8 @@ export function BacktestResultView({
|
||||
<>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
|
||||
<select
|
||||
className="input"
|
||||
style={{ width: 280 }}
|
||||
className="input input--picker"
|
||||
aria-label="选择要查看的个股"
|
||||
value={activeCurve?.symbol ?? ""}
|
||||
onChange={(e) => setActive(e.target.value)}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*
|
||||
* 组件**不持有业务状态**:value/onChange 由父组件控制,父组件负责提交与落库。
|
||||
*/
|
||||
import { useId, useState } from "react";
|
||||
import type { FactorMeta, ResearchCondition, ResearchSpec, StrategyDefinition } from "@/lib/types";
|
||||
import { Btn, Field } from "@/components/ui";
|
||||
|
||||
@@ -274,6 +275,14 @@ export interface StrategyParamsFormProps {
|
||||
errors?: Record<string, string>;
|
||||
/** 因子选择是否允许加权多项(默认允许) */
|
||||
multiFactor?: boolean;
|
||||
/**
|
||||
* 是否立即显示全部校验错误。
|
||||
*
|
||||
* 默认 false:只在**字段失焦过**之后才显示该字段的错误。理由:受控输入在用户
|
||||
* 清空内容准备重填的瞬间就会被判为「至少为 1」,立刻标红属于打扰式提示;
|
||||
* 提交被拦下时父组件把本值设为 true,确保此时所有问题一次看清。
|
||||
*/
|
||||
revealErrors?: boolean;
|
||||
}
|
||||
|
||||
export function StrategyParamsForm({
|
||||
@@ -285,7 +294,15 @@ export function StrategyParamsForm({
|
||||
disabled = false,
|
||||
errors = {},
|
||||
multiFactor = true,
|
||||
revealErrors = false,
|
||||
}: StrategyParamsFormProps) {
|
||||
// 稳定唯一前缀:同一个页面可能挂两份表单(策略库编辑 + 回测页),
|
||||
// 写死 id 会撞车,label/for 与 aria 关联就会指错控件。
|
||||
const fid = useId();
|
||||
// 失焦过的字段才提示错误(见 revealErrors 说明)
|
||||
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
||||
const blur = (key: string) => () => setTouched((t) => ({ ...t, [key]: true }));
|
||||
const showErr = (key: string) => (revealErrors || touched[key] ? errors[key] : undefined);
|
||||
const set = (patch: Partial<StrategyParams>) => onChange({ ...p, ...patch });
|
||||
const setFactor = (i: number, patch: Partial<{ name: string; weight: number }>) =>
|
||||
set({ factors: p.factors.map((f, j) => (j === i ? { ...f, ...patch } : f)) });
|
||||
@@ -321,21 +338,25 @@ export function StrategyParamsForm({
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ description: e.target.value })}
|
||||
/>
|
||||
{errors.description && (
|
||||
{showErr("description") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.description}
|
||||
{showErr("description")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---------- 因子(打分公式) ---------- */}
|
||||
{/* ---------- 因子(打分公式) ----------
|
||||
用「表头 + 栅格行」表达多项因子:列宽由 CSS 栅格统一控制(不再在 JSX 里写
|
||||
内联宽度),这样因子下拉、权重输入、删除按钮在所有行严格成列对齐;
|
||||
视觉表头给正常用户,aria-label 给读屏(重复行只有表头时读屏无法分辨)。 */}
|
||||
<div className="between" style={{ marginBottom: 8 }}>
|
||||
<b style={{ fontSize: 13 }}>打分因子(score = Σ 权重 × 因子值,越大越优先)</b>
|
||||
<b className="form-section__title">打分因子(score = Σ 权重 × 因子值,越大越优先)</b>
|
||||
{multiFactor && (
|
||||
<Btn
|
||||
icon="layers"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => set({ factors: [...p.factors, { name: factorOptions[0]?.name ?? "", weight: 1 }] })}
|
||||
>
|
||||
@@ -343,12 +364,18 @@ export function StrategyParamsForm({
|
||||
</Btn>
|
||||
)}
|
||||
</div>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 8, marginBottom: 6 }}>
|
||||
<div className="factor-rows">
|
||||
<div className="factor-rows__head" aria-hidden="true">
|
||||
<span>因子</span>
|
||||
<span>权重</span>
|
||||
<span />
|
||||
</div>
|
||||
{p.factors.map((f, i) => (
|
||||
<div className="row" key={i} style={{ gap: 6 }}>
|
||||
<div className="factor-row" key={i}>
|
||||
<select
|
||||
id={`${fid}-name-${i}`}
|
||||
className="input"
|
||||
style={{ width: 220 }}
|
||||
aria-label={`第 ${i + 1} 个因子的名称`}
|
||||
value={f.name}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setFactor(i, { name: e.target.value })}
|
||||
@@ -363,27 +390,37 @@ export function StrategyParamsForm({
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
id={`${fid}-weight-${i}`}
|
||||
className="input mono"
|
||||
style={{ width: 84 }}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.1"
|
||||
min={0}
|
||||
aria-label={`第 ${i + 1} 个因子的权重`}
|
||||
title="权重:越大表示该因子在打分中越重要(1 = 等权)"
|
||||
value={f.weight}
|
||||
disabled={disabled}
|
||||
title="权重"
|
||||
onChange={(e) => setFactor(i, { weight: Number(e.target.value) })}
|
||||
/>
|
||||
{multiFactor && p.factors.length > 1 && (
|
||||
{multiFactor && p.factors.length > 1 ? (
|
||||
/* 与同行的下拉/输入同为 md 高度:行内控件必须等高,否则整行看起来是斜的 */
|
||||
<Btn
|
||||
icon="x"
|
||||
disabled={disabled}
|
||||
aria-label={`删除第 ${i + 1} 个因子`}
|
||||
title="删除该因子"
|
||||
onClick={() => set({ factors: p.factors.filter((_, j) => j !== i) })}
|
||||
>
|
||||
删除
|
||||
</Btn>
|
||||
/>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hint" style={{ marginTop: 6 }}>
|
||||
权重 1 = 等权;只想用单个因子时把其它因子删掉即可。多个因子会先各自做横截面
|
||||
z-score 标准化再加权,避免量纲不同互相压制。
|
||||
</div>
|
||||
{errors.factors && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.factors}
|
||||
@@ -392,66 +429,82 @@ export function StrategyParamsForm({
|
||||
|
||||
{/* ---------- 选股规模与周期 ---------- */}
|
||||
<div className="form-grid" style={{ marginTop: 12 }}>
|
||||
<Field label="候选池 n(择股条件选出股数)">
|
||||
<Field
|
||||
label="候选池 n(择股条件选出股数)"
|
||||
hint="范围 1~500;n 越大越分散,越小越集中"
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.topN ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
max={500}
|
||||
value={p.topN}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ topN: Number(e.target.value) })}
|
||||
onBlur={blur("topN")}
|
||||
/>
|
||||
{showErr("topN") && (
|
||||
<div className="field-err" role="alert">
|
||||
{showErr("topN")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="持仓数 x(≤ n)">
|
||||
<Field label="持仓数 x(≤ n)" hint="最终实际持有只数,通常等于 n">
|
||||
<input
|
||||
className={`input${errors.holdX ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
max={p.topN}
|
||||
value={p.holdX}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ holdX: Number(e.target.value) })}
|
||||
onBlur={blur("holdX")}
|
||||
/>
|
||||
{errors.holdX && (
|
||||
{showErr("holdX") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.holdX}
|
||||
{showErr("holdX")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="择股间隔 m(月,0=跟随 y)" hint="m:多久重新挑一次股">
|
||||
<Field label="择股间隔 m(月)" hint="多久重新挑一次股;0 = 跟随 y">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.mMonths ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
max={60}
|
||||
value={p.mMonths}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ mMonths: Number(e.target.value) })}
|
||||
onBlur={blur("mMonths")}
|
||||
/>
|
||||
{errors.mMonths && (
|
||||
{showErr("mMonths") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.mMonths}
|
||||
{showErr("mMonths")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="调仓间隔 y(月,0=跟随 m)" hint="y:多久按最新选股结果换一次仓">
|
||||
<Field label="调仓间隔 y(月)" hint="多久按最新选股结果换一次仓;0 = 跟随 m">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.yMonths ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
max={60}
|
||||
value={p.yMonths}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ yMonths: Number(e.target.value) })}
|
||||
onBlur={blur("yMonths")}
|
||||
/>
|
||||
{errors.yMonths && (
|
||||
{showErr("yMonths") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.yMonths}
|
||||
{showErr("yMonths")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="调仓频率(m、y 均为 0 时生效)">
|
||||
<Field label="调仓频率" hint="仅当 m、y 都为 0 时生效(否则由 m/y 决定)">
|
||||
<select
|
||||
className="input"
|
||||
value={p.rebalance}
|
||||
@@ -465,6 +518,7 @@ export function StrategyParamsForm({
|
||||
<Field
|
||||
label="买不进时的补位(涨停 / 停牌)"
|
||||
hint="后端两个字段互斥,这里用三选一表达,避免出现「都不生效」的静默组合"
|
||||
className="field--wide"
|
||||
>
|
||||
<div className="radio-col">
|
||||
{(
|
||||
@@ -474,10 +528,12 @@ export function StrategyParamsForm({
|
||||
["none", "不补位", "买不进就空着,实际持仓可能少于持仓数 x"],
|
||||
] as const
|
||||
).map(([val, label, desc]) => (
|
||||
<label key={val} className="radio-row" title={desc}>
|
||||
<label key={val} className="radio-row">
|
||||
<input
|
||||
type="radio"
|
||||
name="fill-policy"
|
||||
/* 同一页面可能挂两份表单:name 必须带表单实例前缀,
|
||||
否则两边的单选会互相取消选中(原生 radio 按 name 分组) */
|
||||
name={`${fid}-fill-policy`}
|
||||
checked={p.fillPolicy === val}
|
||||
disabled={disabled}
|
||||
onChange={() => set({ fillPolicy: val })}
|
||||
@@ -506,21 +562,22 @@ export function StrategyParamsForm({
|
||||
<option value="none">不复权 none</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="标的范围">
|
||||
<label className="row" style={{ gap: 6, color: "var(--text-2)", fontSize: 13, cursor: "pointer" }}>
|
||||
<Field label="标的范围" hint="ST 按择股日当时的股票名称判定">
|
||||
<label className="check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={p.excludeSt}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ excludeSt: e.target.checked })}
|
||||
/>
|
||||
剔除 ST(按择股日当时名称判定)
|
||||
剔除 ST
|
||||
</label>
|
||||
</Field>
|
||||
<Field label="最少上市天数" hint="避免次新股噪声;0 = 不限制">
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
step={10}
|
||||
value={p.minListingDays}
|
||||
@@ -528,86 +585,113 @@ export function StrategyParamsForm({
|
||||
onChange={(e) => set({ minListingDays: Number(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="手续费率 %">
|
||||
<Field
|
||||
label="手续费率 %"
|
||||
hint="买卖双向收取;A 股常见 0.025%~0.03%"
|
||||
>
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.costs ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.01"
|
||||
min={0}
|
||||
max={1}
|
||||
value={p.commission}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ commission: Number(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="印花税率 %" hint="仅卖出收取">
|
||||
<Field label="印花税率 %" hint="仅卖出收取;A 股 0.05%(2023 年 8 月起减半)">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.costs ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.01"
|
||||
min={0}
|
||||
max={1}
|
||||
value={p.stamp}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ stamp: Number(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="滑点率 %">
|
||||
<Field label="滑点率 %" hint="按成交价的比例估计冲击成本;0.05%~0.1% 较常见">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.costs ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.01"
|
||||
min={0}
|
||||
max={1}
|
||||
value={p.slippage}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ slippage: Number(e.target.value) })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="最低佣金(元/笔)">
|
||||
<Field label="最低佣金(元/笔)" hint="按笔收取的下限,如 5 元">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.minCommission ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="1"
|
||||
min={0}
|
||||
value={p.minCommission}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ minCommission: Number(e.target.value) })}
|
||||
onBlur={blur("minCommission")}
|
||||
/>
|
||||
{errors.minCommission && (
|
||||
{showErr("minCommission") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.minCommission}
|
||||
{showErr("minCommission")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
{/* 费率类错误的行内落点:三个费率共用一个错误键(费率不能为负) */}
|
||||
{errors.costs && (
|
||||
<div className="field-err" role="alert" style={{ gridColumn: "1 / -1" }}>
|
||||
{errors.costs}
|
||||
</div>
|
||||
)}
|
||||
{showPeriod && (
|
||||
<>
|
||||
<Field label="初始资金(元)">
|
||||
<Field label="初始资金(元)" hint="回测初始本金,默认 100 万">
|
||||
<input
|
||||
className="input"
|
||||
className={`input${errors.capital ? " input--invalid" : ""}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
step={100000}
|
||||
min={10000}
|
||||
value={p.capital}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ capital: Number(e.target.value) })}
|
||||
onBlur={blur("capital")}
|
||||
/>
|
||||
{errors.capital && (
|
||||
{showErr("capital") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.capital}
|
||||
{showErr("capital")}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="开始日期">
|
||||
<Field
|
||||
label="开始日期"
|
||||
hint="回测起始日(含)"
|
||||
className={showErr("period") ? "field--invalid" : undefined}
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
className={`input${showErr("period") ? " input--invalid" : ""}`}
|
||||
value={p.start}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ start: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="结束日期">
|
||||
<Field
|
||||
label="结束日期"
|
||||
hint="回测结束日(含)"
|
||||
className={showErr("period") ? "field--invalid" : undefined}
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
className={`input${showErr("period") ? " input--invalid" : ""}`}
|
||||
value={p.end}
|
||||
disabled={disabled}
|
||||
onChange={(e) => set({ end: e.target.value })}
|
||||
@@ -616,18 +700,21 @@ export function StrategyParamsForm({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{errors.period && (
|
||||
{showErr("period") && (
|
||||
<div className="field-err" role="alert">
|
||||
{errors.period}
|
||||
{showErr("period")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---------- 选股过滤条件 ---------- */}
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div className="between" style={{ marginBottom: 8 }}>
|
||||
<b style={{ fontSize: 13 }}>选股过滤条件(AND,universe 之后、因子排序之前执行)</b>
|
||||
<b className="form-section__title">
|
||||
选股过滤条件(AND,universe 之后、因子排序之前执行)
|
||||
</b>
|
||||
<Btn
|
||||
icon="layers"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => set({ conditions: [...p.conditions, { field: "", op: "gte", value: 0 }] })}
|
||||
>
|
||||
@@ -637,12 +724,19 @@ export function StrategyParamsForm({
|
||||
{p.conditions.length === 0 ? (
|
||||
<div className="hint">未设置条件:候选池 = universe 内因子分最高的 n 只。</div>
|
||||
) : (
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 8 }}>
|
||||
<div className="cond-rows">
|
||||
<div className="cond-rows__head" aria-hidden="true">
|
||||
<span>字段</span>
|
||||
<span>比较</span>
|
||||
<span>取值</span>
|
||||
<span />
|
||||
</div>
|
||||
{p.conditions.map((c, i) => (
|
||||
<div className="row" key={i} style={{ gap: 6 }}>
|
||||
<div className="cond-row" key={i}>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: 190 }}
|
||||
className="input mono"
|
||||
list={`${fid}-cond-fields`}
|
||||
aria-label={`第 ${i + 1} 个条件的字段名`}
|
||||
placeholder="字段(dv_ratio / pe / ma60 …)"
|
||||
value={c.field}
|
||||
disabled={disabled}
|
||||
@@ -656,7 +750,7 @@ export function StrategyParamsForm({
|
||||
/>
|
||||
<select
|
||||
className="input"
|
||||
style={{ width: 76 }}
|
||||
aria-label={`第 ${i + 1} 个条件的比较符`}
|
||||
value={c.op}
|
||||
disabled={disabled}
|
||||
onChange={(e) =>
|
||||
@@ -674,8 +768,10 @@ export function StrategyParamsForm({
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: 110 }}
|
||||
className="input mono"
|
||||
aria-label={`第 ${i + 1} 个条件的取值`}
|
||||
inputMode="decimal"
|
||||
placeholder="数值或字段名"
|
||||
value={String(c.value ?? "")}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
@@ -693,10 +789,10 @@ export function StrategyParamsForm({
|
||||
<Btn
|
||||
icon="x"
|
||||
disabled={disabled}
|
||||
aria-label={`删除第 ${i + 1} 个条件`}
|
||||
title="删除该条件"
|
||||
onClick={() => set({ conditions: p.conditions.filter((_, j) => j !== i) })}
|
||||
>
|
||||
删除
|
||||
</Btn>
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -131,11 +131,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
<div className="topbar">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
className="icon-btn icon-btn--lg"
|
||||
aria-label="打开导航菜单"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen(true)}
|
||||
style={{ width: 36, height: 36 }}
|
||||
>
|
||||
<Icon name="filter" size={18} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user