Compare commits

..

No commits in common. 'caeb1a56be3a046eba1e009c1c32ee5ed4aecd6c' and '83e81bc3cbf69d69d67ec8acd3525287648352c6' have entirely different histories.

@ -297,135 +297,43 @@
.badge-info { background: #dbeafe; color: #1e40af; } .badge-info { background: #dbeafe; color: #1e40af; }
.badge-gray { background: var(--gray-100); color: var(--gray-600); } .badge-gray { background: var(--gray-100); color: var(--gray-600); }
/* Symbol Table */ /* Symbol Grid */
.symbol-table { .symbol-grid {
width: 100%; display: grid;
border-collapse: collapse; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin-top: 16px; margin-top: 16px;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
display: block; padding-right: 8px;
} }
.symbol-table thead { .symbol-card {
position: sticky;
top: 0;
background: var(--gray-50); background: var(--gray-50);
z-index: 1;
}
.symbol-table th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: var(--gray-700);
font-size: 13px;
border-bottom: 2px solid var(--gray-200);
}
.symbol-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
font-size: 14px;
}
.symbol-table tr:hover {
background: var(--gray-50);
}
.symbol-table .symbol-name {
font-weight: 600;
color: var(--gray-800);
}
.symbol-table .symbol-code-input {
font-family: 'SF Mono', 'Cascadia Code', monospace;
font-size: 13px;
color: var(--primary);
padding: 6px 10px;
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
border-radius: 6px; border-radius: var(--radius);
width: 120px; padding: 14px;
transition: all 0.2s; text-align: center;
}
.symbol-table .symbol-code-input:focus {
outline: none;
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.symbol-table .symbol-category-select {
font-size: 13px;
color: var(--gray-700);
padding: 6px 10px;
border: 1px solid var(--gray-200);
border-radius: 6px;
width: 120px;
background: white;
cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
position: relative;
} }
.symbol-table .symbol-category-select:hover { .symbol-card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-md); }
border-color: var(--primary-light); .symbol-name { font-weight: 600; color: var(--gray-800); font-size: 14px; }
} .symbol-code { font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 13px; color: var(--primary); margin-top: 4px; }
.symbol-table .symbol-category-select:focus {
outline: none;
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.symbol-table .symbol-actions { .symbol-actions {
display: flex; display: flex;
gap: 6px; gap: 6px;
margin-top: 10px;
justify-content: center;
} }
.symbol-table .symbol-actions .btn-sm { .symbol-actions .btn-sm {
padding: 4px 10px; padding: 4px 8px;
font-size: 12px; font-size: 12px;
} }
/* 分类 Tab 样式 */
.category-tabs-container {
margin-top: 16px;
}
.category-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.category-tab {
padding: 8px 16px;
border: 1px solid var(--gray-300);
background: white;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: var(--gray-700);
cursor: pointer;
transition: all 0.2s;
}
.category-tab:hover {
border-color: var(--primary-light);
color: var(--primary);
}
.category-tab.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.category-content {
margin-top: 0;
}
.add-symbol-form { .add-symbol-form {
background: var(--gray-50); background: var(--gray-50);
border: 2px dashed var(--gray-300); border: 2px dashed var(--gray-300);
@ -924,7 +832,7 @@
<div class="add-symbol-form"> <div class="add-symbol-form">
<div class="card-title" style="margin-bottom: 16px; font-size: 15px;">手动添加品种</div> <div class="card-title" style="margin-bottom: 16px; font-size: 15px;">手动添加品种</div>
<div class="form-row" style="grid-template-columns: 1fr 1fr 1fr auto;"> <div class="form-row">
<div class="form-group" style="margin-bottom: 0;"> <div class="form-group" style="margin-bottom: 0;">
<label class="form-label">品种名称</label> <label class="form-label">品种名称</label>
<input class="form-input" id="newSymbolName" placeholder="如: 沪银"> <input class="form-input" id="newSymbolName" placeholder="如: 沪银">
@ -940,12 +848,6 @@
<option value="stock">股票</option> <option value="stock">股票</option>
</select> </select>
</div> </div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">品类</label>
<select class="form-select" id="newSymbolCategory">
<option value="">请选择品类</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;"> <div class="form-group" style="margin-bottom: 0;">
<button class="btn btn-primary" onclick="addSymbol()" style="height: 42px;"> <button class="btn btn-primary" onclick="addSymbol()" style="height: 42px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
@ -1424,38 +1326,6 @@
let taskSymbolsData = []; let taskSymbolsData = [];
let selectedTaskSymbols = []; let selectedTaskSymbols = [];
let currentTaskId = null; let currentTaskId = null;
// 品种分类映射表
const SYMBOL_CATEGORIES = {
// 贵金属
'沪金': '贵金属', '沪银': '贵金属', '铂金': '贵金属', '钯金': '贵金属',
// 有色金属
'沪铜': '有色金属', '沪铝': '有色金属', '沪锌': '有色金属', '沪铅': '有色金属',
'沪镍': '有色金属', '沪锡': '有色金属', '氧化铝': '有色金属', '国际铜': '有色金属',
// 黑色系
'螺纹钢': '黑色系', '热卷': '黑色系', '铁矿石': '黑色系', '焦炭': '黑色系',
'焦煤': '黑色系', '锰硅': '黑色系', '硅铁': '黑色系',
// 能源化工
'原油': '能源化工', '燃料油': '能源化工', '沥青': '能源化工', '低硫燃料油': '能源化工',
'PTA': '能源化工', 'PVC': '能源化工', '聚乙烯': '能源化工', '聚丙烯': '能源化工',
'苯乙烯': '能源化工', '乙二醇': '能源化工', '甲醇': '能源化工', '纯碱': '能源化工',
'玻璃': '能源化工', '橡胶': '能源化工', '20号胶': '能源化工', '丁二烯橡胶': '能源化工',
'尿素': '能源化工', '烧碱': '能源化工',
// 农产品
'豆粕': '农产品', '豆油': '农产品', '棕榈油': '农产品', '菜籽': '农产品',
'菜粕': '农产品', '菜油': '农产品', '玉米': '农产品', '玉米淀粉': '农产品',
'白糖': '农产品', '棉花': '农产品', '棉纱': '农产品', '苹果': '农产品',
'红枣': '农产品', '花生': '农产品', '粳米': '农产品',
// 金融期货
'中证1000': '金融期货', '中证500': '金融期货', '沪深300': '金融期货', '上证50': '金融期货',
'中证2000': '金融期货', '十年国债': '金融期货', '五年国债': '金融期货', '三十年国债': '金融期货',
// 其他
'集运指数': '其他', '碳酸锂': '其他', '工业硅': '其他', '多晶硅': '其他',
'欧线': '其他', '铝合金': '其他'
};
// 分类显示顺序
const CATEGORY_ORDER = ['贵金属', '有色金属', '黑色系', '能源化工', '农产品', '金融期货', '其他'];
let allTasks = []; let allTasks = [];
// Navigation // Navigation
@ -1581,7 +1451,7 @@
const stockCount = Object.keys(stock).length; const stockCount = Object.keys(stock).length;
document.getElementById('configCount').textContent = `期货 ${futuresCount} 个, 股票 ${stockCount} 个`; document.getElementById('configCount').textContent = `期货 ${futuresCount} 个, 股票 ${stockCount} 个`;
const renderTable = (items, type, containerId) => { const renderGrid = (items, type) => {
let filteredItems = items; let filteredItems = items;
if (currentSearchTerm) { if (currentSearchTerm) {
@ -1606,99 +1476,20 @@
return text.replace(regex, '<span class="highlight">$1</span>'); return text.replace(regex, '<span class="highlight">$1</span>');
}; };
// 按分类分组 return `<div class="symbol-grid">${Object.entries(filteredItems).map(([name, code]) =>
const grouped = {}; `<div class="symbol-card">
for (const [name, code] of Object.entries(filteredItems)) { <div class="symbol-name">${highlightText(name)}</div>
const category = SYMBOL_CATEGORIES[name] || '其他'; <div class="symbol-code">${highlightText(code)}</div>
if (!grouped[category]) grouped[category] = []; <div class="symbol-actions">
grouped[category].push({ name, code }); <button class="btn btn-primary btn-sm" onclick="editSymbol('${type}', '${code}', '${name}')">修改</button>
}
// 获取有数据的分类列表
const activeCategories = [];
for (const category of CATEGORY_ORDER) {
if (grouped[category] && grouped[category].length > 0) {
activeCategories.push(category);
}
}
// 处理未在映射表中的分类
for (const category of Object.keys(grouped)) {
if (!CATEGORY_ORDER.includes(category) && grouped[category].length > 0) {
activeCategories.push(category);
}
}
if (activeCategories.length === 0) {
return '<div class="empty-state"><p>暂无品种</p></div>';
}
// 生成 tab 导航
const tabsHtml = activeCategories.map((category, index) =>
`<button class="category-tab ${index === 0 ? 'active' : ''}"
onclick="switchCategoryTab('${containerId}', '${category}', this)">
${category} (${grouped[category].length})
</button>`
).join('');
// 生成表格内容(每个分类一个表格,默认只显示第一个)
const tablesHtml = activeCategories.map((category, index) => {
const rows = grouped[category].map(({ name, code }) => {
const currentCategory = SYMBOL_CATEGORIES[name] || '其他';
const categoryOptions = CATEGORY_ORDER.map(cat =>
`<option value="${cat}" ${cat === currentCategory ? 'selected' : ''}>${cat}</option>`
).join('');
return `<tr>
<td class="symbol-name">${highlightText(name)}</td>
<td><input type="text" class="symbol-code-input" value="${code}" data-type="${type}" data-name="${name}" data-original="${code}"></td>
<td>
<select class="symbol-category-select" data-name="${name}" data-original="${currentCategory}">
${categoryOptions}
</select>
</td>
<td class="symbol-actions">
<button class="btn btn-primary btn-sm" onclick="saveSymbolRow('${type}', '${name}', this)">保存</button>
<button class="btn btn-danger btn-sm" onclick="deleteSymbol('${type}', '${code}')">删除</button> <button class="btn btn-danger btn-sm" onclick="deleteSymbol('${type}', '${code}')">删除</button>
</td> </div>
</tr>`; </div>`
}).join(''); ).join('')}</div>`;
return `<div class="category-table-wrapper" data-category="${category}" style="display: ${index === 0 ? 'block' : 'none'}">
<table class="symbol-table">
<thead>
<tr>
<th>品种名称</th>
<th>合约代码</th>
<th>品类</th>
<th>操作</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>`;
}).join('');
return `<div class="category-tabs-container">
<div class="category-tabs">${tabsHtml}</div>
<div class="category-content">${tablesHtml}</div>
</div>`;
}; };
document.getElementById('tab-futures').innerHTML = renderTable(futures, 'futures', 'futures'); document.getElementById('tab-futures').innerHTML = renderGrid(futures, 'futures');
document.getElementById('tab-stock').innerHTML = renderTable(stock, 'stock', 'stock'); document.getElementById('tab-stock').innerHTML = renderGrid(stock, 'stock');
}
// 切换分类 tab
function switchCategoryTab(containerId, category, btn) {
const container = btn.closest('.category-tabs-container');
// 更新 tab 按钮状态
container.querySelectorAll('.category-tab').forEach(tab => tab.classList.remove('active'));
btn.classList.add('active');
// 显示对应的表格
container.querySelectorAll('.category-table-wrapper').forEach(wrapper => {
wrapper.style.display = wrapper.dataset.category === category ? 'block' : 'none';
});
} }
function filterSymbols(searchTerm) { function filterSymbols(searchTerm) {
@ -1710,16 +1501,11 @@
const name = document.getElementById('newSymbolName').value.trim(); const name = document.getElementById('newSymbolName').value.trim();
const code = document.getElementById('newSymbolCode').value.trim(); const code = document.getElementById('newSymbolCode').value.trim();
const type = document.getElementById('newSymbolType').value; const type = document.getElementById('newSymbolType').value;
const category = document.getElementById('newSymbolCategory').value;
if (!name || !code) { if (!name || !code) {
return showToast('请填写品种名称和代码', 'error'); return showToast('请填写品种名称和代码', 'error');
} }
if (!category) {
return showToast('请选择品种品类', 'error');
}
const symbols = type === 'futures' ? (currentConfig.futures || {}) : (currentConfig.stock || {}); const symbols = type === 'futures' ? (currentConfig.futures || {}) : (currentConfig.stock || {});
if (symbols[name]) { if (symbols[name]) {
@ -1728,9 +1514,6 @@
symbols[name] = code; symbols[name] = code;
// 保存品类到 SYMBOL_CATEGORIES
SYMBOL_CATEGORIES[name] = category;
const fullConfig = { const fullConfig = {
futures: currentConfig.futures || {}, futures: currentConfig.futures || {},
stock: currentConfig.stock || {} stock: currentConfig.stock || {}
@ -1825,73 +1608,6 @@
openEditSymbolModal(type, code, name); openEditSymbolModal(type, code, name);
} }
async function saveSymbolRow(type, name, btn) {
const row = btn.closest('tr');
const codeInput = row.querySelector('.symbol-code-input');
const categorySelect = row.querySelector('.symbol-category-select');
const newCode = codeInput.value.trim();
const originalCode = codeInput.dataset.original;
const newCategory = categorySelect.value;
const originalCategory = categorySelect.dataset.original;
if (!newCode) {
return showToast('合约代码不能为空', 'error');
}
const codeChanged = newCode !== originalCode;
const categoryChanged = newCategory !== originalCategory;
if (!codeChanged && !categoryChanged) {
return showToast('未做任何修改', 'info');
}
// 更新配置
const symbols = type === 'futures' ? (currentConfig.futures || {}) : (currentConfig.stock || {});
if (codeChanged) {
symbols[name] = newCode;
}
if (categoryChanged) {
SYMBOL_CATEGORIES[name] = newCategory;
}
const fullConfig = {
futures: currentConfig.futures || {},
stock: currentConfig.stock || {}
};
fullConfig[type] = symbols;
try {
const res = await fetch(`${API}/config/upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fullConfig)
});
if (res.ok) {
const changes = [];
if (codeChanged) changes.push('合约代码');
if (categoryChanged) changes.push('品类');
showToast(`${changes.join('、')}修改成功`, 'success');
if (codeChanged) codeInput.dataset.original = newCode;
if (categoryChanged) categorySelect.dataset.original = newCategory;
// 如果品类修改了,需要重新渲染以更新 tab 分组
if (categoryChanged) {
loadConfig();
}
} else {
const data = await res.json();
showToast(data.detail || '修改失败', 'error');
// 恢复原始值
codeInput.value = originalCode;
categorySelect.value = originalCategory;
}
} catch (e) {
showToast(`修改失败: ${e.message}`, 'error');
codeInput.value = originalCode;
categorySelect.value = originalCategory;
}
}
async function deleteSymbol(type, code) { async function deleteSymbol(type, code) {
if (!confirm('确定删除此品种?')) return; if (!confirm('确定删除此品种?')) return;
@ -3448,22 +3164,8 @@
document.getElementById('batchExportEndTime').value = today; document.getElementById('batchExportEndTime').value = today;
} }
// 初始化品类下拉框
function initCategorySelect() {
const select = document.getElementById('newSymbolCategory');
if (!select) return;
select.innerHTML = '<option value="">请选择品类</option>';
CATEGORY_ORDER.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
select.appendChild(option);
});
}
// 初始化 // 初始化
initDefaultDate(); initDefaultDate();
initCategorySelect();
loadConfig(); loadConfig();
loadTasks(); loadTasks();
</script> </script>

