|
|
|
|
@ -924,29 +924,16 @@
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="card-title">批量创建定时任务</div>
|
|
|
|
|
<div class="card-subtitle">为配置中的所有品种自动创建定时采集任务</div>
|
|
|
|
|
<div class="card-title">定时任务管理</div>
|
|
|
|
|
<div class="card-subtitle">创建、管理和监控定时数据采集任务</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">数据类型</label>
|
|
|
|
|
<select class="form-select" id="taskDataType">
|
|
|
|
|
<option value="futures">期货</option>
|
|
|
|
|
<option value="stock">股票</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">采集周期</label>
|
|
|
|
|
<input class="form-input" id="taskPeriods" value="5min,15min,60min">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">轮询间隔(秒)</label>
|
|
|
|
|
<input class="form-input" type="number" id="taskInterval" value="300" min="30" max="86400">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<button class="btn btn-warning" id="btnBatchTasks" onclick="batchCreateTasks()" style="height: 42px;">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
|
|
|
<div style="display: flex; gap: 12px;">
|
|
|
|
|
<button class="btn btn-outline" onclick="showHistoryTasks()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
|
|
|
历史任务
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-warning" onclick="openCreateTaskModal()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
|
|
|
批量创建任务
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
@ -956,11 +943,17 @@
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div class="card-title">任务列表</div>
|
|
|
|
|
<div style="display: flex; gap: 12px;">
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="batchDeleteTasks()" id="btnBatchDeleteTasks" style="display: none;">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
|
|
|
批量删除
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-outline btn-sm" onclick="loadTasks()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
|
|
|
刷新
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="table-container" id="taskTable">
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
|
|
|
@ -970,6 +963,38 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- History Tasks -->
|
|
|
|
|
<div class="page" id="page-history-tasks" style="display: none;">
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="card-title">历史任务</div>
|
|
|
|
|
<div class="card-subtitle">查看已完成的任务记录</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn btn-outline" onclick="showActiveTasks()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
|
|
|
|
|
返回任务列表
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div class="card-title">已完成任务列表</div>
|
|
|
|
|
<button class="btn btn-outline btn-sm" onclick="loadHistoryTasks()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
|
|
|
刷新
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="table-container" id="historyTaskTable">
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
|
|
|
<p>暂无历史任务</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Cache -->
|
|
|
|
|
<div class="page" id="page-cache">
|
|
|
|
|
<div class="card">
|
|
|
|
|
@ -1047,6 +1072,150 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create Task Modal -->
|
|
|
|
|
<div class="modal-overlay" id="createTaskModal">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<div class="modal-title">批量创建定时任务</div>
|
|
|
|
|
<button class="modal-close" onclick="closeCreateTaskModal()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 24px; height: 24px;">
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">数据类型</label>
|
|
|
|
|
<select class="form-select" id="taskDataType">
|
|
|
|
|
<option value="futures">期货</option>
|
|
|
|
|
<option value="stock">股票</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom: 16px;">
|
|
|
|
|
<div style="position: relative; margin-bottom: 12px;">
|
|
|
|
|
<input class="form-input" id="taskSymbolSearch" placeholder="搜索合约..." style="width: 100%; padding-left: 36px;" oninput="filterTaskSymbols(this.value)">
|
|
|
|
|
<svg style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--gray-400);" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
|
|
|
</div>
|
|
|
|
|
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 12px;">
|
|
|
|
|
<input type="checkbox" id="taskSelectAll" onchange="toggleTaskSelectAll()" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
|
|
|
|
|
<span style="font-weight: 500;">全选</span>
|
|
|
|
|
</label>
|
|
|
|
|
<span id="taskSelectedCount" style="color: var(--gray-500); font-size: 14px;">已选择 0 个合约</span>
|
|
|
|
|
<div class="symbol-select-list" id="taskSymbolList" style="max-height: 200px; margin-top: 12px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">采集周期</label>
|
|
|
|
|
<input class="form-input" id="taskPeriods" value="5min,15min,60min" placeholder="如: 5min,15min,60min">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">任务类型</label>
|
|
|
|
|
<select class="form-select" id="taskType" onchange="onTaskTypeChange()">
|
|
|
|
|
<option value="daily">每天定时执行</option>
|
|
|
|
|
<option value="once">仅执行一次</option>
|
|
|
|
|
<option value="interval" selected>指定周期循环</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="taskIntervalGroup" class="form-group">
|
|
|
|
|
<label class="form-label">轮询间隔(秒)</label>
|
|
|
|
|
<input class="form-input" type="number" id="taskInterval" value="300" min="30" max="86400">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="taskTimeGroup" class="form-group" style="display: none;">
|
|
|
|
|
<label class="form-label">执行时间</label>
|
|
|
|
|
<input class="form-input" type="time" id="taskTime" value="09:00">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-outline" onclick="closeCreateTaskModal()">取消</button>
|
|
|
|
|
<button class="btn btn-warning" onclick="confirmCreateTasks()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
|
|
|
创建任务
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Task Detail Modal -->
|
|
|
|
|
<div class="modal-overlay" id="taskDetailModal">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<div class="modal-title">任务详情</div>
|
|
|
|
|
<button class="modal-close" onclick="closeTaskDetailModal()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 24px; height: 24px;">
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body" id="taskDetailContent"></div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-outline" onclick="closeTaskDetailModal()">关闭</button>
|
|
|
|
|
<button class="btn btn-primary" id="btnEditTask" onclick="">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
|
|
|
修改
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Edit Task Modal -->
|
|
|
|
|
<div class="modal-overlay" id="editTaskModal">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<div class="modal-title">修改任务</div>
|
|
|
|
|
<button class="modal-close" onclick="closeEditTaskModal()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 24px; height: 24px;">
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<input type="hidden" id="editTaskId">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">品种代码</label>
|
|
|
|
|
<input class="form-input" id="editTaskSymbol" readonly style="background: var(--gray-100);">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">数据类型</label>
|
|
|
|
|
<select class="form-select" id="editTaskDataType">
|
|
|
|
|
<option value="futures">期货</option>
|
|
|
|
|
<option value="stock">股票</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">采集周期</label>
|
|
|
|
|
<input class="form-input" id="editTaskPeriods" placeholder="如: 5min,15min,60min">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">任务类型</label>
|
|
|
|
|
<select class="form-select" id="editTaskType" onchange="onEditTaskTypeChange()">
|
|
|
|
|
<option value="daily">每天定时执行</option>
|
|
|
|
|
<option value="once">仅执行一次</option>
|
|
|
|
|
<option value="interval">指定周期循环</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="editTaskIntervalGroup" class="form-group">
|
|
|
|
|
<label class="form-label">轮询间隔(秒)</label>
|
|
|
|
|
<input class="form-input" type="number" id="editTaskInterval" value="300" min="30" max="86400">
|
|
|
|
|
</div>
|
|
|
|
|
<div id="editTaskTimeGroup" class="form-group" style="display: none;">
|
|
|
|
|
<label class="form-label">执行时间</label>
|
|
|
|
|
<input class="form-input" type="time" id="editTaskTime" value="09:00">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-outline" onclick="closeEditTaskModal()">取消</button>
|
|
|
|
|
<button class="btn btn-primary" onclick="saveEditTask()">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
|
|
|
保存
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
|
|
|
<script>
|
|
|
|
|
const API = '/api/v1';
|
|
|
|
|
@ -1055,6 +1224,10 @@
|
|
|
|
|
let currentSearchTerm = '';
|
|
|
|
|
let selectedSymbolsForFetch = [];
|
|
|
|
|
let modalSymbolsData = [];
|
|
|
|
|
let taskSymbolsData = [];
|
|
|
|
|
let selectedTaskSymbols = [];
|
|
|
|
|
let currentTaskId = null;
|
|
|
|
|
let allTasks = [];
|
|
|
|
|
|
|
|
|
|
// Navigation
|
|
|
|
|
function navigateTo(page) {
|
|
|
|
|
@ -2005,6 +2178,481 @@
|
|
|
|
|
loadTasks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Task Management
|
|
|
|
|
function openCreateTaskModal() {
|
|
|
|
|
const dataType = document.getElementById('taskDataType').value;
|
|
|
|
|
taskSymbolsData = Object.entries(currentConfig[dataType] || {}).map(([name, code]) => ({ name, code }));
|
|
|
|
|
|
|
|
|
|
renderTaskSymbols();
|
|
|
|
|
document.getElementById('taskSelectAll').checked = true;
|
|
|
|
|
updateTaskSelectedCount();
|
|
|
|
|
|
|
|
|
|
document.getElementById('createTaskModal').classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeCreateTaskModal() {
|
|
|
|
|
document.getElementById('createTaskModal').classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTaskSymbols(filterText = '') {
|
|
|
|
|
const listContainer = document.getElementById('taskSymbolList');
|
|
|
|
|
const filterLower = filterText.toLowerCase();
|
|
|
|
|
|
|
|
|
|
const filteredSymbols = filterText
|
|
|
|
|
? taskSymbolsData.filter(s =>
|
|
|
|
|
s.name.toLowerCase().includes(filterLower) ||
|
|
|
|
|
s.code.toLowerCase().includes(filterLower)
|
|
|
|
|
)
|
|
|
|
|
: taskSymbolsData;
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
filteredSymbols.forEach(({ name, code }) => {
|
|
|
|
|
html += `
|
|
|
|
|
<label class="symbol-select-item">
|
|
|
|
|
<input type="checkbox" value="${code}" data-name="${name}" checked onchange="updateTaskSelectedCount()">
|
|
|
|
|
<div class="symbol-select-info">
|
|
|
|
|
<div class="symbol-select-name">${name}</div>
|
|
|
|
|
<div class="symbol-select-code">${code}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (filteredSymbols.length === 0) {
|
|
|
|
|
html = `<div class="empty-state" style="padding: 20px;"><p>未找到匹配的合约</p></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listContainer.innerHTML = html;
|
|
|
|
|
updateTaskSelectedCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterTaskSymbols(searchTerm) {
|
|
|
|
|
renderTaskSymbols(searchTerm.trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleTaskSelectAll() {
|
|
|
|
|
const selectAll = document.getElementById('taskSelectAll').checked;
|
|
|
|
|
const checkboxes = document.querySelectorAll('#taskSymbolList input[type="checkbox"]');
|
|
|
|
|
checkboxes.forEach(cb => cb.checked = selectAll);
|
|
|
|
|
updateTaskSelectedCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTaskSelectedCount() {
|
|
|
|
|
const checkboxes = document.querySelectorAll('#taskSymbolList input[type="checkbox"]:checked');
|
|
|
|
|
const count = checkboxes.length;
|
|
|
|
|
const total = document.querySelectorAll('#taskSymbolList input[type="checkbox"]').length;
|
|
|
|
|
document.getElementById('taskSelectedCount').textContent = `已选择 ${count}/${total} 个合约`;
|
|
|
|
|
|
|
|
|
|
const selectAll = document.getElementById('taskSelectAll');
|
|
|
|
|
if (count === total) {
|
|
|
|
|
selectAll.checked = true;
|
|
|
|
|
} else if (count === 0) {
|
|
|
|
|
selectAll.checked = false;
|
|
|
|
|
} else {
|
|
|
|
|
selectAll.checked = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTaskTypeChange() {
|
|
|
|
|
const taskType = document.getElementById('taskType').value;
|
|
|
|
|
document.getElementById('taskIntervalGroup').style.display = taskType === 'interval' ? 'block' : 'none';
|
|
|
|
|
document.getElementById('taskTimeGroup').style.display = (taskType === 'daily' || taskType === 'once') ? 'block' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmCreateTasks() {
|
|
|
|
|
const checkboxes = document.querySelectorAll('#taskSymbolList input[type="checkbox"]:checked');
|
|
|
|
|
|
|
|
|
|
if (checkboxes.length === 0) {
|
|
|
|
|
showToast('请至少选择一个合约', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dataType = document.getElementById('taskDataType').value;
|
|
|
|
|
const periods = document.getElementById('taskPeriods').value;
|
|
|
|
|
const taskType = document.getElementById('taskType').value;
|
|
|
|
|
const interval = document.getElementById('taskInterval').value;
|
|
|
|
|
const time = document.getElementById('taskTime').value;
|
|
|
|
|
|
|
|
|
|
let intervalSeconds = parseInt(interval);
|
|
|
|
|
if (taskType === 'daily') {
|
|
|
|
|
intervalSeconds = 86400; // 24小时
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedSymbols = Array.from(checkboxes).map(cb => cb.value);
|
|
|
|
|
|
|
|
|
|
closeCreateTaskModal();
|
|
|
|
|
|
|
|
|
|
addLog(`批量创建定时任务: ${dataType}, ${selectedSymbols.length}个合约, 类型: ${taskType}`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let createdCount = 0;
|
|
|
|
|
for (const symbol of selectedSymbols) {
|
|
|
|
|
const body = {
|
|
|
|
|
symbol: symbol,
|
|
|
|
|
data_type: dataType,
|
|
|
|
|
periods: periods,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
task_type: taskType
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (taskType === 'daily' || taskType === 'once') {
|
|
|
|
|
body.run_time = time;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await fetch(`${API}/tasks`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
createdCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast(`成功创建 ${createdCount} 个定时任务`, 'success');
|
|
|
|
|
loadTasks();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast(`创建任务失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTasks() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/tasks`);
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
allTasks = data.tasks || [];
|
|
|
|
|
|
|
|
|
|
if (!allTasks.length) {
|
|
|
|
|
document.getElementById('taskTable').innerHTML = '<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><p>暂无定时任务</p></div>';
|
|
|
|
|
document.getElementById('btnBatchDeleteTasks').style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let html = `
|
|
|
|
|
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px;">
|
|
|
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
|
|
|
<input type="checkbox" id="selectAllTasks" onchange="toggleSelectAllTasks()" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
|
|
|
|
|
<span style="font-weight: 500;">全选</span>
|
|
|
|
|
</label>
|
|
|
|
|
<span id="taskSelectedTaskCount" style="color: var(--gray-500); font-size: 14px;">已选择 0 个任务</span>
|
|
|
|
|
</div>
|
|
|
|
|
<table>
|
|
|
|
|
<thead><tr><th><input type="checkbox" style="width: 18px; height: 18px; cursor: pointer;" onchange="toggleSelectAllTasks()"></th><th>ID</th><th>品种</th><th>周期</th><th>类型</th><th>状态</th><th>最后执行</th><th>下次执行</th><th>操作</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
for (const t of allTasks) {
|
|
|
|
|
const statusBadge = t.running
|
|
|
|
|
? '<span class="badge badge-success">运行中</span>'
|
|
|
|
|
: t.enabled
|
|
|
|
|
? '<span class="badge badge-warning">已停止</span>'
|
|
|
|
|
: '<span class="badge badge-gray">已禁用</span>';
|
|
|
|
|
|
|
|
|
|
const taskTypeText = t.task_type === 'daily' ? '每天' : t.task_type === 'once' ? '一次' : '循环';
|
|
|
|
|
const nextRunText = t.next_run ? new Date(t.next_run).toLocaleString() : '-';
|
|
|
|
|
|
|
|
|
|
html += `<tr>
|
|
|
|
|
<td><input type="checkbox" class="task-checkbox" value="${t.id}" onchange="updateSelectedTaskCount()" style="width: 18px; height: 18px; cursor: pointer;"></td>
|
|
|
|
|
<td style="cursor: pointer; color: var(--primary);" onclick="showTaskDetail(${t.id})">${t.id}</td>
|
|
|
|
|
<td><code style="color: var(--primary);">${t.symbol}</code></td>
|
|
|
|
|
<td>${t.periods ? t.periods.join(', ') : '-'}</td>
|
|
|
|
|
<td><span class="badge badge-gray">${taskTypeText}</span></td>
|
|
|
|
|
<td>${statusBadge}</td>
|
|
|
|
|
<td>${t.last_run ? new Date(t.last_run).toLocaleString() : '-'}</td>
|
|
|
|
|
<td>${nextRunText}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="showTaskDetail(${t.id})">详情</button>
|
|
|
|
|
${t.running
|
|
|
|
|
? `<button class="btn btn-warning btn-sm" onclick="stopTask(${t.id})">停止</button>`
|
|
|
|
|
: `<button class="btn btn-success btn-sm" onclick="startTask(${t.id})">启动</button>`
|
|
|
|
|
}
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="deleteTask(${t.id})">删除</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
}
|
|
|
|
|
html += '</tbody></table>';
|
|
|
|
|
document.getElementById('taskTable').innerHTML = html;
|
|
|
|
|
document.getElementById('btnBatchDeleteTasks').style.display = 'inline-flex';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addLog(`加载任务失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSelectAllTasks() {
|
|
|
|
|
const selectAll = document.getElementById('selectAllTasks').checked;
|
|
|
|
|
const checkboxes = document.querySelectorAll('.task-checkbox');
|
|
|
|
|
checkboxes.forEach(cb => cb.checked = selectAll);
|
|
|
|
|
updateSelectedTaskCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateSelectedTaskCount() {
|
|
|
|
|
const checkboxes = document.querySelectorAll('.task-checkbox:checked');
|
|
|
|
|
const count = checkboxes.length;
|
|
|
|
|
document.getElementById('taskSelectedTaskCount').textContent = `已选择 ${count} 个任务`;
|
|
|
|
|
document.getElementById('btnBatchDeleteTasks').disabled = count === 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function batchDeleteTasks() {
|
|
|
|
|
const checkboxes = document.querySelectorAll('.task-checkbox:checked');
|
|
|
|
|
|
|
|
|
|
if (checkboxes.length === 0) {
|
|
|
|
|
showToast('请至少选择一个任务', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!confirm(`确定删除选中的 ${checkboxes.length} 个任务?`)) return;
|
|
|
|
|
|
|
|
|
|
const taskIds = Array.from(checkboxes).map(cb => cb.value);
|
|
|
|
|
let deletedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const id of taskIds) {
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`${API}/tasks/${id}`, { method: 'DELETE' });
|
|
|
|
|
deletedCount++;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`删除任务 ${id} 失败:`, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast(`成功删除 ${deletedCount} 个任务`, 'success');
|
|
|
|
|
loadTasks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showTaskDetail(id) {
|
|
|
|
|
const task = allTasks.find(t => t.id === id);
|
|
|
|
|
if (!task) return;
|
|
|
|
|
|
|
|
|
|
currentTaskId = id;
|
|
|
|
|
|
|
|
|
|
const taskTypeText = task.task_type === 'daily' ? '每天定时执行' : task.task_type === 'once' ? '仅执行一次' : '指定周期循环';
|
|
|
|
|
const statusText = task.running ? '运行中' : task.enabled ? '已停止' : '已禁用';
|
|
|
|
|
|
|
|
|
|
let html = `
|
|
|
|
|
<div style="display: grid; gap: 16px;">
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">任务ID:</div>
|
|
|
|
|
<div>${task.id}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">品种:</div>
|
|
|
|
|
<div><code style="color: var(--primary);">${task.symbol}</code></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">数据类型:</div>
|
|
|
|
|
<div>${task.data_type === 'futures' ? '期货' : '股票'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">采集周期:</div>
|
|
|
|
|
<div>${task.periods ? task.periods.join(', ') : '-'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">任务类型:</div>
|
|
|
|
|
<div><span class="badge badge-info">${taskTypeText}</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">轮询间隔:</div>
|
|
|
|
|
<div>${task.interval_seconds}秒 (${Math.floor(task.interval_seconds / 60)}分钟)</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">状态:</div>
|
|
|
|
|
<div>${statusText}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">创建时间:</div>
|
|
|
|
|
<div>${task.created_at ? new Date(task.created_at).toLocaleString() : '-'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">最后执行:</div>
|
|
|
|
|
<div>${task.last_run ? new Date(task.last_run).toLocaleString() : '尚未执行'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 12px;">
|
|
|
|
|
<div style="color: var(--gray-500); font-weight: 500;">下次执行:</div>
|
|
|
|
|
<div>${task.next_run ? new Date(task.next_run).toLocaleString() : '-'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.getElementById('taskDetailContent').innerHTML = html;
|
|
|
|
|
document.getElementById('btnEditTask').setAttribute('onclick', `closeTaskDetailModal(); editTask(${id})`);
|
|
|
|
|
document.getElementById('taskDetailModal').classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeTaskDetailModal() {
|
|
|
|
|
document.getElementById('taskDetailModal').classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editTask(id) {
|
|
|
|
|
showToast('修改功能开发中...', 'info');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// History Tasks
|
|
|
|
|
function showHistoryTasks() {
|
|
|
|
|
document.getElementById('page-tasks').style.display = 'none';
|
|
|
|
|
document.getElementById('page-history-tasks').style.display = 'block';
|
|
|
|
|
loadHistoryTasks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showActiveTasks() {
|
|
|
|
|
document.getElementById('page-history-tasks').style.display = 'none';
|
|
|
|
|
document.getElementById('page-tasks').style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadHistoryTasks() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/tasks/history`);
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
const historyTasks = data.tasks || [];
|
|
|
|
|
|
|
|
|
|
if (!historyTasks.length) {
|
|
|
|
|
document.getElementById('historyTaskTable').innerHTML = '<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><p>暂无历史任务</p></div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let html = `<table>
|
|
|
|
|
<thead><tr><th>ID</th><th>品种</th><th>周期</th><th>类型</th><th>状态</th><th>最后执行</th><th>完成时间</th><th>操作</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
for (const t of historyTasks) {
|
|
|
|
|
const statusBadge = t.last_status === 'success'
|
|
|
|
|
? '<span class="badge badge-success">已完成</span>'
|
|
|
|
|
: '<span class="badge badge-danger">失败</span>';
|
|
|
|
|
|
|
|
|
|
const taskTypeText = t.task_type === 'daily' ? '每天' : t.task_type === 'once' ? '一次' : '循环';
|
|
|
|
|
const finishedTime = t.updated_at ? new Date(t.updated_at).toLocaleString() : '-';
|
|
|
|
|
|
|
|
|
|
html += `<tr>
|
|
|
|
|
<td style="cursor: pointer; color: var(--primary);" onclick="showTaskDetail(${t.id})">${t.id}</td>
|
|
|
|
|
<td><code style="color: var(--primary);">${t.symbol}</code></td>
|
|
|
|
|
<td>${t.periods ? t.periods.join(', ') : '-'}</td>
|
|
|
|
|
<td><span class="badge badge-gray">${taskTypeText}</span></td>
|
|
|
|
|
<td>${statusBadge}</td>
|
|
|
|
|
<td>${t.last_run ? new Date(t.last_run).toLocaleString() : '-'}</td>
|
|
|
|
|
<td>${finishedTime}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<button class="btn btn-success btn-sm" onclick="rerunTask(${t.id})">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
|
|
|
重新执行
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="openEditTaskModal(${t.id})">修改</button>
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="deleteTask(${t.id})">删除</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
}
|
|
|
|
|
html += '</tbody></table>';
|
|
|
|
|
document.getElementById('historyTaskTable').innerHTML = html;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addLog(`加载历史任务失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function rerunTask(id) {
|
|
|
|
|
if (!confirm('确定重新执行此任务?')) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/tasks/${id}/rerun`, { method: 'POST' });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
showToast('任务已重新开始执行', 'success');
|
|
|
|
|
showActiveTasks();
|
|
|
|
|
loadTasks();
|
|
|
|
|
} else {
|
|
|
|
|
showToast(data.detail || '重新执行失败', 'error');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast(`重新执行失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Edit Task
|
|
|
|
|
function openEditTaskModal(id) {
|
|
|
|
|
const task = allTasks.find(t => t.id === id);
|
|
|
|
|
if (!task) {
|
|
|
|
|
showToast('任务不存在', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('editTaskId').value = id;
|
|
|
|
|
document.getElementById('editTaskSymbol').value = task.symbol;
|
|
|
|
|
document.getElementById('editTaskDataType').value = task.data_type;
|
|
|
|
|
document.getElementById('editTaskPeriods').value = task.periods ? task.periods.join(',') : '';
|
|
|
|
|
document.getElementById('editTaskType').value = task.task_type || 'interval';
|
|
|
|
|
document.getElementById('editTaskInterval').value = task.interval_seconds;
|
|
|
|
|
|
|
|
|
|
// 如果有run_time则填充
|
|
|
|
|
if (task.run_time) {
|
|
|
|
|
document.getElementById('editTaskTime').value = task.run_time;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onEditTaskTypeChange();
|
|
|
|
|
document.getElementById('editTaskModal').classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeEditTaskModal() {
|
|
|
|
|
document.getElementById('editTaskModal').classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onEditTaskTypeChange() {
|
|
|
|
|
const taskType = document.getElementById('editTaskType').value;
|
|
|
|
|
document.getElementById('editTaskIntervalGroup').style.display = taskType === 'interval' ? 'block' : 'none';
|
|
|
|
|
document.getElementById('editTaskTimeGroup').style.display = (taskType === 'daily' || taskType === 'once') ? 'block' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveEditTask() {
|
|
|
|
|
const id = document.getElementById('editTaskId').value;
|
|
|
|
|
const dataType = document.getElementById('editTaskDataType').value;
|
|
|
|
|
const periods = document.getElementById('editTaskPeriods').value;
|
|
|
|
|
const taskType = document.getElementById('editTaskType').value;
|
|
|
|
|
const interval = document.getElementById('editTaskInterval').value;
|
|
|
|
|
const time = document.getElementById('editTaskTime').value;
|
|
|
|
|
|
|
|
|
|
let intervalSeconds = parseInt(interval);
|
|
|
|
|
if (taskType === 'daily') {
|
|
|
|
|
intervalSeconds = 86400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const body = {
|
|
|
|
|
symbol: document.getElementById('editTaskSymbol').value,
|
|
|
|
|
data_type: dataType,
|
|
|
|
|
periods: periods,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
task_type: taskType
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (taskType === 'daily' || taskType === 'once') {
|
|
|
|
|
body.run_time = time;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 先停止旧任务
|
|
|
|
|
await fetch(`${API}/tasks/${id}/stop`, { method: 'POST' });
|
|
|
|
|
|
|
|
|
|
// 删除旧任务
|
|
|
|
|
await fetch(`${API}/tasks/${id}`, { method: 'DELETE' });
|
|
|
|
|
|
|
|
|
|
// 创建新任务
|
|
|
|
|
const res = await fetch(`${API}/tasks`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
showToast('任务修改成功', 'success');
|
|
|
|
|
closeEditTaskModal();
|
|
|
|
|
loadTasks();
|
|
|
|
|
} else {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
showToast(data.detail || '修改失败', 'error');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast(`修改失败: ${e.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache Status
|
|
|
|
|
async function checkCacheStatus() {
|
|
|
|
|
const symbol = document.getElementById('cacheSymbol').value.trim();
|
|
|
|
|
|