|
|
@@ -0,0 +1,655 @@
|
|
|
+<script setup>
|
|
|
+import { TaskStatus, TaskItemStatus } from '@/enums'
|
|
|
+import {
|
|
|
+ listSmsTasks,
|
|
|
+ createSmsTask,
|
|
|
+ updateSmsTask,
|
|
|
+ startSmsTask,
|
|
|
+ pauseSmsTask,
|
|
|
+ deleteSmsTask,
|
|
|
+ listSmsTaskItems
|
|
|
+} from '@/services/api'
|
|
|
+import { Form } from '@primevue/forms'
|
|
|
+import { zodResolver } from '@primevue/forms/resolvers/zod'
|
|
|
+import { useDateFormat } from '@vueuse/core'
|
|
|
+import Button from 'primevue/button'
|
|
|
+import Column from 'primevue/column'
|
|
|
+import DataTable from 'primevue/datatable'
|
|
|
+import Dialog from 'primevue/dialog'
|
|
|
+import Select from 'primevue/select'
|
|
|
+import FloatLabel from 'primevue/floatlabel'
|
|
|
+import IconField from 'primevue/iconfield'
|
|
|
+import InputIcon from 'primevue/inputicon'
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
+import Message from 'primevue/message'
|
|
|
+import Textarea from 'primevue/textarea'
|
|
|
+import FileUpload from 'primevue/fileupload'
|
|
|
+import Tag from 'primevue/tag'
|
|
|
+import ProgressBar from 'primevue/progressbar'
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
+import { useConfirm } from 'primevue/useconfirm'
|
|
|
+import { computed, onMounted, ref } from 'vue'
|
|
|
+import { z } from 'zod'
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
+
|
|
|
+const toast = useToast()
|
|
|
+const confirm = useConfirm()
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+// 是否是管理员
|
|
|
+const isAdmin = computed(() => userStore.userInfo?.role === 'admin')
|
|
|
+
|
|
|
+// 表格数据
|
|
|
+const tableData = ref({
|
|
|
+ items: [],
|
|
|
+ total: 0,
|
|
|
+ page: 0,
|
|
|
+ size: 20,
|
|
|
+ totalPages: 0
|
|
|
+})
|
|
|
+
|
|
|
+// 筛选条件
|
|
|
+const filters = ref({
|
|
|
+ status: null
|
|
|
+})
|
|
|
+
|
|
|
+// 获取任务列表
|
|
|
+const fetchData = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: tableData.value.page,
|
|
|
+ size: tableData.value.size
|
|
|
+ }
|
|
|
+
|
|
|
+ if (filters.value.status) {
|
|
|
+ params.status = filters.value.status
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await listSmsTasks(params)
|
|
|
+ tableData.value = response
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '加载任务列表失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 分页处理
|
|
|
+const handlePageChange = (event) => {
|
|
|
+ tableData.value.page = event.page
|
|
|
+ tableData.value.size = event.rows
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期
|
|
|
+const formatDate = (date) => {
|
|
|
+ if (!date) return '-'
|
|
|
+ return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
|
|
|
+}
|
|
|
+
|
|
|
+// 获取任务状态标签样式
|
|
|
+const getStatusSeverity = (status) => {
|
|
|
+ const severityMap = {
|
|
|
+ idle: 'secondary',
|
|
|
+ pending: 'info',
|
|
|
+ running: 'primary',
|
|
|
+ cutting: 'info',
|
|
|
+ paused: 'warn',
|
|
|
+ queued: 'info',
|
|
|
+ scheduled: 'info',
|
|
|
+ completed: 'success',
|
|
|
+ vip: 'contrast',
|
|
|
+ error: 'danger'
|
|
|
+ }
|
|
|
+ return severityMap[status] || 'secondary'
|
|
|
+}
|
|
|
+
|
|
|
+// 计算进度
|
|
|
+const calculateProgress = (task) => {
|
|
|
+ if (task.total === 0) return 0
|
|
|
+ return Math.round((task.processed / task.total) * 100)
|
|
|
+}
|
|
|
+
|
|
|
+// 状态选项
|
|
|
+const statusOptions = computed(() => {
|
|
|
+ return Object.keys(TaskStatus).map((key) => ({
|
|
|
+ label: TaskStatus[key],
|
|
|
+ value: key
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+// 任务项状态选项
|
|
|
+const taskItemStatusOptions = computed(() => {
|
|
|
+ return Object.keys(TaskItemStatus).map((key) => ({
|
|
|
+ label: TaskItemStatus[key],
|
|
|
+ value: key
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+// ========== 创建/编辑任务 ==========
|
|
|
+const taskDialog = ref(false)
|
|
|
+const isEditMode = ref(false)
|
|
|
+const taskForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ message: '',
|
|
|
+ remark: '',
|
|
|
+ file: null
|
|
|
+})
|
|
|
+const taskFormLoading = ref(false)
|
|
|
+const fileUploadRef = ref()
|
|
|
+
|
|
|
+const taskFormResolver = computed(() => {
|
|
|
+ const schema = {
|
|
|
+ name: z.string().min(1, { message: '任务名称不能为空' }),
|
|
|
+ message: z.string().min(1, { message: '短信内容不能为空' }),
|
|
|
+ remark: z.string().optional()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建模式下文件必填
|
|
|
+ if (!isEditMode.value) {
|
|
|
+ schema.file = z.any().refine((val) => val !== null, { message: '请上传手机号文件' })
|
|
|
+ }
|
|
|
+
|
|
|
+ return zodResolver(z.object(schema))
|
|
|
+})
|
|
|
+
|
|
|
+const openNewTaskDialog = () => {
|
|
|
+ taskForm.value = {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ message: '',
|
|
|
+ remark: '',
|
|
|
+ file: null
|
|
|
+ }
|
|
|
+ isEditMode.value = false
|
|
|
+ taskDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const openEditTaskDialog = (task) => {
|
|
|
+ taskForm.value = {
|
|
|
+ id: task.id,
|
|
|
+ name: task.name,
|
|
|
+ message: task.message,
|
|
|
+ remark: task.remark || '',
|
|
|
+ file: null
|
|
|
+ }
|
|
|
+ isEditMode.value = true
|
|
|
+ taskDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 文件选择处理
|
|
|
+const onFileSelect = (event) => {
|
|
|
+ const files = event.files
|
|
|
+ if (files && files.length > 0) {
|
|
|
+ taskForm.value.file = files[0]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 清除文件
|
|
|
+const clearFile = () => {
|
|
|
+ taskForm.value.file = null
|
|
|
+ if (fileUploadRef.value) {
|
|
|
+ fileUploadRef.value.clear()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const saveTask = async ({ valid }) => {
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ taskFormLoading.value = true
|
|
|
+ try {
|
|
|
+ if (isEditMode.value) {
|
|
|
+ // 更新任务
|
|
|
+ const updateData = {
|
|
|
+ id: taskForm.value.id,
|
|
|
+ name: taskForm.value.name,
|
|
|
+ message: taskForm.value.message
|
|
|
+ }
|
|
|
+ if (taskForm.value.remark) {
|
|
|
+ updateData.remark = taskForm.value.remark
|
|
|
+ }
|
|
|
+
|
|
|
+ await updateSmsTask(updateData)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '任务更新成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 创建任务
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('name', taskForm.value.name)
|
|
|
+ formData.append('message', taskForm.value.message)
|
|
|
+ if (taskForm.value.remark) {
|
|
|
+ formData.append('remark', taskForm.value.remark)
|
|
|
+ }
|
|
|
+ formData.append('file', taskForm.value.file)
|
|
|
+
|
|
|
+ await createSmsTask(formData)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '任务创建成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ taskDialog.value = false
|
|
|
+ clearFile()
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || (isEditMode.value ? '更新任务失败' : '创建任务失败'),
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ taskFormLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭对话框
|
|
|
+const closeTaskDialog = () => {
|
|
|
+ taskDialog.value = false
|
|
|
+ clearFile()
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 任务操作 ==========
|
|
|
+// 开始任务
|
|
|
+const handleStartTask = async (task) => {
|
|
|
+ try {
|
|
|
+ await startSmsTask(task.id)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '任务已开始',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '开始任务失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 暂停任务
|
|
|
+const handlePauseTask = async (task) => {
|
|
|
+ try {
|
|
|
+ await pauseSmsTask(task.id)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '任务已暂停',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '暂停任务失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除任务
|
|
|
+const handleDeleteTask = (task) => {
|
|
|
+ confirm.require({
|
|
|
+ message: `确定要删除任务 "${task.name}" 吗?`,
|
|
|
+ header: '确认删除',
|
|
|
+ icon: 'pi pi-exclamation-triangle',
|
|
|
+ acceptLabel: '确定',
|
|
|
+ rejectLabel: '取消',
|
|
|
+ accept: async () => {
|
|
|
+ try {
|
|
|
+ await deleteSmsTask(task.id)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '任务删除成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '删除任务失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 判断是否可以开始任务
|
|
|
+const canStartTask = (task) => {
|
|
|
+ return ['idle', 'paused', 'scheduled'].includes(task.status)
|
|
|
+}
|
|
|
+
|
|
|
+// 判断是否可以暂停任务
|
|
|
+const canPauseTask = (task) => {
|
|
|
+ return ['running', 'cutting', 'queued', 'scheduled'].includes(task.status)
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 任务详情(任务项列表) ==========
|
|
|
+const taskDetailDialog = ref(false)
|
|
|
+const currentTask = ref(null)
|
|
|
+const taskItemsData = ref({
|
|
|
+ items: [],
|
|
|
+ total: 0,
|
|
|
+ page: 0,
|
|
|
+ size: 50,
|
|
|
+ totalPages: 0
|
|
|
+})
|
|
|
+const taskItemFilter = ref({
|
|
|
+ status: null
|
|
|
+})
|
|
|
+
|
|
|
+const openTaskDetailDialog = async (task) => {
|
|
|
+ currentTask.value = task
|
|
|
+ taskDetailDialog.value = true
|
|
|
+ taskItemsData.value.page = 0
|
|
|
+ taskItemFilter.value.status = null
|
|
|
+ await fetchTaskItems()
|
|
|
+}
|
|
|
+
|
|
|
+const fetchTaskItems = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: taskItemsData.value.page,
|
|
|
+ size: taskItemsData.value.size,
|
|
|
+ taskId: currentTask.value.id
|
|
|
+ }
|
|
|
+
|
|
|
+ if (taskItemFilter.value.status) {
|
|
|
+ params.status = taskItemFilter.value.status
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await listSmsTaskItems(params)
|
|
|
+ taskItemsData.value = response
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '加载任务项失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskItemPageChange = (event) => {
|
|
|
+ taskItemsData.value.page = event.page
|
|
|
+ taskItemsData.value.size = event.rows
|
|
|
+ fetchTaskItems()
|
|
|
+}
|
|
|
+
|
|
|
+// 应用筛选
|
|
|
+const applyFilters = () => {
|
|
|
+ tableData.value.page = 0
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置筛选
|
|
|
+const resetFilters = () => {
|
|
|
+ filters.value = {
|
|
|
+ status: null
|
|
|
+ }
|
|
|
+ tableData.value.page = 0
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
|
|
|
+ <!-- 操作栏 -->
|
|
|
+ <div class="mb-4 overflow-x-auto py-2">
|
|
|
+ <div class="flex items-center gap-2 flex-nowrap min-w-[760px]">
|
|
|
+ <div class="field w-36">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <Select id="filterStatus" v-model="filters.status" :options="statusOptions" optionLabel="label"
|
|
|
+ optionValue="value" showClear fluid size="small" />
|
|
|
+ <label for="filterStatus">任务状态</label>
|
|
|
+ </FloatLabel>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 左侧按钮组 -->
|
|
|
+ <div class="flex items-center gap-2 flex-nowrap">
|
|
|
+ <Button icon="pi pi-filter-slash" label="重置" severity="secondary" @click="resetFilters"
|
|
|
+ size="small" />
|
|
|
+ <Button icon="pi pi-search" label="搜索" @click="applyFilters" size="small" />
|
|
|
+
|
|
|
+ <span class="w-px h-6 bg-[var(--p-content-border-color)] mx-1"></span>
|
|
|
+ <Button icon="pi pi-refresh" @click="fetchData" label="刷新" size="small" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧按钮:新建任务 -->
|
|
|
+ <div class="ml-auto">
|
|
|
+ <Button icon="pi pi-plus" @click="openNewTaskDialog" label="新建任务" severity="success" size="small" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <DataTable :value="tableData.items" :paginator="true"
|
|
|
+ paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
|
|
+ currentPageReportTemplate="{totalRecords} 条记录" :rows="tableData.size"
|
|
|
+ :rowsPerPageOptions="[10, 20, 50, 100]" :totalRecords="tableData.total" @page="handlePageChange" lazy
|
|
|
+ scrollable>
|
|
|
+ <Column field="id" header="ID" style="min-width: 80px"></Column>
|
|
|
+ <Column field="name" header="任务名称" style="min-width: 150px"></Column>
|
|
|
+ <Column field="message" header="短信内容" style="min-width: 200px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div class="truncate max-w-xs" :title="slotProps.data.message">
|
|
|
+ {{ slotProps.data.message }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="status" header="状态" style="min-width: 100px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <Tag :value="TaskStatus[slotProps.data.status]"
|
|
|
+ :severity="getStatusSeverity(slotProps.data.status)" />
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column header="进度" style="min-width: 200px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div class="space-y-1">
|
|
|
+ <div class="text-sm">
|
|
|
+ {{ slotProps.data.processed }} / {{ slotProps.data.total }}
|
|
|
+ (成功: {{ slotProps.data.successed }})
|
|
|
+ </div>
|
|
|
+ <ProgressBar :value="calculateProgress(slotProps.data)" :showValue="false"
|
|
|
+ style="height: 6px" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="createdAt" header="创建时间" style="min-width: 180px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.createdAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="startedAt" header="开始时间" style="min-width: 180px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.startedAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column header="操作" style="min-width: 250px" frozen alignFrozen="right">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div class="flex gap-1">
|
|
|
+ <Button v-if="canStartTask(slotProps.data)" icon="pi pi-play" severity="success" size="small"
|
|
|
+ text rounded aria-label="开始" v-tooltip.top="'开始'"
|
|
|
+ @click="handleStartTask(slotProps.data)" />
|
|
|
+ <Button v-if="canPauseTask(slotProps.data)" icon="pi pi-pause" severity="warn" size="small" text
|
|
|
+ rounded aria-label="暂停" v-tooltip.top="'暂停'" @click="handlePauseTask(slotProps.data)" />
|
|
|
+ <Button icon="pi pi-eye" severity="info" size="small" text rounded aria-label="详情"
|
|
|
+ v-tooltip.top="'详情'" @click="openTaskDetailDialog(slotProps.data)" v-if="isAdmin" />
|
|
|
+ <Button icon="pi pi-pencil" severity="info" size="small" text rounded aria-label="编辑"
|
|
|
+ v-tooltip.top="'编辑'" @click="openEditTaskDialog(slotProps.data)" />
|
|
|
+ <Button v-if="isAdmin" icon="pi pi-trash" severity="danger" size="small" text rounded
|
|
|
+ aria-label="删除" v-tooltip.top="'删除'" @click="handleDeleteTask(slotProps.data)" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ </DataTable>
|
|
|
+
|
|
|
+ <!-- 创建/编辑任务对话框 -->
|
|
|
+ <Dialog v-model:visible="taskDialog" :modal="true" :header="isEditMode ? '编辑任务' : '创建任务'"
|
|
|
+ :style="{ width: '600px' }" position="center">
|
|
|
+ <Form v-slot="$form" :resolver="taskFormResolver" :initialValues="taskForm" @submit="saveTask"
|
|
|
+ class="p-fluid">
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <InputText id="taskName" name="name" v-model="taskForm.name" fluid />
|
|
|
+ <label for="taskName">任务名称 *</label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.name?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.name.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <Textarea id="taskMessage" name="message" v-model="taskForm.message" rows="4" fluid />
|
|
|
+ <label for="taskMessage">短信内容 *</label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.message?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.message.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <Textarea id="taskRemark" name="remark" v-model="taskForm.remark" rows="2" fluid />
|
|
|
+ <label for="taskRemark">备注</label>
|
|
|
+ </FloatLabel>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4" v-if="!isEditMode">
|
|
|
+ <label class="block mb-2 text-sm">手机号文件 * (.txt格式,每行一个手机号)</label>
|
|
|
+ <FileUpload ref="fileUploadRef" name="file" accept=".txt" :maxFileSize="10000000"
|
|
|
+ @select="onFileSelect" :auto="false" :showUploadButton="false" :showCancelButton="false"
|
|
|
+ chooseLabel="选择文件">
|
|
|
+ <template #empty>
|
|
|
+ <p>拖拽文件到这里上传</p>
|
|
|
+ </template>
|
|
|
+ </FileUpload>
|
|
|
+ <Message v-if="$form.file?.invalid" severity="error" size="small" variant="simple" class="mt-2">
|
|
|
+ {{ $form.file.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex justify-end gap-2 mt-6">
|
|
|
+ <Button label="取消" severity="secondary" type="button" @click="closeTaskDialog"
|
|
|
+ :disabled="taskFormLoading" />
|
|
|
+ <Button label="保存" type="submit" :loading="taskFormLoading" />
|
|
|
+ </div>
|
|
|
+ </Form>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ <!-- 任务详情对话框(任务项列表) -->
|
|
|
+ <Dialog v-model:visible="taskDetailDialog" :modal="true" header="任务详情"
|
|
|
+ :style="{ width: '90vw', maxWidth: '1200px' }" position="center">
|
|
|
+ <div v-if="currentTask" class="mb-4 p-4 bg-[var(--p-surface-50)] rounded-lg">
|
|
|
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
+ <div>
|
|
|
+ <div class="text-sm text-gray-500">任务名称</div>
|
|
|
+ <div class="font-semibold">{{ currentTask.name }}</div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm text-gray-500">状态</div>
|
|
|
+ <Tag :value="TaskStatus[currentTask.status]"
|
|
|
+ :severity="getStatusSeverity(currentTask.status)" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm text-gray-500">总数</div>
|
|
|
+ <div class="font-semibold">{{ currentTask.total }}</div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm text-gray-500">已处理/成功</div>
|
|
|
+ <div class="font-semibold">{{ currentTask.processed }} / {{ currentTask.successed }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mb-4 flex items-center gap-2">
|
|
|
+ <FloatLabel variant="on" class="flex-1">
|
|
|
+ <Select id="itemStatusFilter" v-model="taskItemFilter.status" :options="taskItemStatusOptions"
|
|
|
+ optionLabel="label" optionValue="value" showClear fluid />
|
|
|
+ <label for="itemStatusFilter">任务项状态</label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Button icon="pi pi-search" label="筛选" @click="fetchTaskItems" size="small" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <DataTable :value="taskItemsData.items" :paginator="true"
|
|
|
+ paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink"
|
|
|
+ currentPageReportTemplate="{totalRecords} 条记录" :rows="taskItemsData.size"
|
|
|
+ :totalRecords="taskItemsData.total" @page="handleTaskItemPageChange" lazy scrollable
|
|
|
+ :scrollHeight="'400px'">
|
|
|
+ <Column field="id" header="ID" style="min-width: 80px"></Column>
|
|
|
+ <Column field="target" header="手机号" style="min-width: 150px"></Column>
|
|
|
+ <Column field="status" header="状态" style="min-width: 100px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <Tag :value="TaskItemStatus[slotProps.data.status]" :severity="slotProps.data.status === 'success'
|
|
|
+ ? 'success'
|
|
|
+ : slotProps.data.status === 'failed'
|
|
|
+ ? 'danger'
|
|
|
+ : slotProps.data.status === 'processing'
|
|
|
+ ? 'info'
|
|
|
+ : 'secondary'
|
|
|
+ " />
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="errorMsg" header="错误信息" style="min-width: 200px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div v-if="slotProps.data.errorMsg" class="text-red-500 text-sm">
|
|
|
+ {{ slotProps.data.errorMsg }}
|
|
|
+ </div>
|
|
|
+ <div v-else>-</div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="operatingAt" header="操作时间" style="min-width: 180px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.operatingAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ </DataTable>
|
|
|
+ </Dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.truncate {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保条件框和按钮高度一致 */
|
|
|
+.field {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.field :deep(.p-floatlabel) {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.field :deep(.p-select) {
|
|
|
+ height: auto;
|
|
|
+}
|
|
|
+</style>
|