feat(UI): 展开箭头全站统一为 ExpandChevron 共享组件
- 新增 src/client/views/ExpandChevron.jsx:14x14 chevron-right、success 绿、展开旋转 90°(源自策略 tab 表格行图标) - 设置页策略分组行首 ▸/▾ 文字箭头改用该组件,并修复箭头与策略名的水平对齐(基线 trick 改 flex 精确居中) - StrategyTab / TradeRecordsTab 两处重复内联 SVG 收敛为共享组件 - 去掉字段编辑器展开区冗余「收起」按钮(行首箭头/「收起字段」已可收起),清理 onClose prop - docs: 新增 UI约束-006(展开/收起箭头全站统一)
This commit is contained in:
@@ -16,6 +16,8 @@
|
|||||||
| UI约束-005 | 策略自定义字段配置 UI(R-013,2026-09-02):设置页「策略分组」子 tab 策略行**可展开** → 展开区 = 字段列表(label/key/type/enum/默认值)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值)+ 保存(走 strategies/update 整表);策略持仓 tab 持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 holdings/values-update,Toast 反馈;未配置字段定义的策略持仓行不渲染字段区 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6 老师确认:值编辑入口 = 持仓行展开区) |
|
| UI约束-005 | 策略自定义字段配置 UI(R-013,2026-09-02):设置页「策略分组」子 tab 策略行**可展开** → 展开区 = 字段列表(label/key/type/enum/默认值)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值)+ 保存(走 strategies/update 整表);策略持仓 tab 持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 holdings/values-update,Toast 反馈;未配置字段定义的策略持仓行不渲染字段区 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6 老师确认:值编辑入口 = 持仓行展开区) |
|
||||||
| UI约束-004 | 神之一手 UI 适配 DSH 主题:所有颜色引用宿主 `--dsw-*` token(带 fallback `var(--dsw-alias-xxx, #原色)`),随 DSH 浅色 / 深色 / 跟随系统自动切换,不自行维护主题偏好(跟随宿主);语义映射:表面底→bg-layer-1、次级面→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1..l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;淡底用 color-mix(in srgb, var(--语义色) 10-12%, transparent);禁止硬编码色值(#hex/rgb)进运行代码 | 2026-09-02 | 生效 | - | R-012 定稿(2026-09-02,暂定跟随系统)+ 迭代 10 实施:141 处硬编码色 token 化,宿主机制实证(body[data-ds-dark-theme] + alias token 双值定义) |
|
| UI约束-004 | 神之一手 UI 适配 DSH 主题:所有颜色引用宿主 `--dsw-*` token(带 fallback `var(--dsw-alias-xxx, #原色)`),随 DSH 浅色 / 深色 / 跟随系统自动切换,不自行维护主题偏好(跟随宿主);语义映射:表面底→bg-layer-1、次级面→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1..l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;淡底用 color-mix(in srgb, var(--语义色) 10-12%, transparent);禁止硬编码色值(#hex/rgb)进运行代码 | 2026-09-02 | 生效 | - | R-012 定稿(2026-09-02,暂定跟随系统)+ 迭代 10 实施:141 处硬编码色 token 化,宿主机制实证(body[data-ds-dark-theme] + alias token 双值定义) |
|
||||||
|
|
||||||
|
| UI约束-006 | 展开/收起箭头全站统一:使用共享组件 `ExpandChevron`(src/client/views/ExpandChevron.jsx)——14×14 chevron-right、描边圆角、主题 success 绿、展开时旋转 90°(0.15s 过渡);适用:策略持仓 tab / 交易记录 tab 表格行、设置页「策略分组」行首箭头(原 ▸/▾ 文字箭头废弃);不得再散落内联 SVG 或文字符号实现 | 2026-09-03 | 生效 | - | 新增(2026-09-03 老师反馈:设置页策略分组箭头与策略 tab 表格行展开图标一致;顺带消除两处内联 SVG 重复) |
|
||||||
|
|
||||||
<!-- 示例条目(确认格式后删除):
|
<!-- 示例条目(确认格式后删除):
|
||||||
| UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
|
| UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
|
||||||
-->
|
-->
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* ExpandChevron —— 全站统一的「展开/收起」箭头图标
|
||||||
|
*
|
||||||
|
* 视觉语言与策略 tab 表格行展开图标一致:
|
||||||
|
* - 14×14 chevron-right(描边圆角),展开时顺时针旋转 90°(0.15s 过渡)
|
||||||
|
* - 默认使用主题 success 色,可通过 color 覆盖
|
||||||
|
*
|
||||||
|
* 使用方:
|
||||||
|
* - StrategyTab / TradeRecordsTab:表格行内(纯装饰,行点击切换展开)
|
||||||
|
* - SettingsSection 策略分组:包在 button 内作为展开开关
|
||||||
|
*/
|
||||||
|
export function ExpandChevron({ expanded = false, size = 14, color, style }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
transition: 'transform .15s ease',
|
||||||
|
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||||
|
color: color ?? 'var(--dsw-alias-state-success-primary, #2e7d32)',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.5 2.5 L9.5 7 L4.5 11.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { useState, useEffect, useCallback, Fragment } from 'react';
|
|||||||
import { useRpc } from './connection.jsx';
|
import { useRpc } from './connection.jsx';
|
||||||
import { ToastProvider, useToast } from './Toast.jsx';
|
import { ToastProvider, useToast } from './Toast.jsx';
|
||||||
import { StrategyFieldsEditor } from './StrategyFieldsEditor.jsx'; // R-013:策略自定义字段编辑器
|
import { StrategyFieldsEditor } from './StrategyFieldsEditor.jsx'; // R-013:策略自定义字段编辑器
|
||||||
|
import { ExpandChevron } from './ExpandChevron.jsx'; // 展开箭头与策略 tab 表格行图标统一
|
||||||
|
|
||||||
/** 确认弹窗 */
|
/** 确认弹窗 */
|
||||||
function ConfirmDialog({ title, message, onConfirm, onCancel }) {
|
function ConfirmDialog({ title, message, onConfirm, onCancel }) {
|
||||||
@@ -349,12 +350,12 @@ function StrategyGroupSettings() {
|
|||||||
<button onClick={() => setEditingId(null)} style={{ padding: '2px 8px', cursor: 'pointer', marginLeft: 4 }}>取消</button>
|
<button onClick={() => setEditingId(null)} style={{ padding: '2px 8px', cursor: 'pointer', marginLeft: 4 }}>取消</button>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setExpandedId(expandedId === s.id ? null : s.id); }}
|
onClick={() => { setExpandedId(expandedId === s.id ? null : s.id); }}
|
||||||
style={{ padding: '2px 6px', cursor: 'pointer', border: 'none', background: 'none', fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)', marginRight: 6 }}
|
style={{ padding: '2px 4px', cursor: 'pointer', border: 'none', background: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', lineHeight: 0 }}
|
||||||
title={expandedId === s.id ? '收起配置' : '展开配置自定义字段'}
|
title={expandedId === s.id ? '收起配置' : '展开配置自定义字段'}
|
||||||
>{expandedId === s.id ? '▾' : '▸'}</button>
|
><ExpandChevron expanded={expandedId === s.id} /></button>
|
||||||
{s.name}
|
{s.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -382,7 +383,6 @@ function StrategyGroupSettings() {
|
|||||||
strategy={s}
|
strategy={s}
|
||||||
saving={fieldSaving}
|
saving={fieldSaving}
|
||||||
onSave={(fields) => handleSaveConfig(s.id, fields)}
|
onSave={(fields) => handleSaveConfig(s.id, fields)}
|
||||||
onClose={() => setExpandedId(null)}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ const dangerBtn = { ...btnStyle, borderColor: 'var(--dsw-alias-state-error-prima
|
|||||||
* @param {object} props
|
* @param {object} props
|
||||||
* @param {object} props.strategy 策略(含 configSchema)
|
* @param {object} props.strategy 策略(含 configSchema)
|
||||||
* @param {Function} props.onSave 保存回调 (fields) => Promise
|
* @param {Function} props.onSave 保存回调 (fields) => Promise
|
||||||
* @param {Function} props.onClose 收起回调
|
|
||||||
* @param {boolean} props.saving 保存中
|
* @param {boolean} props.saving 保存中
|
||||||
*/
|
*/
|
||||||
export function StrategyFieldsEditor({ strategy, onSave, onClose, saving }) {
|
export function StrategyFieldsEditor({ strategy, onSave, saving }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [fields, setFields] = useState(() => (Array.isArray(strategy?.configSchema) ? strategy.configSchema.map((x) => ({ ...x })) : []));
|
const [fields, setFields] = useState(() => (Array.isArray(strategy?.configSchema) ? strategy.configSchema.map((x) => ({ ...x })) : []));
|
||||||
// 表单草稿:null=关闭;{ index:-1=新增 | 编辑下标, ... }
|
// 表单草稿:null=关闭;{ index:-1=新增 | 编辑下标, ... }
|
||||||
@@ -102,7 +101,6 @@ export function StrategyFieldsEditor({ strategy, onSave, onClose, saving }) {
|
|||||||
<span>
|
<span>
|
||||||
<button onClick={startAdd} style={solidBtn('var(--dsw-alias-state-success-primary, #2e7d32)')}>+ 添加字段</button>
|
<button onClick={startAdd} style={solidBtn('var(--dsw-alias-state-success-primary, #2e7d32)')}>+ 添加字段</button>
|
||||||
<button onClick={saveAll} disabled={saving} style={solidBtn('var(--dsw-alias-state-business-primary, #1565c0)')}>{saving ? '保存中...' : '保存字段'}</button>
|
<button onClick={saveAll} disabled={saving} style={solidBtn('var(--dsw-alias-state-business-primary, #1565c0)')}>{saving ? '保存中...' : '保存字段'}</button>
|
||||||
<button onClick={onClose} style={btnStyle}>收起</button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useMarket } from '../market/MarketDataProvider.jsx';
|
|||||||
import { PriceCell } from './PriceCell.jsx';
|
import { PriceCell } from './PriceCell.jsx';
|
||||||
import { ColumnSettingsPopover } from './ColumnSettingsPopover.jsx'; // 迭代11:列显隐设置
|
import { ColumnSettingsPopover } from './ColumnSettingsPopover.jsx'; // 迭代11:列显隐设置
|
||||||
import { FieldCellEditor } from './FieldCellEditor.jsx'; // 迭代11:自定义字段单元格点击编辑
|
import { FieldCellEditor } from './FieldCellEditor.jsx'; // 迭代11:自定义字段单元格点击编辑
|
||||||
|
import { ExpandChevron } from './ExpandChevron.jsx'; // 行展开图标共享组件
|
||||||
|
|
||||||
/** 价格格式化(2 位小数) */
|
/** 价格格式化(2 位小数) */
|
||||||
function fmtPrice(v) {
|
function fmtPrice(v) {
|
||||||
@@ -508,27 +509,7 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
|||||||
>
|
>
|
||||||
<td style={{ padding: '8px 6px', textAlign: 'center', width: 28 }}>
|
<td style={{ padding: '8px 6px', textAlign: 'center', width: 28 }}>
|
||||||
{hasHolding ? (
|
{hasHolding ? (
|
||||||
<svg
|
<ExpandChevron expanded={isExpanded} />
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
transition: 'transform .15s ease',
|
|
||||||
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
||||||
color: 'var(--dsw-alias-state-success-primary, #2e7d32)',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4.5 2.5 L9.5 7 L4.5 11.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)', fontSize: 10 }}>·</span>
|
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)', fontSize: 10 }}>·</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useRpc } from './connection.jsx';
|
import { useRpc } from './connection.jsx';
|
||||||
import { LoadState } from './LoadState.jsx';
|
import { LoadState } from './LoadState.jsx';
|
||||||
|
import { ExpandChevron } from './ExpandChevron.jsx'; // 行展开图标共享组件
|
||||||
import { RangeSelector } from './RangeSelector.jsx';
|
import { RangeSelector } from './RangeSelector.jsx';
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 4000; // 3-5s 轮询(取 4s)
|
const POLL_INTERVAL_MS = 4000; // 3-5s 轮询(取 4s)
|
||||||
@@ -411,28 +412,7 @@ function OrderRows({ order, expanded, onToggle, attributionCandidates, onLoadCan
|
|||||||
>
|
>
|
||||||
<td style={{ ...tdStyle, textAlign: 'center', width: 28 }}>
|
<td style={{ ...tdStyle, textAlign: 'center', width: 28 }}>
|
||||||
{hasTrades ? (
|
{hasTrades ? (
|
||||||
<svg
|
<ExpandChevron expanded={expanded} style={{ cursor: 'pointer' }} />
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
transition: 'transform .15s ease',
|
|
||||||
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: 'var(--dsw-alias-state-success-primary, #2e7d32)',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4.5 2.5 L9.5 7 L4.5 11.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)', fontSize: 10 }}>·</span>
|
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)', fontSize: 10 }}>·</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user