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:
2026-06-14 15:30:52 +08:00
parent 21fa7cffd2
commit 6c6db59ba9
18 changed files with 1715 additions and 0 deletions
@@ -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>