|
|
|
@ -651,6 +651,7 @@ function trSwitchSubtab(tab) {
|
|
|
|
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
|
|
|
|
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
if (tab === 'experience') trExpLoad();
|
|
|
|
if (tab === 'experience') trExpLoad();
|
|
|
|
|
|
|
|
if (tab === 'data') trLoadDataOverview();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function trSwitchToReflection(date) {
|
|
|
|
function trSwitchToReflection(date) {
|
|
|
|
@ -1642,6 +1643,58 @@ async function trInitDefaultDate() {
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
console.error('获取最后交易日失败', e);
|
|
|
|
console.error('获取最后交易日失败', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 初始化快捷日期按钮
|
|
|
|
|
|
|
|
trInitQuickDates();
|
|
|
|
|
|
|
|
trLoadAllData();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trInitQuickDates() {
|
|
|
|
|
|
|
|
document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => {
|
|
|
|
|
|
|
|
btn.addEventListener('click', () => trSetDateRange(btn.dataset.range));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trSetDateRange(range) {
|
|
|
|
|
|
|
|
// 更新按钮状态
|
|
|
|
|
|
|
|
document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => {
|
|
|
|
|
|
|
|
btn.classList.toggle('active', btn.dataset.range === range);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
const end = today.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
let start = end;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (range) {
|
|
|
|
|
|
|
|
case 'today':
|
|
|
|
|
|
|
|
start = end;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'week':
|
|
|
|
|
|
|
|
const weekAgo = new Date(today);
|
|
|
|
|
|
|
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
|
|
|
|
|
|
start = weekAgo.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'half-month':
|
|
|
|
|
|
|
|
const halfMonthAgo = new Date(today);
|
|
|
|
|
|
|
|
halfMonthAgo.setDate(halfMonthAgo.getDate() - 15);
|
|
|
|
|
|
|
|
start = halfMonthAgo.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'month':
|
|
|
|
|
|
|
|
const monthAgo = new Date(today);
|
|
|
|
|
|
|
|
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
|
|
|
|
|
|
start = monthAgo.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'quarter':
|
|
|
|
|
|
|
|
const quarterAgo = new Date(today);
|
|
|
|
|
|
|
|
quarterAgo.setMonth(quarterAgo.getMonth() - 3);
|
|
|
|
|
|
|
|
start = quarterAgo.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'custom':
|
|
|
|
|
|
|
|
// 自定义模式,不自动设置日期,让用户手动选择
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-start-date').value = start;
|
|
|
|
|
|
|
|
document.getElementById('tr-end-date').value = end;
|
|
|
|
trLoadAllData();
|
|
|
|
trLoadAllData();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -2326,8 +2379,9 @@ async function trHandleFileImport(e) {
|
|
|
|
const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData });
|
|
|
|
const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData });
|
|
|
|
const json = await res.json();
|
|
|
|
const json = await res.json();
|
|
|
|
if (json.success) {
|
|
|
|
if (json.success) {
|
|
|
|
alert(json.message);
|
|
|
|
trShowImportResult(json.message, json.skipped_details || []);
|
|
|
|
trLoadAllData();
|
|
|
|
trLoadAllData();
|
|
|
|
|
|
|
|
trLoadDataOverview();
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
alert('导入失败: ' + (json.message || '未知错误'));
|
|
|
|
alert('导入失败: ' + (json.message || '未知错误'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2339,6 +2393,53 @@ async function trHandleFileImport(e) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 显示导入结果(含去重详情)
|
|
|
|
|
|
|
|
function trShowImportResult(message, skippedDetails) {
|
|
|
|
|
|
|
|
let html = `<div style="padding:16px;max-width:500px;">
|
|
|
|
|
|
|
|
<div style="font-size:14px;margin-bottom:12px;">${message}</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (skippedDetails && skippedDetails.length > 0) {
|
|
|
|
|
|
|
|
html += `<div style="margin-top:12px;border-top:1px solid #eee;padding-top:12px;">
|
|
|
|
|
|
|
|
<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:#666;">
|
|
|
|
|
|
|
|
被过滤的重复记录(${skippedDetails.length} 条):
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div style="max-height:200px;overflow-y:auto;font-size:12px;background:#f9f9f9;border-radius:4px;padding:8px;">`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 按类型分组显示
|
|
|
|
|
|
|
|
const grouped = {};
|
|
|
|
|
|
|
|
skippedDetails.forEach(d => {
|
|
|
|
|
|
|
|
const key = d.type || '未知';
|
|
|
|
|
|
|
|
if (!grouped[key]) grouped[key] = [];
|
|
|
|
|
|
|
|
grouped[key].push(d);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [type, items] of Object.entries(grouped)) {
|
|
|
|
|
|
|
|
html += `<div style="margin-bottom:6px;"><strong>${type}</strong>(${items.length} 条):</div>`;
|
|
|
|
|
|
|
|
items.slice(0, 10).forEach(d => {
|
|
|
|
|
|
|
|
html += `<div style="padding:2px 0;color:#555;">• ${d.variety} ${d.date} ${d.time} @ ${d.price}</div>`;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (items.length > 10) {
|
|
|
|
|
|
|
|
html += `<div style="color:#999;">... 还有 ${items.length - 10} 条</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html += `</div></div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html += `</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 使用自定义弹窗
|
|
|
|
|
|
|
|
const modal = document.createElement('div');
|
|
|
|
|
|
|
|
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
|
|
|
|
|
|
|
modal.innerHTML = `<div style="background:white;border-radius:8px;max-width:600px;max-height:80vh;overflow-y:auto;">
|
|
|
|
|
|
|
|
${html}
|
|
|
|
|
|
|
|
<div style="padding:12px 16px;border-top:1px solid #eee;text-align:right;">
|
|
|
|
|
|
|
|
<button onclick="this.closest('div[style*=fixed]').remove()" style="padding:6px 16px;background:#007aff;color:white;border:none;border-radius:4px;cursor:pointer;">确定</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function trHandleBatchImport(e) {
|
|
|
|
async function trHandleBatchImport(e) {
|
|
|
|
const files = e.target.files;
|
|
|
|
const files = e.target.files;
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
@ -2355,7 +2456,10 @@ async function trHandleBatchImport(e) {
|
|
|
|
const json = await res.json();
|
|
|
|
const json = await res.json();
|
|
|
|
if (json.success) {
|
|
|
|
if (json.success) {
|
|
|
|
trShowBatchImportResult(json);
|
|
|
|
trShowBatchImportResult(json);
|
|
|
|
if (json.data.total_futures > 0 || json.data.total_options > 0) trLoadAllData();
|
|
|
|
if (json.data.total_futures > 0 || json.data.total_options > 0) {
|
|
|
|
|
|
|
|
trLoadAllData();
|
|
|
|
|
|
|
|
trLoadDataOverview();
|
|
|
|
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
alert('批量导入失败: ' + (json.message || '未知错误'));
|
|
|
|
alert('批量导入失败: ' + (json.message || '未知错误'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2375,9 +2479,27 @@ function trShowBatchImportResult(json) {
|
|
|
|
summaryEl.textContent = json.message;
|
|
|
|
summaryEl.textContent = json.message;
|
|
|
|
detailsEl.innerHTML = data.details.map(d => {
|
|
|
|
detailsEl.innerHTML = data.details.map(d => {
|
|
|
|
let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌';
|
|
|
|
let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌';
|
|
|
|
|
|
|
|
let skippedHtml = '';
|
|
|
|
|
|
|
|
if (d.skipped_details && d.skipped_details.length > 0) {
|
|
|
|
|
|
|
|
const grouped = {};
|
|
|
|
|
|
|
|
d.skipped_details.forEach(item => {
|
|
|
|
|
|
|
|
const key = item.type || '未知';
|
|
|
|
|
|
|
|
if (!grouped[key]) grouped[key] = [];
|
|
|
|
|
|
|
|
grouped[key].push(item);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
skippedHtml = `<div style="margin-top:6px;font-size:11px;color:#888;max-height:80px;overflow-y:auto;">`;
|
|
|
|
|
|
|
|
for (const [type, items] of Object.entries(grouped)) {
|
|
|
|
|
|
|
|
skippedHtml += `<div><strong>${type}</strong> ${items.length}条: `;
|
|
|
|
|
|
|
|
skippedHtml += items.slice(0, 5).map(item => `${item.variety}@${item.price}`).join(', ');
|
|
|
|
|
|
|
|
if (items.length > 5) skippedHtml += ` ...`;
|
|
|
|
|
|
|
|
skippedHtml += `</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
skippedHtml += `</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
return `<div style="padding:10px 12px;margin-bottom:8px;border-radius:6px;background:#F5F5F7;border-left:3px solid ${d.success ? '#34C759' : '#FF3B30'};">
|
|
|
|
return `<div style="padding:10px 12px;margin-bottom:8px;border-radius:6px;background:#F5F5F7;border-left:3px solid ${d.success ? '#34C759' : '#FF3B30'};">
|
|
|
|
<div style="font-weight:600;font-size:13px;">${icon} ${d.filename}</div>
|
|
|
|
<div style="font-weight:600;font-size:13px;">${icon} ${d.filename}</div>
|
|
|
|
<div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">${d.message}</div>
|
|
|
|
<div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">${d.message}</div>
|
|
|
|
|
|
|
|
${skippedHtml}
|
|
|
|
</div>`;
|
|
|
|
</div>`;
|
|
|
|
}).join('');
|
|
|
|
}).join('');
|
|
|
|
modal.classList.add('show');
|
|
|
|
modal.classList.add('show');
|
|
|
|
@ -2434,11 +2556,8 @@ function trCloseBatchModal() {
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 删除当日 =====
|
|
|
|
// ===== 删除当日 =====
|
|
|
|
async function trDeleteByDate() {
|
|
|
|
async function trDeleteByDate() {
|
|
|
|
const startDate = document.getElementById('tr-start-date').value;
|
|
|
|
const tradeDate = document.getElementById('tr-delete-date-input').value;
|
|
|
|
const endDate = document.getElementById('tr-end-date').value;
|
|
|
|
if (!tradeDate) { alert('请先选择要删除的日期'); return; }
|
|
|
|
if (!startDate || !endDate) { alert('请先选择要删除的日期范围'); return; }
|
|
|
|
|
|
|
|
if (startDate !== endDate) { alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同'); return; }
|
|
|
|
|
|
|
|
const tradeDate = startDate;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
|
|
|
|
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
|
|
|
|
@ -2453,12 +2572,52 @@ async function trDeleteByDate() {
|
|
|
|
if (json.success) {
|
|
|
|
if (json.success) {
|
|
|
|
alert(`✅ ${json.message}`);
|
|
|
|
alert(`✅ ${json.message}`);
|
|
|
|
trLoadAllData();
|
|
|
|
trLoadAllData();
|
|
|
|
|
|
|
|
trLoadDataOverview();
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
alert(json.message || '删除失败');
|
|
|
|
alert(json.message || '删除失败');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) { alert('删除失败: ' + e.message); }
|
|
|
|
} catch (e) { alert('删除失败: ' + e.message); }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 数据概览 =====
|
|
|
|
|
|
|
|
async function trLoadDataOverview() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 获取总记录数和日期范围
|
|
|
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/statistics`);
|
|
|
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
|
|
|
if (json.success && json.data) {
|
|
|
|
|
|
|
|
document.getElementById('tr-total-records').textContent = json.data.total_trades || 0;
|
|
|
|
|
|
|
|
document.getElementById('tr-total-batches').textContent = json.data.trading_days || 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 获取批次信息
|
|
|
|
|
|
|
|
const batchRes = await fetch(`${TR_API_BASE}/batches`);
|
|
|
|
|
|
|
|
const batchJson = await batchRes.json();
|
|
|
|
|
|
|
|
if (batchJson.success && batchJson.data && batchJson.data.length > 0) {
|
|
|
|
|
|
|
|
document.getElementById('tr-total-batches').textContent = batchJson.data.length;
|
|
|
|
|
|
|
|
// 获取最早和最新日期
|
|
|
|
|
|
|
|
const dates = batchJson.data.map(b => b.trade_dates).filter(d => d);
|
|
|
|
|
|
|
|
if (dates.length > 0) {
|
|
|
|
|
|
|
|
const allDates = [];
|
|
|
|
|
|
|
|
dates.forEach(d => {
|
|
|
|
|
|
|
|
const parts = d.split(' ~ ');
|
|
|
|
|
|
|
|
parts.forEach(p => {
|
|
|
|
|
|
|
|
const date = p.trim();
|
|
|
|
|
|
|
|
if (date) allDates.push(date);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (allDates.length > 0) {
|
|
|
|
|
|
|
|
allDates.sort();
|
|
|
|
|
|
|
|
document.getElementById('tr-earliest-date').textContent = allDates[0];
|
|
|
|
|
|
|
|
document.getElementById('tr-latest-date').textContent = allDates[allDates.length - 1];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('加载数据概览失败:', e);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatNumber(num) {
|
|
|
|
function formatNumber(num) {
|
|
|
|
if (num === 0 || num === undefined || num === null) return '--';
|
|
|
|
if (num === 0 || num === undefined || num === null) return '--';
|
|
|
|
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
|
|
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
|
|
|