982 lines
30 KiB
Vue
982 lines
30 KiB
Vue
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import api from '@/api'
|
||
|
||
const loading = ref(false)
|
||
const tableData = ref([])
|
||
const total = ref(0)
|
||
const query = reactive({
|
||
page: 1,
|
||
size: 20,
|
||
tenant_id: '',
|
||
status: ''
|
||
})
|
||
|
||
// 租户列表
|
||
const tenants = ref([])
|
||
|
||
// 通知渠道和企微应用
|
||
const notifyChannels = ref([])
|
||
const wecomApps = ref([])
|
||
|
||
// 对话框
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('')
|
||
const editingId = ref(null)
|
||
const formRef = ref(null)
|
||
const form = reactive({
|
||
tenant_id: '',
|
||
task_name: '',
|
||
task_type: 'script',
|
||
schedule_type: 'simple',
|
||
time_points: ['09:00'],
|
||
cron_expression: '',
|
||
webhook_url: '',
|
||
webhook_method: 'POST',
|
||
webhook_headers: {},
|
||
script_content: `# 示例脚本
|
||
# 注意:不需要 import!以下模块已内置可直接使用
|
||
# 内置函数: log, print, ai, dingtalk, wecom, http_get, http_post, db_query
|
||
# 内置函数: get_var, set_var, del_var, get_param, get_params
|
||
# 内置函数: get_tenants, get_tenant_config, get_all_tenant_configs, get_secret
|
||
# 内置变量: task_id, tenant_id, trace_id
|
||
# 内置模块: json, re, math, random, hashlib, base64, datetime, date, timedelta
|
||
|
||
log('任务开始执行')
|
||
|
||
# 获取当前时间(datetime 已内置,无需 import)
|
||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
log(f'执行时间: {now}')
|
||
|
||
# 获取参数
|
||
prompt = get_param('prompt', '默认提示词')
|
||
log(f'参数: {prompt}')
|
||
|
||
# 调用 AI 生成内容
|
||
content = ai(prompt, system='你是一个助手')
|
||
log(f'生成内容: {content[:50]}...')
|
||
|
||
# 设置返回值(会自动发送到配置的通知渠道)
|
||
result = {
|
||
'content': content,
|
||
'title': '每日推送'
|
||
}
|
||
|
||
log('任务执行完成')
|
||
`,
|
||
script_timeout: 300,
|
||
input_params: {},
|
||
retry_count: 0,
|
||
retry_interval: 60,
|
||
alert_on_failure: false,
|
||
alert_webhook: '',
|
||
notify_channels: [],
|
||
notify_wecom_app_id: null
|
||
})
|
||
|
||
const rules = {
|
||
task_name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
|
||
}
|
||
|
||
// 日志对话框
|
||
const logsDialogVisible = ref(false)
|
||
const logsLoading = ref(false)
|
||
const logsData = ref([])
|
||
const logsTaskId = ref(null)
|
||
const logsQuery = reactive({ page: 1, size: 10 })
|
||
const logsTotal = ref(0)
|
||
|
||
// SDK 文档对话框
|
||
const sdkDialogVisible = ref(false)
|
||
const sdkDocs = ref({ functions: [], variables: [], libraries: [] })
|
||
|
||
// 测试脚本对话框
|
||
const testDialogVisible = ref(false)
|
||
const testLoading = ref(false)
|
||
const testResult = ref(null)
|
||
const testParams = ref('{}')
|
||
|
||
// 参数编辑
|
||
const paramsDialogVisible = ref(false)
|
||
const paramsJson = ref('{}')
|
||
|
||
// 计算属性
|
||
const timePointsStr = computed({
|
||
get: () => form.time_points.join(', '),
|
||
set: (val) => {
|
||
form.time_points = val.split(',').map(s => s.trim()).filter(s => s)
|
||
}
|
||
})
|
||
|
||
async function fetchList() {
|
||
loading.value = true
|
||
try {
|
||
const params = { ...query }
|
||
if (!params.tenant_id) delete params.tenant_id
|
||
if (params.status === '') delete params.status
|
||
|
||
const res = await api.get('/api/scheduled-tasks', { params })
|
||
tableData.value = res.data.items || []
|
||
total.value = res.data.total || 0
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function fetchTenants() {
|
||
try {
|
||
const res = await api.get('/api/tenants', { params: { size: 1000 } })
|
||
tenants.value = res.data.items || []
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
async function fetchNotifyChannels(tenantId) {
|
||
try {
|
||
const params = tenantId ? { tenant_id: tenantId } : {}
|
||
const res = await api.get('/api/notification-channels', { params })
|
||
notifyChannels.value = res.data.items || []
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
async function fetchWecomApps(tenantId) {
|
||
try {
|
||
const params = tenantId ? { tenant_id: tenantId } : {}
|
||
const res = await api.get('/api/tenant-wechat-apps', { params })
|
||
wecomApps.value = res.data.items || []
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
query.page = 1
|
||
fetchList()
|
||
}
|
||
|
||
function handlePageChange(page) {
|
||
query.page = page
|
||
fetchList()
|
||
}
|
||
|
||
function handleCreate() {
|
||
editingId.value = null
|
||
dialogTitle.value = '新建任务'
|
||
Object.assign(form, {
|
||
tenant_id: '',
|
||
task_name: '',
|
||
task_type: 'script',
|
||
schedule_type: 'simple',
|
||
time_points: ['09:00'],
|
||
cron_expression: '',
|
||
webhook_url: '',
|
||
webhook_method: 'POST',
|
||
webhook_headers: {},
|
||
script_content: `# 示例脚本(无需 import,模块已内置)
|
||
log('任务开始执行')
|
||
|
||
# 获取当前时间
|
||
now = datetime.now().strftime('%H:%M:%S')
|
||
log(f'执行时间: {now}')
|
||
|
||
# 获取参数
|
||
prompt = get_param('prompt', '默认提示词')
|
||
|
||
# 调用 AI 生成内容
|
||
content = ai(prompt, system='你是一个助手')
|
||
|
||
# 设置返回值(自动发送到通知渠道)
|
||
result = {'content': content, 'title': '推送通知'}
|
||
|
||
log('任务执行完成')
|
||
`,
|
||
script_timeout: 300,
|
||
input_params: {},
|
||
retry_count: 0,
|
||
retry_interval: 60,
|
||
alert_on_failure: false,
|
||
alert_webhook: '',
|
||
notify_channels: [],
|
||
notify_wecom_app_id: null
|
||
})
|
||
notifyChannels.value = []
|
||
wecomApps.value = []
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function handleEdit(row) {
|
||
editingId.value = row.id
|
||
dialogTitle.value = '编辑任务'
|
||
|
||
try {
|
||
const res = await api.get(`/api/scheduled-tasks/${row.id}`)
|
||
const task = res.data
|
||
Object.assign(form, {
|
||
tenant_id: task.tenant_id || '',
|
||
task_name: task.task_name,
|
||
task_type: task.task_type,
|
||
schedule_type: task.schedule_type,
|
||
time_points: task.time_points || ['09:00'],
|
||
cron_expression: task.cron_expression || '',
|
||
webhook_url: task.webhook_url || '',
|
||
webhook_method: task.webhook_method || 'POST',
|
||
webhook_headers: task.webhook_headers || {},
|
||
script_content: task.script_content || '',
|
||
script_timeout: task.script_timeout || 300,
|
||
input_params: task.input_params || {},
|
||
retry_count: task.retry_count || 0,
|
||
retry_interval: task.retry_interval || 60,
|
||
alert_on_failure: task.alert_on_failure || false,
|
||
alert_webhook: task.alert_webhook || '',
|
||
notify_channels: task.notify_channels || [],
|
||
notify_wecom_app_id: task.notify_wecom_app_id || null
|
||
})
|
||
// 加载该租户的通知渠道和企微应用
|
||
if (task.tenant_id) {
|
||
await fetchNotifyChannels(task.tenant_id)
|
||
await fetchWecomApps(task.tenant_id)
|
||
}
|
||
dialogVisible.value = true
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
await formRef.value.validate()
|
||
|
||
const data = {
|
||
...form,
|
||
time_points: form.schedule_type === 'simple' ? form.time_points : null,
|
||
cron_expression: form.schedule_type === 'cron' ? form.cron_expression : null
|
||
}
|
||
|
||
try {
|
||
if (editingId.value) {
|
||
await api.put(`/api/scheduled-tasks/${editingId.value}`, data)
|
||
ElMessage.success('更新成功')
|
||
} else {
|
||
await api.post('/api/scheduled-tasks', data)
|
||
ElMessage.success('创建成功')
|
||
}
|
||
dialogVisible.value = false
|
||
fetchList()
|
||
} catch (e) {
|
||
// 错误已在拦截器处理
|
||
}
|
||
}
|
||
|
||
async function handleDelete(row) {
|
||
await ElMessageBox.confirm(`确定删除任务 "${row.task_name}" 吗?`, '提示', {
|
||
type: 'warning'
|
||
})
|
||
|
||
try {
|
||
await api.delete(`/api/scheduled-tasks/${row.id}`)
|
||
ElMessage.success('删除成功')
|
||
fetchList()
|
||
} catch (e) {
|
||
// 错误已在拦截器处理
|
||
}
|
||
}
|
||
|
||
async function handleToggle(row) {
|
||
try {
|
||
const res = await api.post(`/api/scheduled-tasks/${row.id}/toggle`)
|
||
ElMessage.success(res.data.status === 1 ? '已启用' : '已禁用')
|
||
fetchList()
|
||
} catch (e) {
|
||
// 错误已在拦截器处理
|
||
}
|
||
}
|
||
|
||
async function handleRunNow(row) {
|
||
try {
|
||
ElMessage.info('任务执行中...')
|
||
const res = await api.post(`/api/scheduled-tasks/${row.id}/run`)
|
||
if (res.data.success) {
|
||
ElMessage.success('执行成功')
|
||
} else {
|
||
ElMessage.error(`执行失败: ${res.data.error}`)
|
||
}
|
||
fetchList()
|
||
} catch (e) {
|
||
// 错误已在拦截器处理
|
||
}
|
||
}
|
||
|
||
async function handleViewLogs(row) {
|
||
logsTaskId.value = row.id
|
||
logsQuery.page = 1
|
||
logsDialogVisible.value = true
|
||
await fetchLogs()
|
||
}
|
||
|
||
async function fetchLogs() {
|
||
logsLoading.value = true
|
||
try {
|
||
const res = await api.get(`/api/scheduled-tasks/${logsTaskId.value}/logs`, {
|
||
params: logsQuery
|
||
})
|
||
logsData.value = res.data.items || []
|
||
logsTotal.value = res.data.total || 0
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
logsLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleLogsPageChange(page) {
|
||
logsQuery.page = page
|
||
fetchLogs()
|
||
}
|
||
|
||
async function handleShowSdkDocs() {
|
||
try {
|
||
const res = await api.get('/api/scheduled-tasks/sdk-docs')
|
||
sdkDocs.value = res.data
|
||
sdkDialogVisible.value = true
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
async function handleTestScript() {
|
||
testLoading.value = true
|
||
testResult.value = null
|
||
|
||
try {
|
||
let params = {}
|
||
try {
|
||
params = JSON.parse(testParams.value)
|
||
} catch (e) {
|
||
ElMessage.error('参数 JSON 格式错误')
|
||
testLoading.value = false
|
||
return
|
||
}
|
||
|
||
const res = await api.post('/api/scheduled-tasks/test-script', {
|
||
script_content: form.script_content,
|
||
tenant_id: form.tenant_id || null,
|
||
params
|
||
})
|
||
testResult.value = res.data
|
||
} catch (e) {
|
||
testResult.value = { success: false, error: e.message }
|
||
} finally {
|
||
testLoading.value = false
|
||
}
|
||
}
|
||
|
||
function showTestDialog() {
|
||
testParams.value = JSON.stringify(form.input_params || {}, null, 2)
|
||
testResult.value = null
|
||
testDialogVisible.value = true
|
||
}
|
||
|
||
function showParamsDialog() {
|
||
paramsJson.value = JSON.stringify(form.input_params || {}, null, 2)
|
||
paramsDialogVisible.value = true
|
||
}
|
||
|
||
function saveParams() {
|
||
try {
|
||
form.input_params = JSON.parse(paramsJson.value)
|
||
paramsDialogVisible.value = false
|
||
ElMessage.success('参数已保存')
|
||
} catch (e) {
|
||
ElMessage.error('JSON 格式错误')
|
||
}
|
||
}
|
||
|
||
function getTenantName(tenantId) {
|
||
const tenant = tenants.value.find(t => t.code === tenantId)
|
||
return tenant ? tenant.name : tenantId
|
||
}
|
||
|
||
function formatSchedule(row) {
|
||
if (row.schedule_type === 'cron') {
|
||
return `CRON: ${row.cron_expression}`
|
||
}
|
||
return (row.time_points || []).join(', ')
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchList()
|
||
fetchTenants()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-container">
|
||
<div class="page-header">
|
||
<div class="title">定时任务</div>
|
||
<div class="actions">
|
||
<el-button @click="handleShowSdkDocs">
|
||
<el-icon><Document /></el-icon>
|
||
SDK 文档
|
||
</el-button>
|
||
<el-button type="primary" @click="handleCreate">
|
||
<el-icon><Plus /></el-icon>
|
||
新建任务
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选 -->
|
||
<div class="filter-bar">
|
||
<el-select v-model="query.tenant_id" placeholder="全部租户" clearable style="width: 180px">
|
||
<el-option label="全局任务" value="" />
|
||
<el-option v-for="t in tenants" :key="t.code" :label="t.name" :value="t.code" />
|
||
</el-select>
|
||
<el-select v-model="query.status" placeholder="全部状态" clearable style="width: 120px">
|
||
<el-option label="启用" :value="1" />
|
||
<el-option label="禁用" :value="0" />
|
||
</el-select>
|
||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||
</div>
|
||
|
||
<!-- 表格 -->
|
||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||
<el-table-column prop="id" label="ID" width="60" />
|
||
<el-table-column prop="task_name" label="任务名称" min-width="150" />
|
||
<el-table-column label="租户" width="120">
|
||
<template #default="{ row }">
|
||
<span v-if="row.tenant_id">{{ getTenantName(row.tenant_id) }}</span>
|
||
<el-tag v-else size="small" type="info">全局</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.task_type === 'script' ? 'success' : 'warning'" size="small">
|
||
{{ row.task_type === 'script' ? '脚本' : 'Webhook' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="执行时间" min-width="150">
|
||
<template #default="{ row }">
|
||
<span style="font-family: monospace; font-size: 12px">{{ formatSchedule(row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="最后执行" width="180">
|
||
<template #default="{ row }">
|
||
<div v-if="row.last_run_at">
|
||
<div style="font-size: 12px">{{ row.last_run_at }}</div>
|
||
<el-tag :type="row.last_run_status === 'success' ? 'success' : 'danger'" size="small">
|
||
{{ row.last_run_status === 'success' ? '成功' : '失败' }}
|
||
</el-tag>
|
||
</div>
|
||
<span v-else style="color: #999">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="280" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||
<el-button type="success" link size="small" @click="handleRunNow(row)">执行</el-button>
|
||
<el-button type="info" link size="small" @click="handleViewLogs(row)">日志</el-button>
|
||
<el-button :type="row.status === 1 ? 'warning' : 'success'" link size="small" @click="handleToggle(row)">
|
||
{{ row.status === 1 ? '禁用' : '启用' }}
|
||
</el-button>
|
||
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div style="margin-top: 20px; display: flex; justify-content: flex-end">
|
||
<el-pagination
|
||
v-model:current-page="query.page"
|
||
:page-size="query.size"
|
||
:total="total"
|
||
layout="total, prev, pager, next"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 编辑对话框 -->
|
||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="900px" top="5vh">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="任务名称" prop="task_name">
|
||
<el-input v-model="form.task_name" placeholder="如: 每日销售播报" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="所属租户">
|
||
<el-select v-model="form.tenant_id" placeholder="全局任务" clearable style="width: 100%">
|
||
<el-option label="全局任务" value="" />
|
||
<el-option v-for="t in tenants" :key="t.code" :label="t.name" :value="t.code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="任务类型">
|
||
<el-radio-group v-model="form.task_type">
|
||
<el-radio value="script">Python 脚本</el-radio>
|
||
<el-radio value="webhook">Webhook</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="调度方式">
|
||
<el-radio-group v-model="form.schedule_type">
|
||
<el-radio value="simple">指定时间点</el-radio>
|
||
<el-radio value="cron">CRON 表达式</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 调度配置 -->
|
||
<el-form-item v-if="form.schedule_type === 'simple'" label="执行时间">
|
||
<el-input v-model="timePointsStr" placeholder="09:00, 12:00, 18:00" />
|
||
<div class="form-tip">多个时间点用逗号分隔,格式: HH:MM</div>
|
||
</el-form-item>
|
||
<el-form-item v-else label="CRON表达式">
|
||
<el-input v-model="form.cron_expression" placeholder="0 9 * * *" />
|
||
<div class="form-tip">格式: 分 时 日 月 周,如: 0 9 * * * (每天9点)</div>
|
||
</el-form-item>
|
||
|
||
<!-- Webhook 配置 -->
|
||
<template v-if="form.task_type === 'webhook'">
|
||
<el-form-item label="Webhook URL">
|
||
<el-input v-model="form.webhook_url" placeholder="https://example.com/webhook" />
|
||
</el-form-item>
|
||
<el-form-item label="请求方法">
|
||
<el-select v-model="form.webhook_method" style="width: 120px">
|
||
<el-option value="GET" />
|
||
<el-option value="POST" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- 脚本配置 -->
|
||
<template v-if="form.task_type === 'script'">
|
||
<el-form-item label="脚本内容">
|
||
<div class="script-editor-container">
|
||
<div class="script-toolbar">
|
||
<el-button size="small" @click="handleShowSdkDocs">SDK 文档</el-button>
|
||
<el-button size="small" type="primary" @click="showTestDialog">测试运行</el-button>
|
||
</div>
|
||
<el-input
|
||
v-model="form.script_content"
|
||
type="textarea"
|
||
:rows="15"
|
||
placeholder="Python 脚本"
|
||
class="script-textarea"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="超时时间">
|
||
<el-input-number v-model="form.script_timeout" :min="10" :max="3600" />
|
||
<span style="margin-left: 8px; color: #999">秒</span>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- 任务参数 -->
|
||
<el-form-item label="任务参数">
|
||
<div class="params-preview">
|
||
<code>{{ JSON.stringify(form.input_params) }}</code>
|
||
<el-button size="small" @click="showParamsDialog">编辑参数</el-button>
|
||
</div>
|
||
<div class="form-tip">脚本中使用 get_param('key') 获取</div>
|
||
</el-form-item>
|
||
|
||
<!-- 高级配置 -->
|
||
<el-divider>高级配置</el-divider>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="失败重试">
|
||
<el-input-number v-model="form.retry_count" :min="0" :max="10" />
|
||
<span style="margin-left: 8px; color: #999">次</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="重试间隔">
|
||
<el-input-number v-model="form.retry_interval" :min="10" :max="3600" />
|
||
<span style="margin-left: 8px; color: #999">秒</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="失败告警">
|
||
<el-switch v-model="form.alert_on_failure" />
|
||
</el-form-item>
|
||
<el-form-item v-if="form.alert_on_failure" label="告警地址">
|
||
<el-input v-model="form.alert_webhook" placeholder="钉钉/企微机器人 Webhook 地址" />
|
||
</el-form-item>
|
||
|
||
<!-- 通知配置 -->
|
||
<el-divider>通知配置(脚本设置 result 变量后自动发送)</el-divider>
|
||
|
||
<el-form-item label="通知渠道">
|
||
<div v-if="!form.tenant_id" class="form-tip" style="color: #e6a23c">
|
||
请先选择租户,再配置通知渠道
|
||
</div>
|
||
<template v-else>
|
||
<el-select
|
||
v-model="form.notify_channels"
|
||
multiple
|
||
placeholder="选择通知渠道(可多选)"
|
||
style="width: 100%"
|
||
@focus="fetchNotifyChannels(form.tenant_id)"
|
||
>
|
||
<el-option
|
||
v-for="ch in notifyChannels"
|
||
:key="ch.id"
|
||
:label="`${ch.channel_name} (${ch.channel_type === 'dingtalk_bot' ? '钉钉' : '企微'})`"
|
||
:value="ch.id"
|
||
/>
|
||
</el-select>
|
||
<div class="form-tip">
|
||
脚本中设置 <code>result = {'content': '内容', 'title': '标题'}</code> 变量,执行后自动发送
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="企微应用">
|
||
<div v-if="!form.tenant_id" class="form-tip" style="color: #e6a23c">
|
||
请先选择租户
|
||
</div>
|
||
<template v-else>
|
||
<el-select
|
||
v-model="form.notify_wecom_app_id"
|
||
placeholder="选择企微应用(可选)"
|
||
clearable
|
||
style="width: 100%"
|
||
@focus="fetchWecomApps(form.tenant_id)"
|
||
>
|
||
<el-option
|
||
v-for="app in wecomApps"
|
||
:key="app.id"
|
||
:label="app.app_name"
|
||
:value="app.id"
|
||
/>
|
||
</el-select>
|
||
<div class="form-tip">发送到企微应用的全员消息</div>
|
||
</template>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 日志对话框 -->
|
||
<el-dialog v-model="logsDialogVisible" title="执行日志" width="900px">
|
||
<el-table v-loading="logsLoading" :data="logsData" max-height="400">
|
||
<el-table-column label="执行时间" width="180">
|
||
<template #default="{ row }">{{ row.started_at }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'success' ? 'success' : row.status === 'running' ? 'warning' : 'danger'" size="small">
|
||
{{ row.status === 'success' ? '成功' : row.status === 'running' ? '运行中' : '失败' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="耗时" width="100">
|
||
<template #default="{ row }">
|
||
<span v-if="row.duration_ms">{{ row.duration_ms }}ms</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输出" min-width="200">
|
||
<template #default="{ row }">
|
||
<el-popover v-if="row.output" trigger="hover" width="400">
|
||
<template #reference>
|
||
<span class="output-preview">{{ row.output.slice(0, 50) }}...</span>
|
||
</template>
|
||
<pre class="output-full">{{ row.output }}</pre>
|
||
</el-popover>
|
||
<span v-else style="color: #999">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="错误" min-width="200">
|
||
<template #default="{ row }">
|
||
<el-popover v-if="row.error" trigger="hover" width="400">
|
||
<template #reference>
|
||
<span class="error-preview">{{ row.error.slice(0, 50) }}...</span>
|
||
</template>
|
||
<pre class="error-full">{{ row.error }}</pre>
|
||
</el-popover>
|
||
<span v-else style="color: #999">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
|
||
<el-pagination
|
||
v-model:current-page="logsQuery.page"
|
||
:page-size="logsQuery.size"
|
||
:total="logsTotal"
|
||
layout="total, prev, pager, next"
|
||
@current-change="handleLogsPageChange"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- SDK 文档对话框 -->
|
||
<el-dialog v-model="sdkDialogVisible" title="SDK 文档" width="800px">
|
||
<el-tabs>
|
||
<el-tab-pane label="内置函数">
|
||
<div class="sdk-docs">
|
||
<div v-for="fn in sdkDocs.functions" :key="fn.name" class="sdk-item">
|
||
<div class="sdk-name">{{ fn.name }}</div>
|
||
<code class="sdk-signature">{{ fn.signature }}</code>
|
||
<div class="sdk-desc">{{ fn.description }}</div>
|
||
<code class="sdk-example">{{ fn.example }}</code>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="上下文变量">
|
||
<div class="sdk-docs">
|
||
<div v-for="v in sdkDocs.variables" :key="v.name" class="sdk-item">
|
||
<div class="sdk-name">{{ v.name }}</div>
|
||
<div class="sdk-desc">{{ v.description }}</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="可用库">
|
||
<div class="sdk-docs">
|
||
<div v-for="lib in sdkDocs.libraries" :key="lib.name" class="sdk-item">
|
||
<div class="sdk-name">{{ lib.name }}</div>
|
||
<div class="sdk-desc">{{ lib.description }}</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-dialog>
|
||
|
||
<!-- 测试脚本对话框 -->
|
||
<el-dialog v-model="testDialogVisible" title="测试脚本" width="700px">
|
||
<el-form label-width="80px">
|
||
<el-form-item label="测试参数">
|
||
<el-input v-model="testParams" type="textarea" :rows="5" placeholder='{"key": "value"}' />
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div style="margin-bottom: 16px">
|
||
<el-button type="primary" :loading="testLoading" @click="handleTestScript">运行测试</el-button>
|
||
</div>
|
||
|
||
<div v-if="testResult" class="test-result">
|
||
<el-alert :type="testResult.success ? 'success' : 'error'" :closable="false">
|
||
{{ testResult.success ? '执行成功' : '执行失败' }}
|
||
<span v-if="testResult.duration_ms"> ({{ testResult.duration_ms }}ms)</span>
|
||
</el-alert>
|
||
|
||
<div v-if="testResult.output" class="result-section">
|
||
<div class="result-label">输出:</div>
|
||
<pre class="result-content">{{ testResult.output }}</pre>
|
||
</div>
|
||
|
||
<div v-if="testResult.error" class="result-section">
|
||
<div class="result-label">错误:</div>
|
||
<pre class="result-content error">{{ testResult.error }}</pre>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 参数编辑对话框 -->
|
||
<el-dialog v-model="paramsDialogVisible" title="编辑任务参数" width="600px">
|
||
<el-input v-model="paramsJson" type="textarea" :rows="15" placeholder='{"prompt": "...", "history_days": 30}' />
|
||
<div class="form-tip" style="margin-top: 8px">
|
||
JSON 格式,脚本中使用 get_param('key') 获取
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="paramsDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="saveParams">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.page-header .title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.page-header .actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-tip {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.form-tip code {
|
||
background: #f5f7fa;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.script-editor-container {
|
||
width: 100%;
|
||
}
|
||
|
||
.script-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.script-textarea :deep(textarea) {
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.params-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.params-preview code {
|
||
flex: 1;
|
||
padding: 8px 12px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
max-width: 400px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.output-preview, .error-preview {
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.error-preview {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.output-full, .error-full {
|
||
max-height: 300px;
|
||
overflow: auto;
|
||
font-size: 12px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.error-full {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.sdk-docs {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.sdk-item {
|
||
padding: 12px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.sdk-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.sdk-name {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.sdk-signature {
|
||
display: block;
|
||
margin: 4px 0;
|
||
padding: 4px 8px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sdk-desc {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.sdk-example {
|
||
display: block;
|
||
padding: 4px 8px;
|
||
background: #fef0f0;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.test-result {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.result-section {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.result-label {
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: #606266;
|
||
}
|
||
|
||
.result-content {
|
||
padding: 12px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
max-height: 200px;
|
||
overflow: auto;
|
||
font-size: 12px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.result-content.error {
|
||
background: #fef0f0;
|
||
color: #f56c6c;
|
||
}
|
||
</style>
|