- 实体: 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
194 lines
9.0 KiB
Vue
194 lines
9.0 KiB
Vue
<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>
|