feat: admin.html task management tab (TaskConfig + TaskLog timeline)

- Add 任务管理 tab button
- Show all 7 task modules table with enable/disable switch, schedule, params
- Edit dialog for schedule/params editing
- Click module to show el-timeline of recent 30 run logs
- View result data dialog
- Supports toggleModule, saveTaskConfig, loadTaskLogs
This commit is contained in:
Yuzhiran Dev
2026-06-17 20:10:56 +08:00
parent 1ff015e281
commit d3ea832b3b
+201
View File
@@ -35,6 +35,7 @@
<el-button size="default" :type="activeTab === 'searchrankings' ? 'primary' : ''" @click="switchTab('searchrankings')">搜索排名</el-button> <el-button size="default" :type="activeTab === 'searchrankings' ? 'primary' : ''" @click="switchTab('searchrankings')">搜索排名</el-button>
<el-button size="default" :type="activeTab === 'geo' ? 'primary' : ''" @click="switchTab('geo')">GEO 就绪度</el-button> <el-button size="default" :type="activeTab === 'geo' ? 'primary' : ''" @click="switchTab('geo')">GEO 就绪度</el-button>
<el-button size="default" :type="activeTab === 'configitems' ? 'primary' : ''" @click="switchTab('configitems')">配置管理</el-button> <el-button size="default" :type="activeTab === 'configitems' ? 'primary' : ''" @click="switchTab('configitems')">配置管理</el-button>
<el-button size="default" :type="activeTab === 'taskmanagement' ? 'primary' : ''" @click="switchTab('taskmanagement')">任务管理</el-button>
</div> </div>
</div> </div>
@@ -816,6 +817,122 @@
</template> </template>
</template> </template>
</div> </div>
<div v-if="activeTab === 'taskmanagement'">
<div class="toolbar">
<span style="font-size:13px;color:#909399;">定时任务配置与运行记录 — 共 {{ taskConfigs.length }} 个模块</span>
</div>
<div v-if="tcLoading" class="card-loading">加载中...</div>
<template v-else-if="taskConfigs.length === 0">
<div class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconTimer /></el-icon>
<div class="empty-text">暂无任务配置</div>
</div>
</template>
<template v-else>
<el-table :data="taskConfigs" border stripe style="width:100%" size="small"
@row-click="selectModule">
<el-table-column type="index" label="#" width="50"></el-table-column>
<el-table-column prop="module_id" label="模块ID" min-width="160"></el-table-column>
<el-table-column prop="enabled" label="启用" width="70">
<template #default="{ row }">
<el-switch :model-value="row.enabled" @change="(val) => toggleModule(row, val)" @click.stop
style="--el-switch-on-color:#67c23a;"></el-switch>
</template>
</el-table-column>
<el-table-column label="定时" width="160">
<template #default="{ row }">
<code style="font-size:12px;background:#f4f4f5;padding:2px 6px;border-radius:4px;">{{ row.schedule || '-' }}</code>
</template>
</el-table-column>
<el-table-column label="参数" min-width="180">
<template #default="{ row }">
<span style="font-size:12px;color:#606266;word-break:break-all;">
{{ row.params ? JSON.stringify(row.params).slice(0, 60) + (JSON.stringify(row.params).length > 60 ? '...' : '') : '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="上次运行" width="150">
<template #default="{ row }">
<span v-if="row.last_log" style="font-size:12px;">
<el-tag :type="row.last_log.status === 'success' ? 'success' : row.last_log.status === 'failed' ? 'danger' : row.last_log.status === 'running' ? 'warning' : 'info'" size="small" style="margin-right:4px;">
{{ row.last_log.status || '?' }}
</el-tag>
{{ formatDate(row.last_log.started_at) }}
</span>
<span v-else style="color:#c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" @click.stop="showEditConfig(row)">编辑</el-button>
<el-button size="small" @click.stop="selectModule(row); showHistory = true">历史</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="tcEditDialogVisible">
<el-dialog v-model="tcEditDialogVisible" title="编辑任务配置" width="600px" :close-on-click-modal="false">
<el-form :model="tcEditForm" label-width="100px">
<el-form-item label="模块ID"><el-input v-model="tcEditForm.module_id" disabled/></el-form-item>
<el-form-item label="启用"><el-switch v-model="tcEditForm.enabled"/></el-form-item>
<el-form-item label="定时">
<el-input v-model="tcEditForm.schedule" placeholder="cron 表达式,如 0 30 1 * *"/>
<div style="font-size:12px;color:#909399;margin-top:4px;">格式: 分 时 日 月 周 (5段cron)</div>
</el-form-item>
<el-form-item label="参数">
<el-input type="textarea" v-model="tcEditForm.paramsStr" :rows="5" placeholder="JSON 格式参数"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="tcEditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveTaskConfig" :loading="tcEditSaving">保存</el-button>
</template>
</el-dialog>
</div>
<div v-if="selectedModule && showHistory" style="margin-top:20px;">
<h4 style="margin:0 0 12px;font-size:14px;color:#303133;display:flex;align-items:center;gap:8px;">
运行记录:{{ selectedModule.module_id }}
<el-tag size="small" type="info" style="margin-left:4px;">最近 {{ taskLogs.length }} 条</el-tag>
<el-button size="small" style="margin-left:auto;" @click="showHistory = false">收起</el-button>
<el-button size="small" @click="loadTaskLogs(selectedModule)" :loading="tlLoading">刷新</el-button>
</h4>
<div v-if="tlLoading" class="card-loading">加载中...</div>
<template v-else-if="taskLogs.length === 0">
<div class="empty-state" style="padding:20px;">
<el-icon style="font-size:36px;color:#c0c4cc;"><IconDocument /></el-icon>
<div class="empty-text">暂无运行记录</div>
</div>
</template>
<el-timeline v-else>
<el-timeline-item v-for="log in taskLogs" :key="log.id"
:timestamp="formatDate(log.started_at)"
placement="top"
:color="log.status === 'success' ? '#67c23a' : log.status === 'failed' ? '#f56c6c' : log.status === 'running' ? '#e6a23c' : '#c0c4cc'">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<el-tag :type="log.status === 'success' ? 'success' : log.status === 'failed' ? 'danger' : log.status === 'running' ? 'warning' : 'info'" size="small" effect="dark">
{{ log.status }}
</el-tag>
<span v-if="log.task_name" style="font-size:12px;color:#909399;">{{ log.task_name }}</span>
<span v-if="log.duration" style="font-size:12px;color:#909399;">耗时 {{ log.duration }}s</span>
<span v-if="log.triggered_by" style="font-size:12px;color:#909399;">触发: {{ log.triggered_by }}</span>
</div>
<div v-if="log.message" style="margin-top:6px;font-size:13px;color:#303133;background:#f9fafb;padding:8px 12px;border-radius:6px;border:1px solid #ebeef5;white-space:pre-wrap;word-break:break-word;">{{ log.message }}</div>
<div v-if="log.error_trace" style="margin-top:4px;font-size:12px;color:#f56c6c;background:#fef0f0;padding:6px 10px;border-radius:4px;white-space:pre-wrap;word-break:break-word;">{{ log.error_trace }}</div>
<div v-if="log.result_data" style="margin-top:4px;">
<el-button size="small" text @click="showResultData(log)">查看结果数据</el-button>
</div>
</el-timeline-item>
</el-timeline>
</div>
</template>
<el-dialog v-model="resultDialogVisible" title="结果数据" width="600px" :close-on-click-modal="false">
<pre style="max-height:400px;overflow-y:auto;background:#f9fafb;padding:12px;border-radius:6px;font-size:12px;white-space:pre-wrap;word-break:break-word;">{{ resultDialogContent }}</pre>
<template #footer><el-button @click="resultDialogVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</div> </div>
</main> </main>
</div> </div>
@@ -1465,6 +1582,85 @@ const llmConfigs = ref([]);
const formatDate = (dateStr) => { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; const formatDate = (dateStr) => { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); };
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; }; const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
const taskConfigs = ref([]);
const tcLoading = ref(false);
const selectedModule = ref(null);
const showHistory = ref(false);
const taskLogs = ref([]);
const tlLoading = ref(false);
const loadTaskConfigs = async () => {
tcLoading.value = true;
try {
const data = await api.get('/api/admin/task-configs');
taskConfigs.value = Array.isArray(data) ? data : (data.data || []);
} catch (e) { ElMessage.error('加载任务配置失败: ' + e.message); }
finally { tcLoading.value = false; }
};
const toggleModule = async (row, val) => {
try {
await api.put(`/api/admin/task-configs/${row.module_id}`, { enabled: val });
row.enabled = val;
ElMessage.success(val ? '已启用' : '已停用');
} catch (e) { ElMessage.error('操作失败: ' + e.message); }
};
const tcEditDialogVisible = ref(false);
const tcEditSaving = ref(false);
const tcEditForm = reactive({ module_id: '', enabled: true, schedule: '', paramsStr: '{}' });
const showEditConfig = (row) => {
tcEditForm.module_id = row.module_id;
tcEditForm.enabled = row.enabled;
tcEditForm.schedule = row.schedule || '';
tcEditForm.paramsStr = row.params ? JSON.stringify(row.params, null, 2) : '{}';
tcEditDialogVisible.value = true;
};
const saveTaskConfig = async () => {
tcEditSaving.value = true;
try {
let params;
try { params = JSON.parse(tcEditForm.paramsStr); }
catch (e) { ElMessage.warning('参数格式不是有效的 JSON'); tcEditSaving.value = false; return; }
await api.put(`/api/admin/task-configs/${tcEditForm.module_id}`, {
enabled: tcEditForm.enabled,
schedule: tcEditForm.schedule,
params: params,
});
ElMessage.success('保存成功');
tcEditDialogVisible.value = false;
await loadTaskConfigs();
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
finally { tcEditSaving.value = false; }
};
const selectModule = (row) => {
selectedModule.value = row;
showHistory.value = true;
loadTaskLogs(row);
};
const loadTaskLogs = async (row) => {
tlLoading.value = true;
try {
const data = await api.get(`/api/admin/task-configs/history/${row.module_id}?limit=30`);
taskLogs.value = Array.isArray(data) ? data : (data.data || []);
} catch (e) { ElMessage.error('加载运行记录失败: ' + e.message); taskLogs.value = []; }
finally { tlLoading.value = false; }
};
const resultDialogVisible = ref(false);
const resultDialogContent = ref('');
const showResultData = (log) => {
try {
const d = typeof log.result_data === 'string' ? log.result_data : JSON.stringify(log.result_data, null, 2);
resultDialogContent.value = d;
} catch (e) { resultDialogContent.value = String(log.result_data || ''); }
resultDialogVisible.value = true;
};
const tabLoaders = { const tabLoaders = {
llmconfigs: loadLLMConfigs, platformconfigs: loadPlatformConfigs, systemconfigs: loadSystemConfigs, llmconfigs: loadLLMConfigs, platformconfigs: loadPlatformConfigs, systemconfigs: loadSystemConfigs,
users: fetchUsers, users: fetchUsers,
@@ -1473,6 +1669,7 @@ const llmConfigs = ref([]);
searchrankings: loadRankings, searchrankings: loadRankings,
geo: loadGeoData, geo: loadGeoData,
configitems: () => { loadSensitiveWords(); loadContentCleanRules(); }, configitems: () => { loadSensitiveWords(); loadContentCleanRules(); },
taskmanagement: loadTaskConfigs,
}; };
const loadedTabs = new Set([]); const loadedTabs = new Set([]);
@@ -1533,6 +1730,10 @@ const llmConfigs = ref([]);
loadContentCleanRules, showAddCleanRule, editCleanRule, saveCleanRule, deleteCleanRule, loadContentCleanRules, showAddCleanRule, editCleanRule, saveCleanRule, deleteCleanRule,
rankings, rankingStats, rankingsLoading, loadRankings, rankings, rankingStats, rankingsLoading, loadRankings,
geoOverview, geoReadinessScores, geoLoading, geoTracking, loadGeoData, triggerGeoTrack, geoOverview, geoReadinessScores, geoLoading, geoTracking, loadGeoData, triggerGeoTrack,
taskConfigs, tcLoading, selectedModule, showHistory, taskLogs, tlLoading,
loadTaskConfigs, selectModule, toggleModule, loadTaskLogs,
tcEditDialogVisible, tcEditSaving, tcEditForm, showEditConfig, saveTaskConfig,
resultDialogVisible, resultDialogContent, showResultData,
}; };
} }
}); });