feat(Tab设置+UI主题): 迭代09 Tab设置统一管理 + 迭代10 UI适配DSH主题
迭代09 (R-011): Tab 设置统一管理所有会话 tab
- settings.tabs 布尔对象 → 统一有序数组 (kind: builtin|strategy)
- 旧配置 schema union 兼容 + 读取归一化无感迁移
- strategies 收窄为 {id,name},策略行 name join 自动跟随改名
- 策略增删联动 tabs 条目 (add 追加末尾 / remove 联动删除)
- 客户端统一注册读 tabs 数组,内置与策略可任意混排
- 设置页 Tab 设置子 tab:混排表格 + 拖动手柄 + 显隐开关 + 落点立即持久化
- 策略分组瘦身:仅 新增/重命名/删除 + 指引提示
- 回归测试: test-r011-tabs (23) + test-r011-api (12)
迭代10 (R-012): UI 适配 DSH 浅色/深色/跟随系统主题
- 10 个视图文件 141 处硬编码色 → var(--dsw-alias-*, fallback)
- 语义映射: bg-layer/label/border/state/button-contrast 等
- 淡底 color-mix 自适应;遮罩 bg-mask-1;阴影 shadow-lv3
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* 神之一手设置 section(迭代 02:设置 tab 化扩展)
|
||||
* 神之一手设置 section
|
||||
*
|
||||
* 结构:
|
||||
* - 三个子 tab:通用设置 / 策略分组 / QMT 连接配置(迭代 03,R-004)
|
||||
* - 通用设置:三个 switch(全部持仓 / 交易记录 / 关注列表),控制对应会话 tab 显隐,切换后提示「刷新页面后生效」
|
||||
* - 策略分组:策略 CRUD(新增/重命名/删除确认/排序/显隐)+ 就地提示
|
||||
* 结构(R-011 迭代 09):
|
||||
* - 三个子 tab:Tab 设置 / 策略分组 / QMT 连接配置
|
||||
* - Tab 设置:统一管理所有会话 tab(内置 + 策略分组混排),拖动排序(落点立即持久化)+ 显示/隐藏开关;
|
||||
* 任何 tab 均不支持重命名/删除(产品约束-009、UI约束-003)
|
||||
* - 策略分组:策略 CRUD(新增/重命名/删除确认)+ 提示「顺序与显示请在 Tab 设置中调整」
|
||||
* - QMT 连接配置:R-004 卡片形式(迭代 03)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
@@ -15,17 +17,17 @@ import { ToastProvider, useToast } from './Toast.jsx';
|
||||
function ConfirmDialog({ title, message, onConfirm, onCancel }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
||||
position: 'fixed', inset: 0, background: 'var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.4))',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000,
|
||||
}}>
|
||||
<div style={{ background: '#fff', padding: 24, borderRadius: 8, maxWidth: 420, width: '90%' }}>
|
||||
<div style={{ background: 'var(--dsw-alias-bg-layer-1, #fff)', padding: 24, borderRadius: 8, maxWidth: 420, width: '90%' }}>
|
||||
<h3 style={{ margin: '0 0 12px', fontSize: 16 }}>{title}</h3>
|
||||
<div style={{ fontSize: 14, color: '#555', marginBottom: 20 }}>{message}</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--dsw-alias-label-secondary, #555)', marginBottom: 20 }}>{message}</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<button onClick={onCancel} style={{ padding: '6px 16px', cursor: 'pointer', border: '1px solid #ccc', borderRadius: 4, background: '#fff' }}>
|
||||
<button onClick={onCancel} style={{ padding: '6px 16px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)' }}>
|
||||
取消
|
||||
</button>
|
||||
<button onClick={onConfirm} style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: '#c62828', color: '#fff' }}>
|
||||
<button onClick={onConfirm} style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: 'var(--dsw-alias-state-error-primary, #c62828)', color: 'var(--dsw-alias-button-contrast-fill, #fff)' }}>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
@@ -42,30 +44,32 @@ function Switch({ checked, onChange, disabled }) {
|
||||
disabled={disabled}
|
||||
style={{
|
||||
width: 44, height: 24, borderRadius: 12, border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
background: checked ? '#2e7d32' : '#9e9e9e', position: 'relative', padding: 0, transition: 'background 0.2s',
|
||||
background: checked ? 'var(--dsw-alias-state-success-primary, #2e7d32)' : 'var(--dsw-alias-border-l4, #9e9e9e)', position: 'relative', padding: 0, transition: 'background 0.2s',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: checked ? 22 : 2, width: 20, height: 20,
|
||||
borderRadius: '50%', background: '#fff', transition: 'left 0.2s',
|
||||
borderRadius: '50%', background: 'var(--dsw-alias-button-contrast-fill, #fff)', transition: 'left 0.2s',
|
||||
}} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** 通用设置子 tab */
|
||||
function GeneralSettings({ onChanged }) {
|
||||
const [tabConfig, setTabConfig] = useState(null);
|
||||
/** Tab 设置子 tab(R-011:统一管理所有会话 tab —— 内置 + 策略分组混排,拖动排序 + 显隐开关) */
|
||||
function TabSettings() {
|
||||
const [tabs, setTabs] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [dragId, setDragId] = useState(null);
|
||||
const [overId, setOverId] = useState(null);
|
||||
const call = useRpc();
|
||||
const toast = useToast();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await call('one-divine-lot/tabs', {});
|
||||
if (res && res.ok) setTabConfig(res.value);
|
||||
if (res && res.ok && Array.isArray(res.value)) setTabs(res.value);
|
||||
else setError((res && res.error && res.error.message) || '加载失败');
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
@@ -74,48 +78,97 @@ function GeneralSettings({ onChanged }) {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const toggle = async (key, label) => {
|
||||
if (!tabConfig) return;
|
||||
// 整表持久化(拖动落点 / 显隐切换共用;Q1 落点立即持久化)
|
||||
const persist = async (nextTabs, msg) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const next = { ...tabConfig, [key]: !tabConfig[key] };
|
||||
const res = await call('one-divine-lot/tabs/update', { args: { tabs: next } });
|
||||
const res = await call('one-divine-lot/tabs/update', { args: { tabs: nextTabs } });
|
||||
if (res && res.ok) {
|
||||
setTabConfig(next);
|
||||
toast.success(`「${label}」已${next[key] ? '显示' : '隐藏'}`);
|
||||
onChanged?.(label, next[key]);
|
||||
setTabs(res.value);
|
||||
toast.success(msg + '(刷新页面后生效)');
|
||||
} else {
|
||||
toast.error((res && res.error && res.error.message) || '保存失败');
|
||||
await load();
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
await load();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ key: 'allPositions', label: '全部持仓', desc: '会话窗中的「全部持仓」标签页' },
|
||||
{ key: 'tradeRecords', label: '交易记录', desc: '会话窗中的「交易记录」标签页(开发中)' },
|
||||
{ key: 'watchlist', label: '关注列表', desc: '会话窗中的「关注列表」标签页(开发中)' },
|
||||
];
|
||||
const toggleVisible = async (t) => {
|
||||
if (!tabs) return;
|
||||
const next = tabs.map((x) => (x.id === t.id ? { ...x, visible: !x.visible } : x));
|
||||
await persist(next, '「' + t.name + '」已' + (next.find((x) => x.id === t.id).visible ? '显示' : '隐藏'));
|
||||
};
|
||||
|
||||
// 原生 HTML5 拖动:落点重排 + 立即持久化(Q1)
|
||||
const handleDrop = async (targetId) => {
|
||||
if (!tabs || !dragId || dragId === targetId) { setDragId(null); setOverId(null); return; }
|
||||
const from = tabs.findIndex((x) => x.id === dragId);
|
||||
const to = tabs.findIndex((x) => x.id === targetId);
|
||||
if (from < 0 || to < 0) { setDragId(null); setOverId(null); return; }
|
||||
const next = tabs.slice();
|
||||
const [moved] = next.splice(from, 1);
|
||||
next.splice(to, 0, moved);
|
||||
// 重写 order(整表连续)
|
||||
const ordered = next.map((x, i) => ({ ...x, order: i }));
|
||||
setDragId(null);
|
||||
setOverId(null);
|
||||
await persist(ordered, '「' + moved.name + '」已移动');
|
||||
};
|
||||
|
||||
const badgeStyle = (kind) => ({
|
||||
fontSize: 11, borderRadius: 8, padding: '0 6px', marginRight: 6, flex: 'none',
|
||||
color: kind === 'builtin' ? 'var(--dsw-alias-state-business-primary, #1565c0)' : 'var(--dsw-alias-state-warn-primary, #6a1b9a)',
|
||||
border: kind === 'builtin' ? '1px solid var(--dsw-alias-border-l3, #90caf9)' : '1px solid var(--dsw-alias-border-l3, #ce93d8)',
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && <div style={{ color: '#c62828', marginBottom: 12 }}>{error}</div>}
|
||||
{!tabConfig && !error && <div style={{ color: '#666' }}>加载中...</div>}
|
||||
{tabConfig && items.map((item) => (
|
||||
<div key={item.key} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '12px 0', borderBottom: '1px solid #eee',
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{item.label}</div>
|
||||
<div style={{ fontSize: 12, color: '#888', marginTop: 2 }}>{item.desc}</div>
|
||||
</div>
|
||||
<Switch checked={tabConfig[item.key] !== false} onChange={() => toggle(item.key, item.label)} disabled={saving} />
|
||||
</div>
|
||||
))}
|
||||
<div style={{ fontSize: 12, color: 'var(--dsw-alias-label-secondary, #888)', marginBottom: 12 }}>
|
||||
拖动调整所有标签页顺序,开关控制显示/隐藏;内置与策略标签可任意混排。名称不支持在此修改(策略名称在「策略分组」中修改)。
|
||||
</div>
|
||||
{error && <div style={{ color: 'var(--dsw-alias-state-error-primary, #c62828)', marginBottom: 12 }}>{error}</div>}
|
||||
{!tabs && !error && <div style={{ color: 'var(--dsw-alias-label-secondary, #666)' }}>加载中...</div>}
|
||||
{tabs && (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: 'left', borderBottom: '2px solid var(--dsw-alias-border-l2, #ddd)' }}>
|
||||
<th style={{ width: 44, padding: '8px 6px' }}></th>
|
||||
<th style={{ padding: '8px 6px' }}>标签页</th>
|
||||
<th style={{ width: 80, padding: '8px 6px' }}>显示</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tabs.map((t) => (
|
||||
<tr
|
||||
key={t.id}
|
||||
draggable={!saving}
|
||||
onDragStart={(e) => { setDragId(t.id); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverId(t.id); }}
|
||||
onDragLeave={() => { if (overId === t.id) setOverId(null); }}
|
||||
onDrop={(e) => { e.preventDefault(); handleDrop(t.id); }}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)', cursor: saving ? 'default' : 'grab',
|
||||
background: overId === t.id ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '8px 6px', color: 'var(--dsw-alias-label-tertiary, #999)', textAlign: 'center' }}>≡</td>
|
||||
<td style={{ padding: '8px 6px' }}>
|
||||
<span style={badgeStyle(t.kind)}>{t.kind === 'builtin' ? '内置' : '策略'}</span>
|
||||
{t.name}
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px' }}>
|
||||
<Switch checked={t.visible !== false} onChange={() => toggleVisible(t)} disabled={saving} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -210,53 +263,18 @@ function StrategyGroupSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMove = async (id, dir) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await call('one-divine-lot/strategies/move', { args: { strategyId: id, dir } });
|
||||
if (res && res.ok) {
|
||||
await load();
|
||||
} else {
|
||||
toast.error((res && res.error && res.error.message) || '排序失败');
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVisible = async (id) => {
|
||||
if (!strategies) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const next = strategies.map((s) => (s.id === id ? { ...s, visible: !s.visible } : s));
|
||||
const res = await call('one-divine-lot/strategies/update', { args: { strategies: next } });
|
||||
if (res && res.ok) {
|
||||
setStrategies(next);
|
||||
handleChanged('显示设置已更新');
|
||||
} else {
|
||||
toast.error((res && res.error && res.error.message) || '保存失败');
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{changedMsg && (
|
||||
<div style={{
|
||||
padding: '10px 14px', marginBottom: 12, borderRadius: 6,
|
||||
background: '#e3f2fd', color: '#1565c0', fontSize: 13, border: '1px solid #90caf9',
|
||||
background: 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 10%, var(--dsw-alias-bg-layer-1, #fff))', color: 'var(--dsw-alias-state-business-primary, #1565c0)', fontSize: 13, border: '1px solid var(--dsw-alias-border-l3, #90caf9)',
|
||||
}}>
|
||||
{changedMsg} —— <strong>刷新页面后生效</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div style={{ color: '#c62828', marginBottom: 12 }}>{error}</div>}
|
||||
{error && <div style={{ color: 'var(--dsw-alias-state-error-primary, #c62828)', marginBottom: 12 }}>{error}</div>}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center' }}>
|
||||
<input
|
||||
@@ -265,44 +283,34 @@ function StrategyGroupSettings() {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
||||
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ccc', borderRadius: 4 }}
|
||||
style={{ flex: 1, padding: '6px 10px', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4 }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={saving}
|
||||
style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: '#2e7d32', color: '#fff' }}
|
||||
style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: 'var(--dsw-alias-state-success-primary, #2e7d32)', color: 'var(--dsw-alias-button-contrast-fill, #fff)' }}
|
||||
>
|
||||
+ 新增策略
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!strategies && !error && <div style={{ color: '#666' }}>加载中...</div>}
|
||||
<div style={{ fontSize: 12, color: 'var(--dsw-alias-label-secondary, #888)', marginBottom: 12 }}>
|
||||
顺序与显示请在「Tab 设置」中调整。
|
||||
</div>
|
||||
|
||||
{!strategies && !error && <div style={{ color: 'var(--dsw-alias-label-secondary, #666)' }}>加载中...</div>}
|
||||
{strategies && (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: 'left', borderBottom: '2px solid #ddd' }}>
|
||||
<th style={{ padding: '8px 6px' }}>排序</th>
|
||||
<tr style={{ textAlign: 'left', borderBottom: '2px solid var(--dsw-alias-border-l2, #ddd)' }}>
|
||||
<th style={{ padding: '8px 6px' }}>策略</th>
|
||||
<th style={{ padding: '8px 6px' }}>ID</th>
|
||||
<th style={{ padding: '8px 6px' }}>显示</th>
|
||||
<th style={{ padding: '8px 6px' }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{strategies.map((s, idx) => (
|
||||
<tr key={s.id} style={{ borderBottom: '1px solid #eee' }}>
|
||||
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>
|
||||
<button
|
||||
onClick={() => handleMove(s.id, 'up')}
|
||||
disabled={saving || idx === 0}
|
||||
style={{ padding: '2px 6px', cursor: idx === 0 ? 'not-allowed' : 'pointer', border: '1px solid #ccc', borderRadius: 3, background: '#fff', marginRight: 4 }}
|
||||
>↑</button>
|
||||
<button
|
||||
onClick={() => handleMove(s.id, 'down')}
|
||||
disabled={saving || idx === strategies.length - 1}
|
||||
style={{ padding: '2px 6px', cursor: idx === strategies.length - 1 ? 'not-allowed' : 'pointer', border: '1px solid #ccc', borderRadius: 3, background: '#fff' }}
|
||||
>↓</button>
|
||||
</td>
|
||||
{strategies.map((s) => (
|
||||
<tr key={s.id} style={{ borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)' }}>
|
||||
<td style={{ padding: '8px 6px' }}>
|
||||
{editingId === s.id ? (
|
||||
<span>
|
||||
@@ -320,18 +328,15 @@ function StrategyGroupSettings() {
|
||||
s.name
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px', color: '#666', fontSize: 12 }}>{s.id}</td>
|
||||
<td style={{ padding: '8px 6px' }}>
|
||||
<Switch checked={s.visible !== false} onChange={() => toggleVisible(s.id)} disabled={saving} />
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px', color: 'var(--dsw-alias-label-secondary, #666)', fontSize: 12 }}>{s.id}</td>
|
||||
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>
|
||||
<button
|
||||
onClick={() => { setEditingId(s.id); setEditingName(s.name); }}
|
||||
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid #ccc', borderRadius: 3, background: '#fff', marginRight: 4 }}
|
||||
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 3, background: 'var(--dsw-alias-bg-layer-1, #fff)', marginRight: 4 }}
|
||||
>重命名</button>
|
||||
<button
|
||||
onClick={() => setConfirmDelete({ id: s.id, name: s.name })}
|
||||
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid #c62828', borderRadius: 3, background: '#fff', color: '#c62828' }}
|
||||
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid var(--dsw-alias-state-error-primary, #c62828)', borderRadius: 3, background: 'var(--dsw-alias-bg-layer-1, #fff)', color: 'var(--dsw-alias-state-error-primary, #c62828)' }}
|
||||
>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -354,16 +359,16 @@ function StrategyGroupSettings() {
|
||||
|
||||
/** 设置 section 主组件:三个子 tab */
|
||||
function SettingsSectionInner(props) {
|
||||
const [activeTab, setActiveTab] = useState('general'); // general | strategies | qmt
|
||||
const [activeTab, setActiveTab] = useState('tabs'); // tabs | strategies | qmt
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, fontFamily: 'system-ui' }}>
|
||||
<h2 style={{ margin: '0 0 16px' }}>神之一手</h2>
|
||||
|
||||
{/* 子 tab 导航 */}
|
||||
<div style={{ display: 'flex', gap: 4, borderBottom: '1px solid #ddd', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--dsw-alias-border-l2, #ddd)', marginBottom: 16 }}>
|
||||
{[
|
||||
{ key: 'general', label: '通用设置' },
|
||||
{ key: 'tabs', label: 'Tab 设置' },
|
||||
{ key: 'strategies', label: '策略分组' },
|
||||
{ key: 'qmt', label: 'QMT 连接配置' },
|
||||
].map((tab) => (
|
||||
@@ -371,8 +376,8 @@ function SettingsSectionInner(props) {
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
style={{
|
||||
padding: '8px 16px', cursor: 'pointer', border: 'none', borderBottom: activeTab === tab.key ? '2px solid #2e7d32' : '2px solid transparent',
|
||||
background: 'none', fontSize: 14, color: activeTab === tab.key ? '#2e7d32' : '#666', fontWeight: activeTab === tab.key ? 600 : 400,
|
||||
padding: '8px 16px', cursor: 'pointer', border: 'none', borderBottom: activeTab === tab.key ? '2px solid var(--dsw-alias-state-success-primary, #2e7d32)' : '2px solid transparent',
|
||||
background: 'none', fontSize: 14, color: activeTab === tab.key ? 'var(--dsw-alias-state-success-primary, #2e7d32)' : 'var(--dsw-alias-label-secondary, #666)', fontWeight: activeTab === tab.key ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
@@ -380,9 +385,7 @@ function SettingsSectionInner(props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'general' && <GeneralSettings onChanged={(label, visible) => {
|
||||
// 通用设置变更 → 就地提示(由 GeneralSettings 内 toast 提示,这里可额外提示)
|
||||
}} />}
|
||||
{activeTab === 'tabs' && <TabSettings />}
|
||||
{activeTab === 'strategies' && <StrategyGroupSettings />}
|
||||
{activeTab === 'qmt' && <QmtConnectionsSettings />}
|
||||
</div>
|
||||
@@ -411,34 +414,34 @@ function EllipsisText({ text, maxWidth = 220, title, style }) {
|
||||
|
||||
/** QMT 连接配置卡片(R-004;产品约束-005、UI约束-002;2026-08-29 老师反馈改卡片形式) */
|
||||
function QmtConnectionCard({ conn, isActive, isDefault, busy, testResult, onActivate, onSetDefault, onTest, onEdit, onDelete }) {
|
||||
const btn = { padding: '3px 10px', cursor: 'pointer', border: '1px solid #ccc', borderRadius: 4, background: '#fff', fontSize: 12 };
|
||||
const btn = { padding: '3px 10px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)', fontSize: 12 };
|
||||
return (
|
||||
<div style={{
|
||||
border: isActive ? '1px solid #2e7d32' : '1px solid #ddd',
|
||||
background: isActive ? '#f1f8f2' : '#fff',
|
||||
border: isActive ? '1px solid var(--dsw-alias-state-success-primary, #2e7d32)' : '1px solid var(--dsw-alias-border-l2, #ddd)',
|
||||
background: isActive ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 8%, var(--dsw-alias-bg-layer-1, #fff))' : 'var(--dsw-alias-bg-layer-1, #fff)',
|
||||
borderRadius: 8,
|
||||
padding: '12px 14px',
|
||||
display: 'flex', flexDirection: 'column', gap: 8,
|
||||
minWidth: 0, overflow: 'hidden',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
|
||||
boxShadow: 'var(--dsw-shadow-lv3, 0 1px 2px rgba(0,0,0,0.04))',
|
||||
}}>
|
||||
{/* 标题行:名称 + 徽标 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
||||
<EllipsisText text={conn.name} maxWidth={140} style={{ fontWeight: 600, fontSize: 14 }} />
|
||||
{isActive && <span style={{ flex: 'none', fontSize: 11, color: '#2e7d32', border: '1px solid #2e7d32', borderRadius: 8, padding: '0 6px' }}>生效中</span>}
|
||||
{isDefault && <span style={{ flex: 'none', fontSize: 11, color: '#1565c0', border: '1px solid #90caf9', borderRadius: 8, padding: '0 6px' }}>默认</span>}
|
||||
{isActive && <span style={{ flex: 'none', fontSize: 11, color: 'var(--dsw-alias-state-success-primary, #2e7d32)', border: '1px solid var(--dsw-alias-state-success-primary, #2e7d32)', borderRadius: 8, padding: '0 6px' }}>生效中</span>}
|
||||
{isDefault && <span style={{ flex: 'none', fontSize: 11, color: 'var(--dsw-alias-state-business-primary, #1565c0)', border: '1px solid var(--dsw-alias-border-l3, #90caf9)', borderRadius: 8, padding: '0 6px' }}>默认</span>}
|
||||
</div>
|
||||
|
||||
{/* 地址行:省略+悬停 */}
|
||||
<div style={{ fontSize: 12, color: '#666', minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)', minWidth: 0 }}>
|
||||
<EllipsisText text={conn.baseUrl} maxWidth={240} />
|
||||
</div>
|
||||
|
||||
{/* 测试结果行:可换行但受卡片约束;超长悬停看全文 */}
|
||||
{testResult && (
|
||||
<div style={{
|
||||
fontSize: 12, lineHeight: '16px', color: testResult.ok ? '#2e7d32' : '#c62828',
|
||||
background: testResult.ok ? '#e8f5e9' : '#fdecea',
|
||||
fontSize: 12, lineHeight: '16px', color: testResult.ok ? 'var(--dsw-alias-state-success-primary, #2e7d32)' : 'var(--dsw-alias-state-error-primary, #c62828)',
|
||||
background: testResult.ok ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 10%, var(--dsw-alias-bg-layer-1, #fff))' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary, #c62828) 10%, var(--dsw-alias-bg-layer-1, #fff))',
|
||||
borderRadius: 4, padding: '4px 8px', minWidth: 0,
|
||||
}}>
|
||||
<EllipsisText text={testResult.text} maxWidth={9999} title={testResult.fullText || testResult.text} />
|
||||
@@ -447,11 +450,11 @@ function QmtConnectionCard({ conn, isActive, isDefault, busy, testResult, onActi
|
||||
|
||||
{/* 操作行:按钮自动换行排列,不挤压卡片 */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 2 }}>
|
||||
{!isActive && <button onClick={onActivate} disabled={busy} style={{ ...btn, color: '#2e7d32', borderColor: '#2e7d32' }}>激活</button>}
|
||||
{!isActive && <button onClick={onActivate} disabled={busy} style={{ ...btn, color: 'var(--dsw-alias-state-success-primary, #2e7d32)', borderColor: 'var(--dsw-alias-state-success-primary, #2e7d32)' }}>激活</button>}
|
||||
{!isDefault && <button onClick={onSetDefault} disabled={busy} style={btn}>设为默认</button>}
|
||||
<button onClick={onTest} disabled={busy}>{busy ? '测试中…' : '测试连接'}</button>
|
||||
<button onClick={onEdit} disabled={busy} style={btn}>编辑</button>
|
||||
<button onClick={onDelete} disabled={busy} style={{ ...btn, color: '#c62828', borderColor: '#c62828' }}>删除</button>
|
||||
<button onClick={onDelete} disabled={busy} style={{ ...btn, color: 'var(--dsw-alias-state-error-primary, #c62828)', borderColor: 'var(--dsw-alias-state-error-primary, #c62828)' }}>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -620,19 +623,19 @@ function QmtConnectionsSettings() {
|
||||
return (
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 13, color: '#666', minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--dsw-alias-label-secondary, #666)', minWidth: 0 }}>
|
||||
激活配置为当前生效连接(切换立即生效);默认配置在插件启动时自动激活。
|
||||
</div>
|
||||
<button onClick={openAdd} disabled={saving} style={{ flex: 'none', padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: '#2e7d32', color: '#fff' }}>
|
||||
<button onClick={openAdd} disabled={saving} style={{ flex: 'none', padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: 'var(--dsw-alias-state-success-primary, #2e7d32)', color: 'var(--dsw-alias-button-contrast-fill, #fff)' }}>
|
||||
+ 新增连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: '#c62828', marginBottom: 12 }}><EllipsisText text={error} maxWidth={560} /></div>}
|
||||
{!state && !error && <div style={{ color: '#666' }}>加载中...</div>}
|
||||
{error && <div style={{ color: 'var(--dsw-alias-state-error-primary, #c62828)', marginBottom: 12 }}><EllipsisText text={error} maxWidth={560} /></div>}
|
||||
{!state && !error && <div style={{ color: 'var(--dsw-alias-label-secondary, #666)' }}>加载中...</div>}
|
||||
|
||||
{state && state.list.length === 0 && (
|
||||
<div style={{ padding: '24px 0', color: '#888', fontSize: 13 }}>
|
||||
<div style={{ padding: '24px 0', color: 'var(--dsw-alias-label-secondary, #888)', fontSize: 13 }}>
|
||||
暂无连接配置。点击「新增连接」录入第一个 QMT Bridge 地址(如 http://192.168.3.43:8610)。
|
||||
</div>
|
||||
)}
|
||||
@@ -662,40 +665,40 @@ function QmtConnectionsSettings() {
|
||||
)}
|
||||
|
||||
{formOpen && (
|
||||
<div style={{ marginTop: 16, padding: 16, border: '1px solid #ddd', borderRadius: 8, background: '#fafafa', maxWidth: 640 }}>
|
||||
<div style={{ marginTop: 16, padding: 16, border: '1px solid var(--dsw-alias-border-l2, #ddd)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2, #fafafa)', maxWidth: 640 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12 }}>{editingId === null ? '新增连接配置' : '编辑连接配置'}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 10 }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: '#666' }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)' }}>
|
||||
连接名称
|
||||
<input type="text" placeholder="如:本机 QMT" value={formName}
|
||||
onChange={(e) => setFormName(e.target.value)}
|
||||
style={{ padding: '6px 10px', border: '1px solid #ccc', borderRadius: 4, fontSize: 13 }} />
|
||||
style={{ padding: '6px 10px', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, fontSize: 13 }} />
|
||||
</label>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: '#666' }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)' }}>
|
||||
HTTP 地址
|
||||
<input type="text" placeholder="如 http://192.168.3.43:8610" value={formUrl}
|
||||
onChange={(e) => setFormUrl(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
style={{ padding: '6px 10px', border: '1px solid #ccc', borderRadius: 4, fontSize: 13 }} />
|
||||
style={{ padding: '6px 10px', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, fontSize: 13 }} />
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
||||
<button onClick={handleSave} disabled={saving}
|
||||
style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: '#2e7d32', color: '#fff' }}>
|
||||
style={{ padding: '6px 16px', cursor: 'pointer', border: 'none', borderRadius: 4, background: 'var(--dsw-alias-state-success-primary, #2e7d32)', color: 'var(--dsw-alias-button-contrast-fill, #fff)' }}>
|
||||
保存
|
||||
</button>
|
||||
<button onClick={handleTestForm} disabled={testingId !== null}
|
||||
style={{ padding: '3px 10px', cursor: 'pointer', border: '1px solid #ccc', borderRadius: 4, background: '#fff', fontSize: 12 }}>
|
||||
style={{ padding: '3px 10px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)', fontSize: 12 }}>
|
||||
{testingId === '__form__' ? '测试中…' : '测试连接'}
|
||||
</button>
|
||||
<button onClick={closeForm} disabled={saving}
|
||||
style={{ padding: '3px 10px', cursor: 'pointer', border: '1px solid #ccc', borderRadius: 4, background: '#fff', fontSize: 12 }}>
|
||||
style={{ padding: '3px 10px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)', fontSize: 12 }}>
|
||||
取消
|
||||
</button>
|
||||
{testResult && testResult.key === '__form__' && (
|
||||
<span style={{
|
||||
fontSize: 12, color: testResult.ok ? '#2e7d32' : '#c62828',
|
||||
background: testResult.ok ? '#e8f5e9' : '#fdecea', borderRadius: 4, padding: '3px 8px',
|
||||
fontSize: 12, color: testResult.ok ? 'var(--dsw-alias-state-success-primary, #2e7d32)' : 'var(--dsw-alias-state-error-primary, #c62828)',
|
||||
background: testResult.ok ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 10%, var(--dsw-alias-bg-layer-1, #fff))' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary, #c62828) 10%, var(--dsw-alias-bg-layer-1, #fff))', borderRadius: 4, padding: '3px 8px',
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<EllipsisText text={testResult.text} maxWidth={360} title={testResult.fullText || testResult.text} />
|
||||
|
||||
Reference in New Issue
Block a user