|
|
|
|
@ -936,6 +936,50 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div id="queryResult" class="hidden" style="margin-top: 20px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="card-title">批量导出数据</div>
|
|
|
|
|
<div class="card-subtitle">导出所有配置品种的K线数据</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">数据类型</label>
|
|
|
|
|
<select class="form-select" id="batchExportDataType">
|
|
|
|
|
<option value="futures">期货</option>
|
|
|
|
|
<option value="stock">股票</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">周期</label>
|
|
|
|
|
<select class="form-select" id="batchExportPeriod">
|
|
|
|
|
<option value="">全部周期</option>
|
|
|
|
|
<option value="5min">5分钟</option>
|
|
|
|
|
<option value="15min">15分钟</option>
|
|
|
|
|
<option value="30min">30分钟</option>
|
|
|
|
|
<option value="60min">60分钟</option>
|
|
|
|
|
<option value="daily">日线</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">结束日期</label>
|
|
|
|
|
<input type="date" class="form-input" id="batchExportEndTime">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<button class="btn btn-success" onclick="batchExportData()" style="height: 42px;" id="btnBatchExport">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
|
|
|
批量导出
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="batchExportProgress" class="hidden" style="margin-top: 16px;">
|
|
|
|
|
<div class="progress-bar"><div class="progress-fill" id="batchExportProgressFill" style="width: 0%"></div></div>
|
|
|
|
|
<p id="batchExportProgressText" style="text-align: center; color: var(--gray-500); font-size: 14px; margin-top: 8px;"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="batchExportResult" class="hidden" style="margin-top: 16px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tasks -->
|
|
|
|
|
@ -1992,6 +2036,163 @@
|
|
|
|
|
showToast(`已导出 ${periodCount} 个周期数据`, 'success');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量导出数据
|
|
|
|
|
async function batchExportData() {
|
|
|
|
|
const dataType = document.getElementById('batchExportDataType').value;
|
|
|
|
|
const period = document.getElementById('batchExportPeriod').value;
|
|
|
|
|
const endDate = document.getElementById('batchExportEndTime').value;
|
|
|
|
|
|
|
|
|
|
// 获取所有配置的品种
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/config/symbols`);
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
if (!data.symbols || data.symbols.length === 0) {
|
|
|
|
|
showToast('暂无可导出的品种,请先添加品种配置', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤指定数据类型的品种
|
|
|
|
|
const symbols = data.symbols.filter(s => s.data_type === dataType);
|
|
|
|
|
if (symbols.length === 0) {
|
|
|
|
|
showToast(`暂无${dataType === 'futures' ? '期货' : '股票'}品种配置`, 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const symbolList = symbols.map(s => s.symbol);
|
|
|
|
|
|
|
|
|
|
// 显示进度条
|
|
|
|
|
const progressContainer = document.getElementById('batchExportProgress');
|
|
|
|
|
const progressFill = document.getElementById('batchExportProgressFill');
|
|
|
|
|
const progressText = document.getElementById('batchExportProgressText');
|
|
|
|
|
const resultContainer = document.getElementById('batchExportResult');
|
|
|
|
|
|
|
|
|
|
progressContainer.classList.remove('hidden');
|
|
|
|
|
resultContainer.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
const endDateDisplay = endDate || new Date().toISOString().slice(0, 10);
|
|
|
|
|
addLog(`开始批量导出: ${dataType === 'futures' ? '期货' : '股票'} ${symbolList.length} 个品种, 结束日期: ${endDateDisplay}`);
|
|
|
|
|
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
let failedCount = 0;
|
|
|
|
|
let totalCandles = 0;
|
|
|
|
|
const failedSymbols = [];
|
|
|
|
|
|
|
|
|
|
// 逐个获取并导出数据
|
|
|
|
|
for (let i = 0; i < symbolList.length; i++) {
|
|
|
|
|
const symbol = symbolList[i];
|
|
|
|
|
const progress = ((i + 1) / symbolList.length * 100).toFixed(0);
|
|
|
|
|
progressFill.style.width = `${progress}%`;
|
|
|
|
|
progressText.textContent = `正在导出: ${symbol} (${i + 1}/${symbolList.length})`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 构建查询参数
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
if (period) params.append('period', period);
|
|
|
|
|
if (endDate) {
|
|
|
|
|
const endDateTime = new Date(endDate);
|
|
|
|
|
endDateTime.setHours(23, 59, 59, 999);
|
|
|
|
|
params.append('end_time', endDateTime.toISOString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const queryString = params.toString();
|
|
|
|
|
const url = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`;
|
|
|
|
|
|
|
|
|
|
const fetchRes = await fetch(url);
|
|
|
|
|
if (!fetchRes.ok) {
|
|
|
|
|
failedCount++;
|
|
|
|
|
failedSymbols.push(symbol);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchData = await fetchRes.json();
|
|
|
|
|
|
|
|
|
|
if (!fetchData.timeframes || fetchData.timeframes.length === 0) {
|
|
|
|
|
failedCount++;
|
|
|
|
|
failedSymbols.push(symbol);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 导出单个品种数据
|
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
|
|
|
const filename = `${symbol}_${period || '多周期'}_截至${endDateDisplay}_${timestamp}.json`;
|
|
|
|
|
|
|
|
|
|
const exportObj = {
|
|
|
|
|
symbol: fetchData.symbol || symbol,
|
|
|
|
|
type: fetchData.type || dataType,
|
|
|
|
|
period: period || 'all',
|
|
|
|
|
end_date: endDateDisplay,
|
|
|
|
|
current_price: fetchData.current_price,
|
|
|
|
|
timestamp: fetchData.timestamp || new Date().toISOString(),
|
|
|
|
|
timeframes: {}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchData.timeframes.forEach(tf => {
|
|
|
|
|
exportObj.timeframes[tf.period] = tf.candles || [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const jsonStr = JSON.stringify(exportObj, null, 2);
|
|
|
|
|
const blob = new Blob([jsonStr], { type: 'application/json' });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = filename;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
const candleCount = fetchData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
|
|
|
|
|
successCount++;
|
|
|
|
|
totalCandles += candleCount;
|
|
|
|
|
|
|
|
|
|
// 添加小延迟避免浏览器阻止多次下载
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`导出 ${symbol} 失败:`, e);
|
|
|
|
|
failedCount++;
|
|
|
|
|
failedSymbols.push(symbol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示结果
|
|
|
|
|
progressContainer.classList.add('hidden');
|
|
|
|
|
resultContainer.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
let resultHtml = `
|
|
|
|
|
<div style="padding: 16px; background: var(--success-light); border-radius: 8px; border-left: 4px solid var(--success);">
|
|
|
|
|
<h4 style="color: var(--success); margin-bottom: 12px;">批量导出完成</h4>
|
|
|
|
|
<div style="display: grid; gap: 8px;">
|
|
|
|
|
<div>成功: <strong>${successCount}</strong> 个品种</div>
|
|
|
|
|
<div>失败: <strong>${failedCount}</strong> 个品种</div>
|
|
|
|
|
<div>总计: <strong>${totalCandles}</strong> 条K线</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
if (failedSymbols.length > 0) {
|
|
|
|
|
resultHtml += `
|
|
|
|
|
<div style="margin-top: 12px; padding: 12px; background: white; border-radius: 6px;">
|
|
|
|
|
<div style="color: var(--danger); font-weight: 500; margin-bottom: 8px;">失败的品种:</div>
|
|
|
|
|
<div style="color: var(--gray-600); font-size: 13px;">${failedSymbols.join(', ')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resultHtml += '</div>';
|
|
|
|
|
resultContainer.innerHTML = resultHtml;
|
|
|
|
|
|
|
|
|
|
addLog(`批量导出完成: 成功 ${successCount}, 失败 ${failedCount}, 总计 ${totalCandles} 条K线`, successCount > 0 ? 'success' : 'error');
|
|
|
|
|
showToast(`批量导出完成: ${successCount} 个品种成功`, successCount > 0 ? 'success' : 'error');
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('批量导出失败:', e);
|
|
|
|
|
showToast(`批量导出失败: ${e.message}`, 'error');
|
|
|
|
|
addLog(`批量导出失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderKlineChart(timeframe, symbol) {
|
|
|
|
|
const resultContainer = document.getElementById('queryResult');
|
|
|
|
|
|
|
|
|
|
@ -3001,6 +3202,7 @@
|
|
|
|
|
function initDefaultDate() {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
|
document.getElementById('queryEndTime').value = today;
|
|
|
|
|
document.getElementById('batchExportEndTime').value = today;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|