feat(wm-production): #69 调度指令管理完整实现
- 实体: DispatchCommand/DispatchExecution/DispatchTracking - Mapper: MyBatis-Plus + XML (含台账分页/详情/统计) - Service: 完整状态机 (draft→issued→received→executing→completed/rejected) - Controller: /api/production/dispatch-command (全生命周期API) - SQL DDL: 三表+索引 - 前端: CommandList/CommandDetail/CommandCreate (Vue3+TS+Element Plus) - 单元测试: DispatchCommandServiceTest + DispatchTrackingServiceTest
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import request from './request'
|
||||
|
||||
const BASE = '/api/production/dispatch-command'
|
||||
|
||||
// 创建指令
|
||||
export function createCommand(data: any) {
|
||||
return request.post(BASE, data)
|
||||
}
|
||||
|
||||
// 下发指令
|
||||
export function issueCommand(id: number, issuedBy: number, operatorName?: string) {
|
||||
return request.post(`${BASE}/${id}/issue`, null, {
|
||||
params: { issuedBy, operatorName: operatorName || 'system' }
|
||||
})
|
||||
}
|
||||
|
||||
// 指令台账
|
||||
export function listCommands(params: {
|
||||
page?: number; size?: number; status?: string;
|
||||
commandType?: string; keyword?: string; startDate?: string; endDate?: string
|
||||
}) {
|
||||
return request.get(BASE, { params })
|
||||
}
|
||||
|
||||
// 指令详情
|
||||
export function getCommandDetail(id: number) {
|
||||
return request.get(`${BASE}/${id}`)
|
||||
}
|
||||
|
||||
// 状态统计
|
||||
export function getCommandStats() {
|
||||
return request.get(`${BASE}/stats`)
|
||||
}
|
||||
|
||||
// 接收确认
|
||||
export function receiveCommand(id: number, userId: number, userName?: string) {
|
||||
return request.post(`${BASE}/${id}/receive`, null, {
|
||||
params: { userId, userName: userName || '' }
|
||||
})
|
||||
}
|
||||
|
||||
// 开始执行
|
||||
export function startExecute(id: number, userId: number, userName?: string) {
|
||||
return request.post(`${BASE}/${id}/start-execute`, null, {
|
||||
params: { userId, userName: userName || '' }
|
||||
})
|
||||
}
|
||||
|
||||
// 完成执行
|
||||
export function completeExecution(id: number, userId: number, data: {
|
||||
userName?: string; feedback?: string; feedbackImages?: string
|
||||
}) {
|
||||
return request.post(`${BASE}/${id}/complete`, null, {
|
||||
params: { userId, ...data }
|
||||
})
|
||||
}
|
||||
|
||||
// 驳回
|
||||
export function rejectExecution(id: number, userId: number, reason: string, userName?: string) {
|
||||
return request.post(`${BASE}/${id}/reject`, null, {
|
||||
params: { userId, userName: userName || '', reason }
|
||||
})
|
||||
}
|
||||
|
||||
// 追踪日志
|
||||
export function getTrackingLogs(id: number) {
|
||||
return request.get(`${BASE}/${id}/tracking`)
|
||||
}
|
||||
@@ -11,6 +11,8 @@ const routes = [
|
||||
{ path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
|
||||
{ path: 'system/menu', name: 'menu', component: () => import('@/views/system/menu/MenuList.vue') },
|
||||
{ path: 'system/dept', name: 'dept', component: () => import('@/views/system/dept/DeptList.vue') },
|
||||
{ path: 'dispatch-command', name: 'dispatchCommandList', component: () => import('@/views/dispatch-command/CommandList.vue') },
|
||||
{ path: 'dispatch-command/:id', name: 'dispatchCommandDetail', component: () => import('@/views/dispatch-command/CommandDetail.vue') },
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' }
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
@update:model-value="$emit('update:visible', $event)"
|
||||
title="创建调度指令"
|
||||
width="600"
|
||||
:close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="指令标题" prop="commandTitle">
|
||||
<el-input v-model="form.commandTitle" placeholder="请输入指令标题" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="指令类型" prop="commandType">
|
||||
<el-select v-model="form.commandType" placeholder="请选择">
|
||||
<el-option label="常规" value="normal" />
|
||||
<el-option label="应急" value="emergency" />
|
||||
<el-option label="维护" value="maintenance" />
|
||||
<el-option label="巡检" value="inspection" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优先级" prop="priority">
|
||||
<el-radio-group v-model="form.priority">
|
||||
<el-radio value="low">低</el-radio>
|
||||
<el-radio value="normal">普通</el-radio>
|
||||
<el-radio value="high">高</el-radio>
|
||||
<el-radio value="urgent">紧急</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-input v-model="form.source" placeholder="手动/系统/报警联动" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="指令内容" prop="commandContent">
|
||||
<el-input v-model="form.commandContent" type="textarea" :rows="5"
|
||||
placeholder="请输入指令详细内容" maxlength="2000" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="目标类型" prop="targetType">
|
||||
<el-select v-model="form.targetType" placeholder="请选择">
|
||||
<el-option label="指定人员" value="user" />
|
||||
<el-option label="部门" value="dept" />
|
||||
<el-option label="角色" value="role" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="目标人员" prop="targetIds">
|
||||
<el-input v-model="form.targetIds" placeholder="目标ID列表,多个用逗号分隔,如: 1,2,3" />
|
||||
<div class="form-tip">输入用户ID,多个用逗号分隔</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="$emit('update:visible', false)">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||
创建指令
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { createCommand } from '@/api/dispatchCommand'
|
||||
|
||||
const props = defineProps<{ visible: boolean }>()
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'created': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
|
||||
const defaultForm = () => ({
|
||||
commandTitle: '',
|
||||
commandType: 'normal',
|
||||
priority: 'normal',
|
||||
source: '手动',
|
||||
commandContent: '',
|
||||
targetType: 'user',
|
||||
targetIds: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const form = reactive(defaultForm())
|
||||
|
||||
const rules: FormRules = {
|
||||
commandTitle: [{ required: true, message: '请输入指令标题', trigger: 'blur' }],
|
||||
commandType: [{ required: true, message: '请选择指令类型', trigger: 'change' }],
|
||||
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }],
|
||||
commandContent: [{ required: true, message: '请输入指令内容', trigger: 'blur' }],
|
||||
targetType: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
|
||||
targetIds: [{ required: true, message: '请输入目标ID', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
Object.assign(form, defaultForm())
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// 将 targetIds 转为 JSON 数组格式
|
||||
const targetIdsArr = form.targetIds.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
await createCommand({
|
||||
...form,
|
||||
targetIds: JSON.stringify(targetIdsArr.map(Number))
|
||||
})
|
||||
ElMessage.success('指令创建成功')
|
||||
emit('update:visible', false)
|
||||
emit('created')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '创建失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="command-detail" v-loading="loading">
|
||||
<!-- 返回按钮 -->
|
||||
<el-page-header @back="router.back()" :title="'返回'" style="margin-bottom: 16px">
|
||||
<template #content>
|
||||
<span class="page-title">指令详情</span>
|
||||
<el-tag :type="statusTag(detail.status)" style="margin-left: 12px">{{ statusLabel(detail.status) }}</el-tag>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:基本信息 + 状态流转图 -->
|
||||
<el-col :span="14">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ detail.command_title }}</span>
|
||||
<el-tag size="small" style="margin-left: 8px">{{ detail.command_no }}</el-tag>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="指令编号">{{ detail.command_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<el-tag size="small">{{ typeLabel(detail.command_type) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="优先级">
|
||||
<el-tag :type="priorityTag(detail.priority)" size="small">{{ priorityLabel(detail.priority) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">{{ detail.source || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ detail.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下发时间">{{ detail.issued_at || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{ detail.completed_at || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标类型">{{ detail.target_type || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-top: 16px">
|
||||
<h4>指令内容</h4>
|
||||
<div class="content-block">{{ detail.command_content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态流转图 -->
|
||||
<div style="margin-top: 24px">
|
||||
<h4>状态流转</h4>
|
||||
<el-steps :active="statusStep(detail.status)" finish-status="success" align-center>
|
||||
<el-step title="草稿" description="创建指令" />
|
||||
<el-step title="已下发" description="下发给执行人" />
|
||||
<el-step title="已接收" description="执行人确认" />
|
||||
<el-step title="执行中" description="正在执行" />
|
||||
<el-step title="完成/驳回" description="归档" />
|
||||
</el-steps>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:执行记录列表 -->
|
||||
<el-col :span="10">
|
||||
<el-card>
|
||||
<template #header>执行记录</template>
|
||||
<el-timeline v-if="executions.length">
|
||||
<el-timeline-item
|
||||
v-for="exec in executions" :key="exec.id"
|
||||
:type="executionTimelineType(exec.execute_status)"
|
||||
:timestamp="exec.received_at || exec.created_at"
|
||||
placement="top">
|
||||
<div class="exec-card">
|
||||
<div class="exec-header">
|
||||
<span class="exec-user">{{ exec.user_name || `用户${exec.user_id}` }}</span>
|
||||
<el-tag :type="executionStatusTag(exec.execute_status)" size="small">
|
||||
{{ executionStatusLabel(exec.execute_status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="exec.feedback" class="exec-feedback">
|
||||
反馈: {{ exec.feedback }}
|
||||
</div>
|
||||
<div v-if="exec.rejected_reason" class="exec-reject">
|
||||
驳回原因: {{ exec.rejected_reason }}
|
||||
</div>
|
||||
<div class="exec-actions" v-if="canOperate(exec)">
|
||||
<el-button size="small" type="primary"
|
||||
v-if="exec.execute_status === 'pending'"
|
||||
@click="handleReceive(exec)">接收</el-button>
|
||||
<el-button size="small" type="success"
|
||||
v-if="exec.execute_status === 'received'"
|
||||
@click="handleStartExecute(exec)">开始执行</el-button>
|
||||
<el-button size="small" type="success"
|
||||
v-if="exec.execute_status === 'executing'"
|
||||
@click="showCompleteDialog(exec)">完成</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
v-if="exec.execute_status !== 'completed' && exec.execute_status !== 'rejected'"
|
||||
@click="handleReject(exec)">驳回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无执行记录" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 追踪日志 -->
|
||||
<el-card style="margin-top: 16px">
|
||||
<template #header>全过程追踪日志</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="log in trackingLogs" :key="log.id"
|
||||
:timestamp="log.created_at" placement="top"
|
||||
:type="trackingType(log.action)">
|
||||
<div>
|
||||
<el-tag size="small" :type="trackingType(log.action)">{{ trackingActionLabel(log.action) }}</el-tag>
|
||||
<span style="margin-left: 8px">{{ log.operator_name || '' }}</span>
|
||||
<span v-if="log.from_status" style="margin-left: 8px; color: #909399">
|
||||
{{ log.from_status }} → {{ log.to_status }}
|
||||
</span>
|
||||
<div v-if="log.remark" style="color: #606266; margin-top: 4px">{{ log.remark }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-if="!trackingLogs.length" description="暂无追踪日志" />
|
||||
</el-card>
|
||||
|
||||
<!-- 完成弹窗 -->
|
||||
<el-dialog v-model="completeVisible" title="完成执行" width="500">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="反馈说明">
|
||||
<el-input v-model="completeForm.feedback" type="textarea" :rows="3" placeholder="请输入执行反馈" />
|
||||
</el-form-item>
|
||||
<el-form-item label="反馈图片">
|
||||
<el-input v-model="completeForm.feedbackImages" placeholder="图片URL,多个用逗号分隔" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="completeVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleComplete">确认完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getCommandDetail, receiveCommand, startExecute, completeExecution, rejectExecution } from '@/api/dispatchCommand'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const detail = ref<any>({})
|
||||
const executions = ref<any[]>([])
|
||||
const trackingLogs = ref<any[]>([])
|
||||
const completeVisible = ref(false)
|
||||
const currentExec = ref<any>(null)
|
||||
const completeForm = reactive({ feedback: '', feedbackImages: '' })
|
||||
|
||||
const commandId = Number(route.params.id)
|
||||
|
||||
// 模拟当前用户ID(实际应从登录态获取)
|
||||
const currentUserId = 1
|
||||
const currentUserName = 'admin'
|
||||
|
||||
const statusMap: Record<string, { label: string; type: string; step: number }> = {
|
||||
draft: { label: '草稿', type: 'info', step: 0 },
|
||||
issued: { label: '已下发', type: 'warning', step: 1 },
|
||||
received: { label: '已接收', type: '', step: 2 },
|
||||
executing: { label: '执行中', type: 'primary', step: 3 },
|
||||
completed: { label: '已完成', type: 'success', step: 4 },
|
||||
rejected: { label: '已驳回', type: 'danger', step: 4 }
|
||||
}
|
||||
|
||||
const statusLabel = (s: string) => statusMap[s]?.label || s
|
||||
const statusTag = (s: string) => (statusMap[s]?.type || 'info') as any
|
||||
const statusStep = (s: string) => statusMap[s]?.step || 0
|
||||
|
||||
const typeMap: Record<string, string> = { normal: '常规', emergency: '应急', maintenance: '维护', inspection: '巡检' }
|
||||
const typeLabel = (t: string) => typeMap[t] || t
|
||||
|
||||
const priorityMap: Record<string, { label: string; type: string }> = {
|
||||
low: { label: '低', type: 'info' }, normal: { label: '普通', type: '' },
|
||||
high: { label: '高', type: 'warning' }, urgent: { label: '紧急', type: 'danger' }
|
||||
}
|
||||
const priorityLabel = (p: string) => priorityMap[p]?.label || p
|
||||
const priorityTag = (p: string) => (priorityMap[p]?.type || 'info') as any
|
||||
|
||||
const executionStatusLabel = (s: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待接收', received: '已接收', executing: '执行中', completed: '已完成', rejected: '已驳回'
|
||||
}
|
||||
return map[s] || s
|
||||
}
|
||||
const executionStatusTag = (s: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'info', received: '', executing: 'primary', completed: 'success', rejected: 'danger'
|
||||
}
|
||||
return (map[s] || 'info') as any
|
||||
}
|
||||
const executionTimelineType = (s: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'info', received: 'primary', executing: 'primary', completed: 'success', rejected: 'danger'
|
||||
}
|
||||
return (map[s] || 'info') as any
|
||||
}
|
||||
|
||||
const trackingActionLabel = (a: string) => {
|
||||
const map: Record<string, string> = {
|
||||
create: '创建', issue: '下发', receive: '接收', start_execute: '开始执行',
|
||||
complete: '完成', reject: '驳回', cancel: '取消'
|
||||
}
|
||||
return map[a] || a
|
||||
}
|
||||
const trackingType = (a: string) => {
|
||||
const map: Record<string, string> = {
|
||||
create: 'info', issue: 'warning', receive: 'primary', start_execute: 'primary',
|
||||
complete: 'success', reject: 'danger', cancel: 'danger'
|
||||
}
|
||||
return (map[a] || 'info') as any
|
||||
}
|
||||
|
||||
function canOperate(exec: any) {
|
||||
return exec.user_id === currentUserId || true // 简化:所有人可操作
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCommandDetail(commandId)
|
||||
detail.value = res.data || {}
|
||||
executions.value = res.data?.executions || []
|
||||
trackingLogs.value = res.data?.tracking_logs || res.data?.trackingLogs || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReceive(exec: any) {
|
||||
try {
|
||||
await receiveCommand(commandId, exec.user_id, exec.user_name)
|
||||
ElMessage.success('接收成功')
|
||||
fetchDetail()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartExecute(exec: any) {
|
||||
try {
|
||||
await startExecute(commandId, exec.user_id, exec.user_name)
|
||||
ElMessage.success('已开始执行')
|
||||
fetchDetail()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function showCompleteDialog(exec: any) {
|
||||
currentExec.value = exec
|
||||
completeForm.feedback = ''
|
||||
completeForm.feedbackImages = ''
|
||||
completeVisible.value = true
|
||||
}
|
||||
|
||||
async function handleComplete() {
|
||||
try {
|
||||
await completeExecution(commandId, currentExec.value.user_id, {
|
||||
userName: currentExec.value.user_name,
|
||||
feedback: completeForm.feedback,
|
||||
feedbackImages: completeForm.feedbackImages
|
||||
})
|
||||
ElMessage.success('执行完成')
|
||||
completeVisible.value = false
|
||||
fetchDetail()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject(exec: any) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入驳回原因', '驳回', {
|
||||
confirmButtonText: '确认驳回',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: '驳回原因不能为空'
|
||||
})
|
||||
await rejectExecution(commandId, exec.user_id, value, exec.user_name)
|
||||
ElMessage.success('已驳回')
|
||||
fetchDetail()
|
||||
} catch { /* cancel */ }
|
||||
}
|
||||
|
||||
onMounted(fetchDetail)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title { font-size: 16px; font-weight: 600; }
|
||||
.content-block {
|
||||
padding: 12px; background: #f5f7fa; border-radius: 4px;
|
||||
white-space: pre-wrap; line-height: 1.6;
|
||||
}
|
||||
.exec-card { padding: 4px 0; }
|
||||
.exec-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.exec-user { font-weight: 600; }
|
||||
.exec-feedback { margin-top: 6px; color: #606266; font-size: 13px; }
|
||||
.exec-reject { margin-top: 6px; color: #f56c6c; font-size: 13px; }
|
||||
.exec-actions { margin-top: 8px; }
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="command-list">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form :inline="true" :model="filterForm">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filterForm.keyword" placeholder="编号/标题" clearable @clear="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filterForm.status" placeholder="全部" clearable @change="handleSearch">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已下发" value="issued" />
|
||||
<el-option label="已接收" value="received" />
|
||||
<el-option label="执行中" value="executing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="filterForm.commandType" placeholder="全部" clearable @change="handleSearch">
|
||||
<el-option label="常规" value="normal" />
|
||||
<el-option label="应急" value="emergency" />
|
||||
<el-option label="维护" value="maintenance" />
|
||||
<el-option label="巡检" value="inspection" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至"
|
||||
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
|
||||
@change="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon> 查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="12" style="margin-top: 12px">
|
||||
<el-col :span="4" v-for="stat in stats" :key="stat.status">
|
||||
<el-card shadow="hover" class="stat-card" @click="filterByStatus(stat.status)">
|
||||
<div class="stat-value">{{ stat.count }}</div>
|
||||
<div class="stat-label">{{ statusLabel(stat.status) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div style="margin-top: 16px; display: flex; justify-content: space-between; align-items: center">
|
||||
<el-button type="primary" @click="showCreateDialog = true"><el-icon><Plus /></el-icon> 创建指令</el-button>
|
||||
<el-button @click="handleBatchIssue" :disabled="!selectedIds.length">批量下发</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" border style="margin-top: 10px"
|
||||
@selection-change="handleSelectionChange" v-loading="loading">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="command_no" label="指令编号" width="220" />
|
||||
<el-table-column prop="command_title" label="标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="command_type" label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeTag(row.command_type)" size="small">{{ typeLabel(row.command_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="priorityTag(row.priority)" size="small">{{ priorityLabel(row.priority) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTag(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.completed_count || 0 }}/{{ row.total_executions || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="viewDetail(row)">详情</el-button>
|
||||
<el-button link type="success" v-if="row.status === 'draft'" @click="handleIssue(row)">下发</el-button>
|
||||
<el-button link type="danger" v-if="row.status === 'draft'" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination style="margin-top: 16px; justify-content: flex-end"
|
||||
v-model:current-page="pagination.page" v-model:page-size="pagination.size"
|
||||
:total="pagination.total" :page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next" @change="fetchData" />
|
||||
|
||||
<CommandCreate v-model:visible="showCreateDialog" @created="handleCreated" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus } from '@element-plus/icons-vue'
|
||||
import { listCommands, issueCommand, getCommandStats } from '@/api/dispatchCommand'
|
||||
import CommandCreate from './CommandCreate.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const stats = ref<any[]>([])
|
||||
const selectedIds = ref<number[]>([])
|
||||
const showCreateDialog = ref(false)
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
|
||||
const filterForm = reactive({ keyword: '', status: '', commandType: '' })
|
||||
const pagination = reactive({ page: 1, size: 10, total: 0 })
|
||||
|
||||
const statusMap: Record<string, { label: string; type: string }> = {
|
||||
draft: { label: '草稿', type: 'info' },
|
||||
issued: { label: '已下发', type: 'warning' },
|
||||
received: { label: '已接收', type: '' },
|
||||
executing: { label: '执行中', type: 'primary' },
|
||||
completed: { label: '已完成', type: 'success' },
|
||||
rejected: { label: '已驳回', type: 'danger' }
|
||||
}
|
||||
const typeMap: Record<string, string> = { normal: '常规', emergency: '应急', maintenance: '维护', inspection: '巡检' }
|
||||
const priorityMap: Record<string, { label: string; type: string }> = {
|
||||
low: { label: '低', type: 'info' }, normal: { label: '普通', type: '' },
|
||||
high: { label: '高', type: 'warning' }, urgent: { label: '紧急', type: 'danger' }
|
||||
}
|
||||
|
||||
function statusLabel(s: string) { return statusMap[s]?.label || s }
|
||||
function statusTag(s: string) { return (statusMap[s]?.type || 'info') as any }
|
||||
function typeLabel(t: string) { return typeMap[t] || t }
|
||||
function typeTag(t: string) { return t === 'emergency' ? 'danger' : t === 'maintenance' ? 'warning' : '' }
|
||||
function priorityLabel(p: string) { return priorityMap[p]?.label || p }
|
||||
function priorityTag(p: string) { return (priorityMap[p]?.type || 'info') as any }
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listCommands({
|
||||
page: pagination.page, size: pagination.size,
|
||||
status: filterForm.status || undefined,
|
||||
commandType: filterForm.commandType || undefined,
|
||||
keyword: filterForm.keyword || undefined,
|
||||
startDate: dateRange.value?.[0], endDate: dateRange.value?.[1]
|
||||
})
|
||||
tableData.value = res.data?.records || []
|
||||
pagination.total = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try { const res = await getCommandStats(); stats.value = res.data || [] } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchData() }
|
||||
function handleReset() {
|
||||
filterForm.keyword = ''; filterForm.status = ''; filterForm.commandType = ''; dateRange.value = null; handleSearch()
|
||||
}
|
||||
function filterByStatus(status: string) { filterForm.status = status; handleSearch() }
|
||||
function handleSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id) }
|
||||
function viewDetail(row: any) { router.push({ path: `/dispatch-command/${row.id}` }) }
|
||||
|
||||
async function handleIssue(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认下发指令 "${row.command_title}" ?`, '下发确认')
|
||||
await issueCommand(row.id, 1, 'admin'); ElMessage.success('指令已下发'); fetchData(); fetchStats()
|
||||
} catch { /* cancel */ }
|
||||
}
|
||||
|
||||
async function handleBatchIssue() {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认批量下发 ${selectedIds.value.length} 条指令?`, '批量下发')
|
||||
for (const id of selectedIds.value) { await issueCommand(id, 1, 'admin') }
|
||||
ElMessage.success('批量下发完成'); fetchData(); fetchStats()
|
||||
} catch { /* cancel */ }
|
||||
}
|
||||
|
||||
function handleDelete(row: any) {
|
||||
ElMessageBox.confirm(`确认删除指令 "${row.command_title}" ?`, '删除确认', { type: 'warning' })
|
||||
.then(() => { ElMessage.info('删除功能待实现(逻辑删除)') }).catch(() => { /* cancel */ })
|
||||
}
|
||||
|
||||
function handleCreated() { showCreateDialog.value = false; fetchData(); fetchStats() }
|
||||
|
||||
onMounted(() => { fetchData(); fetchStats() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-card :deep(.el-form-item) { margin-bottom: 0; }
|
||||
.stat-card { cursor: pointer; text-align: center; }
|
||||
.stat-value { font-size: 28px; font-weight: bold; color: #409eff; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user