feat: AGV排队管控
This commit is contained in:
parent
d4b629cc25
commit
41729665b5
|
|
@ -1,3 +1,3 @@
|
|||
# 开发环境 — 留空,走 vite.config.ts 中的 proxy
|
||||
VITE_API_BASE=
|
||||
VITE_API_BASE=http://localhost:8114
|
||||
VITE_ENTRY_OPTIONS=ops,monitor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export interface MachineStationQueueVO {
|
||||
warehouse: string
|
||||
stationCode: string
|
||||
queueSize: number
|
||||
configuredMaxCapacity: number | null
|
||||
effectiveMaxCapacity: number
|
||||
queuedChainCodes: string[]
|
||||
}
|
||||
|
||||
export interface MachineStationQueueMaxUpdateDTO {
|
||||
warehouse: string
|
||||
stationCode: string
|
||||
maxCapacity: number
|
||||
}
|
||||
|
||||
export function listStationQueues(warehouse: string) {
|
||||
return request.get<never, { code: number; data: MachineStationQueueVO[] }>(
|
||||
'/api/robot/external/admin/station-queue/list',
|
||||
{ params: { warehouse } },
|
||||
)
|
||||
}
|
||||
|
||||
/** 固定工作站编码(排除 A1、A2、A5) */
|
||||
export function listStationCodes(warehouse: string) {
|
||||
return request.get<never, { code: number; data: string[] }>(
|
||||
'/api/robot/external/admin/station-queue/station-codes',
|
||||
{ params: { warehouse } },
|
||||
)
|
||||
}
|
||||
|
||||
export function getStationQueueSnapshot(warehouse: string, stationCode: string) {
|
||||
return request.get<never, { code: number; data: MachineStationQueueVO }>(
|
||||
'/api/robot/external/admin/station-queue/snapshot',
|
||||
{ params: { warehouse, stationCode } },
|
||||
)
|
||||
}
|
||||
|
||||
export function updateStationQueueMax(payload: MachineStationQueueMaxUpdateDTO) {
|
||||
return request.post<never, { code: number; data: boolean }>(
|
||||
'/api/robot/external/admin/station-queue/max',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
|
@ -246,6 +246,7 @@ export const callbackScenarioMap: Record<string, CallbackScenario[]> = {
|
|||
HK_OUTBOUND_WAIT: [hkArriveWcs, hkArriveStation],
|
||||
HK_ARRIVE_STATION: [hkArriveStation],
|
||||
HK_PICK_PALLET: [{ key: 'pickPalletDone', label: 'AGV完成取货', url: '/api/robot/external/hk/pickPalletDone', fields: hkFields }],
|
||||
HK_PICK_UP: [{ key: 'pickUp', label: '海康举起托盘', url: '/api/robot/external/hk/pickUp', fields: hkFields }],
|
||||
HK_PUT_PALLET: [{ key: 'putPalletDone', label: 'AGV完成放货', url: '/api/robot/external/hk/putPalletDone', fields: hkFields }],
|
||||
HK_LEAVE_START_POINT: [{ key: 'leaveStartPoint', label: 'AGV离开起点', url: '/api/robot/external/hk/leaveStartPoint', fields: hkFields }],
|
||||
ZS_INBOUND: [{ key: 'taskInfoBack', label: '任务状态回传', url: '/api/robot/external/zs/wms/taskInfoBack', fields: zsTaskInfoBackFields }],
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
<el-icon><SetUp /></el-icon>
|
||||
<template #title>提升机查看</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/station/queue">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<template #title>站点排队</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="!isMonitorMode" index="/vendor/load">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
<template #title>厂商负载</template>
|
||||
|
|
@ -164,7 +168,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useDevice } from '@/composables/useDevice'
|
||||
import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh } from '@element-plus/icons-vue'
|
||||
import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh, Tickets } from '@element-plus/icons-vue'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
|
||||
const { isMobile } = useDevice()
|
||||
|
|
@ -195,9 +199,17 @@ const userStore = useUserStore()
|
|||
const isMonitorMode = computed(() => userStore.isMonitorMode())
|
||||
const keepAliveNames = computed(() => {
|
||||
if (isMonitorMode.value) {
|
||||
return ['TaskChainList', 'TaskChainDetail', 'ElevatorList']
|
||||
return ['TaskChainList', 'TaskChainDetail', 'ElevatorList', 'StationQueueView']
|
||||
}
|
||||
return ['DashboardView', 'TaskChainList', 'TaskChainDetail', 'ElevatorList', 'VendorLoadView', 'TaskGenerateView']
|
||||
return [
|
||||
'DashboardView',
|
||||
'TaskChainList',
|
||||
'TaskChainDetail',
|
||||
'ElevatorList',
|
||||
'StationQueueView',
|
||||
'VendorLoadView',
|
||||
'TaskGenerateView',
|
||||
]
|
||||
})
|
||||
|
||||
// 必须在 watch 之前恢复,否则 watch 的 immediate 会先执行并覆盖 storage
|
||||
|
|
@ -317,6 +329,7 @@ const tabs = computed(() => [
|
|||
...(!isMonitorMode.value ? [{ path: '/dashboard', label: '首页', icon: Monitor }] : []),
|
||||
{ path: '/task/chain', label: '任务链', icon: List },
|
||||
{ path: '/elevator', label: '提升机', icon: SetUp },
|
||||
{ path: '/station/queue', label: '排队', icon: Tickets },
|
||||
...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []),
|
||||
...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []),
|
||||
])
|
||||
|
|
@ -324,12 +337,14 @@ const tabs = computed(() => [
|
|||
const activeMenu = computed(() => {
|
||||
const path = route.path
|
||||
if (path.startsWith('/task/chain')) return '/task/chain'
|
||||
if (path.startsWith('/station/queue')) return '/station/queue'
|
||||
return path
|
||||
})
|
||||
|
||||
const activeTab = computed(() => {
|
||||
const path = route.path
|
||||
if (path.startsWith('/task/chain')) return '/task/chain'
|
||||
if (path.startsWith('/station/queue')) return '/station/queue'
|
||||
return path
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ const router = createRouter({
|
|||
component: () => import('@/views/elevator/ElevatorList.vue'),
|
||||
meta: { title: '提升机管理' },
|
||||
},
|
||||
{
|
||||
path: 'station/queue',
|
||||
name: 'StationQueueView',
|
||||
component: () => import('@/views/station/StationQueueView.vue'),
|
||||
meta: { title: '站点排队' },
|
||||
},
|
||||
{
|
||||
path: 'vendor/load',
|
||||
name: 'VendorLoadView',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,411 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="hover" class="filter-card">
|
||||
<div class="filter-actions">
|
||||
<div class="filter-warehouse">
|
||||
<span class="filter-label">仓库</span>
|
||||
<el-input
|
||||
v-model="warehouse"
|
||||
placeholder="如 WH01"
|
||||
clearable
|
||||
@keyup.enter="loadData"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
|
||||
</div>
|
||||
<el-alert type="info" show-icon :closable="false" class="hint">
|
||||
站点为固定工作站列表(ASRS 工作站中排除 A1、A2、A5,含 B1–B4、A3、A4、A6);排队数据来自 Redis;未单独配置上限时使用默认上限。
|
||||
</el-alert>
|
||||
</el-card>
|
||||
|
||||
<div class="board-toolbar">
|
||||
<div class="board-titles">
|
||||
<span class="board-title">站点排队</span>
|
||||
<span v-if="rows.length" class="board-sub">{{ rows.length }} 个站点</span>
|
||||
</div>
|
||||
<div class="board-actions">
|
||||
<span class="board-hint">点击链码查看详情 · 排队无序(Set)</span>
|
||||
<el-button type="primary" size="small" :loading="loading" @click="loadData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-body" v-loading="loading">
|
||||
<el-empty v-if="rows.length === 0 && !loading" description="暂无数据,请确认仓库编码" />
|
||||
<div v-else class="station-grid">
|
||||
<el-card
|
||||
v-for="row in rows"
|
||||
:key="row.stationCode"
|
||||
class="station-card"
|
||||
shadow="hover"
|
||||
>
|
||||
<template #header>
|
||||
<div class="station-card-head">
|
||||
<div class="station-card-head__main">
|
||||
<span class="station-code">{{ row.stationCode }}</span>
|
||||
<el-tag :type="queueStressType(row)" size="small" effect="plain" class="queue-pill">
|
||||
排队 {{ row.queueSize }} / 上限 {{ row.effectiveMaxCapacity }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-tooltip v-if="!isMonitorMode" content="修改上限" placement="top">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="card-edit-btn"
|
||||
:icon="Edit"
|
||||
@click.stop="openEditDialog(row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="station-card-body">
|
||||
<div class="stat-row">
|
||||
<span class="stat-k">已配置上限</span>
|
||||
<span v-if="row.configuredMaxCapacity != null" class="stat-v">{{ row.configuredMaxCapacity }}</span>
|
||||
<el-tag v-else type="info" size="small">默认</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="chains-section">
|
||||
<div class="chains-head">
|
||||
<span class="chains-title">排队任务链</span>
|
||||
<span v-if="row.queuedChainCodes?.length" class="chains-n">{{ row.queuedChainCodes.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="row.queuedChainCodes?.length" class="chains-scroll">
|
||||
<el-tag
|
||||
v-for="c in row.queuedChainCodes"
|
||||
:key="c"
|
||||
type="primary"
|
||||
effect="plain"
|
||||
size="small"
|
||||
class="chain-chip"
|
||||
@click="goChainDetail(c)"
|
||||
>{{ c }}</el-tag>
|
||||
</div>
|
||||
<div v-else class="chains-empty">无</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editVisible" title="修改排队上限" width="420px" destroy-on-close>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="仓库">
|
||||
<el-input :model-value="editForm.warehouse" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点">
|
||||
<el-input :model-value="editForm.stationCode" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="排队上限">
|
||||
<el-input-number v-model="editForm.maxCapacity" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'StationQueueView' })
|
||||
import {
|
||||
listStationQueues,
|
||||
updateStationQueueMax,
|
||||
type MachineStationQueueVO,
|
||||
} from '@/api/stationQueue'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isMonitorMode = computed(() => userStore.isMonitorMode())
|
||||
const router = useRouter()
|
||||
|
||||
const warehouse = ref('WH01')
|
||||
const rows = ref<MachineStationQueueVO[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({
|
||||
warehouse: '',
|
||||
stationCode: '',
|
||||
maxCapacity: 3,
|
||||
})
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
function queueStressType(row: MachineStationQueueVO): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const max = row.effectiveMaxCapacity
|
||||
const q = row.queueSize
|
||||
if (max <= 0) return q > 0 ? 'danger' : 'info'
|
||||
if (q >= max) return 'danger'
|
||||
if (q / max >= 0.8) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const wh = warehouse.value?.trim()
|
||||
if (!wh) {
|
||||
ElMessage.warning('请输入仓库编码')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listStationQueues(wh)
|
||||
rows.value = res.data || []
|
||||
} catch {
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goChainDetail(code: string) {
|
||||
if (!code) return
|
||||
router.push(`/task/chain/${encodeURIComponent(code)}`)
|
||||
}
|
||||
|
||||
function openEditDialog(row: MachineStationQueueVO) {
|
||||
editForm.warehouse = row.warehouse
|
||||
editForm.stationCode = row.stationCode
|
||||
editForm.maxCapacity = row.configuredMaxCapacity ?? row.effectiveMaxCapacity
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updateStationQueueMax({
|
||||
warehouse: editForm.warehouse.trim(),
|
||||
stationCode: editForm.stationCode.trim(),
|
||||
maxCapacity: editForm.maxCapacity,
|
||||
})
|
||||
ElMessage.success('已保存')
|
||||
editVisible.value = false
|
||||
await loadData()
|
||||
} catch { /* interceptor */ } finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.filter-card .hint {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.filter-warehouse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 200px;
|
||||
flex: 1;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.board-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.board-titles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.board-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.board-sub {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.board-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 12px;
|
||||
}
|
||||
|
||||
.board-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
max-width: min(420px, 100%);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.board-body {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.station-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.station-card :deep(.el-card__header) {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.station-card :deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.station-card-head {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.station-card-head__main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.station-code {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.queue-pill {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-edit-btn {
|
||||
flex-shrink: 0;
|
||||
margin: -6px -10px -6px 0;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.station-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-k {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-v {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.chains-section {
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.chains-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chains-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.chains-n {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chains-scroll {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-content: flex-start;
|
||||
max-height: 168px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 2px;
|
||||
margin: 0 -2px;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.chain-chip {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.chains-empty {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.filter-warehouse {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.board-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/main.ts","./src/api/auth.ts","./src/api/elevator.ts","./src/api/generate.ts","./src/api/task.ts","./src/api/vendorload.ts","./src/composables/usedevice.ts","./src/constants/chainstatus.ts","./src/constants/taskstatus.ts","./src/router/index.ts","./src/stores/tabs.ts","./src/stores/user.ts","./src/utils/request.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/dashboard/dashboardview.vue","./src/views/elevator/elevatorlist.vue","./src/views/generate/taskgenerateview.vue","./src/views/login/loginview.vue","./src/views/task/taskchaindetail.vue","./src/views/task/taskchainlist.vue","./src/views/vendor/vendorloadview.vue","./env.d.ts"],"version":"5.7.3"}
|
||||
{"root":["./src/main.ts","./src/api/auth.ts","./src/api/elevator.ts","./src/api/generate.ts","./src/api/stationqueue.ts","./src/api/task.ts","./src/api/vendorload.ts","./src/composables/usedevice.ts","./src/constants/chainstatus.ts","./src/constants/taskstatus.ts","./src/router/index.ts","./src/stores/tabs.ts","./src/stores/user.ts","./src/utils/request.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/dashboard/dashboardview.vue","./src/views/elevator/elevatorlist.vue","./src/views/generate/taskgenerateview.vue","./src/views/login/loginview.vue","./src/views/station/stationqueueview.vue","./src/views/task/taskchaindetail.vue","./src/views/task/taskchainlist.vue","./src/views/vendor/vendorloadview.vue","./env.d.ts"],"version":"5.7.3"}
|
||||
Loading…
Reference in New Issue