Binary file not shown.

Binary file not shown.

@ -1,8 +0,0 @@
name: symbol-config-category-group
phase: verify
workflow: tweak
created_at: "2026-07-07T23:15:00+08:00"
updated_at: "2026-07-07T23:15:00+08:00"
build_mode: direct
verify_result: pass
archived: true

@ -1,31 +0,0 @@
# 设计文档
## 实现方案
### 1. 品种分类映射表
在前端定义 `SYMBOL_CATEGORIES` 对象,将品种名称映射到类别:
```javascript
const SYMBOL_CATEGORIES = {
// 贵金属
'沪金': '贵金属', '沪银': '贵金属', '铂金': '贵金属', '钯金': '贵金属',
// 有色金属
'沪铜': '有色金属', '沪铝': '有色金属', ...
// 黑色系
'螺纹钢': '黑色系', '热卷': '黑色系', ...
// 能源化工
'原油': '能源化工', 'PTA': '能源化工', ...
// 农产品
'豆粕': '农产品', '豆油': '农产品', ...
// 金融期货
'中证1000': '金融期货', ...
};
```
### 2. 分组渲染逻辑
修改 `renderTable` 函数:
1. 将品种按类别分组
2. 未分类的品种归入"其他"组
3. 按类别顺序渲染,每组显示标题行
### 3. CSS 样式
新增 `.symbol-category-header` 样式,用于分组标题行

