- 新增重试和失败告警功能(支持自动重试N次,失败后钉钉/企微通知) - 新增密钥管理(安全存储API Key等敏感信息) - 新增脚本模板库(预置常用脚本模板) - 新增脚本版本管理(自动保存历史版本,支持回滚) - 新增执行统计(成功率、平均耗时、7日趋势) - SDK 新增多租户遍历能力(get_tenants/get_tenant_config/get_all_tenant_configs) - SDK 新增密钥读取方法(get_secret)
This commit is contained in:
@@ -36,9 +36,30 @@ const form = reactive({
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
script_content: '',
|
||||
is_enabled: true
|
||||
is_enabled: true,
|
||||
// 重试和告警
|
||||
retry_count: 0,
|
||||
retry_interval: 60,
|
||||
alert_on_failure: false,
|
||||
alert_webhook: ''
|
||||
})
|
||||
|
||||
// 模板
|
||||
const templateList = ref([])
|
||||
const templateDialogVisible = ref(false)
|
||||
|
||||
// 版本
|
||||
const versionsDialogVisible = ref(false)
|
||||
const versionsLoading = ref(false)
|
||||
const versionsList = ref([])
|
||||
const currentVersionTaskId = ref(null)
|
||||
|
||||
// 统计
|
||||
const statsDialogVisible = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const statsData = ref(null)
|
||||
const currentStatsTaskId = ref(null)
|
||||
|
||||
// 时间选择器
|
||||
const newTimePoint = ref('')
|
||||
|
||||
@@ -149,7 +170,11 @@ function handleCreate() {
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
script_content: '',
|
||||
is_enabled: true
|
||||
is_enabled: true,
|
||||
retry_count: 0,
|
||||
retry_interval: 60,
|
||||
alert_on_failure: false,
|
||||
alert_webhook: ''
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
testResult.value = null
|
||||
@@ -170,7 +195,11 @@ function handleEdit(row) {
|
||||
webhook_url: row.webhook_url || '',
|
||||
input_params: row.input_params ? JSON.stringify(row.input_params, null, 2) : '',
|
||||
script_content: row.script_content || '',
|
||||
is_enabled: row.is_enabled
|
||||
is_enabled: row.is_enabled,
|
||||
retry_count: row.retry_count || 0,
|
||||
retry_interval: row.retry_interval || 60,
|
||||
alert_on_failure: !!row.alert_on_failure,
|
||||
alert_webhook: row.alert_webhook || ''
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
testResult.value = null
|
||||
@@ -237,7 +266,11 @@ async function handleSubmit() {
|
||||
webhook_url: form.execution_type === 'webhook' ? form.webhook_url : null,
|
||||
script_content: form.execution_type === 'script' ? form.script_content : null,
|
||||
input_params: inputParams,
|
||||
is_enabled: form.is_enabled
|
||||
is_enabled: form.is_enabled,
|
||||
retry_count: form.retry_count,
|
||||
retry_interval: form.retry_interval,
|
||||
alert_on_failure: form.alert_on_failure,
|
||||
alert_webhook: form.alert_webhook || null
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -379,6 +412,86 @@ function selectTenant(code) {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// ============ 模板功能 ============
|
||||
async function fetchTemplates() {
|
||||
try {
|
||||
const res = await api.get('/api/scheduled-tasks/templates')
|
||||
templateList.value = res.data.items || []
|
||||
} catch (e) {
|
||||
console.error('获取模板列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectTemplate() {
|
||||
fetchTemplates()
|
||||
templateDialogVisible.value = true
|
||||
}
|
||||
|
||||
function applyTemplate(template) {
|
||||
form.script_content = template.script_content
|
||||
templateDialogVisible.value = false
|
||||
ElMessage.success(`已应用模板:${template.name}`)
|
||||
}
|
||||
|
||||
// ============ 版本功能 ============
|
||||
async function handleViewVersions(row) {
|
||||
currentVersionTaskId.value = row.id
|
||||
versionsLoading.value = true
|
||||
versionsDialogVisible.value = true
|
||||
|
||||
try {
|
||||
const res = await api.get(`/api/scheduled-tasks/${row.id}/versions`)
|
||||
versionsList.value = res.data.items || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
versionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRollback(version) {
|
||||
await ElMessageBox.confirm(`确定回滚到版本 ${version} 吗?`, '版本回滚', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await api.post(`/api/scheduled-tasks/${currentVersionTaskId.value}/versions/${version}/rollback`)
|
||||
ElMessage.success(res.data.message)
|
||||
versionsDialogVisible.value = false
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewVersionContent(version) {
|
||||
try {
|
||||
const res = await api.get(`/api/scheduled-tasks/${currentVersionTaskId.value}/versions/${version}`)
|
||||
ElMessageBox.alert(res.data.script_content, `版本 ${version} 脚本内容`, {
|
||||
customClass: 'version-content-dialog',
|
||||
dangerouslyUseHTMLString: false
|
||||
})
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 统计功能 ============
|
||||
async function handleViewStats(row) {
|
||||
currentStatsTaskId.value = row.id
|
||||
statsLoading.value = true
|
||||
statsDialogVisible.value = true
|
||||
|
||||
try {
|
||||
const res = await api.get(`/api/scheduled-tasks/${row.id}/stats`)
|
||||
statsData.value = res.data
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTenants()
|
||||
fetchList()
|
||||
@@ -485,9 +598,20 @@ onMounted(() => {
|
||||
{{ row.is_enabled ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button type="info" link size="small" @click="handleViewLogs(row)">
|
||||
<el-icon><Document /></el-icon>
|
||||
日志
|
||||
</el-button>
|
||||
<el-button type="primary" link size="small" @click="handleViewStats(row)">
|
||||
统计
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.execution_type === 'script'"
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
@click="handleViewVersions(row)"
|
||||
>
|
||||
版本
|
||||
</el-button>
|
||||
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
@@ -613,9 +737,14 @@ onMounted(() => {
|
||||
<template v-if="form.execution_type === 'script'">
|
||||
<el-form-item label="脚本内容">
|
||||
<div class="script-editor-header">
|
||||
<el-button type="primary" link size="small" @click="handleShowSdkDocs">
|
||||
查看 SDK 文档
|
||||
</el-button>
|
||||
<div>
|
||||
<el-button type="primary" link size="small" @click="handleShowSdkDocs">
|
||||
查看 SDK 文档
|
||||
</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleSelectTemplate">
|
||||
选择模板
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
@@ -665,6 +794,35 @@ log('执行完成')"
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider content-position="left">高级配置</el-divider>
|
||||
|
||||
<el-form-item label="失败重试">
|
||||
<div class="retry-config">
|
||||
<span>失败后重试</span>
|
||||
<el-input-number v-model="form.retry_count" :min="0" :max="10" size="small" style="width: 100px" />
|
||||
<span>次,间隔</span>
|
||||
<el-input-number v-model="form.retry_interval" :min="10" :max="3600" :step="10" size="small" style="width: 100px" />
|
||||
<span>秒</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="失败告警">
|
||||
<el-switch v-model="form.alert_on_failure" />
|
||||
<span v-if="form.alert_on_failure" style="margin-left: 16px; color: #909399; font-size: 12px">
|
||||
执行失败时发送通知
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.alert_on_failure" label="告警地址">
|
||||
<el-input
|
||||
v-model="form.alert_webhook"
|
||||
placeholder="钉钉或企微机器人 Webhook URL"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
填写钉钉或企微机器人 Webhook 地址,任务执行失败时会自动发送告警通知
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="form.is_enabled" />
|
||||
</el-form-item>
|
||||
@@ -748,6 +906,109 @@ log('执行完成')"
|
||||
<el-button @click="logsDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 模板选择对话框 -->
|
||||
<el-dialog v-model="templateDialogVisible" title="选择脚本模板" width="600px">
|
||||
<div v-if="templateList.length === 0" class="empty-tip">
|
||||
暂无可用模板
|
||||
</div>
|
||||
<div v-else class="template-list">
|
||||
<div
|
||||
v-for="template in templateList"
|
||||
:key="template.id"
|
||||
class="template-item"
|
||||
@click="applyTemplate(template)"
|
||||
>
|
||||
<div class="template-name">{{ template.name }}</div>
|
||||
<div class="template-desc">{{ template.description || '暂无描述' }}</div>
|
||||
<el-tag v-if="template.category" size="small" type="info">{{ template.category }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 版本管理对话框 -->
|
||||
<el-dialog v-model="versionsDialogVisible" title="脚本版本历史" width="700px">
|
||||
<el-table v-loading="versionsLoading" :data="versionsList" style="width: 100%">
|
||||
<el-table-column prop="version" label="版本" width="80" />
|
||||
<el-table-column prop="change_note" label="变更说明">
|
||||
<template #default="{ row }">
|
||||
{{ row.change_note || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_by" label="创建人" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.created_by || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleViewVersionContent(row.version)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleRollback(row.version)">
|
||||
回滚
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="versionsDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 统计对话框 -->
|
||||
<el-dialog v-model="statsDialogVisible" title="执行统计" width="600px">
|
||||
<div v-loading="statsLoading">
|
||||
<template v-if="statsData">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ statsData.total }}</div>
|
||||
<div class="stats-label">总执行次数</div>
|
||||
</div>
|
||||
<div class="stats-item success">
|
||||
<div class="stats-value">{{ statsData.success }}</div>
|
||||
<div class="stats-label">成功</div>
|
||||
</div>
|
||||
<div class="stats-item danger">
|
||||
<div class="stats-value">{{ statsData.failed }}</div>
|
||||
<div class="stats-label">失败</div>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ statsData.success_rate }}%</div>
|
||||
<div class="stats-label">成功率</div>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div class="stats-value">{{ statsData.avg_duration }}s</div>
|
||||
<div class="stats-label">平均耗时</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="statsData.trend && statsData.trend.length" class="stats-trend">
|
||||
<h4>最近7天趋势</h4>
|
||||
<div class="trend-chart">
|
||||
<div v-for="item in statsData.trend" :key="item.date" class="trend-day">
|
||||
<div class="trend-bars">
|
||||
<div class="trend-bar success" :style="{ height: (item.success / (item.success + item.failed) * 100) + '%' }"></div>
|
||||
<div class="trend-bar danger" :style="{ height: (item.failed / (item.success + item.failed) * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="trend-date">{{ item.date?.slice(5) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="statsDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -897,4 +1158,142 @@ log('执行完成')"
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 重试配置 */
|
||||
.retry-config {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 模板列表 */
|
||||
.template-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
border-color: #409eff;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 统计 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stats-item.success {
|
||||
background: #f0f9eb;
|
||||
}
|
||||
|
||||
.stats-item.danger {
|
||||
background: #fef0f0;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-item.success .stats-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.stats-item.danger .stats-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stats-trend h4 {
|
||||
margin: 0 0 12px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.trend-day {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
width: 100%;
|
||||
transition: height 0.3s;
|
||||
}
|
||||
|
||||
.trend-bar.success {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
.trend-bar.danger {
|
||||
background: #f56c6c;
|
||||
}
|
||||
|
||||
.trend-date {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user