156 lines
28 KiB
HTML
156 lines
28 KiB
HTML
{% 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 %}
|