This commit is contained in:
2026-10-20 23:34:11 +08:00
parent f155976f52
commit 13e4aa56ec
321 changed files with 6756 additions and 2110 deletions
@@ -163,13 +163,13 @@ describe('OutboundDispatchesView', () => {
it('keeps the gate detail dialog responsive with internal scrolling', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8')
expect(source).toContain('width="min(900px, calc(100vw - 28px))"')
expect(source).toContain('max-height: 90vh')
expect(source).toContain('overflow-y: auto')
expect(source).toContain('outbound-detail-dialog admin-density-dialog admin-workbench is-dialog')
expect(source).not.toContain(':global(.outbound-detail-dialog')
})
it('styles the native gate table with prototype borders, stripes and hover', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8')
expect(source).toContain('<el-table class="gate-table" :data="detail.aggregate.gate?.items || []" border stripe>')
expect(source).toContain('.gate-table { --el-table-row-hover-bg-color: #f1f7ff; }')
expect(source).toContain('.gate-table { --el-table-row-hover-bg-color: var(--workbench-accent-soft); }')
})
it('uses semantic bordered and filled business buttons instead of text links', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8')
@@ -1,4 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Eye, RotateCcw, Send } from '@lucide/vue'
@@ -192,35 +194,36 @@ onBeforeUnmount(() => { disposed = true; listRequestId += 1; optionRequestId +=
</script>
<template>
<AdminPage as="div">
<AdminPage class="admin-workbench admin-density" as="div">
<ListPageLayout aria-label="国内出仓列表">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="国内出仓筛选" :fields="fields" :search-on-reset="false" @search="search" @reset="resetSearch" /></template>
<template #toolbar><QueryToolbar v-model="filters" class="workbench-query" ariaLabel="国内出仓筛选" :fields="fields" :search-on-reset="false" @search="search" @reset="resetSearch" /></template>
<template v-if="optionsError || error" #state>
<el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions()">重新加载选项</el-button></el-alert>
<LoadErrorState v-if="error" title="国内出仓加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
</template>
<section v-if="!error" class="surface-panel table-panel">
<section v-if="!error" class="table-panel">
<el-empty v-if="!queried" description="请选择入仓码或柜次后查询" />
<DataTable v-else :data="rows" :columns="columns" :loading="loading" stripe row-key="inboundId" preference-key="outbound-dispatches" empty-text="暂无符合条件的出仓记录">
<div class="workbench-table is-page-surface" v-else><DataTable :data="rows" :columns="columns" :loading="loading" stripe row-key="inboundId" preference-key="outbound-dispatches" empty-text="暂无符合条件的出仓记录">
<template #actions="{ row }"><div class="table-actions"><el-button v-if="access.can('platform:firstleg:outbound-dispatch:view')" size="small" type="primary" plain @click="open(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:confirm') && row.dispatchStatus === 'READY_FOR_DISPATCH'" size="small" type="primary" @click="action(row, 'confirm-dispatch')"><Send :size="15" />确认</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:reverse') && row.dispatchStatus === 'WAITING_FOR_LOADING'" size="small" type="danger" plain @click="action(row, 'reverse-dispatch')"><RotateCcw :size="15" />撤销</el-button></div></template>
<template #booking="{ row }"><StatusBadge :status="oceanBookingStatusLabel(row.oceanBookingStatus)" /></template><template #truck="{ row }"><StatusBadge :status="truckStatusLabel(row.truckAppointmentStatus)" /></template><template #pick="{ row }"><StatusBadge :status="pickTaskStatusLabel(row.pickTaskStatus)" /></template><template #status="{ row }"><StatusBadge :status="outboundDispatchStatusLabel(row.dispatchStatus)" /></template><template #gate="{ row }"><el-tag :type="row.gate?.passed ? 'success' : 'danger'">{{ row.gate?.passed ? '通过' : '未通过' }}</el-tag></template>
</DataTable>
</DataTable></div>
</section>
</ListPageLayout>
<AppDialog v-model="detailOpen" class="outbound-detail-dialog" title="国内出仓详情" width="min(900px, calc(100vw - 28px))" :show-confirm="false">
<AppDialog v-model="detailOpen" class="outbound-detail-dialog admin-density-dialog admin-workbench is-dialog" title="国内出仓详情" width="min(900px, calc(100vw - 28px))" :show-confirm="false">
<LoadErrorState v-if="actionError" title="出仓详情加载失败" :message="actionError.message" :loading="detailLoading" @retry="retryDetail"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState>
<div v-else-if="detailLoading" class="detail-loading" aria-busy="true"><el-skeleton :rows="5" animated /></div>
<template v-else-if="detail"><el-descriptions :column="2" border><el-descriptions-item label="入仓码">{{ detail.aggregate.inboundCode || '—' }}</el-descriptions-item><el-descriptions-item label="柜次号">{{ detail.aggregate.cabinetNo || '—' }}</el-descriptions-item><el-descriptions-item label="集装箱号">{{ detail.aggregate.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="出仓状态"><StatusBadge :status="outboundDispatchStatusLabel(detail.aggregate.dispatchStatus)" /></el-descriptions-item></el-descriptions><h3>统一门禁</h3><el-table class="gate-table" :data="detail.aggregate.gate?.items || []" border stripe><el-table-column label="结果" width="80"><template #default="scope"><el-tag :type="scope.row.passed ? 'success' : 'danger'">{{ scope.row.passed ? '通过' : '阻断' }}</el-tag></template></el-table-column><el-table-column prop="name" label="门禁" min-width="140" /><el-table-column prop="actual" label="当前情况" min-width="180" /><el-table-column prop="requirement" label="要求" min-width="180" /></el-table><h3>整体时间线</h3><el-timeline><el-timeline-item v-for="entry in detail.timeline" :key="`${entry.module}-${entry.occurredAt}-${entry.action}`" :timestamp="entry.occurredAt">{{ timelineModuleLabel(entry.module) }} · {{ timelineActionLabel(entry.action) }}:{{ timelineStatusLabel(entry.module, entry.fromStatus) }} → {{ timelineStatusLabel(entry.module, entry.toStatus) }}<small>{{ entry.operatorName || '系统' }} {{ entry.remark || '' }}</small></el-timeline-item></el-timeline></template>
<template v-else-if="detail"><el-descriptions class="density-descriptions" :column="2" border><el-descriptions-item label="入仓码">{{ detail.aggregate.inboundCode || '—' }}</el-descriptions-item><el-descriptions-item label="柜次号">{{ detail.aggregate.cabinetNo || '—' }}</el-descriptions-item><el-descriptions-item label="集装箱号">{{ detail.aggregate.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="出仓状态"><StatusBadge :status="outboundDispatchStatusLabel(detail.aggregate.dispatchStatus)" /></el-descriptions-item></el-descriptions><h3>统一门禁</h3><el-table class="gate-table" :data="detail.aggregate.gate?.items || []" border stripe><el-table-column label="结果" width="80"><template #default="scope"><el-tag :type="scope.row.passed ? 'success' : 'danger'">{{ scope.row.passed ? '通过' : '阻断' }}</el-tag></template></el-table-column><el-table-column prop="name" label="门禁" min-width="140" /><el-table-column prop="actual" label="当前情况" min-width="180" /><el-table-column prop="requirement" label="要求" min-width="180" /></el-table><h3>整体时间线</h3><el-timeline><el-timeline-item v-for="entry in detail.timeline" :key="`${entry.module}-${entry.occurredAt}-${entry.action}`" :timestamp="entry.occurredAt">{{ timelineModuleLabel(entry.module) }} · {{ timelineActionLabel(entry.action) }}:{{ timelineStatusLabel(entry.module, entry.fromStatus) }} → {{ timelineStatusLabel(entry.module, entry.toStatus) }}<small>{{ entry.operatorName || '系统' }} {{ entry.remark || '' }}</small></el-timeline-item></el-timeline></template>
</AppDialog>
</AdminPage>
</template>
<style scoped>
h3 { margin: 20px 0 12px; }
small { display: block; color: var(--cc-muted); }
small { display: block; color: var(--workbench-muted); }
.detail-loading { padding: 12px 0; }
.gate-table { --el-table-row-hover-bg-color: #f1f7ff; }
.gate-table { --el-table-row-hover-bg-color: var(--workbench-accent-soft); }
.table-actions { display: flex; align-items: center; gap: 6px; }.table-actions :deep(.el-button) { margin-left: 0; }
:global(.outbound-detail-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
:global(.outbound-detail-dialog .el-dialog__body) { max-height: calc(90vh - 120px); overflow-y: auto; }
</style>