|
|
|
|
@ -297,40 +297,71 @@
|
|
|
|
|
.badge-info { background: #dbeafe; color: #1e40af; }
|
|
|
|
|
.badge-gray { background: var(--gray-100); color: var(--gray-600); }
|
|
|
|
|
|
|
|
|
|
/* Symbol Grid */
|
|
|
|
|
.symbol-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
|
|
|
gap: 12px;
|
|
|
|
|
/* Symbol Table */
|
|
|
|
|
.symbol-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.symbol-table thead {
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
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-card {
|
|
|
|
|
.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-radius: var(--radius);
|
|
|
|
|
padding: 14px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
width: 120px;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.symbol-card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-md); }
|
|
|
|
|
.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-code-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--primary-light);
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.symbol-actions {
|
|
|
|
|
.symbol-table .symbol-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.symbol-actions .btn-sm {
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
.symbol-table .symbol-actions .btn-sm {
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1451,7 +1482,7 @@
|
|
|
|
|
const stockCount = Object.keys(stock).length;
|
|
|
|
|
document.getElementById('configCount').textContent = `期货 ${futuresCount} 个, 股票 ${stockCount} 个`;
|
|
|
|
|
|
|
|
|
|
const renderGrid = (items, type) => {
|
|
|
|
|
const renderTable = (items, type) => {
|
|
|
|
|
let filteredItems = items;
|
|
|
|
|
|
|
|
|
|
if (currentSearchTerm) {
|
|
|
|
|
@ -1476,20 +1507,31 @@
|
|
|
|
|
return text.replace(regex, '<span class="highlight">$1</span>');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return `<div class="symbol-grid">${Object.entries(filteredItems).map(([name, code]) =>
|
|
|
|
|
`<div class="symbol-card">
|
|
|
|
|
<div class="symbol-name">${highlightText(name)}</div>
|
|
|
|
|
<div class="symbol-code">${highlightText(code)}</div>
|
|
|
|
|
<div class="symbol-actions">
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="editSymbol('${type}', '${code}', '${name}')">修改</button>
|
|
|
|
|
const rows = Object.entries(filteredItems).map(([name, code]) =>
|
|
|
|
|
`<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 class="symbol-actions">
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="saveSymbolCode('${type}', '${name}', this)">保存</button>
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="deleteSymbol('${type}', '${code}')">删除</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`
|
|
|
|
|
).join('')}</div>`;
|
|
|
|
|
</td>
|
|
|
|
|
</tr>`
|
|
|
|
|
).join('');
|
|
|
|
|
|
|
|
|
|
return `<table class="symbol-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>品种名称</th>
|
|
|
|
|
<th>合约代码</th>
|
|
|
|
|
<th>操作</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>${rows}</tbody>
|
|
|
|
|
</table>`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.getElementById('tab-futures').innerHTML = renderGrid(futures, 'futures');
|
|
|
|
|
document.getElementById('tab-stock').innerHTML = renderGrid(stock, 'stock');
|
|
|
|
|
document.getElementById('tab-futures').innerHTML = renderTable(futures, 'futures');
|
|
|
|
|
document.getElementById('tab-stock').innerHTML = renderTable(stock, 'stock');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterSymbols(searchTerm) {
|
|
|
|
|
@ -1608,6 +1650,52 @@
|
|
|
|
|
openEditSymbolModal(type, code, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveSymbolCode(type, name, btn) {
|
|
|
|
|
const row = btn.closest('tr');
|
|
|
|
|
const input = row.querySelector('.symbol-code-input');
|
|
|
|
|
const newCode = input.value.trim();
|
|
|
|
|
const originalCode = input.dataset.original;
|
|
|
|
|
|
|
|
|
|
if (!newCode) {
|
|
|
|
|
return showToast('合约代码不能为空', 'error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newCode === originalCode) {
|
|
|
|
|
return showToast('合约代码未修改', 'info');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新配置
|
|
|
|
|
const symbols = type === 'futures' ? (currentConfig.futures || {}) : (currentConfig.stock || {});
|
|
|
|
|
symbols[name] = newCode;
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
showToast('合约代码修改成功', 'success');
|
|
|
|
|
input.dataset.original = newCode;
|
|
|
|
|
} else {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
showToast(data.detail || '修改失败', 'error');
|
|
|
|
|
// 恢复原始值
|
|
|
|
|
input.value = originalCode;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast(`修改失败: ${e.message}`, 'error');
|
|
|
|
|
input.value = originalCode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteSymbol(type, code) {
|
|
|
|
|
if (!confirm('确定删除此品种?')) return;
|
|
|
|
|
|
|
|
|
|
|