Files
abot/admin/dashboard/templates/message_push_management.html
2025-06-10 11:32:33 +08:00

752 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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="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="task_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
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="task_name">
<el-input {% raw %}v-model="taskForm.task_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="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: {
task_name: '',
schedule_time: '',
groups: [],
content_text: '',
content_image: '',
content_link: '',
content_miniprogram: {
title: '',
path: ''
},
preview_recipients: []
},
taskRules: {
task_name: [
{ required: true, message: '请输入任务名称', trigger: 'blur' }
],
schedule_time: [
{ 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 = {
task_name: '',
schedule_time: '',
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) {
this.imageList = [{
name: '已上传图片',
url: task.content_image
}];
}
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;
this.imageList = [{
name: file.name,
url: response.data.url
}];
} 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) {
this.previewUrl = file.url;
this.previewVisible = true;
},
// 工具函数
getStatusType(status) {
const typeMap = {
'draft': 'info',
'scheduled': 'success',
'paused': 'warning',
'completed': '',
'failed': 'danger'
};
return typeMap[status] || 'info';
},
getStatusText(status) {
const textMap = {
'draft': '草稿',
'scheduled': '已排期',
'paused': '已暂停',
'completed': '已完成',
'failed': '失败'
};
return textMap[status] || status;
},
getProgressStatus(status) {
const statusMap = {
'completed': 'success',
'failed': 'exception',
'scheduled': 'warning',
'paused': 'info'
};
return statusMap[status] || '';
},
formatDateTime(datetime) {
return new Date(datetime).toLocaleString();
},
refreshTasks() {
this.loadTasks();
this.loadStatistics();
this.$message.success('数据已刷新');
}
}
});
</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 %}