فهرست منبع

添加短信任务管理功能,包括任务创建、更新、查询和删除API,新增短信任务视图及相关路由,更新用户角色和菜单权限。

wuyi 4 روز پیش
والد
کامیت
c84b694eb7
7فایلهای تغییر یافته به همراه787 افزوده شده و 28 حذف شده
  1. 24 5
      src/enums/index.js
  2. 2 0
      src/main.js
  3. 6 0
      src/router/index.js
  4. 53 0
      src/services/api.js
  5. 6 1
      src/views/MainView.vue
  6. 655 0
      src/views/SmsTaskView.vue
  7. 41 22
      src/views/UserView.vue

+ 24 - 5
src/enums/index.js

@@ -1,10 +1,7 @@
 export const UserRole = {
-  user: '普通用户',
   admin: '管理员',
-  channel: '渠道',
-  operator: '运营',
-  mss: 'MSS',
-  show: 'SHOW'
+  manager: '运营',
+  user: '普通用户'
 }
 
 export const ConfigType = {
@@ -31,3 +28,25 @@ export const PlatformType = {
   iOS: 'iOS',
   PC: 'PC'
 }
+
+// SMS 任务状态
+export const TaskStatus = {
+  idle: '未发送',
+  pending: '待发送',
+  running: '发送中',
+  paused: '已暂停',
+  queued: '队列中',
+  scheduled: '已计划',
+  completed: '已完成',
+  error: '错误'
+}
+
+// SMS 任务项状态
+export const TaskItemStatus = {
+  idle: '未发送',
+  pending: '待发送',
+  waiting: '等待中',
+  processing: '发送中',
+  success: '发送成功',
+  failed: '发送失败'
+}

+ 2 - 0
src/main.js

@@ -6,6 +6,7 @@ import { useUserStore } from '@/stores/user'
 import PrimeVue from 'primevue/config'
 import ToastService from 'primevue/toastservice'
 import ConfirmService from 'primevue/confirmationservice'
+import Tooltip from 'primevue/tooltip'
 import Aura from '@primeuix/themes/aura'
 import 'primeicons/primeicons.css'
 
@@ -19,6 +20,7 @@ app.use(router)
 app.use(PrimeVue, { ripple: true, theme: { preset: Aura } })
 app.use(ToastService)
 app.use(ConfirmService)
+app.directive('tooltip', Tooltip)
 
 app.provide(
   'isAdmin',

+ 6 - 0
src/router/index.js

@@ -27,6 +27,12 @@ const router = createRouter({
           component: DashboardView,
           meta: { roles: ['admin', 'manager', 'user'] }
         },
+        {
+          path: 'sms-task',
+          name: 'sms-task',
+          component: () => import('@/views/SmsTaskView.vue'),
+          meta: { roles: ['admin', 'manager', 'user'] }
+        },
         {
           path: 'user',
           name: 'user',

+ 53 - 0
src/services/api.js

@@ -134,3 +134,56 @@ export const downloadFile = async (key) => {
   )
   return response.data
 }
+
+// SMS 任务相关API
+// 创建 SMS 任务
+export const createSmsTask = async (formData) => {
+  const response = await api.post('/sms-tasks/create', formData, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+  return response.data
+}
+
+// 查询任务列表
+export const listSmsTasks = async (params) => {
+  const response = await api.get('/sms-tasks/list', { params })
+  return response.data
+}
+
+// 查询单个任务
+export const getSmsTask = async (id) => {
+  const response = await api.get(`/sms-tasks/${id}`)
+  return response.data
+}
+
+// 更新任务
+export const updateSmsTask = async (data) => {
+  const response = await api.post('/sms-tasks/update', data)
+  return response.data
+}
+
+// 开始任务
+export const startSmsTask = async (id) => {
+  const response = await api.post(`/sms-tasks/${id}/start`)
+  return response.data
+}
+
+// 暂停任务
+export const pauseSmsTask = async (id) => {
+  const response = await api.post(`/sms-tasks/${id}/pause`)
+  return response.data
+}
+
+// 删除任务(需要管理员权限)
+export const deleteSmsTask = async (id) => {
+  const response = await api.get(`/sms-tasks/${id}/delete`)
+  return response.data
+}
+
+// 查询任务项列表(需要管理员权限)
+export const listSmsTaskItems = async (params) => {
+  const response = await api.get('/sms-tasks/items', { params })
+  return response.data
+}

+ 6 - 1
src/views/MainView.vue

@@ -37,6 +37,12 @@ const allNavItems = [
     name: 'dashboard',
     roles: ['admin', 'manager', 'user']
   },
+  {
+    label: '短信任务',
+    icon: 'pi pi-fw pi-send',
+    name: 'sms-task',
+    roles: ['admin', 'manager', 'user']
+  },
   {
     label: '用户管理',
     icon: 'pi pi-fw pi-user',
@@ -51,7 +57,6 @@ const allNavItems = [
   }
 ]
 
-// 根据用户角色过滤菜单项
 const navItems = computed(() => {
   const userRole = userStore.userInfo?.role
   if (!userRole) return []

+ 655 - 0
src/views/SmsTaskView.vue

@@ -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>

+ 41 - 22
src/views/UserView.vue

@@ -8,7 +8,7 @@ import Button from 'primevue/button'
 import Column from 'primevue/column'
 import DataTable from 'primevue/datatable'
 import Dialog from 'primevue/dialog'
-import Dropdown from 'primevue/dropdown'
+import Select from 'primevue/select'
 import FloatLabel from 'primevue/floatlabel'
 import IconField from 'primevue/iconfield'
 import InputIcon from 'primevue/inputicon'
@@ -170,6 +170,38 @@ onMounted(() => {
 
 <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">
+          <IconField>
+            <InputIcon>
+              <i class="pi pi-search" />
+            </InputIcon>
+            <InputText v-model="search" placeholder="搜索" fluid />
+          </IconField>
+        </div>
+
+        <!-- 左侧按钮组 -->
+        <div class="flex items-center gap-2 flex-nowrap">
+          <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="openNewUserDialog"
+            label="新增用户"
+            severity="success"
+            size="small"
+          />
+        </div>
+      </div>
+    </div>
+
+    <!-- 数据表格 -->
     <DataTable
       :value="tableData.content"
       :paginator="true"
@@ -182,26 +214,6 @@ onMounted(() => {
       lazy
       scrollable
     >
-      <template #header>
-        <div class="flex flex-wrap items-center">
-          <Button icon="pi pi-refresh" @click="fetchData" label="刷新" size="small" />
-          <Button
-            icon="pi pi-plus"
-            @click="openNewUserDialog"
-            label="新增用户"
-            severity="success"
-            size="small"
-            class="ml-2"
-          />
-          <div class="flex-1"></div>
-          <IconField>
-            <InputIcon>
-              <i class="pi pi-search" />
-            </InputIcon>
-            <InputText v-model="search" placeholder="搜素" />
-          </IconField>
-        </div>
-      </template>
       <Column field="id" header="ID"></Column>
       <Column field="name" header="用户名"></Column>
       <Column field="role" header="角色">
@@ -299,7 +311,7 @@ onMounted(() => {
 
         <div class="field mt-4">
           <FloatLabel variant="on">
-            <Dropdown
+            <Select
               id="role"
               name="role"
               v-model="userForm.role"
@@ -329,3 +341,10 @@ onMounted(() => {
     </Dialog>
   </div>
 </template>
+
+<style scoped>
+/* 确保操作栏元素高度一致 */
+.overflow-x-auto :deep(.p-inputtext) {
+  height: auto;
+}
+</style>