Files
abot/admin/dashboard/templates/message_push_management.html
2026-03-09 11:32:08 +08:00

156 lines
28 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 class="page-shell push-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Push Workspace</div>
<h1>消息推送管理</h1>
<p>统一管理定时任务、预览发送、状态流转与日志追踪,把推送能力拉进新版控制台。</p>
</div>
<div class="page-hero-actions">
<el-button type="success" @click="showCreateTaskDialog">新建任务</el-button>
<el-button type="primary" plain @click="refreshTasks">刷新数据</el-button>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :span="4"><el-card class="overview-card overview-card--primary"><div class="overview-label">总任务数</div><div class="overview-value">{% raw %}{{ statistics.total }}{% endraw %}</div><div class="overview-note">系统内已创建任务</div></el-card></el-col>
<el-col :span="4"><el-card class="overview-card"><div class="overview-label">已排期</div><div class="overview-value">{% raw %}{{ statistics.scheduled }}{% endraw %}</div><div class="overview-note">等待执行</div></el-card></el-col>
<el-col :span="4"><el-card class="overview-card"><div class="overview-label">已暂停</div><div class="overview-value">{% raw %}{{ statistics.paused }}{% endraw %}</div><div class="overview-note">暂未继续</div></el-card></el-col>
<el-col :span="4"><el-card class="overview-card"><div class="overview-label">已完成</div><div class="overview-value">{% raw %}{{ statistics.completed }}{% endraw %}</div><div class="overview-note">执行完成</div></el-card></el-col>
<el-col :span="4"><el-card class="overview-card"><div class="overview-label">失败</div><div class="overview-value">{% raw %}{{ statistics.failed }}{% endraw %}</div><div class="overview-note">需排查重试</div></el-card></el-col>
<el-col :span="4"><el-card class="overview-card overview-card--soft"><div class="overview-label">今日任务</div><div class="overview-value">{% raw %}{{ statistics.today }}{% endraw %}</div><div class="overview-note">今天相关任务量</div></el-card></el-col>
</el-row>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div><h3>任务筛选与列表</h3><p>按状态、时间范围和多选操作管理消息推送任务。</p></div>
</div>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="状态">
<el-select v-model="searchForm.status" 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 v-model="searchForm.timeRange" 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" @click="searchTasks">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-toolbar" v-if="selectedTasks.length > 0">
<el-button-group>
<el-button size="small" type="primary" @click="batchPause">批量暂停</el-button>
<el-button size="small" type="success" @click="batchResume">批量恢复</el-button>
<el-button size="small" type="danger" @click="batchDelete">批量删除</el-button>
</el-button-group>
<span class="selected-count">已选择 {% raw %}{{ selectedTasks.length }}{% endraw %} 项</span>
</div>
<el-table :data="taskList" style="width:100%" v-loading="loading" @selection-change="handleSelectionChange">
<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="任务名称" min-width="220"></el-table-column>
<el-table-column prop="status" label="状态" width="110" align="center"><template slot-scope="scope"><el-tag :type="getStatusType(scope.row.status)">{% 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="180"><template slot-scope="scope"><el-progress :percentage="scope.row.progress || 0" :status="getProgressStatus(scope.row.status)"></el-progress></template></el-table-column>
<el-table-column label="操作" min-width="320"><template slot-scope="scope"><div class="action-row"><el-button v-if="scope.row.status === 'draft'" size="mini" type="success" @click="auditTask(scope.row)">审核</el-button><el-button size="mini" type="primary" plain @click="previewTask(scope.row)">预览</el-button><el-button size="mini" type="warning" plain @click="editTask(scope.row)">编辑</el-button><el-button v-if="scope.row.status === 'scheduled'" size="mini" type="info" @click="pauseTask(scope.row)">暂停</el-button><el-button v-if="scope.row.status === 'paused'" size="mini" type="success" @click="resumeTask(scope.row)">恢复</el-button><el-button size="mini" type="danger" @click="deleteTask(scope.row)">删除</el-button><el-button size="mini" type="text" @click="viewLogs(scope.row)">日志</el-button></div></template></el-table-column>
</el-table>
<div class="pagination-container"><el-pagination @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"></el-pagination></div>
</el-card>
<el-dialog :title="dialogTitle" :visible.sync="taskDialogVisible" width="60%">
<el-form :model="taskForm" :rules="taskRules" ref="taskForm" label-width="100px">
<el-form-item label="任务类型" prop="schedule_type"><el-select v-model="taskForm.schedule_type" placeholder="请选择任务类型" :disabled="isEdit"><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 v-model="taskForm.name"></el-input></el-form-item>
<el-form-item label="计划时间" prop="schedule_time"><el-date-picker v-model="taskForm.schedule_time" type="datetime" placeholder="选择日期时间" value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker></el-form-item>
<el-form-item label="重复间隔" prop="recurring_interval" v-if="taskForm.schedule_type === 'recurring'"><el-select v-model="taskForm.recurring_interval" 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" v-if="taskForm.schedule_type === 'recurring'"><el-time-picker v-model="taskForm.recurring_time" format="HH:mm" placeholder="选择时间" value-format="HH:mm"></el-time-picker></el-form-item>
<el-form-item label="每周执行日" prop="weekly_days" v-if="taskForm.schedule_type === 'recurring' && taskForm.recurring_interval === 'weekly'"><el-select v-model="taskForm.weekly_days" 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" v-if="taskForm.schedule_type === 'recurring' && taskForm.recurring_interval === 'monthly'"><el-select v-model="taskForm.monthly_day" placeholder="请选择每月执行日"><el-option v-for="day in 31" :key="day" :label="`${day}日`" :value="day"></el-option></el-select></el-form-item>
<el-form-item label="重复结束时间" prop="recurring_end" v-if="taskForm.schedule_type === 'recurring'"><el-date-picker v-model="taskForm.recurring_end" 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 v-model="taskForm.priority" 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 v-model="taskForm.groups" multiple filterable placeholder="请选择群组"><el-option v-for="group in groupList" :key="group.wxid" :label="group.name" :value="group.wxid"></el-option></el-select></el-form-item>
<el-form-item label="消息内容">
<el-tabs v-model="activeContentTab">
<el-tab-pane label="文本" name="text"><el-input type="textarea" v-model="taskForm.content_text" :rows="4" placeholder="请输入文本内容"></el-input></el-tab-pane>
<el-tab-pane label="图片" name="image"><el-upload class="upload-demo" action="/message_push/api/upload" :on-success="handleImageSuccess" :before-upload="beforeImageUpload" :on-preview="handleImagePreview" :file-list="imageList"><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" :on-success="handleVoiceSuccess" :before-upload="beforeVoiceUpload" :on-preview="handleVoicePreview" :file-list="voiceList"><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" :on-success="handleVideoSuccess" :before-upload="beforeVideoUpload" :on-preview="handleVideoPreview" :file-list="videoList"><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 v-model="taskForm.content_link.title"></el-input></el-form-item><el-form-item label="链接描述"><el-input type="textarea" v-model="taskForm.content_link.des"></el-input></el-form-item><el-form-item label="链接地址"><el-input v-model="taskForm.content_link.url"></el-input></el-form-item><el-form-item label="缩略图"><el-upload class="upload-demo" action="/message_push/api/upload" :on-success="handleThumbnailSuccess" :before-upload="beforeImageUpload" :on-preview="handleThumbnailPreview" :file-list="thumbnailList"><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 v-model="taskForm.content_miniprogram.title"></el-input></el-form-item><el-form-item label="路径"><el-input v-model="taskForm.content_miniprogram.path"></el-input></el-form-item></el-tab-pane>
</el-tabs>
</el-form-item>
<el-form-item label="预览接收人"><el-select v-model="taskForm.preview_recipients" multiple filterable placeholder="请选择预览接收人"><el-option v-for="user in userList" :key="user.wxid" :label="user.name" :value="user.wxid"></el-option></el-select></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer"><el-button @click="taskDialogVisible = false">取消</el-button><el-button type="primary" @click="saveTask">确定</el-button></div>
</el-dialog>
<el-dialog title="任务日志" :visible.sync="logsDialogVisible" width="70%"><el-table :data="taskLogs" 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 class="changes-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(e){ 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(e){ 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(e){ 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(e){ 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.voiceList=[]; this.videoList=[]; this.thumbnailList=[]; 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'){ 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(e){ 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(e){ 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',' '):''}; if(task.weekly_days){ try{ this.taskForm.weekly_days=typeof task.weekly_days==='string'?JSON.parse(task.weekly_days):task.weekly_days; }catch(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){ this.thumbnailList=[{name:'已上传缩略图',url:`/static/uploads/${this.taskForm.content_link.thumburl.split('/').pop()}`}] } }catch(e){ this.taskForm.content_link={title:'',des:'',url:'',thumburl:''}; } } if(task.content_image){ this.imageList=[{name:'已上传图片',url:`/static/uploads/${task.content_image.split('/').pop()}`}] } if(task.content_voice){ this.voiceList=[{name:'已上传语音',url:`/static/uploads/${task.content_voice.split('/').pop()}`}] } if(task.content_video){ this.videoList=[{name:'已上传视频',url:`/static/uploads/${task.content_video.split('/').pop()}`}] } 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(e){ 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(e){ 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(e){ 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(e){ 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{ await Promise.all(this.selectedTasks.map(task=>axios.post(`/message_push/api/tasks/${task.task_id}/pause`))); this.$message.success('批量暂停成功'); this.loadTasks(); this.loadStatistics(); }catch(e){ this.$message.error('批量暂停失败'); } },
async batchResume(){ try{ await Promise.all(this.selectedTasks.map(task=>axios.post(`/message_push/api/tasks/${task.task_id}/resume`))); this.$message.success('批量恢复成功'); this.loadTasks(); this.loadStatistics(); }catch(e){ this.$message.error('批量恢复失败'); } },
batchDelete(){ this.$confirm(`确认删除选中的 ${this.selectedTasks.length} 个任务吗?`,'提示',{type:'warning'}).then(async()=>{ try{ await Promise.all(this.selectedTasks.map(task=>axios.delete(`/message_push/api/tasks/${task.task_id}`))); this.$message.success('批量删除成功'); this.loadTasks(); this.loadStatistics(); }catch(e){ this.$message.error('批量删除失败'); } }); },
handleImageSuccess(response,file){ if(response.success){ this.taskForm.content_image=response.data.url; this.imageList=[{name:file.name,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; this.thumbnailList=[{name:file.name,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; this.voiceList=[{name:file.name,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; this.videoList=[{name:file.name,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){ 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(e){ this.$message.error('审核任务失败'); } }
}
});
</script>
<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}.overview-grid .el-col{margin-bottom:16px}.overview-card{min-height:112px}.overview-card--primary{background:linear-gradient(180deg, rgba(79,70,229,.10), rgba(255,255,255,.94)) !important}.overview-card--soft{background:linear-gradient(180deg, rgba(59,130,246,.08), rgba(255,255,255,.94)) !important}.overview-label{font-size:13px;color:#64748b;margin-bottom:14px}.overview-value{font-size:28px;font-weight:700;color:#0f172a;margin-bottom:10px}.overview-note{font-size:12px;color:#94a3b8}.workspace-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.workspace-header h3{font-size:18px;margin-bottom:4px}.workspace-header p{font-size:13px;color:#64748b}.search-form{margin-bottom:20px}.pagination-container{margin-top:20px;text-align:right}.batch-toolbar{margin-bottom:15px;display:flex;align-items:center}.selected-count{margin-left:15px;color:#64748b}.action-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.changes-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:14px;color:#334155}
</style>
{% endblock %}