优化
This commit is contained in:
@@ -188,5 +188,6 @@ describe('ContainersView', () => {
|
||||
expect(source).toContain("row.planStatus === 'EDITING' && canSmartLoading")
|
||||
expect(source).not.toContain('canSmartLoading && row.containerNo')
|
||||
expect(source).not.toContain('link type="primary" :aria-label="containerViewLabel')
|
||||
expect(source).toContain(':loading="loading" stripe preference-key="container-loading-table"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -323,7 +323,7 @@ onMounted(() => { void Promise.all([loadWarehouses(), loadBusinessOptions(), loa
|
||||
<LoadErrorState title="货柜列表加载失败" :message="loadError.message" :loading="loading" @retry="loadRows"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
</template>
|
||||
<section v-if="!loadError" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" preference-key="container-loading-table" empty-text="暂无货柜">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" stripe preference-key="container-loading-table" empty-text="暂无货柜">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增货柜</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canView" size="small" type="primary" plain :aria-label="containerViewLabel(row as ContainerBookingResource)" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="canView && canEdit && row.planStatus !== 'COMPLETED'" size="small" @click="openEdit(row)"><Pencil :size="15" />{{ row.planStatus === 'LOADING' ? '录入集装箱号' : '编辑' }}</el-button><el-button v-if="row.planStatus === 'EDITING' && canSmartLoading" size="small" type="primary" @click="goContainerProcess(row)"><Sparkles :size="15" />排柜</el-button><el-button v-else-if="row.planStatus === 'LOADING' && canViewPreload" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />查看预装清单</el-button><el-button v-else-if="row.planStatus === 'COMPLETED' && canViewLoadingList" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />查看装柜清单</el-button></div></template>
|
||||
<template #cabinet="{ row }"><strong>{{ row.cabinetNo }}</strong><small>{{ row.bookingNo }}</small></template>
|
||||
|
||||
@@ -222,5 +222,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).toContain('type="danger" plain')
|
||||
expect(source).not.toContain('link type="danger"')
|
||||
expect(source).toContain('@media (max-width: 720px)')
|
||||
expect(source.match(/<DataTable[\s\S]*?\bstripe\b/g)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -362,6 +362,7 @@ onMounted(load)
|
||||
:data="filteredCandidates"
|
||||
:columns="candidateColumns"
|
||||
:loading="loading || selectionLoading || mutating"
|
||||
stripe
|
||||
preference-key="global-smart-loading-candidates"
|
||||
column-settings-mode="hide"
|
||||
:empty-text="selectedContainer ? '当前仓库暂无可装货物' : '暂无可用柜次'"
|
||||
@@ -436,7 +437,7 @@ onMounted(load)
|
||||
<div><div class="meter-title"><span>体积</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent, 100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div>
|
||||
<div><div class="meter-title"><span>重量</span><strong>{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent, 100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div>
|
||||
</div>
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无已装货物">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" stripe preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无已装货物">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
|
||||
@@ -92,5 +92,6 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
expect(source).toContain("canPush && plan?.status === 'EDITING'")
|
||||
expect(source).not.toContain("canPush && plan?.status === 'EDITING' && container?.containerNo")
|
||||
expect(source).not.toContain('link type="primary" :disabled="!routeMatches(row)')
|
||||
expect(source.match(/<DataTable[^>]*\bstripe\b/g)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -227,7 +227,7 @@ onMounted(load)
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
<template v-else-if="plan">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" stripe preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<template #toolbar-start><div><h2>装柜方案</h2><p>计划 {{ plan.planNo }} · 版本 {{ plan.version }};智能建议必须经人工核对。</p></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
@@ -236,7 +236,7 @@ onMounted(load)
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
<template #destination="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template>
|
||||
</DataTable>
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" stripe preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于仓库、目的仓、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="!routeMatches(row) || assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
|
||||
@@ -36,5 +36,6 @@ describe('CustomsCasesView', () => {
|
||||
it('uses a bordered view action instead of a text link', () => {
|
||||
expect(source).toContain('size="small" type="primary" plain')
|
||||
expect(source).not.toContain("customs:view')\" link")
|
||||
expect(source).toContain(':total="total" stripe row-key="id"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,6 +27,6 @@ async function openDetail(id: string) { detailOpen.value = true; detailLoading.v
|
||||
async function submit() { if (!detail.value || !submitForm.customsAgentName.trim()) return; actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } }
|
||||
watch([page, pageSize], () => void load()); onMounted(load)
|
||||
</script>
|
||||
<template><AdminPage as="div"><ListPageLayout aria-label="报关协同列表"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="报关协同筛选" :fields="fields" @search="search" /></template><template v-if="error" #state><LoadErrorState 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"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" size="small" type="primary" plain @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="customsStatusLabel(row.status)" /></template></DataTable></section></ListPageLayout>
|
||||
<template><AdminPage as="div"><ListPageLayout aria-label="报关协同列表"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="报关协同筛选" :fields="fields" @search="search" /></template><template v-if="error" #state><LoadErrorState 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"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" size="small" type="primary" plain @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="customsStatusLabel(row.status)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="detailOpen" title="报关协同详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError && !detail" title="详情加载失败" :message="actionError.message" :loading="detailLoading" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-if="detail"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-descriptions :column="2" border><el-descriptions-item label="报关编号">{{ detail.customsCase.customsNo }}</el-descriptions-item><el-descriptions-item label="状态"><StatusBadge :status="customsStatusLabel(detail.customsCase.status)" /></el-descriptions-item><el-descriptions-item label="资料清单">{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}</el-descriptions-item><el-descriptions-item label="资料/回执">{{ detail.documents.length }} / {{ detail.receipts.length }}</el-descriptions-item></el-descriptions><el-form v-if="canSubmit" label-position="top" class="submit-form"><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理"><el-input v-model="submitForm.customsAgentName" placeholder="请输入线下报关代理名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="外部参考号"><el-input v-model="submitForm.externalReference" placeholder="请输入代理回传的参考号(可选)" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="提交备注"><el-input v-model="submitForm.note" placeholder="请输入本次提交说明(可选)" /></el-form-item></el-col></el-row><el-button type="primary" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
|
||||
<style scoped>.submit-form{margin-top:18px}.submit-form+.el-button{margin-bottom:12px}h3{margin:20px 0 12px}small{display:block;color:var(--cc-muted)}:global(.el-dialog:has(.submit-form,.el-descriptions)){width:min(900px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}:global(.el-dialog:has(.submit-form,.el-descriptions) .el-dialog__body){max-height:calc(90vh - 120px);overflow-y:auto}</style>
|
||||
|
||||
@@ -39,6 +39,13 @@ describe('PlatformFirstLegOrdersPanel review queue', () => {
|
||||
expect(source).toContain('内部备注')
|
||||
expect(source).toContain('取消订单')
|
||||
expect(source).toContain("minWidth: 288, fixed: 'left'")
|
||||
expect(source).toContain('size="small" plain type="success"')
|
||||
expect(source).toContain('size="small" plain type="primary"')
|
||||
expect(source).toContain('size="small" plain type="danger"')
|
||||
expect(source).toContain('<Eye :size="15" />详情')
|
||||
expect(source).not.toContain('v-if="actionLabel(row as PlatformFirstLegOrderResource)" link')
|
||||
expect(source).toContain('@media (max-width: 360px)')
|
||||
expect(source).toContain('.order-row-actions :deep(.el-button) { width: 100%; min-height: 40px; }')
|
||||
})
|
||||
|
||||
it('distinguishes overflow action buttons by order and keeps a stable missing-field fallback', () => {
|
||||
|
||||
@@ -602,10 +602,10 @@ onMounted(() => void loadTenants())
|
||||
<template #actions="{ row }">
|
||||
<div class="table-actions order-row-actions">
|
||||
<el-button link type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openDetail(row as PlatformFirstLegOrderResource)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="actionLabel(row as PlatformFirstLegOrderResource)" link type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)"><CircleCheckBig :size="15" />{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'express')" link type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openExpress(row as PlatformFirstLegOrderResource)"><PackageCheck :size="15" />维护快递</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'note')" link type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openNote(row as PlatformFirstLegOrderResource)"><FilePenLine :size="15" />内部备注</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'cancel')" link type="danger" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="cancelOrder(row as PlatformFirstLegOrderResource)"><CircleX :size="15" />取消订单</el-button>
|
||||
<el-button v-if="actionLabel(row as PlatformFirstLegOrderResource)" size="small" plain type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)"><CircleCheckBig :size="15" />{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'express')" size="small" plain type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openExpress(row as PlatformFirstLegOrderResource)"><PackageCheck :size="15" />维护快递</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'note')" size="small" plain type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openNote(row as PlatformFirstLegOrderResource)"><FilePenLine :size="15" />内部备注</el-button>
|
||||
<el-button v-if="isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'cancel')" size="small" plain type="danger" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="cancelOrder(row as PlatformFirstLegOrderResource)"><CircleX :size="15" />取消订单</el-button>
|
||||
<el-dropdown v-if="hasOverflowActions(row as PlatformFirstLegOrderResource)" trigger="click" popper-class="first-leg-action-menu" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @command="runSecondaryAction($event, row as PlatformFirstLegOrderResource)">
|
||||
<el-button link type="primary" :aria-label="moreOrderActionsLabel(row as PlatformFirstLegOrderResource)" :title="moreOrderActionsLabel(row as PlatformFirstLegOrderResource)"><MoreHorizontal :size="17" /></el-button>
|
||||
<template #dropdown>
|
||||
@@ -661,4 +661,8 @@ onMounted(() => void loadTenants())
|
||||
.detail-page-header .el-button { width: 100%; min-height: 40px; }
|
||||
.detail-table-scroll :deep(.el-table) { min-width: 560px; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.order-row-actions { align-items: stretch; flex-direction: column; }
|
||||
.order-row-actions :deep(.el-button) { width: 100%; min-height: 40px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -106,6 +106,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
<DataTable
|
||||
:data="detail.items"
|
||||
:columns="itemColumns"
|
||||
stripe
|
||||
preference-key="loading-list-detail-items"
|
||||
row-key="itemId"
|
||||
empty-text="暂无装柜货物明细"
|
||||
|
||||
@@ -100,6 +100,8 @@ describe('LoadingListsView', () => {
|
||||
expect(source).toContain('flex-wrap: wrap')
|
||||
expect(source).toContain('@media (max-width: 720px)')
|
||||
expect(source).toContain('.row-actions { gap: 4px; }')
|
||||
expect(source).toContain(':loading="loading"\n stripe')
|
||||
expect(detailSource).toContain(':columns="itemColumns"\n stripe')
|
||||
})
|
||||
|
||||
it('does not expose unknown history action codes', () => {
|
||||
|
||||
@@ -300,6 +300,7 @@ onMounted(() => {
|
||||
:data="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
stripe
|
||||
:total="total"
|
||||
:preference-key="`loading-lists-${mode}`"
|
||||
row-key="planId"
|
||||
|
||||
@@ -22,3 +22,16 @@ describe('OceanBookingsView prototype field matrix', () => {
|
||||
expect(source).not.toContain('打印')
|
||||
})
|
||||
})
|
||||
|
||||
describe('OceanBookingsView visual action contract', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingsView.vue'), 'utf8')
|
||||
|
||||
it('uses bordered business buttons and stacks them on narrow screens', () => {
|
||||
expect(source).toContain('size="small" type="primary" plain')
|
||||
expect(source).toContain('size="small" @click="open(row.id)"')
|
||||
expect(source).not.toContain('link type="primary" @click="open(row.id)')
|
||||
expect(source).toContain('@media(max-width:420px)')
|
||||
expect(source).toContain('.table-actions')
|
||||
expect(source).toContain(':total="total" stripe preference-key="ocean-bookings"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,9 +54,9 @@ watch([page, pageSize], () => void load()); onMounted(load)
|
||||
<template #toolbar><QueryToolbar v-model="query" class="surface-panel" ariaLabel="订舱筛选" :fields="fields" @search="search" /></template>
|
||||
<template v-if="error" #state><LoadErrorState 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">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="ocean-bookings" aria-label="订舱列表" row-key="id" empty-text="暂无符合筛选条件的订舱">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe preference-key="ocean-bookings" aria-label="订舱列表" row-key="id" empty-text="暂无符合筛选条件的订舱">
|
||||
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:ocean-booking:create')" type="primary" @click="open()"><Plus :size="16" />新增订舱</el-button></template>
|
||||
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:ocean-booking:view')" link type="primary" @click="open(row.id)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" link @click="open(row.id)"><Pencil :size="15" />编辑</el-button></template>
|
||||
<template #actions="{ row }"><div class="table-actions"><el-button v-if="access.can('platform:firstleg:ocean-booking:view')" size="small" type="primary" plain @click="open(row.id)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" size="small" @click="open(row.id)"><Pencil :size="15" />编辑</el-button></div></template>
|
||||
<template #vessel="{ row }">{{ row.vesselName || '—' }}<span v-if="row.voyageNo"> / {{ row.voyageNo }}</span></template>
|
||||
<template #etd="{ row }">{{ formatDateTime(row.etd) }}</template>
|
||||
<template #eta="{ row }">{{ formatDateTime(row.eta) }}</template>
|
||||
@@ -67,4 +67,4 @@ watch([page, pageSize], () => void load()); onMounted(load)
|
||||
</ListPageLayout>
|
||||
</AdminPage>
|
||||
</template>
|
||||
<style scoped>.ocean-bookings-page{min-width:0}</style>
|
||||
<style scoped>.ocean-bookings-page{min-width:0}.table-actions{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.table-actions :deep(.el-button){margin-left:0}@media(max-width:420px){.table-actions{flex-direction:column;align-items:stretch}.table-actions :deep(.el-button){width:100%;min-height:40px}}</style>
|
||||
|
||||
@@ -158,5 +158,6 @@ describe('OutboundDispatchesView', () => {
|
||||
expect(source).toContain('size="small" type="primary" @click="action')
|
||||
expect(source).toContain('size="small" type="danger" plain')
|
||||
expect(source).not.toContain("outbound-dispatch:view')\" link")
|
||||
expect(source).toContain(':loading="loading" stripe row-key="inboundId"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -188,7 +188,7 @@ onBeforeUnmount(() => { disposed = true; listRequestId += 1; optionRequestId +=
|
||||
</template>
|
||||
<section v-if="!error" class="surface-panel table-panel">
|
||||
<el-empty v-if="!queried" description="请选择订单 / 入库单或柜次后查询" />
|
||||
<DataTable v-else :data="rows" :columns="columns" :loading="loading" row-key="inboundId" preference-key="outbound-dispatches" empty-text="暂无符合条件的出仓记录">
|
||||
<DataTable v-else :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>
|
||||
|
||||
@@ -78,5 +78,6 @@ describe('PortSchedulesView', () => {
|
||||
expect(source).toContain('size="small" @click="openSchedule')
|
||||
expect(source).not.toContain("port-schedule:view')\" link")
|
||||
expect(source).toContain('<el-button link type="primary" @click="loadCustomers"')
|
||||
expect(source).toContain(':total="total" stripe row-key="schedule.id"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ watch([page, pageSize], () => void load()); onMounted(load)
|
||||
<AdminPage as="div"><ListPageLayout aria-label="船期列表">
|
||||
<template #toolbar><QueryToolbar v-model="query" class="surface-panel" ariaLabel="船期筛选" :fields="fields" @search="search" /></template>
|
||||
<template v-if="error" #state><LoadErrorState 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"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="schedule.id" preference-key="port-schedules" empty-text="暂无符合条件的船期">
|
||||
<section v-if="!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="schedule.id" preference-key="port-schedules" empty-text="暂无符合条件的船期">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增船期</el-button></template>
|
||||
<template #actions="{ row }"><div class="table-actions"><el-button v-if="access.can('platform:firstleg:port-schedule:view')" size="small" type="primary" plain @click="openSchedule(row.schedule.id, 'view')"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:port-schedule:edit')" size="small" @click="openSchedule(row.schedule.id, 'edit')"><Pencil :size="15" />编辑</el-button></div></template>
|
||||
<template #vessel="{ row }">{{ row.schedule.vesselName }} / {{ row.schedule.voyageNo }}</template><template #observedAt="{ row }">{{ formatDateTime(row.schedule.observedAt) }}</template><template #updatedAt="{ row }">{{ formatDateTime(row.schedule.updatedAt) }}</template><template #expired="{ row }"><el-tag :type="row.expired ? 'danger' : 'success'">{{ row.expired ? '已过期' : '有效' }}</el-tag></template>
|
||||
|
||||
@@ -17,5 +17,7 @@ describe('TruckAttachmentDialog interaction governance', () => {
|
||||
expect(source).not.toContain('输入文件服务 ID')
|
||||
expect(source).toContain('deleteTruckAppointmentAttachment')
|
||||
expect(source).toContain('<Trash2 :size="15" />删除')
|
||||
expect(source).toContain('size="small" type="danger" plain')
|
||||
expect(source).not.toContain(' text type="danger"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ async function remove(item: TruckAttachmentReference) { if (!props.appointmentId
|
||||
<el-empty v-if="attachments.length === 0" description="暂无预约附件" :image-size="72" />
|
||||
<div v-for="item in attachments" :key="item.fileId" class="attachment-row">
|
||||
<div><strong>{{ item.fileName || '未命名文件' }}</strong><small>{{ attachmentTypeLabel(item.attachmentType) }}{{ item.uploadedAt ? ` · ${item.uploadedAt}` : '' }}</small></div>
|
||||
<el-button v-if="props.appointmentId && props.canManage && item.id" text type="danger" :loading="busy" @click="remove(item)"><Trash2 :size="15" />删除</el-button>
|
||||
<el-button v-if="props.appointmentId && props.canManage && item.id" size="small" type="danger" plain :loading="busy" @click="remove(item)"><Trash2 :size="15" />删除</el-button>
|
||||
</div>
|
||||
<template #footer><el-button type="primary" @click="emit('update:modelValue', false)">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -123,5 +123,6 @@ describe('TruckAppointmentsView', () => {
|
||||
expect(source).toContain('size="small" type="primary" plain')
|
||||
expect(source).toContain('size="small" :aria-label="appointmentActionLabel(\'编辑\'')
|
||||
expect(source).not.toContain("truck-appointment:view')\" link")
|
||||
expect(source).toContain(':total="total" stripe preference-key="trucking-appointments"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ function openDetail(row: TruckAppointmentResource, edit = false) { router.push({
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="拖车预约查询" :fields="fields" preference-key="trucking-query" @search="search" /></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="拖车预约列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<section v-if="!loadError" class="surface-panel table-panel" :aria-busy="loading">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="trucking-appointments" aria-label="拖车预约列表" empty-text="暂无符合筛选条件的拖车预约">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe preference-key="trucking-appointments" aria-label="拖车预约列表" empty-text="暂无符合筛选条件的拖车预约">
|
||||
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:truck-appointment:create')" type="primary" :disabled="loading" @click="router.push({ name: 'trucking-new' })"><Plus :size="16" />新增拖车</el-button></template>
|
||||
<template #company="{ row }">{{ row.carrierName || (row.carrierId ? '已关联承运商' : '—') }}</template>
|
||||
<template #driver="{ row }">{{ row.driverName || '—' }}<small>{{ row.driverPhone || '—' }}</small></template>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue'), 'utf8')
|
||||
|
||||
describe('SmartLoadingWorkbench business actions', () => {
|
||||
it('uses semantic outlined buttons and keeps narrow action cells usable', () => {
|
||||
expect(source).toContain('size="small" plain type="primary"')
|
||||
expect(source).toContain('size="small" plain type="danger"')
|
||||
expect(source).toContain('<Pencil :size="14" />调整')
|
||||
expect(source).toContain('<Trash2 :size="13" />移除')
|
||||
expect(source).toContain('<Plus :size="14" />加入')
|
||||
expect(source).not.toContain('<el-button link type="primary" :disabled="!canAssign || !editable')
|
||||
expect(source).toContain('@media (max-width: 360px)')
|
||||
})
|
||||
})
|
||||
@@ -306,8 +306,8 @@ onMounted(() => { void load() })
|
||||
<div class="smart-loading__section-title"><h4>当前装柜方案</h4><span>版本 {{ plan.version }} · {{ plan.planNo || '—' }}</span></div>
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading" empty-text="装柜草稿暂无明细">
|
||||
<template #actions="{ row }">
|
||||
<el-button link type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)"><Pencil :size="14" />调整</el-button>
|
||||
<el-button link type="danger" :disabled="!canDelete || !editable || Boolean(action)" @click="openRemove(row as SmartLoadingPlanItem)"><Trash2 :size="13" />移除</el-button>
|
||||
<el-button size="small" plain type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)"><Pencil :size="14" />调整</el-button>
|
||||
<el-button size="small" plain type="danger" :disabled="!canDelete || !editable || Boolean(action)" @click="openRemove(row as SmartLoadingPlanItem)"><Trash2 :size="13" />移除</el-button>
|
||||
</template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingPlanItem).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingPlanItem).inboundCode || '—' }}</small></template>
|
||||
<template #weight="{ row }">{{ formatNumber((row as SmartLoadingPlanItem).grossWeightKg) }}</template>
|
||||
@@ -322,7 +322,7 @@ onMounted(() => { void load() })
|
||||
<template #toolbar><QueryToolbar v-model="filters" ariaLabel="候选货物筛选" :fields="queryFields" @search="applyFilters" /></template>
|
||||
<section class="surface-panel table-panel">
|
||||
<DataTable :data="filteredCandidates" :columns="candidateColumns" :loading="loading" empty-text="当前柜次仓库暂无可用候选货物">
|
||||
<template #actions="{ row }"><el-button link type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)"><Plus :size="14" />加入</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)"><Plus :size="14" />加入</el-button></template>
|
||||
<template #priority="{ $index }"><span class="priority">{{ Number($index) + 1 }}</span></template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingCandidate).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingCandidate).inboundCode || '—' }}</small></template>
|
||||
<template #destination="{ row }">{{ (row as SmartLoadingCandidate).destinationWarehouseName || (row as SmartLoadingCandidate).destinationWarehouseCode || '—' }}</template>
|
||||
@@ -384,4 +384,5 @@ onMounted(() => { void load() })
|
||||
.smart-loading__actions :deep(.el-button:not(.is-circle)) { flex: 1 1 150px; margin-left: 0; }
|
||||
}
|
||||
@media (max-width: 420px) { .smart-loading__summary { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 360px) { .smart-loading :deep(.el-table__cell .el-button) { width: 100%; min-height: 40px; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user