mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 02:16:13 +00:00
feat(strategy): 策略参数 UI 元数据(级联显隐 / 分组 / 预设批量应用) (#447)
* feat(strategy): 策略参数 UI 元数据(级联显隐 / 分组 / 预设批量应用)
META["params"] 的 dict 项本就由后端 _normalize_param_item 原样透传额外
key,三个可选 UI 元数据字段零后端改动即可生效,未声明的参数行为不变:
- visible_if: 级联显隐,支持 {param,in} 单条件与 all/any 递归组合;
隐藏仅是显示层过滤,参数值仍按当前值随请求提交(策略语义不受影响)
- group: 相同组名的相邻参数在参数面板归并为一节(带节标题)
- option_applies: select 选中某选项时批量应用一组参数值(如标的池预设)
实现:
- 新增 lib/strategyParams.ts(递归条件求值 + 分组归并,含 vitest 单测)
- 回测页参数面板:分节渲染 + 级联联动 + 命名组内横排布局
- 策略设置弹窗:同样分节 + 级联 + 预设应用
- paramSweep:级联隐藏参数不参与扫描(按持久化默认值判定显隐)
- strategy-guide 双文档补元数据说明
* fix(strategy): 精简指南移除 UI 元数据注释行 — 修复 5000 字符上限测试
* fix(frontend): 参数扫描面板短标签加组名前缀 — paramDisplayLabel
分组内短标签(「止盈」「入池」)脱离分组上下文后不可区分: 扫描面板是
平铺列表, 同名参数挤在一起无法辨认, 级联隐藏提示行同样不可读。
新增 paramDisplayLabel (组名 · 标签), 用于扫描行与隐藏提示;
参数面板本身有组标题不受影响。
This commit is contained in:
@@ -37,6 +37,14 @@ META = {
|
||||
# 策略参数 (只把用户可能调节的阈值放这里,公式常数不必参数化)
|
||||
# type 支持: float / int / bool / select(带 options) / date(格式 "YYYY-MM-DD") / string(自由文本)
|
||||
# float/int 可带 min/max/step;select 带 options: [{label, value}];date/string 的 default 是字符串
|
||||
#
|
||||
# 可选 UI 元数据 (只影响参数面板展示, 不影响策略语义; 后端原样透传):
|
||||
# "group": "组名" — 相同组名的相邻参数归并为一节显示
|
||||
# "visible_if": {...} — 级联显隐: {"param": "pid", "in": [值...]} 单条件,
|
||||
# 或 {"all": [...]} / {"any": [...]} 递归组合;
|
||||
# in 值类型应与目标参数一致 (bool 用 True/False 而非字符串);
|
||||
# 隐藏只是不渲染, 参数值仍按当前值随请求提交
|
||||
# "option_applies": {选项: {pid: 值}} — select 选中某选项时批量应用一组参数值 (如预设)
|
||||
"params": [
|
||||
],
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { X, Settings2, RotateCcw, Save, Filter, Star, TrendingUp, Sparkles, Down
|
||||
import { api, type StrategyDetail, type StrategyParamDef, type CompositeChildInfo, type ScoringDirection, type CustomSignalCondition } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { toPercentages, normalizeWeights } from '@/lib/weights'
|
||||
import { groupParams, visibleParams } from '@/lib/strategyParams'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { BUILTIN_COLUMNS } from '@/lib/watchlist-columns'
|
||||
import { color } from '@/lib/colors'
|
||||
import { SignalPicker } from './SignalPicker'
|
||||
@@ -167,10 +169,12 @@ function RiskPctField({ label, value, onChange, min, max, step = 0.5, unit = '%'
|
||||
|
||||
// 策略参数字段
|
||||
// memo: 单参数修改只重渲本行 (父组件需传稳定回调, 否则 memo 失效)
|
||||
const ParamField = memo(function ParamField({ def, value, onChange }: {
|
||||
const ParamField = memo(function ParamField({ def, value, onChange, onApplyValues }: {
|
||||
def: StrategyParamDef
|
||||
value: any
|
||||
onChange: (pid: string, v: any) => void
|
||||
/** select 带 option_applies 时, 选中某选项需批量应用一组参数值 (如标的池预设) */
|
||||
onApplyValues?: (values: Record<string, any>) => void
|
||||
}) {
|
||||
if (def.type === 'bool') {
|
||||
const checked = value === true || value === 'true' || value === 'True'
|
||||
@@ -199,7 +203,15 @@ const ParamField = memo(function ParamField({ def, value, onChange }: {
|
||||
<span className={PARAM_LABEL_CLS} title={def.label}>{def.label}</span>
|
||||
<select
|
||||
value={value ?? def.default}
|
||||
onChange={e => onChange(def.id, e.target.value)}
|
||||
onChange={e => {
|
||||
const v = e.target.value
|
||||
const applies = def.option_applies?.[v]
|
||||
if (applies && onApplyValues) {
|
||||
onApplyValues({ [def.id]: v, ...applies })
|
||||
// 预设会覆盖其他参数的当前值, 显式提示避免静默改动
|
||||
toast(`已应用预设「${v}」,覆盖 ${Object.keys(applies).length} 个参数`, 'success')
|
||||
} else onChange(def.id, v)
|
||||
}}
|
||||
className={`${FIELD_INPUT_CLS} w-full max-w-[240px] px-1.5`}
|
||||
>
|
||||
{def.options.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
@@ -221,6 +233,7 @@ const ParamField = memo(function ParamField({ def, value, onChange }: {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={PARAM_LABEL_CLS} title={def.label}>{def.label}</span>
|
||||
@@ -709,8 +722,17 @@ export function StrategySettingsDialog({ strategyId, onClose, onSaved, onAiModif
|
||||
extra={detail.params.length > 0 ? <span className="text-[10px] text-muted/60">{detail.params.length} 项</span> : undefined}
|
||||
>
|
||||
{detail.params.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{detail.params.map(p => <ParamField key={p.id} def={p} value={params[p.id]} onChange={handleParamChange} />)}
|
||||
<div className="space-y-2.5">
|
||||
{groupParams(visibleParams(detail.params, params)).map((g, gi) => (
|
||||
<div key={g.name ?? `ug${gi}`}>
|
||||
{g.name && (
|
||||
<div className="mb-1 border-b border-border/20 pb-0.5 text-[10px] font-medium text-muted/70">{g.name}</div>
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
{g.items.map(p => <ParamField key={p.id} def={p} value={params[p.id]} onChange={handleParamChange} onApplyValues={values => setParams(prev => ({ ...prev, ...values }))} />)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center text-xs text-muted/60">该策略无可调参数</div>
|
||||
|
||||
@@ -792,6 +792,15 @@ export interface AuctionBenchmarkPayload {
|
||||
}
|
||||
|
||||
// ===== Strategy Engine =====
|
||||
/** 参数级联显隐条件 (UI 元数据, 后端原样透传)。三种形态可递归组合:
|
||||
* { param, in } — values[param] ∈ in; { all: [...] } — 全部成立; { any: [...] } — 任一成立 */
|
||||
export interface ParamVisibleIf {
|
||||
param?: string
|
||||
in?: (string | number | boolean)[]
|
||||
all?: ParamVisibleIf[]
|
||||
any?: ParamVisibleIf[]
|
||||
}
|
||||
|
||||
export interface StrategyParamDef {
|
||||
id: string
|
||||
label: string
|
||||
@@ -801,6 +810,12 @@ export interface StrategyParamDef {
|
||||
max?: number
|
||||
step?: number
|
||||
options?: string[]
|
||||
/** UI 元数据: 分组节标题 (相同组名的相邻参数归并) */
|
||||
group?: string
|
||||
/** UI 元数据: 级联显隐 — 条件成立时显示 (见 ParamVisibleIf) */
|
||||
visible_if?: ParamVisibleIf
|
||||
/** UI 元数据: select 各选项被选中时批量应用的参数值 (如标的池预设 → 开关+默认值) */
|
||||
option_applies?: Record<string, Record<string, string | number | boolean>>
|
||||
}
|
||||
|
||||
export interface CompositeChildInfo {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { StrategyParamDef } from '@/lib/api'
|
||||
import { groupParams, isParamVisible, paramDisplayLabel, visibleParams } from '@/lib/strategyParams'
|
||||
|
||||
const p = (id: string, extra: Partial<StrategyParamDef> = {}): StrategyParamDef => ({
|
||||
id,
|
||||
label: id,
|
||||
type: 'float',
|
||||
default: 0,
|
||||
...extra,
|
||||
})
|
||||
|
||||
describe('isParamVisible', () => {
|
||||
it('无 visible_if 恒显示', () => {
|
||||
expect(isParamVisible(p('a'), {})).toBe(true)
|
||||
})
|
||||
|
||||
it('单条件: values 命中/未命中/缺省回落 default', () => {
|
||||
const def = p('use_x', { visible_if: { param: 'pool', in: ['custom'] } })
|
||||
const pool = p('pool', { type: 'select', default: 'classic4' })
|
||||
expect(isParamVisible(def, { pool: 'custom' }, [pool, def])).toBe(true)
|
||||
expect(isParamVisible(def, { pool: 'classic4' }, [pool, def])).toBe(false)
|
||||
// values 缺省 → 按 pool 的 default('classic4') 判定 → 隐藏
|
||||
expect(isParamVisible(def, {}, [pool, def])).toBe(false)
|
||||
})
|
||||
|
||||
it('边界: 空 all 为空真、空 any 为 false', () => {
|
||||
expect(isParamVisible(p('a', { visible_if: { all: [] } }), {})).toBe(true)
|
||||
expect(isParamVisible(p('a', { visible_if: { any: [] } }), {})).toBe(false)
|
||||
})
|
||||
|
||||
it('边界: 引用不存在的 param → 隐藏(不报错)', () => {
|
||||
const def = p('a', { visible_if: { param: 'nonexistent', in: [1] } })
|
||||
expect(isParamVisible(def, {})).toBe(false)
|
||||
})
|
||||
|
||||
it('bool 容错: in 值误写字符串 "true"/"True" 也能匹配布尔值', () => {
|
||||
const def = p('use_x', { type: 'bool', default: true, visible_if: { param: 'use_x', in: ['true'] } })
|
||||
expect(isParamVisible(def, { use_x: true }, [def])).toBe(true)
|
||||
expect(isParamVisible(def, { use_x: false }, [def])).toBe(false)
|
||||
const def2 = p('use_x', { type: 'bool', default: true, visible_if: { param: 'use_x', in: ['True'] } })
|
||||
expect(isParamVisible(def2, { use_x: true }, [def2])).toBe(true)
|
||||
})
|
||||
|
||||
it('递归: all / any 组合(标的池联动场景)', () => {
|
||||
// 显示条件 = pool ∈ presets OR (pool=custom AND use_gold)
|
||||
const def = p('tp_gold', {
|
||||
visible_if: {
|
||||
any: [
|
||||
{ param: 'pool', in: ['classic4', 'no_hs300'] },
|
||||
{ all: [{ param: 'pool', in: ['custom'] }, { param: 'use_gold', in: [true] }] },
|
||||
],
|
||||
},
|
||||
})
|
||||
const useGold = p('use_gold', { type: 'bool', default: true })
|
||||
const all = [def, useGold]
|
||||
// 预设模式: 不看勾选状态
|
||||
expect(isParamVisible(def, { pool: 'classic4', use_gold: false }, all)).toBe(true)
|
||||
// custom 勾选 → 显示; 取消勾选 → 隐藏
|
||||
expect(isParamVisible(def, { pool: 'custom', use_gold: true }, all)).toBe(true)
|
||||
expect(isParamVisible(def, { pool: 'custom', use_gold: false }, all)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('visibleParams / groupParams', () => {
|
||||
it('过滤隐藏参数并按组归并(保序)', () => {
|
||||
const params = [
|
||||
p('pool', { type: 'select', default: 'classic4', group: '标的池' }),
|
||||
p('use_x', { type: 'bool', default: true, group: '标的池', visible_if: { param: 'pool', in: ['custom'] } }),
|
||||
p('m_days', { group: '动量' }),
|
||||
p('tp_x', { group: '止盈' }),
|
||||
p('lock_x', { group: '止盈' }),
|
||||
]
|
||||
const visible = visibleParams(params, { pool: 'classic4' })
|
||||
expect(visible.map(x => x.id)).toEqual(['pool', 'm_days', 'tp_x', 'lock_x'])
|
||||
const groups = groupParams(visible)
|
||||
expect(groups.map(g => [g.name, g.items.length])).toEqual([
|
||||
['标的池', 1],
|
||||
['动量', 1],
|
||||
['止盈', 2],
|
||||
])
|
||||
})
|
||||
|
||||
it('非相邻同组名不归并(保序分节)', () => {
|
||||
const groups = groupParams([
|
||||
p('a', { group: 'X' }),
|
||||
p('b', { group: 'Y' }),
|
||||
p('c', { group: 'X' }),
|
||||
])
|
||||
expect(groups.map(g => [g.name, g.items.length])).toEqual([
|
||||
['X', 1],
|
||||
['Y', 1],
|
||||
['X', 1],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('paramDisplayLabel', () => {
|
||||
it('有组名加「组名 · 标签」前缀(平铺列表可区分),无组名原样', () => {
|
||||
expect(paramDisplayLabel(p('tp_gold', { label: '止盈', group: '黄金 159934.SZ' })))
|
||||
.toBe('黄金 159934.SZ · 止盈')
|
||||
expect(paramDisplayLabel(p('m_days', { label: '动量窗口(交易日)' })))
|
||||
.toBe('动量窗口(交易日)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ParamVisibleIf, StrategyParamDef } from '@/lib/api'
|
||||
|
||||
/**
|
||||
* 策略参数级联显隐与分组(META 里的 visible_if/group 为 UI 元数据,后端原样透传)。
|
||||
*
|
||||
* - visible_if 支持递归条件: { param, in } 单条件 / { all: [...] } 与 / { any: [...] } 或;
|
||||
* values 缺省时回落该参数的 default(未触碰过的控件按默认值判定)。
|
||||
* - group: 相同组名的相邻声明归并为一节;无 group 的参数归入无名节(不显示节标题)。
|
||||
*/
|
||||
function evalVisibleIf(
|
||||
cond: ParamVisibleIf,
|
||||
values: Record<string, any>,
|
||||
allParams?: StrategyParamDef[],
|
||||
): boolean {
|
||||
// 空集合语义: all:[] 按空真(vacuous true)处理; any:[] 无候选可成立 → false
|
||||
if (cond.all !== undefined) return cond.all.every(c => evalVisibleIf(c, values, allParams))
|
||||
if (cond.any !== undefined) return cond.any.length > 0 && cond.any.some(c => evalVisibleIf(c, values, allParams))
|
||||
if (cond.param) {
|
||||
const actual = values[cond.param] ?? allParams?.find(x => x.id === cond.param)?.default
|
||||
const expected = cond.in ?? []
|
||||
if (typeof actual === 'boolean') {
|
||||
// 容错: META 里 bool 条件的 in 值误写字符串 "true"/"false" 也能匹配
|
||||
return expected.some(e => e === actual || String(e).toLowerCase() === String(actual))
|
||||
}
|
||||
return expected.includes(actual)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function isParamVisible(
|
||||
p: StrategyParamDef,
|
||||
values: Record<string, any>,
|
||||
allParams?: StrategyParamDef[],
|
||||
): boolean {
|
||||
if (!p.visible_if) return true
|
||||
return evalVisibleIf(p.visible_if, values, allParams)
|
||||
}
|
||||
|
||||
export function visibleParams(
|
||||
params: StrategyParamDef[],
|
||||
values: Record<string, any>,
|
||||
): StrategyParamDef[] {
|
||||
return params.filter(p => isParamVisible(p, values, params))
|
||||
}
|
||||
|
||||
export interface ParamGroup {
|
||||
name: string | null
|
||||
items: StrategyParamDef[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 带分组前缀的展示标签。分组内短标签(如「止盈」「入池」)脱离分组上下文后
|
||||
* 不可区分(扫描面板是平铺列表),限定为「组名 · 标签」。
|
||||
*/
|
||||
export function paramDisplayLabel(p: StrategyParamDef): string {
|
||||
return p.group ? `${p.group} · ${p.label}` : p.label
|
||||
}
|
||||
|
||||
export function groupParams(params: StrategyParamDef[]): ParamGroup[] {
|
||||
const groups: ParamGroup[] = []
|
||||
for (const p of params) {
|
||||
const name = p.group ?? null
|
||||
const last = groups[groups.length - 1]
|
||||
if (last && last.name === name) last.items.push(p)
|
||||
else groups.push({ name, items: [p] })
|
||||
}
|
||||
return groups
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
REGIME_STATE_COLORS,
|
||||
} from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { groupParams, visibleParams } from '@/lib/strategyParams'
|
||||
import { storage } from '@/lib/storage'
|
||||
import { fmtPct, fmtPrice, priceColorClass } from '@/lib/format'
|
||||
import { boardTag } from '@/lib/board'
|
||||
@@ -718,10 +719,12 @@ function ConfigSection({ title, hint, actions, children }: { title: string; hint
|
||||
|
||||
|
||||
// memo: 单参数修改只重渲本行 (父组件需传稳定回调, 否则 memo 失效)
|
||||
const StrategyParamInput = memo(function StrategyParamInput({ param, value, onChange }: {
|
||||
const StrategyParamInput = memo(function StrategyParamInput({ param, value, onChange, onApplyValues }: {
|
||||
param: StrategyParamDef
|
||||
value: any
|
||||
onChange: (pid: string, value: any) => void
|
||||
/** select 带 option_applies 时, 选中某选项需批量应用一组参数值 (如标的池预设) */
|
||||
onApplyValues?: (values: Record<string, any>) => void
|
||||
}) {
|
||||
if (param.type === 'bool') {
|
||||
const checked = value === true || value === 'true' || value === 'True' || value === true
|
||||
@@ -747,7 +750,19 @@ const StrategyParamInput = memo(function StrategyParamInput({ param, value, onCh
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-[11px] text-secondary">{param.label}</span>
|
||||
<select value={value ?? param.default} onChange={e => onChange(param.id, e.target.value)} className={INPUT_CLS}>
|
||||
<select
|
||||
value={value ?? param.default}
|
||||
onChange={e => {
|
||||
const v = e.target.value
|
||||
const applies = param.option_applies?.[v]
|
||||
if (applies && onApplyValues) {
|
||||
onApplyValues({ [param.id]: v, ...applies })
|
||||
// 预设会覆盖其他参数的当前值, 显式提示避免静默改动
|
||||
toast(`已应用预设「${v}」,覆盖 ${Object.keys(applies).length} 个参数`, 'success')
|
||||
} else onChange(param.id, v)
|
||||
}}
|
||||
className={INPUT_CLS}
|
||||
>
|
||||
{(param.options ?? []).map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
@@ -2935,14 +2950,25 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: {
|
||||
actions={detail.params.length > 0 ? <span className="text-[10px] text-muted">{detail.params.length} 项</span> : undefined}
|
||||
>
|
||||
{detail.params.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{detail.params.map(param => (
|
||||
<StrategyParamInput
|
||||
key={param.id}
|
||||
param={param}
|
||||
value={strategyParams[param.id]}
|
||||
onChange={handleStrategyParamChange}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{groupParams(visibleParams(detail.params, strategyParams)).map((g, gi) => (
|
||||
<div key={g.name ?? `ug${gi}`}>
|
||||
{g.name && (
|
||||
<div className="mb-2 border-b border-border/40 pb-1 text-[11px] font-medium text-muted">{g.name}</div>
|
||||
)}
|
||||
{/* 命名组横排(如「入池开关 + 止盈 + 锁定」同组一行);无名组回退网格 */}
|
||||
<div className={g.name ? 'flex flex-wrap items-end gap-x-4 gap-y-3' : 'grid grid-cols-1 gap-3 sm:grid-cols-2'}>
|
||||
{g.items.map(param => (
|
||||
<StrategyParamInput
|
||||
key={param.id}
|
||||
param={param}
|
||||
value={strategyParams[param.id]}
|
||||
onChange={handleStrategyParamChange}
|
||||
onApplyValues={values => setStrategyParams(prev => ({ ...prev, ...values }))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -105,7 +105,7 @@ export function StrategyOptimizer() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<SweepParamList params={sweep.params} sweeps={sweep.sweeps} updateSweep={sweep.updateSweep} />
|
||||
<SweepParamList params={sweep.params} sweeps={sweep.sweeps} updateSweep={sweep.updateSweep} hiddenLabels={sweep.hiddenParamLabels} />
|
||||
<CombosHint show={!!sweep.strategyId} combos={sweep.combos} gridError={sweep.gridError} />
|
||||
|
||||
{task?.isPending ? (
|
||||
|
||||
@@ -176,7 +176,7 @@ export function StrategyWalkForward() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<SweepParamList params={sweep.params} sweeps={sweep.sweeps} updateSweep={sweep.updateSweep} />
|
||||
<SweepParamList params={sweep.params} sweeps={sweep.sweeps} updateSweep={sweep.updateSweep} hiddenLabels={sweep.hiddenParamLabels} />
|
||||
<CombosHint show={!!sweep.strategyId} combos={sweep.combos} gridError={sweep.gridError} />
|
||||
<div className="text-[11px] text-secondary">每折跑 {sweep.combos || 0} 组优化 × N 折,耗时较长</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { StrategyDetail, StrategyParamDef } from '@/lib/api'
|
||||
import { paramDisplayLabel, visibleParams } from '@/lib/strategyParams'
|
||||
|
||||
/** 参数扫描配置的共享逻辑与 UI — 优化器与 walk-forward 复用。 */
|
||||
|
||||
@@ -69,7 +70,11 @@ export function useParamSweep(strategies: StrategyDetail[], onStrategyChange?: (
|
||||
const [sweeps, setSweeps] = useState<Record<string, Sweep>>({})
|
||||
|
||||
const selected = strategies.find(s => s.id === strategyId)
|
||||
const params = selected?.params ?? []
|
||||
// 级联隐藏参数不参与扫描: 扫描面板未扫描参数固定为默认值, 按默认值判定显隐
|
||||
const params = useMemo(
|
||||
() => visibleParams(selected?.params ?? [], selected?.params_defaults ?? {}),
|
||||
[selected],
|
||||
)
|
||||
|
||||
const selectStrategy = (id: string) => {
|
||||
setStrategyId(id)
|
||||
@@ -112,7 +117,14 @@ export function useParamSweep(strategies: StrategyDetail[], onStrategyChange?: (
|
||||
return grid
|
||||
}
|
||||
|
||||
return { strategyId, selected, selectStrategy, params, sweeps, updateSweep, combos, gridError, buildGrid }
|
||||
// 因级联规则 (visible_if) 在默认配置下隐藏、不参与扫描的参数, 供 UI 提示
|
||||
// (paramDisplayLabel: 平铺列表无组上下文, 短标签需带组名才可区分)
|
||||
const hiddenParamLabels = useMemo(
|
||||
() => (selected?.params ?? []).filter(p => !params.includes(p)).map(p => paramDisplayLabel(p)),
|
||||
[selected, params],
|
||||
)
|
||||
|
||||
return { strategyId, selected, selectStrategy, params, sweeps, updateSweep, combos, gridError, buildGrid, hiddenParamLabels }
|
||||
}
|
||||
|
||||
/** 策略选择器。 */
|
||||
@@ -130,15 +142,22 @@ export function StrategySelect({ strategies, value, onChange }: {
|
||||
}
|
||||
|
||||
/** 可扫参数列表 (勾选 + min/max/step)。 */
|
||||
export function SweepParamList({ params, sweeps, updateSweep }: {
|
||||
export function SweepParamList({ params, sweeps, updateSweep, hiddenLabels = [] }: {
|
||||
params: StrategyParamDef[]
|
||||
sweeps: Record<string, Sweep>
|
||||
updateSweep: (pid: string, patch: Partial<Sweep>) => void
|
||||
/** 因级联规则 (visible_if) 在默认配置下隐藏的参数标签, 仅作提示 */
|
||||
hiddenLabels?: string[]
|
||||
}) {
|
||||
if (!params.length) return null
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1.5 text-xs font-medium text-secondary">扫描参数 (勾选后设范围)</div>
|
||||
{hiddenLabels.length > 0 && (
|
||||
<div className="mb-1.5 text-[11px] text-muted">
|
||||
{hiddenLabels.length} 个参数因级联规则在默认配置下隐藏,不参与扫描:{hiddenLabels.join('、')}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{params.map(p => {
|
||||
const s = sweeps[p.id] ?? defaultSweep(p)
|
||||
@@ -159,7 +178,7 @@ export function SweepParamList({ params, sweeps, updateSweep }: {
|
||||
<div key={p.id} className="rounded-input border border-border/60 p-2">
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" checked={s.enabled} onChange={e => updateSweep(p.id, { enabled: e.target.checked })} />
|
||||
<span className="font-medium text-foreground">{p.label}</span>
|
||||
<span className="font-medium text-foreground">{paramDisplayLabel(p)}</span>
|
||||
<span className="text-secondary">({p.type})</span>
|
||||
</label>
|
||||
{s.enabled && numeric && (
|
||||
|
||||
Reference in New Issue
Block a user