feat(delivery): 新增插件搬运任务列表面板
- 新增 delivery 的 api/types 与 mock 数据,对接 SimpleLite /projection/deliveries(取消 / 重发 / 强制完成) - 新增 workbench/MissionListPanel 展示搬运任务,支持按完成 / 中止状态过滤 - components.d.ts 自动注册 MissionListPanel
This commit is contained in:
@@ -74,6 +74,7 @@ declare module 'vue' {
|
||||
FloatingAlarmCard: typeof import('./src/components/map-monitor/FloatingAlarmCard.vue')['default']
|
||||
FloatingAlarmStack: typeof import('./src/components/map-monitor/FloatingAlarmStack.vue')['default']
|
||||
MapMonitorConfigGroup: typeof import('./src/components/config/MapMonitorConfigGroup.vue')['default']
|
||||
MissionListPanel: typeof import('./src/components/workbench/MissionListPanel.vue')['default']
|
||||
MonitorSelectionPanel: typeof import('./src/components/workbench/MonitorSelectionPanel.vue')['default']
|
||||
PermissionGuard: typeof import('./src/components/PermissionGuard.vue')['default']
|
||||
PluginListPanel: typeof import('./src/components/reflection/PluginListPanel.vue')['default']
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import http from './http'
|
||||
import type { DeliveryTask } from '@/types/delivery'
|
||||
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
const BASE = '/sl/projection/deliveries'
|
||||
|
||||
export async function listDeliveries(opts?: {
|
||||
includeFinished?: boolean
|
||||
includeAborted?: boolean
|
||||
}): Promise<DeliveryTask[]> {
|
||||
if (MOCK) {
|
||||
const { mockDeliveries } = await import('@/mock/data/deliveries')
|
||||
return mockDeliveries()
|
||||
}
|
||||
const params: Record<string, string> = {}
|
||||
if (opts?.includeFinished === false) params.includeFinished = 'false'
|
||||
if (opts?.includeAborted === false) params.includeAborted = 'false'
|
||||
const { data } = await http.get<DeliveryTask[]>(BASE, { params })
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
export async function cancelDelivery(id: number): Promise<void> {
|
||||
if (MOCK) return
|
||||
await http.post(`${BASE}/${id}/cancel`)
|
||||
}
|
||||
|
||||
export async function resendDelivery(id: number): Promise<void> {
|
||||
if (MOCK) return
|
||||
await http.post(`${BASE}/${id}/resend`)
|
||||
}
|
||||
|
||||
export async function forceCompleteDelivery(id: number): Promise<void> {
|
||||
if (MOCK) return
|
||||
await http.post(`${BASE}/${id}/force-complete`)
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="mission-list-panel">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="search"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索任务 ID / 站点 / 车辆"
|
||||
class="search"
|
||||
/>
|
||||
<el-checkbox v-model="showFinished" size="small" @change="emitRefresh">
|
||||
含已完成
|
||||
</el-checkbox>
|
||||
<el-checkbox v-model="showAborted" size="small" @change="emitRefresh">
|
||||
含取消/异常
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredRows"
|
||||
size="small"
|
||||
stripe
|
||||
highlight-current-row
|
||||
:row-class-name="rowClassName"
|
||||
height="100%"
|
||||
class="delivery-table"
|
||||
table-layout="fixed"
|
||||
@row-click="onRowClick"
|
||||
@row-contextmenu="onRowContextMenu"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="40" align="center" />
|
||||
<el-table-column label="起点" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="cell-ellipsis" :title="row.srcLabel">{{ shortSite(row.srcLabel) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="终点" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="cell-ellipsis" :title="row.dstLabel">{{ shortSite(row.dstLabel) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="52" align="center">
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="status-pill"
|
||||
:class="`status-pill--${statusTagType(row.statusCode)}`"
|
||||
:title="row.status"
|
||||
>
|
||||
{{ shortStatus(row.status) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车辆" width="56" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.carName" class="cell-ellipsis" :title="row.carName">
|
||||
{{ shortCar(row.carName) }}
|
||||
</span>
|
||||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="!filteredRows.length" class="empty muted">暂无搬运任务</div>
|
||||
|
||||
<div
|
||||
v-show="ctxVisible"
|
||||
class="ctx-menu"
|
||||
:style="{ left: `${ctxX}px`, top: `${ctxY}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ctx-item"
|
||||
:disabled="!canCancel(ctxRow)"
|
||||
@click="runAction('cancel')"
|
||||
>
|
||||
取消任务
|
||||
</button>
|
||||
<button type="button" class="ctx-item" @click="runAction('resend')">
|
||||
重发任务
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ctx-item"
|
||||
:disabled="!canForceComplete(ctxRow)"
|
||||
@click="runAction('force-complete')"
|
||||
>
|
||||
强制完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { DeliveryTask, DeliveryAction } from '@/types/delivery'
|
||||
import {
|
||||
cancelDelivery,
|
||||
forceCompleteDelivery,
|
||||
resendDelivery
|
||||
} from '@/api/delivery'
|
||||
|
||||
const props = defineProps<{
|
||||
deliveries: DeliveryTask[]
|
||||
selectedId?: number | null
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [task: DeliveryTask]
|
||||
refresh: [opts: { includeFinished: boolean; includeAborted: boolean }]
|
||||
actionDone: []
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const showFinished = ref(true)
|
||||
const showAborted = ref(true)
|
||||
|
||||
const ctxVisible = ref(false)
|
||||
const ctxX = ref(0)
|
||||
const ctxY = ref(0)
|
||||
const ctxRow = ref<DeliveryTask | null>(null)
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
const tokens = q ? q.split(/\s+/).filter(Boolean) : []
|
||||
return props.deliveries.filter((row) => {
|
||||
if (!tokens.length) return true
|
||||
const hay = [
|
||||
String(row.id),
|
||||
row.srcLabel,
|
||||
row.dstLabel,
|
||||
row.status,
|
||||
row.carName ?? '',
|
||||
row.missionName
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
return tokens.every((t) => hay.includes(t))
|
||||
})
|
||||
})
|
||||
|
||||
type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary'
|
||||
|
||||
/** 站点列:优先显示名称段,完整内容靠 tooltip */
|
||||
function shortSite(label: string, maxLen = 9): string {
|
||||
const idx = label.indexOf('-')
|
||||
const name = idx >= 0 ? label.slice(idx + 1) : label
|
||||
if (name.length <= maxLen) return name
|
||||
return `${name.slice(0, maxLen)}…`
|
||||
}
|
||||
|
||||
function shortStatus(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
已下发: '待发',
|
||||
执行中: '执行',
|
||||
已完成: '完成',
|
||||
已取消: '取消',
|
||||
强制结束: '强结',
|
||||
任务异常: '异常'
|
||||
}
|
||||
return map[status] ?? (status.length > 4 ? `${status.slice(0, 3)}…` : status)
|
||||
}
|
||||
|
||||
function shortCar(name: string, maxLen = 6): string {
|
||||
if (name.length <= maxLen) return name
|
||||
return `${name.slice(0, maxLen)}…`
|
||||
}
|
||||
|
||||
function statusTagType(code: string): TagType {
|
||||
switch (code) {
|
||||
case 'Fetching':
|
||||
case 'Putting':
|
||||
return 'success'
|
||||
case 'Waiting':
|
||||
return 'primary'
|
||||
case 'Finished':
|
||||
return 'info'
|
||||
case 'Canceled':
|
||||
case 'Terminated':
|
||||
return 'warning'
|
||||
case 'Error':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
function rowClassName({ row }: { row: DeliveryTask }) {
|
||||
const classes: string[] = []
|
||||
if (row.id === props.selectedId) classes.push('is-selected-row')
|
||||
if (row.overdue) classes.push('is-overdue-row')
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
function isTerminal(code: string) {
|
||||
return ['Finished', 'Canceled', 'Terminated', 'Error'].includes(code)
|
||||
}
|
||||
|
||||
function canCancel(row: DeliveryTask | null) {
|
||||
return row != null && !isTerminal(row.statusCode)
|
||||
}
|
||||
|
||||
function canForceComplete(row: DeliveryTask | null) {
|
||||
return row != null && !isTerminal(row.statusCode)
|
||||
}
|
||||
|
||||
function emitRefresh() {
|
||||
emit('refresh', {
|
||||
includeFinished: showFinished.value,
|
||||
includeAborted: showAborted.value
|
||||
})
|
||||
}
|
||||
|
||||
function onRowClick(row: DeliveryTask) {
|
||||
hideCtx()
|
||||
emit('select', row)
|
||||
}
|
||||
|
||||
function onRowContextMenu(row: DeliveryTask, _col: unknown, e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
ctxRow.value = row
|
||||
ctxX.value = e.clientX
|
||||
ctxY.value = e.clientY
|
||||
ctxVisible.value = true
|
||||
}
|
||||
|
||||
function hideCtx() {
|
||||
ctxVisible.value = false
|
||||
ctxRow.value = null
|
||||
}
|
||||
|
||||
const actionLabels: Record<DeliveryAction, string> = {
|
||||
cancel: '取消任务',
|
||||
resend: '重发任务',
|
||||
'force-complete': '强制完成'
|
||||
}
|
||||
|
||||
async function runAction(action: DeliveryAction) {
|
||||
const row = ctxRow.value
|
||||
hideCtx()
|
||||
if (!row) return
|
||||
|
||||
if (action === 'cancel' && !canCancel(row)) return
|
||||
if (action === 'force-complete' && !canForceComplete(row)) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定对任务 #${row.id}(${row.srcLabel} → ${row.dstLabel})执行「${actionLabels[action]}」?`,
|
||||
'确认操作',
|
||||
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'cancel') await cancelDelivery(row.id)
|
||||
else if (action === 'resend') await resendDelivery(row.id)
|
||||
else await forceCompleteDelivery(row.id)
|
||||
ElMessage.success(`${actionLabels[action]} 已提交`)
|
||||
emit('actionDone')
|
||||
} catch (err) {
|
||||
ElMessage.error(`${actionLabels[action]} 失败:${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function onDocClick() {
|
||||
hideCtx()
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', onDocClick))
|
||||
onUnmounted(() => document.removeEventListener('click', onDocClick))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mission-list-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.toolbar .search {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.delivery-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mission-list-panel :deep(.delivery-table .el-table__inner-wrapper),
|
||||
.mission-list-panel :deep(.delivery-table .el-table__header-wrapper),
|
||||
.mission-list-panel :deep(.delivery-table .el-table__body-wrapper) {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
.mission-list-panel :deep(.delivery-table table) {
|
||||
table-layout: fixed;
|
||||
width: 100% !important;
|
||||
}
|
||||
.mission-list-panel :deep(.delivery-table th.el-table__cell),
|
||||
.mission-list-panel :deep(.delivery-table td.el-table__cell) {
|
||||
padding: 3px 2px;
|
||||
}
|
||||
.mission-list-panel :deep(.delivery-table .cell) {
|
||||
padding: 0 2px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.mission-list-panel :deep(.delivery-table th .cell) {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-ellipsis {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
padding: 0 3px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.status-pill--success { color: var(--mg-status-success); }
|
||||
.status-pill--primary { color: var(--mg-status-info); }
|
||||
.status-pill--warning { color: var(--mg-status-warning); }
|
||||
.status-pill--danger { color: var(--mg-status-danger); }
|
||||
.status-pill--info { color: var(--mg-text-muted); }
|
||||
|
||||
.mission-list-panel :deep(.el-table),
|
||||
.mission-list-panel :deep(.el-table th.el-table__cell),
|
||||
.mission-list-panel :deep(.el-table .cell) {
|
||||
color: var(--mg-text-light);
|
||||
background: transparent;
|
||||
}
|
||||
.mission-list-panel :deep(.el-table tr) {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.mission-list-panel :deep(.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mission-list-panel :deep(.is-selected-row) td {
|
||||
background: rgba(109, 40, 217, 0.35) !important;
|
||||
}
|
||||
.mission-list-panel :deep(.is-overdue-row) td {
|
||||
color: var(--mg-status-danger) !important;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--mg-text-dim);
|
||||
}
|
||||
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
z-index: 9000;
|
||||
min-width: 128px;
|
||||
padding: 4px 0;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.ctx-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ctx-item:hover:not(:disabled) {
|
||||
background: rgba(var(--mg-accent-rgb, 142, 200, 252), 0.2);
|
||||
}
|
||||
.ctx-item:disabled {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { DeliveryTask } from '@/types/delivery'
|
||||
|
||||
export async function mockDeliveries(): Promise<DeliveryTask[]> {
|
||||
await new Promise((r) => setTimeout(r, 60))
|
||||
return [
|
||||
{
|
||||
id: 101,
|
||||
missionId: 1,
|
||||
missionName: '链式搬运',
|
||||
missionTypeName: 'FengTianChainedDeliveryMission',
|
||||
srcSiteId: 1,
|
||||
srcLabel: '1-取货台 A',
|
||||
dstSiteId: 8,
|
||||
dstLabel: '8-放货台 B',
|
||||
status: '执行中',
|
||||
statusCode: 'Fetching',
|
||||
carId: 1,
|
||||
carName: 'AGV-01',
|
||||
priority: 1,
|
||||
createTime: new Date().toISOString(),
|
||||
overdue: false
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
missionId: 1,
|
||||
missionName: '链式搬运',
|
||||
missionTypeName: 'FengTianChainedDeliveryMission',
|
||||
srcSiteId: 3,
|
||||
srcLabel: '3-缓存区',
|
||||
dstSiteId: 12,
|
||||
dstLabel: '12-工位 W2',
|
||||
status: '已下发',
|
||||
statusCode: 'Waiting',
|
||||
carId: null,
|
||||
carName: null,
|
||||
priority: 0,
|
||||
createTime: new Date(Date.now() - 45 * 60_000).toISOString(),
|
||||
overdue: true
|
||||
},
|
||||
{
|
||||
id: 99,
|
||||
missionId: 1,
|
||||
missionName: '链式搬运',
|
||||
missionTypeName: 'FengTianChainedDeliveryMission',
|
||||
srcSiteId: 2,
|
||||
srcLabel: '2-原料区',
|
||||
dstSiteId: 5,
|
||||
dstLabel: '5-成品区',
|
||||
status: '已完成',
|
||||
statusCode: 'Finished',
|
||||
carId: 2,
|
||||
carName: 'AGV-02',
|
||||
priority: 1,
|
||||
createTime: new Date(Date.now() - 3600_000).toISOString(),
|
||||
overdue: false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** 插件搬运任务(AbstractChainedDeliveryMission.GetDeliveries)投影行 */
|
||||
export interface DeliveryTask {
|
||||
id: number
|
||||
missionId: number
|
||||
missionName: string
|
||||
missionTypeName: string
|
||||
srcSiteId: number
|
||||
srcLabel: string
|
||||
dstSiteId: number
|
||||
dstLabel: string
|
||||
status: string
|
||||
statusCode: string
|
||||
carId?: number | null
|
||||
carName?: string | null
|
||||
priority: number
|
||||
createTime?: string | null
|
||||
overdue?: boolean
|
||||
}
|
||||
|
||||
export type DeliveryAction = 'cancel' | 'resend' | 'force-complete'
|
||||
Reference in New Issue
Block a user