- 实体: 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
305 lines
12 KiB
Vue
305 lines
12 KiB
Vue
<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>
|