1135 lines
47 KiB
HTML
1135 lines
47 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-select
|
||
{% raw %}v-model="taskForm.schedule_type" {% endraw %}
|
||
placeholder="请选择任务类型"
|
||
{% raw %}:disabled="isEdit"{% endraw %}>
|
||
<el-option label="一次性任务" value="once"></el-option>
|
||
<el-option label="重复任务" value="recurring"></el-option>
|
||
</el-select>
|
||
</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_time"
|
||
{% raw %}v-if="taskForm.schedule_type === 'recurring'" {% endraw %}>
|
||
<el-time-picker
|
||
{% raw %}v-model="taskForm.recurring_time" {% endraw %}
|
||
format="HH:mm"
|
||
placeholder="选择时间"
|
||
value-format="HH:mm">
|
||
</el-time-picker>
|
||
</el-form-item>
|
||
<el-form-item
|
||
label="每周执行日"
|
||
prop="weekly_days"
|
||
{% raw %}v-if="taskForm.schedule_type === 'recurring' && taskForm.recurring_interval === 'weekly'" {% endraw %}>
|
||
<el-select
|
||
{% raw %}v-model="taskForm.weekly_days" {% endraw %}
|
||
multiple
|
||
placeholder="请选择每周执行日">
|
||
<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-option label="周日" value="0"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item
|
||
label="每月执行日"
|
||
prop="monthly_day"
|
||
{% raw %}v-if="taskForm.schedule_type === 'recurring' && taskForm.recurring_interval === 'monthly'" {% endraw %}>
|
||
<el-select
|
||
{% raw %}v-model="taskForm.monthly_day" {% endraw %}
|
||
placeholder="请选择每月执行日">
|
||
<el-option
|
||
{% raw %}v-for="day in 31"
|
||
:key="day"
|
||
:label="`${day}日`"
|
||
:value="day" {% endraw %}>
|
||
</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="voice">
|
||
<el-upload
|
||
class="upload-demo"
|
||
action="/message_push/api/upload"
|
||
{% raw %}:on-success="handleVoiceSuccess"
|
||
:before-upload="beforeVoiceUpload"
|
||
:on-preview="handleVoicePreview"
|
||
:file-list="voiceList" {% endraw %}>
|
||
<el-button size="small" type="primary">点击上传</el-button>
|
||
<div slot="tip" class="el-upload__tip">只能上传mp3/wav文件,且不超过10MB</div>
|
||
</el-upload>
|
||
<el-dialog :visible.sync="voicePreviewVisible" append-to-body>
|
||
<audio controls :src="voicePreviewUrl" style="width: 100%"></audio>
|
||
</el-dialog>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="视频" name="video">
|
||
<el-upload
|
||
class="upload-demo"
|
||
action="/message_push/api/upload"
|
||
{% raw %}:on-success="handleVideoSuccess"
|
||
:before-upload="beforeVideoUpload"
|
||
:on-preview="handleVideoPreview"
|
||
:file-list="videoList" {% endraw %}>
|
||
<el-button size="small" type="primary">点击上传</el-button>
|
||
<div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过20MB</div>
|
||
</el-upload>
|
||
<el-dialog :visible.sync="videoPreviewVisible" append-to-body>
|
||
<video controls :src="videoPreviewUrl" style="width: 100%"></video>
|
||
</el-dialog>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="链接" name="link">
|
||
<el-form-item label="链接标题">
|
||
<el-input {% raw %}v-model="taskForm.content_link.title" {% endraw %}></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="链接描述">
|
||
<el-input type="textarea" {% raw %}v-model="taskForm.content_link.des" {% endraw %}></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="链接地址">
|
||
<el-input {% raw %}v-model="taskForm.content_link.url" {% endraw %}></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="缩略图">
|
||
<el-upload
|
||
class="upload-demo"
|
||
action="/message_push/api/upload"
|
||
{% raw %}:on-success="handleThumbnailSuccess"
|
||
:before-upload="beforeImageUpload"
|
||
:on-preview="handleThumbnailPreview"
|
||
:file-list="thumbnailList" {% endraw %}>
|
||
<el-button size="small" type="primary">点击上传</el-button>
|
||
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过2MB</div>
|
||
</el-upload>
|
||
</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_voice: '',
|
||
content_video: '',
|
||
content_link: {
|
||
title: '',
|
||
des: '',
|
||
url: '',
|
||
thumburl: ''
|
||
},
|
||
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' }
|
||
],
|
||
recurring_interval: [
|
||
{ required: true, message: '请选择重复间隔', trigger: 'change' }
|
||
],
|
||
recurring_time: [
|
||
{ required: true, message: '请选择执行时间', trigger: 'change' }
|
||
],
|
||
weekly_days: [
|
||
{ required: true, message: '请选择每周执行日', trigger: 'change' }
|
||
],
|
||
monthly_day: [
|
||
{ required: true, message: '请选择每月执行日', trigger: 'change' }
|
||
],
|
||
recurring_end: [
|
||
{ 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: '',
|
||
thumbnailList: [],
|
||
voiceList: [],
|
||
voicePreviewVisible: false,
|
||
voicePreviewUrl: '',
|
||
videoList: [],
|
||
videoPreviewVisible: false,
|
||
videoPreviewUrl: '',
|
||
isEdit: false
|
||
}
|
||
},
|
||
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_voice: '',
|
||
content_video: '',
|
||
content_link: {
|
||
title: '',
|
||
des: '',
|
||
url: '',
|
||
thumburl: ''
|
||
},
|
||
content_miniprogram: {
|
||
title: '',
|
||
path: ''
|
||
},
|
||
preview_recipients: []
|
||
};
|
||
this.imageList = [];
|
||
this.taskDialogVisible = true;
|
||
this.isEdit = false;
|
||
},
|
||
|
||
// 保存任务
|
||
async saveTask() {
|
||
this.$refs.taskForm.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
// 创建任务数据对象
|
||
const taskData = { ...this.taskForm };
|
||
|
||
// 处理重复任务的特殊字段
|
||
if (taskData.schedule_type === 'recurring') {
|
||
// 确保链接内容是JSON字符串
|
||
if (taskData.content_link) {
|
||
taskData.content_link = JSON.stringify(taskData.content_link);
|
||
}
|
||
|
||
// 处理每周执行日
|
||
if (taskData.weekly_days) {
|
||
taskData.weekly_days = JSON.stringify(taskData.weekly_days);
|
||
}
|
||
}
|
||
|
||
let response;
|
||
if (taskData.task_id) {
|
||
// 更新任务
|
||
response = await axios.put(`/message_push/api/tasks/${taskData.task_id}`, taskData);
|
||
} else {
|
||
// 创建新任务
|
||
response = await axios.post('/message_push/api/tasks', taskData);
|
||
}
|
||
|
||
if (response.data.success) {
|
||
this.$message.success(this.taskForm.task_id ? '更新任务成功' : '创建任务成功');
|
||
this.taskDialogVisible = false;
|
||
this.loadTasks();
|
||
this.loadStatistics();
|
||
}else{
|
||
this.$message.error(response.data.message);
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(this.taskForm.task_id ? '更新任务失败' : '创建任务失败');
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 预览任务
|
||
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,
|
||
schedule_time: task.schedule_time ? new Date(task.schedule_time).toISOString().slice(0, 19).replace('T', ' ') : '',
|
||
recurring_end: task.recurring_end ? new Date(task.recurring_end).toISOString().slice(0, 19).replace('T', ' ') : ''
|
||
};
|
||
|
||
// 处理 weekly_days
|
||
if (task.weekly_days) {
|
||
try {
|
||
this.taskForm.weekly_days = typeof task.weekly_days === 'string'
|
||
? JSON.parse(task.weekly_days)
|
||
: task.weekly_days;
|
||
} catch (e) {
|
||
console.error('解析每周执行日失败:', e);
|
||
this.taskForm.weekly_days = [];
|
||
}
|
||
} else {
|
||
this.taskForm.weekly_days = [];
|
||
}
|
||
|
||
// 处理链接内容
|
||
if (task.content_link) {
|
||
try {
|
||
this.taskForm.content_link = typeof task.content_link === 'string'
|
||
? JSON.parse(task.content_link)
|
||
: task.content_link;
|
||
|
||
// 如果有缩略图,显示缩略图
|
||
if (this.taskForm.content_link.thumburl) {
|
||
const fileName = this.taskForm.content_link.thumburl.split('/').pop();
|
||
this.thumbnailList = [{
|
||
name: '已上传缩略图',
|
||
url: `/static/uploads/${fileName}`
|
||
}];
|
||
}
|
||
} catch (e) {
|
||
console.error('解析链接内容失败:', e);
|
||
this.taskForm.content_link = {
|
||
title: '',
|
||
des: '',
|
||
url: '',
|
||
thumburl: ''
|
||
};
|
||
}
|
||
}
|
||
if (task.content_image) {
|
||
// 编辑时显示图片
|
||
const fileName = task.content_image.split('/').pop();
|
||
this.imageList = [{
|
||
name: '已上传图片',
|
||
url: `/static/uploads/${fileName}` // 显示时使用相对路径
|
||
}];
|
||
}
|
||
this.taskDialogVisible = true;
|
||
this.isEdit = 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;
|
||
},
|
||
|
||
// 缩略图上传相关
|
||
handleThumbnailSuccess(response, file) {
|
||
if (response.success) {
|
||
this.taskForm.content_link.thumburl = response.data.url; // 存储绝对路径
|
||
// 显示时使用文件名
|
||
const fileName = file.name;
|
||
this.thumbnailList = [{
|
||
name: fileName,
|
||
url: `/static/uploads/${response.data.url.split('/').pop()}` // 显示时使用相对路径
|
||
}];
|
||
} else {
|
||
this.$message.error('上传失败');
|
||
}
|
||
},
|
||
|
||
handleThumbnailPreview(file) {
|
||
// 预览时使用相对路径
|
||
const fileName = file.url.split('/').pop();
|
||
this.previewUrl = `/static/uploads/${fileName}`;
|
||
this.previewVisible = true;
|
||
},
|
||
|
||
// 语音上传相关
|
||
handleVoiceSuccess(response, file) {
|
||
if (response.success) {
|
||
this.taskForm.content_voice = response.data.url;
|
||
const fileName = file.name;
|
||
this.voiceList = [{
|
||
name: fileName,
|
||
url: `/static/uploads/${response.data.url.split('/').pop()}`
|
||
}];
|
||
} else {
|
||
this.$message.error('上传失败');
|
||
}
|
||
},
|
||
|
||
beforeVoiceUpload(file) {
|
||
const isVoice = file.type === 'audio/mp3' || file.type === 'audio/wav';
|
||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||
|
||
if (!isVoice) {
|
||
this.$message.error('只能上传mp3/wav文件!');
|
||
return false;
|
||
}
|
||
if (!isLt10M) {
|
||
this.$message.error('语音文件大小不能超过 10MB!');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
|
||
handleVoicePreview(file) {
|
||
const fileName = file.url.split('/').pop();
|
||
this.voicePreviewUrl = `/static/uploads/${fileName}`;
|
||
this.voicePreviewVisible = true;
|
||
},
|
||
|
||
// 视频上传相关
|
||
handleVideoSuccess(response, file) {
|
||
if (response.success) {
|
||
this.taskForm.content_video = response.data.url;
|
||
const fileName = file.name;
|
||
this.videoList = [{
|
||
name: fileName,
|
||
url: `/static/uploads/${response.data.url.split('/').pop()}`
|
||
}];
|
||
} else {
|
||
this.$message.error('上传失败');
|
||
}
|
||
},
|
||
|
||
beforeVideoUpload(file) {
|
||
const isVideo = file.type === 'video/mp4';
|
||
const isLt20M = file.size / 1024 / 1024 < 20;
|
||
|
||
if (!isVideo) {
|
||
this.$message.error('只能上传mp4文件!');
|
||
return false;
|
||
}
|
||
if (!isLt20M) {
|
||
this.$message.error('视频文件大小不能超过 20MB!');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
|
||
handleVideoPreview(file) {
|
||
const fileName = file.url.split('/').pop();
|
||
this.videoPreviewUrl = `/static/uploads/${fileName}`;
|
||
this.videoPreviewVisible = 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) {
|
||
if (!datetime) return '';
|
||
try {
|
||
// 确保输入是有效的日期字符串
|
||
const date = new Date(datetime);
|
||
if (isNaN(date.getTime())) {
|
||
return datetime; // 如果解析失败,返回原始字符串
|
||
}
|
||
const year = date.getUTCFullYear();
|
||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
} catch (e) {
|
||
console.error('Date parsing error:', e);
|
||
return datetime; // 如果出错,返回原始字符串
|
||
}
|
||
},
|
||
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 %} |