@ -1,14 +0,0 @@
# 品种配置按类别分组显示
## 动机
当前品种配置列表按添加顺序显示,品种较多时不易查找。需要按品种类别(贵金属、有色金属、黑色系等)分组显示。
## 目标
1. 在前端定义品种分类映射表
2. 列表按分类分组显示,每组有标题
3. 保持现有功能(修改合约、删除)不变
## 范围
- 修改 `app/static/index.html` 中的 `renderTable` 函数
- 新增品种分类映射表
- 新增分组标题样式

@ -1,7 +0,0 @@
# 任务列表
- [x] 1. 新增品种分类映射表 `SYMBOL_CATEGORIES`
- [x] 2. 修改 `renderTable` 函数,按分类分组渲染
- [x] 3. 新增分组标题行 CSS 样式
- [x] 4. 将分组显示改为 tab 切换方式
- [x] 5. 新增 `switchCategoryTab` 函数实现 tab 切换逻辑

@ -1,8 +0,0 @@
name: symbol-config-list-view
phase: verify
workflow: tweak
created_at: "2026-07-07T23:08:00+08:00"
updated_at: "2026-07-07T23:08:00+08:00"
build_mode: direct
verify_result: pass
archived: true

@ -1,27 +0,0 @@
# 设计文档
## 实现方案
### 1. 列表视图结构
使用表格替代卡片网格:
```
| 品种名称 | 合约代码 | 操作 |
|----------|----------|------|
| 螺纹钢 | rb | [保存] [删除] |
| 铁矿石 | i | [保存] [删除] |
```
### 2. 合约代码编辑
- 合约代码列使用 input 输入框
- 修改后点击"保存"按钮提交
- 保存时调用 API 更新配置
### 3. CSS 调整
- 移除 `.symbol-grid``.symbol-card` 样式
- 添加 `.symbol-table` 列表样式
- 保持与现有 UI 风格一致
### 4. JavaScript 调整
- `renderConfig()` 函数改为渲染表格
- 新增 `saveSymbolCode(type, oldCode, newCode, name)` 函数
- 保留删除功能

@ -1,16 +0,0 @@
# 品种配置列表视图改造
## 动机
当前品种配置使用卡片网格展示,修改配置不方便:
1. 卡片占用空间大,品种多时需要大量滚动
2. 修改合约代码需要点击"修改"按钮打开弹窗,操作繁琐
## 目标
1. 将品种卡片改为紧凑列表视图
2. 支持直接在列表中修改合约代码
## 范围
- 修改 `app/static/index.html` 中的品种配置页面
- 将 `symbol-grid` + `symbol-card` 改为表格列表
- 在列表中直接显示可编辑的合约代码输入框
- 保留删除功能

@ -1,5 +0,0 @@
# 任务列表
- [x] 1. 修改 CSS`.symbol-grid` + `.symbol-card` 改为 `.symbol-table` 列表样式
- [x] 2. 修改 JavaScript`renderConfig()` 中的卡片渲染改为表格渲染,合约代码使用 input 输入框
- [x] 3. 新增 `saveSymbolCode()` 函数,支持直接在列表中保存修改的合约代码
Loading…
Cancel
Save