变更项:1) async_job 触发文案把每周数字改为中文星期(周一到周日),消除星期显示歧义。2) async_job 时间序列化改为 yyyy-MM-dd HH:mm:ss,去掉 ISO 格式中的 T。3) 插件定时任务页面统一使用 formatDateTime 渲染下次执行、上次执行与日志触发时间,前端兜底去除 T。4) 补充中文注释说明显示层与调度层格式化意图。
357 lines
17 KiB
HTML
357 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}插件定时任务 - 机器人管理后台{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-shell">
|
|
<div class="page-hero">
|
|
<div class="page-hero-copy">
|
|
<div class="page-eyebrow">Plugin Scheduler</div>
|
|
<h1>插件定时任务</h1>
|
|
<p>统一管理插件的定时动作配置(如秀人群发),支持按群范围执行、立即触发与执行日志追踪。</p>
|
|
</div>
|
|
<div class="page-hero-actions">
|
|
<el-button type="success" @click="loadSchedules">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-card shadow="hover">
|
|
<el-table :data="schedules" style="width:100%" v-loading="loading">
|
|
<el-table-column prop="id" label="ID" width="70"></el-table-column>
|
|
<el-table-column prop="plugin_name" label="插件" min-width="120"></el-table-column>
|
|
<el-table-column prop="action_name" label="动作" min-width="140"></el-table-column>
|
|
<el-table-column prop="trigger_text" label="调度" min-width="170"></el-table-column>
|
|
<el-table-column prop="target_scope" label="目标范围" width="150"></el-table-column>
|
|
<el-table-column label="启用" width="90" align="center">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="scope.row.enabled ? 'success' : 'info'">{% raw %}{{ scope.row.enabled ? '是' : '否' }}{% endraw %}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="下次执行" min-width="165">
|
|
<template slot-scope="scope">
|
|
{% raw %}{{ formatDateTime(scope.row.next_run_at) }}{% endraw %}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="上次执行" min-width="165">
|
|
<template slot-scope="scope">
|
|
{% raw %}{{ formatDateTime(scope.row.last_run_at) }}{% endraw %}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="最近结果" width="120">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="statusTag(scope.row.last_status)">{% raw %}{{ scope.row.last_status || 'never' }}{% endraw %}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" min-width="280">
|
|
<template slot-scope="scope">
|
|
<div class="action-row">
|
|
<el-button size="mini" type="primary" plain @click="openEdit(scope.row)">编辑</el-button>
|
|
<el-button size="mini" type="success" plain @click="triggerNow(scope.row)">立即触发</el-button>
|
|
<el-button size="mini" type="text" @click="viewLogs(scope.row)">日志</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<el-dialog title="编辑插件调度" :visible.sync="editDialogVisible" width="560px">
|
|
<el-form :model="editForm" label-width="110px">
|
|
<el-form-item label="动作名称"><el-input v-model="editForm.action_name"></el-input></el-form-item>
|
|
<el-form-item label="动作说明"><el-input v-model="editForm.description"></el-input></el-form-item>
|
|
<el-form-item label="启用"><el-switch v-model="editForm.enabled"></el-switch></el-form-item>
|
|
<el-form-item label="触发类型">
|
|
<el-select v-model="editForm.trigger_type">
|
|
<el-option label="每天固定时间" value="at_times"></el-option>
|
|
<el-option label="固定间隔(秒)" value="every_seconds"></el-option>
|
|
<el-option label="每周固定时间" value="every_weekday_time"></el-option>
|
|
<el-option label="每周固定时间(兼容)" value="every_week_time"></el-option>
|
|
<el-option label="每月最后一天" value="every_month_last_day_time"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.trigger_type === 'at_times'" label="时间列表">
|
|
<el-input v-model="editForm.time_list_text" placeholder="09:00,21:00"></el-input>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.trigger_type === 'every_seconds'" label="间隔秒">
|
|
<el-input-number v-model="editForm.seconds" :min="1"></el-input-number>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.trigger_type === 'every_weekday_time' || editForm.trigger_type === 'every_week_time'" label="星期">
|
|
<el-select v-model="editForm.weekday">
|
|
<el-option label="周一" :value="0"></el-option>
|
|
<el-option label="周二" :value="1"></el-option>
|
|
<el-option label="周三" :value="2"></el-option>
|
|
<el-option label="周四" :value="3"></el-option>
|
|
<el-option label="周五" :value="4"></el-option>
|
|
<el-option label="周六" :value="5"></el-option>
|
|
<el-option label="周日" :value="6"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.trigger_type === 'every_weekday_time' || editForm.trigger_type === 'every_week_time' || editForm.trigger_type === 'every_month_last_day_time'" label="时间">
|
|
<el-input v-model="editForm.time_str" placeholder="10:00"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="目标范围">
|
|
<el-select v-model="editForm.target_scope">
|
|
<el-option label="所有开启群" value="all_enabled_groups"></el-option>
|
|
<el-option label="群白名单" value="group_whitelist"></el-option>
|
|
<el-option label="单个群" value="single_group"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.target_scope === 'group_whitelist'" label="群ID列表">
|
|
<el-select
|
|
v-model="editForm.selected_group_ids"
|
|
multiple
|
|
filterable
|
|
collapse-tags
|
|
placeholder="请选择群(可多选)"
|
|
style="width: 100%;">
|
|
<el-option
|
|
v-for="group in groupOptions"
|
|
:key="group.wxid"
|
|
:label="group.name"
|
|
:value="group.wxid">
|
|
</el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item v-if="editForm.target_scope === 'single_group'" label="群ID">
|
|
<el-select
|
|
v-model="editForm.selected_single_group_id"
|
|
filterable
|
|
placeholder="请选择群"
|
|
style="width: 100%;">
|
|
<el-option
|
|
v-for="group in groupOptions"
|
|
:key="group.wxid"
|
|
:label="group.name"
|
|
:value="group.wxid">
|
|
</el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="Payload(JSON)">
|
|
<el-input type="textarea" :rows="4" v-model="editForm.payload_text"></el-input>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div slot="footer">
|
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveEdit">保存</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="调度日志" :visible.sync="logsDialogVisible" width="860px">
|
|
<el-table :data="logs" style="width:100%">
|
|
<el-table-column label="触发时间" width="180">
|
|
<template slot-scope="scope">
|
|
{% raw %}{{ formatDateTime(scope.row.triggered_at) }}{% endraw %}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="100"></el-table-column>
|
|
<el-table-column prop="summary" label="摘要" min-width="220"></el-table-column>
|
|
<el-table-column label="详情">
|
|
<template slot-scope="scope">
|
|
<pre class="detail-pre">{% raw %}{{ JSON.stringify(scope.row.detail_json || {}, null, 2) }}{% endraw %}</pre>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-dialog>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
mixins: [baseApp],
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
schedules: [],
|
|
groupOptions: [],
|
|
editDialogVisible: false,
|
|
logsDialogVisible: false,
|
|
logs: [],
|
|
currentId: 0,
|
|
editForm: {
|
|
action_name: '',
|
|
description: '',
|
|
enabled: false,
|
|
trigger_type: 'at_times',
|
|
time_list_text: '',
|
|
seconds: 60,
|
|
weekday: 0,
|
|
time_str: '09:00',
|
|
target_scope: 'all_enabled_groups',
|
|
group_ids_text: '',
|
|
single_group_id: '',
|
|
selected_group_ids: [],
|
|
selected_single_group_id: '',
|
|
payload_text: '{}'
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadGroupOptions()
|
|
this.loadSchedules()
|
|
},
|
|
methods: {
|
|
statusTag(status) {
|
|
if (status === 'success') return 'success'
|
|
if (status === 'failed') return 'danger'
|
|
if (status === 'running') return 'warning'
|
|
return 'info'
|
|
},
|
|
formatDateTime(value) {
|
|
// 统一清洗时间展示:去掉 ISO 'T',并兼容字符串与日期对象。
|
|
if (!value) return ''
|
|
if (typeof value === 'string') {
|
|
return value.replace('T', ' ').slice(0, 19)
|
|
}
|
|
try {
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return String(value)
|
|
const yyyy = date.getFullYear()
|
|
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
|
const dd = String(date.getDate()).padStart(2, '0')
|
|
const hh = String(date.getHours()).padStart(2, '0')
|
|
const mi = String(date.getMinutes()).padStart(2, '0')
|
|
const ss = String(date.getSeconds()).padStart(2, '0')
|
|
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
|
} catch (e) {
|
|
return String(value)
|
|
}
|
|
},
|
|
async loadSchedules() {
|
|
this.loading = true
|
|
try {
|
|
const resp = await axios.get('/plugin_schedules/api/schedules')
|
|
if (resp.data.success) this.schedules = resp.data.data || []
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
async loadGroupOptions() {
|
|
try {
|
|
const resp = await axios.get('/contacts/api/groups')
|
|
if (resp.data && resp.data.success) {
|
|
const groups = (resp.data.data && resp.data.data.groups) || {}
|
|
this.groupOptions = Object.entries(groups).map(([wxid, name]) => ({
|
|
wxid: String(wxid),
|
|
name: String(name || wxid)
|
|
}))
|
|
}
|
|
} catch (e) {
|
|
this.groupOptions = []
|
|
}
|
|
},
|
|
openEdit(row) {
|
|
this.currentId = row.id
|
|
const trigger = row.trigger_config || {}
|
|
const target = row.target_config || {}
|
|
this.editForm.action_name = row.action_name || ''
|
|
this.editForm.description = row.description || ''
|
|
this.editForm.enabled = !!row.enabled
|
|
this.editForm.trigger_type = row.trigger_type || 'at_times'
|
|
this.editForm.time_list_text = (trigger.time_list || []).join(',')
|
|
this.editForm.seconds = Number(trigger.seconds || 60)
|
|
this.editForm.weekday = Number(trigger.weekday || 0)
|
|
this.editForm.time_str = trigger.time_str || '09:00'
|
|
this.editForm.target_scope = row.target_scope || 'all_enabled_groups'
|
|
this.editForm.group_ids_text = (target.group_ids || []).join(',')
|
|
this.editForm.single_group_id = target.group_id || ''
|
|
this.editForm.selected_group_ids = Array.isArray(target.group_ids) ? target.group_ids.map(x => String(x)) : []
|
|
this.editForm.selected_single_group_id = target.group_id ? String(target.group_id) : ''
|
|
this.editForm.payload_text = JSON.stringify(row.payload || {}, null, 2)
|
|
this.editDialogVisible = true
|
|
},
|
|
buildTriggerConfig() {
|
|
if (this.editForm.trigger_type === 'at_times') {
|
|
return {
|
|
time_list: String(this.editForm.time_list_text || '').split(',').map(x => x.trim()).filter(Boolean)
|
|
}
|
|
}
|
|
if (this.editForm.trigger_type === 'every_seconds') {
|
|
return { seconds: Number(this.editForm.seconds || 60) }
|
|
}
|
|
if (this.editForm.trigger_type === 'every_weekday_time' || this.editForm.trigger_type === 'every_week_time') {
|
|
return { weekday: Number(this.editForm.weekday || 0), time_str: String(this.editForm.time_str || '09:00') }
|
|
}
|
|
if (this.editForm.trigger_type === 'every_month_last_day_time') {
|
|
return { time_str: String(this.editForm.time_str || '09:00') }
|
|
}
|
|
return {}
|
|
},
|
|
buildTargetConfig() {
|
|
if (this.editForm.target_scope === 'single_group') {
|
|
const groupId = String(this.editForm.selected_single_group_id || this.editForm.single_group_id || '').trim()
|
|
return { group_id: groupId }
|
|
}
|
|
if (this.editForm.target_scope === 'group_whitelist') {
|
|
const ids = (this.editForm.selected_group_ids || []).map(x => String(x).trim()).filter(Boolean)
|
|
if (ids.length > 0) {
|
|
return { group_ids: ids }
|
|
}
|
|
return {
|
|
group_ids: String(this.editForm.group_ids_text || '').split(',').map(x => x.trim()).filter(Boolean)
|
|
}
|
|
}
|
|
return {}
|
|
},
|
|
async saveEdit() {
|
|
let payloadObj = {}
|
|
try {
|
|
payloadObj = JSON.parse(this.editForm.payload_text || '{}')
|
|
} catch (e) {
|
|
this.$message.error('Payload 不是合法 JSON')
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
action_name: this.editForm.action_name,
|
|
description: this.editForm.description,
|
|
enabled: this.editForm.enabled,
|
|
trigger_type: this.editForm.trigger_type,
|
|
trigger_config: this.buildTriggerConfig(),
|
|
target_scope: this.editForm.target_scope,
|
|
target_config: this.buildTargetConfig(),
|
|
payload: payloadObj
|
|
}
|
|
const resp = await axios.put(`/plugin_schedules/api/schedules/${this.currentId}`, payload)
|
|
if (resp.data.success) {
|
|
this.$message.success('保存成功')
|
|
this.editDialogVisible = false
|
|
await this.loadSchedules()
|
|
} else {
|
|
this.$message.error(resp.data.message || '保存失败')
|
|
}
|
|
},
|
|
async triggerNow(row) {
|
|
const resp = await axios.post(`/plugin_schedules/api/schedules/${row.id}/trigger`)
|
|
if (resp.data.success) {
|
|
this.$message.success(resp.data.message || '触发成功')
|
|
} else {
|
|
this.$message.warning(resp.data.message || '触发失败')
|
|
}
|
|
await this.loadSchedules()
|
|
},
|
|
async viewLogs(row) {
|
|
const resp = await axios.get(`/plugin_schedules/api/schedules/${row.id}/logs`)
|
|
if (resp.data.success) {
|
|
this.logs = resp.data.data || []
|
|
this.logsDialogVisible = true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block styles %}
|
|
<style>
|
|
.page-shell{display:flex;flex-direction:column;gap:16px}
|
|
.page-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}
|
|
.page-hero-actions{display:flex;align-items:center;gap:12px}
|
|
.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}
|
|
.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}
|
|
.page-hero-copy p{color:#64748b;font-size:14px}
|
|
.action-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
.detail-pre{white-space:pre-wrap;word-break:break-word;background:rgba(248,250,252,.85);border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:10px;color:#334155}
|
|
</style>
|
|
{% endblock %}
|