Files
abot/admin/dashboard/templates/message_push_management.html
T

821 lines
33 KiB
HTML

{% extends "base.html" %}
{% block title %}消息推送管理 - 机器人管理后台{% endblock %}
{% block content %}
<div>
<el-row {% raw %}:gutter="20" {% endraw %}>
<el-col {% raw %}:span="24" {% endraw %}>
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>消息推送管理</span>
<el-button
type="primary"
size="small"
style="float: right; margin-left: 10px;"
{% raw %}@click="refreshTasks" {% endraw %}>
刷新数据
</el-button>
<el-button
type="success"
size="small"
style="float: right;"
{% raw %}@click="showCreateTaskDialog" {% endraw %}>
新建任务
</el-button>
</div>
<!-- 统计卡片 -->
<el-row {% raw %}:gutter="20" {% endraw %} style="margin-bottom: 20px;">
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">总任务数</div>
<div class="stat-value">{% raw %}{{ statistics.total }}{% endraw %}</div>
</el-card>
</el-col>
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">已排期</div>
<div class="stat-value">{% raw %}{{ statistics.scheduled }}{% endraw %}</div>
</el-card>
</el-col>
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">已暂停</div>
<div class="stat-value">{% raw %}{{ statistics.paused }}{% endraw %}</div>
</el-card>
</el-col>
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">已完成</div>
<div class="stat-value">{% raw %}{{ statistics.completed }}{% endraw %}</div>
</el-card>
</el-col>
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">失败</div>
<div class="stat-value">{% raw %}{{ statistics.failed }}{% endraw %}</div>
</el-card>
</el-col>
<el-col {% raw %}:span="4" {% endraw %}>
<el-card shadow="hover" class="stat-card">
<div class="stat-title">今日任务</div>
<div class="stat-value">{% raw %}{{ statistics.today }}{% endraw %}</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索栏 -->
<el-form {% raw %}:inline="true" :model="searchForm" {% endraw %} class="search-form">
<el-form-item label="状态">
<el-select {% raw %}v-model="searchForm.status" {% endraw %} placeholder="全部状态" clearable>
<el-option label="草稿" value="draft"></el-option>
<el-option label="已排期" value="scheduled"></el-option>
<el-option label="运行中" value="running"></el-option>
<el-option label="已暂停" value="paused"></el-option>
<el-option label="已完成" value="completed"></el-option>
<el-option label="失败" value="failed"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
{% raw %}v-model="searchForm.timeRange" {% endraw %}
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" {% raw %}@click="searchTasks" {% endraw %}>搜索</el-button>
<el-button {% raw %}@click="resetSearch" {% endraw %}>重置</el-button>
</el-form-item>
</el-form>
<!-- 批量操作工具栏 -->
<div class="batch-toolbar" {% raw %}v-if="selectedTasks.length > 0" {% endraw %}>
<el-button-group>
<el-button size="small" type="primary" {% raw %}@click="batchPause" {% endraw %}>批量暂停</el-button>
<el-button size="small" type="success" {% raw %}@click="batchResume" {% endraw %}>批量恢复</el-button>
<el-button size="small" type="danger" {% raw %}@click="batchDelete" {% endraw %}>批量删除</el-button>
</el-button-group>
<span class="selected-count">已选择 {% raw %}{{ selectedTasks.length }}{% endraw %} 项</span>
</div>
<!-- 任务列表 -->
<el-table
{% raw %}:data="taskList" {% endraw %}
style="width: 100%"
border
{% raw %}v-loading="loading"
@selection-change="handleSelectionChange" {% endraw %}>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column type="index" width="50"></el-table-column>
<el-table-column prop="task_id" label="任务ID" width="180"></el-table-column>
<el-table-column prop="name" label="任务名称"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag {% raw %}:type="getStatusType(scope.row.status)" {% endraw %}>
{% raw %}{{ getStatusText(scope.row.status) }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="schedule_time" label="计划时间" width="180">
<template slot-scope="scope">
{% raw %}{{ formatDateTime(scope.row.schedule_time) }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template slot-scope="scope">
{% raw %}{{ formatDateTime(scope.row.created_at) }}{% endraw %}
</template>
</el-table-column>
<el-table-column label="进度" width="200">
<template slot-scope="scope">
<el-progress
{% raw %}:percentage="scope.row.progress || 0"
:status="getProgressStatus(scope.row.status)" {% endraw %}>
</el-progress>
</template>
</el-table-column>
<el-table-column label="操作" width="300">
<template slot-scope="scope">
<el-button
{% raw %}v-if="scope.row.status === 'draft'" {% endraw %}
size="mini"
type="success"
{% raw %}@click="auditTask(scope.row)" {% endraw %}>
审核
</el-button>
<el-button
size="mini"
type="primary"
{% raw %}@click="previewTask(scope.row)" {% endraw %}>
预览
</el-button>
<el-button
size="mini"
type="warning"
{% raw %}@click="editTask(scope.row)" {% endraw %}>
编辑
</el-button>
<el-button
{% raw %}v-if="scope.row.status === 'scheduled'" {% endraw %}
size="mini"
type="info"
{% raw %}@click="pauseTask(scope.row)" {% endraw %}>
暂停
</el-button>
<el-button
{% raw %}v-if="scope.row.status === 'paused'" {% endraw %}
size="mini"
type="success"
{% raw %}@click="resumeTask(scope.row)" {% endraw %}>
恢复
</el-button>
<el-button
size="mini"
type="danger"
{% raw %}@click="deleteTask(scope.row)" {% endraw %}>
删除
</el-button>
<el-button
size="mini"
type="text"
{% raw %}@click="viewLogs(scope.row)" {% endraw %}>
日志
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
{% raw %}@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total" {% endraw %}>
</el-pagination>
</div>
</el-card>
</el-col>
</el-row>
<!-- 新建/编辑任务对话框 -->
<el-dialog {% raw %}:title="dialogTitle" :visible.sync="taskDialogVisible" {% endraw %} width="60%">
<el-form {% raw %}:model="taskForm" :rules="taskRules" ref="taskForm" {% endraw %} label-width="100px">
<el-form-item label="任务类型" prop="schedule_type">
<el-radio-group {% raw %}v-model="taskForm.schedule_type" {% endraw %}>
<el-radio label="once">单次任务</el-radio>
<el-radio label="recurring">重复任务</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="任务名称" prop="name">
<el-input {% raw %}v-model="taskForm.name" {% endraw %}></el-input>
</el-form-item>
<el-form-item label="计划时间" prop="schedule_time">
<el-date-picker
{% raw %}v-model="taskForm.schedule_time" {% endraw %}
type="datetime"
placeholder="选择日期时间"
value-format="yyyy-MM-dd HH:mm:ss">
</el-date-picker>
</el-form-item>
<el-form-item
label="重复间隔"
prop="recurring_interval"
{% raw %}v-if="taskForm.schedule_type === 'recurring'" {% endraw %}>
<el-select {% raw %}v-model="taskForm.recurring_interval" {% endraw %} placeholder="请选择重复间隔">
<el-option label="每天" value="daily"></el-option>
<el-option label="每周" value="weekly"></el-option>
<el-option label="每月" value="monthly"></el-option>
</el-select>
</el-form-item>
<el-form-item
label="重复结束时间"
prop="recurring_end"
{% raw %}v-if="taskForm.schedule_type === 'recurring'" {% endraw %}>
<el-date-picker
{% raw %}v-model="taskForm.recurring_end" {% endraw %}
type="datetime"
placeholder="选择结束时间"
value-format="yyyy-MM-dd HH:mm:ss">
</el-date-picker>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select {% raw %}v-model="taskForm.priority" {% endraw %} placeholder="请选择优先级">
<el-option label="高" value="high"></el-option>
<el-option label="中" value="medium"></el-option>
<el-option label="低" value="low"></el-option>
</el-select>
</el-form-item>
<el-form-item label="目标群组" prop="groups">
<el-select
{% raw %}v-model="taskForm.groups" {% endraw %}
multiple
filterable
placeholder="请选择群组">
<el-option
{% raw %}v-for="group in groupList"
:key="group.wxid"
:label="group.name"
:value="group.wxid" {% endraw %}>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="消息内容">
<el-tabs {% raw %}v-model="activeContentTab" {% endraw %}>
<el-tab-pane label="文本" name="text">
<el-input
type="textarea"
{% raw %}v-model="taskForm.content_text" {% endraw %}
:rows="4"
placeholder="请输入文本内容">
</el-input>
</el-tab-pane>
<el-tab-pane label="图片" name="image">
<el-upload
class="upload-demo"
action="/message_push/api/upload"
{% raw %}:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
:on-preview="handleImagePreview"
:file-list="imageList" {% endraw %}>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过2MB</div>
</el-upload>
<el-dialog :visible.sync="previewVisible" append-to-body>
<img width="100%" :src="previewUrl" alt="Preview">
</el-dialog>
</el-tab-pane>
<el-tab-pane label="链接" name="link">
<el-form-item label="链接地址">
<el-input {% raw %}v-model="taskForm.content_link" {% endraw %}></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="小程序" name="miniprogram">
<el-form-item label="标题">
<el-input {% raw %}v-model="taskForm.content_miniprogram.title" {% endraw %}></el-input>
</el-form-item>
<el-form-item label="路径">
<el-input {% raw %}v-model="taskForm.content_miniprogram.path" {% endraw %}></el-input>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form-item>
<el-form-item label="预览接收人">
<el-select
{% raw %}v-model="taskForm.preview_recipients" {% endraw %}
multiple
filterable
placeholder="请选择预览接收人">
<el-option
{% raw %}v-for="user in userList"
:key="user.wxid"
:label="user.name"
:value="user.wxid" {% endraw %}>
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button {% raw %}@click="taskDialogVisible = false" {% endraw %}>取 消</el-button>
<el-button type="primary" {% raw %}@click="saveTask" {% endraw %}>确 定</el-button>
</div>
</el-dialog>
<!-- 任务日志对话框 -->
<el-dialog title="任务日志" {% raw %}:visible.sync="logsDialogVisible" {% endraw %} width="70%">
<el-table {% raw %}:data="taskLogs" {% endraw %} border style="width: 100%">
<el-table-column prop="timestamp" label="时间" width="180">
<template slot-scope="scope">
{% raw %}{{ formatDateTime(scope.row.timestamp) }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="action" label="操作" width="120"></el-table-column>
<el-table-column prop="operator_id" label="操作人" width="120"></el-table-column>
<el-table-column prop="changes" label="变更内容">
<template slot-scope="scope">
<pre>{% raw %}{{ JSON.stringify(scope.row.changes, 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 {
currentView: '16', // 设置当前视图为消息推送管理
loading: false,
searchForm: {
status: '',
timeRange: []
},
taskList: [],
currentPage: 1,
pageSize: 10,
total: 0,
taskDialogVisible: false,
logsDialogVisible: false,
dialogTitle: '新建任务',
activeContentTab: 'text',
taskForm: {
name: '',
schedule_time: '',
schedule_type: 'once',
groups: [],
content_text: '',
content_image: '',
content_link: '',
content_miniprogram: {
title: '',
path: ''
},
preview_recipients: []
},
taskRules: {
name: [
{ required: true, message: '请输入任务名称', trigger: 'blur' }
],
schedule_time: [
{ required: true, message: '请选择计划时间', trigger: 'change' }
],
schedule_type: [
{ required: true, message: '请选择任务类型', trigger: 'change' }
],
groups: [
{ required: true, message: '请选择目标群组', trigger: 'change' }
]
},
groupList: [],
userList: [],
taskLogs: [],
selectedTasks: [],
statistics: {
total: 0,
scheduled: 0,
paused: 0,
completed: 0,
failed: 0,
today: 0
},
imageList: [],
previewVisible: false,
previewUrl: ''
}
},
mounted() {
this.loadTasks();
this.loadGroups();
this.loadUsers();
this.loadStatistics();
},
methods: {
// 加载任务列表
async loadTasks() {
this.loading = true;
try {
const params = {
page: this.currentPage,
limit: this.pageSize,
status: this.searchForm.status,
start_time: this.searchForm.timeRange[0],
end_time: this.searchForm.timeRange[1]
};
const response = await axios.get('/message_push/api/tasks', { params });
if (response.data.success) {
this.taskList = response.data.data.tasks;
this.total = response.data.data.total;
}
} catch (error) {
this.$message.error('加载任务列表失败');
}
this.loading = false;
},
// 加载统计数据
async loadStatistics() {
try {
const response = await axios.get('/message_push/api/statistics');
if (response.data.success) {
this.statistics = response.data.data;
}
} catch (error) {
this.$message.error('加载统计数据失败');
}
},
// 加载群组列表
async loadGroups() {
try {
const response = await axios.get('/contacts/api/groups');
if (response.data.success) {
const groups = response.data.data.groups;
this.groupList = Object.entries(groups).map(([wxid, name]) => ({
wxid,
name
}));
}
} catch (error) {
this.$message.error('加载群组列表失败');
}
},
// 加载用户列表
async loadUsers() {
try {
const response = await axios.get('/contacts/api/personal');
if (response.data.success) {
const users = response.data.data.personal;
this.userList = Object.entries(users).map(([wxid, name]) => ({
wxid,
name
}));
}
} catch (error) {
this.$message.error('加载用户列表失败');
}
},
// 搜索任务
searchTasks() {
this.currentPage = 1;
this.loadTasks();
},
// 重置搜索
resetSearch() {
this.searchForm = {
status: '',
timeRange: []
};
this.searchTasks();
},
// 显示新建任务对话框
showCreateTaskDialog() {
this.dialogTitle = '新建任务';
this.taskForm = {
name: '',
schedule_time: '',
schedule_type: 'once',
groups: [],
content_text: '',
content_image: '',
content_link: '',
content_miniprogram: {
title: '',
path: ''
},
preview_recipients: []
};
this.imageList = [];
this.taskDialogVisible = true;
},
// 保存任务
async saveTask() {
this.$refs.taskForm.validate(async (valid) => {
if (valid) {
try {
const response = await axios.post('/message_push/api/tasks', this.taskForm);
if (response.data.success) {
this.$message.success('保存任务成功');
this.taskDialogVisible = false;
this.loadTasks();
this.loadStatistics();
}
} catch (error) {
this.$message.error('保存任务失败');
}
}
});
},
// 预览任务
async previewTask(task) {
try {
const response = await axios.post(`/message_push/api/tasks/${task.task_id}/preview`);
if (response.data.success) {
this.$message.success('预览已发送');
}
} catch (error) {
this.$message.error('发送预览失败');
}
},
// 编辑任务
editTask(task) {
this.dialogTitle = '编辑任务';
this.taskForm = { ...task };
if (task.content_image) {
// 编辑时显示图片
const fileName = task.content_image.split('/').pop();
this.imageList = [{
name: '已上传图片',
url: `/static/uploads/${fileName}` // 显示时使用相对路径
}];
}
this.taskDialogVisible = true;
},
// 暂停任务
async pauseTask(task) {
try {
const response = await axios.post(`/message_push/api/tasks/${task.task_id}/pause`);
if (response.data.success) {
this.$message.success('任务已暂停');
this.loadTasks();
this.loadStatistics();
}
} catch (error) {
this.$message.error('暂停任务失败');
}
},
// 恢复任务
async resumeTask(task) {
try {
const response = await axios.post(`/message_push/api/tasks/${task.task_id}/resume`);
if (response.data.success) {
this.$message.success('任务已恢复');
this.loadTasks();
this.loadStatistics();
}
} catch (error) {
this.$message.error('恢复任务失败');
}
},
// 删除任务
deleteTask(task) {
this.$confirm('确认删除该任务吗?', '提示', {
type: 'warning'
}).then(async () => {
try {
const response = await axios.delete(`/message_push/api/tasks/${task.task_id}`);
if (response.data.success) {
this.$message.success('任务已删除');
this.loadTasks();
this.loadStatistics();
}
} catch (error) {
this.$message.error('删除任务失败');
}
});
},
// 查看日志
async viewLogs(task) {
try {
const response = await axios.get(`/message_push/api/tasks/${task.task_id}/logs`);
if (response.data.success) {
this.taskLogs = response.data.data.logs;
this.logsDialogVisible = true;
}
} catch (error) {
this.$message.error('获取任务日志失败');
}
},
// 处理分页
handleSizeChange(size) {
this.pageSize = size;
this.loadTasks();
},
handleCurrentChange(page) {
this.currentPage = page;
this.loadTasks();
},
// 处理表格选择
handleSelectionChange(selection) {
this.selectedTasks = selection;
},
// 批量操作
async batchPause() {
try {
const promises = this.selectedTasks.map(task =>
axios.post(`/message_push/api/tasks/${task.task_id}/pause`)
);
await Promise.all(promises);
this.$message.success('批量暂停成功');
this.loadTasks();
this.loadStatistics();
} catch (error) {
this.$message.error('批量暂停失败');
}
},
async batchResume() {
try {
const promises = this.selectedTasks.map(task =>
axios.post(`/message_push/api/tasks/${task.task_id}/resume`)
);
await Promise.all(promises);
this.$message.success('批量恢复成功');
this.loadTasks();
this.loadStatistics();
} catch (error) {
this.$message.error('批量恢复失败');
}
},
batchDelete() {
this.$confirm(`确认删除选中的 ${this.selectedTasks.length} 个任务吗?`, '提示', {
type: 'warning'
}).then(async () => {
try {
const promises = this.selectedTasks.map(task =>
axios.delete(`/message_push/api/tasks/${task.task_id}`)
);
await Promise.all(promises);
this.$message.success('批量删除成功');
this.loadTasks();
this.loadStatistics();
} catch (error) {
this.$message.error('批量删除失败');
}
});
},
// 图片上传相关
handleImageSuccess(response, file) {
if (response.success) {
this.taskForm.content_image = response.data.url; // 存储绝对路径
// 显示时使用文件名
const fileName = file.name;
this.imageList = [{
name: fileName,
url: `/static/uploads/${response.data.url.split('/').pop()}` // 显示时使用相对路径
}];
} else {
this.$message.error('上传失败');
}
},
beforeImageUpload(file) {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
this.$message.error('只能上传图片文件!');
return false;
}
if (!isLt2M) {
this.$message.error('图片大小不能超过 2MB!');
return false;
}
return true;
},
handleImagePreview(file) {
// 预览时使用相对路径
const fileName = file.url.split('/').pop();
this.previewUrl = `/static/uploads/${fileName}`;
this.previewVisible = true;
},
// 工具函数
getStatusType(status) {
const typeMap = {
'draft': 'info',
'scheduled': 'success',
'running': 'warning',
'paused': 'warning',
'completed': '',
'failed': 'danger'
};
return typeMap[status] || 'info';
},
getStatusText(status) {
const textMap = {
'draft': '草稿',
'scheduled': '已排期',
'running': '运行中',
'paused': '已暂停',
'completed': '已完成',
'failed': '失败'
};
return textMap[status] || status;
},
getProgressStatus(status) {
const statusMap = {
'completed': 'success',
'failed': 'exception',
'running': 'warning',
'scheduled': 'warning',
'paused': 'info'
};
return statusMap[status] || '';
},
formatDateTime(datetime) {
return new Date(datetime).toLocaleString();
},
refreshTasks() {
this.loadTasks();
this.loadStatistics();
this.$message.success('数据已刷新');
},
// 审核任务
async auditTask(task) {
try {
const response = await axios.post(`/message_push/api/tasks/${task.task_id}/audit`);
if (response.data.success) {
this.$message.success('任务已审核通过');
this.loadTasks();
this.loadStatistics();
}
} catch (error) {
this.$message.error('审核任务失败');
}
}
}
});
</script>
<style>
.search-form {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
.stat-card {
text-align: center;
padding: 10px;
}
.stat-title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409EFF;
}
.batch-toolbar {
margin-bottom: 15px;
display: flex;
align-items: center;
}
.selected-count {
margin-left: 15px;
color: #606266;
}
</style>
{% endblock %}