优化
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -54,13 +55,15 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="inventory-detail-page admin-density">
|
||||
<section class="inventory-overview">
|
||||
<header class="detail-header">
|
||||
<el-button link class="density-back" aria-label="返回整柜业务" @click="back"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<h1>{{ detail?.inventory.containerNo || '整柜库存详情' }}</h1>
|
||||
<StatusBadge v-if="detail" :status="inventoryStatus(detail.inventory.inventoryStatus)" />
|
||||
</header>
|
||||
<AdminPage as="div" class="inventory-detail-page admin-density admin-workbench">
|
||||
<section class="inventory-overview workbench-panel is-flush is-page-surface">
|
||||
<header class="density-detail-header">
|
||||
<div class="density-detail-heading">
|
||||
<el-button link class="density-back" aria-label="返回整柜业务" @click="back"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<h1 class="density-detail-title">{{ detail?.inventory.containerNo || '整柜库存详情' }}</h1>
|
||||
<StatusBadge v-if="detail" :status="inventoryStatus(detail.inventory.inventoryStatus)" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<LoadErrorState v-if="error" title="整柜库存详情加载失败" :message="error.message" :loading="loading" @retry="load">
|
||||
<template #details><ApiErrorMeta :details="error" /></template>
|
||||
@@ -77,7 +80,7 @@ onMounted(load)
|
||||
<div><dt>出库单数量</dt><dd>{{ detail.outboundOrderCount }}</dd></div>
|
||||
<div><dt>入库箱数</dt><dd>{{ detail.inventory.inboundBoxes }} 箱</dd></div>
|
||||
<div><dt>已出库箱数</dt><dd>{{ detail.inventory.outboundBoxes }} 箱</dd></div>
|
||||
<div><dt>当前库存</dt><dd>{{ detail.inventory.availableBoxes }} 箱</dd></div>
|
||||
<div><dt>当前库存</dt><dd><strong class="inventory-available">{{ detail.inventory.availableBoxes }}</strong> 箱</dd></div>
|
||||
<div><dt>累计入库体积</dt><dd>{{ detail.inventory.inboundVolumeCbm.toFixed(2) }} m³</dd></div>
|
||||
<div><dt>最后更新时间</dt><dd>{{ displayTime(detail.inventory.updatedAt) }}</dd></div>
|
||||
<div><dt>库存版本</dt><dd>{{ detail.inventory.version }}</dd></div>
|
||||
@@ -88,25 +91,25 @@ onMounted(load)
|
||||
|
||||
<section v-if="detail && !error" class="transactions" aria-labelledby="transactions-heading">
|
||||
<h2 id="transactions-heading">进销存交易记录</h2>
|
||||
<DataTable :data="detail.transactions" :columns="transactionColumns" row-key="id" stripe column-settings-mode="hide" empty-text="暂无进销存流水">
|
||||
<template #businessType="{ row }"><StatusBadge :status="row.businessType === 'INBOUND' ? '入库' : '出库'" /></template>
|
||||
</DataTable>
|
||||
<div class="transactions-table workbench-table is-page-surface"><DataTable :data="detail.transactions" :columns="transactionColumns" row-key="id" stripe column-settings-mode="hide" empty-text="暂无进销存流水">
|
||||
<template #businessType="{ row }"><el-tag :type="row.businessType === 'INBOUND' ? 'success' : 'primary'" effect="light" size="small" round>{{ row.businessType === 'INBOUND' ? '入库' : '出库' }}</el-tag></template>
|
||||
</DataTable></div>
|
||||
</section>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inventory-detail-page { display: grid; min-width: 0; gap: var(--admin-space-5, 12px); }
|
||||
.inventory-overview { min-width: 0; border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
|
||||
.detail-header { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-5, 12px); min-height: 40px; padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); }
|
||||
.detail-header h1 { margin: 0; font-size: var(--admin-font-title, 16px); letter-spacing: 0; }
|
||||
.transactions { min-width: 0; border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
|
||||
.inventory-overview { min-width: 0; }
|
||||
.transactions { min-width: 0; }
|
||||
.detail-section { min-width: 0; padding: 0 var(--admin-space-5, 12px) var(--admin-space-5, 12px); }
|
||||
.detail-section h2, .transactions h2 { margin: 0; font-size: var(--admin-font-section, 14px); letter-spacing: 0; }
|
||||
.detail-fields { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: var(--admin-space-3, 8px) 0 0; }
|
||||
.transactions { padding-top: var(--admin-space-5, 12px); overflow: hidden; }
|
||||
.transactions h2 { margin: 0 var(--admin-space-5, 12px) var(--admin-space-3, 8px); }
|
||||
.detail-loading { min-height: 180px; padding: var(--admin-space-7, 24px); border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); color: var(--cc-muted); }
|
||||
.transactions-table { padding: 0 var(--admin-space-5, 12px); }
|
||||
.inventory-available { color: var(--workbench-number); font-variant-numeric: tabular-nums; }
|
||||
.detail-loading { min-height: 180px; padding: var(--admin-space-7, 24px); border: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); color: var(--workbench-muted); }
|
||||
@media (max-width: 960px) { .detail-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 520px) { .detail-header { flex-wrap: wrap; } .detail-header h1 { width: 100%; font-size: 18px; } .detail-fields { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 520px) { .density-detail-heading { flex-wrap: wrap; } .density-detail-title { width: 100%; font-size: 18px; } .detail-fields { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,20 @@ describe('DomesticContainersView', () => {
|
||||
it('gates create actions with backend permissions', () => { expect(source).toContain("access.can('platform:domestic-container:inbound:create')"); expect(source).toContain("access.can('platform:domestic-container:outbound:create')") })
|
||||
it('loads only active domestic warehouse candidates and surfaces errors', () => { expect(source).toContain("listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })"); expect(source).toContain('国内仓候选加载失败') })
|
||||
it('keeps idempotency key stable through retry and handles conflicts by refreshing inventory', () => { expect(source).toContain('idempotencyKey.value'); expect(source).toContain("submitError.value.code === 'A0409'"); expect(source).toContain('await loadCandidates()'); expect(source).not.toContain('localStorage') })
|
||||
it('reuses the order-list tab panel and keyboard interaction', () => { expect(source).toContain('class="business-query-panel integrated-query-group"'); expect(source).toContain('role="tablist"'); expect(source).toContain('role="tab"'); expect(source).toContain('handleTabKeydown'); expect(source).toContain('class="status-tabs density-status-tabs"'); expect(source).not.toContain('<el-tabs') })
|
||||
it('keeps business tabs in the list toolbar with keyboard interaction and separate filters', () => {
|
||||
const toolbar = source.match(/<template #toolbar-start>([\s\S]*?)<\/template>/)?.[1]
|
||||
const filters = source.match(/<template #filters>([\s\S]*?)<\/template>/)?.[1]
|
||||
expect(toolbar).toContain('class="status-tabs density-status-tabs workbench-list-tabs"')
|
||||
expect(toolbar).toContain('role="tablist"')
|
||||
expect(toolbar).toContain('role="tab"')
|
||||
expect(toolbar).toContain('@click="switchTab(tab.value)"')
|
||||
expect(toolbar).toContain('@keydown="handleTabKeydown($event, index)"')
|
||||
expect(toolbar).not.toContain('<h1>')
|
||||
expect(filters).toContain('class="business-query-panel integrated-query-group"')
|
||||
expect(filters).toContain('<QueryToolbar')
|
||||
expect(filters).not.toContain('role="tablist"')
|
||||
expect(source).not.toContain('<el-tabs')
|
||||
})
|
||||
it('keeps desktop filters and actions on one row with explicit field widths', () => { expect(source).toContain("key: 'warehouseId', type: 'select', width: 210"); expect(source).toContain("key: 'orderNo', type: 'input', width: 220"); expect(source).toContain("key: 'containerNo', type: 'input', width: 220"); expect(source).toContain("key: 'cargoName', type: 'input', width: 220"); expect(source).toContain("key: 'status', type: 'select', width: 180") })
|
||||
it('uses the shared query toolbar flow without a page-level layout override', () => { expect(source).not.toContain('.query-toolbar__filters') })
|
||||
it('keeps all four inventory facts in the approved compact summary', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus } from '@lucide/vue'
|
||||
@@ -72,18 +73,18 @@ async function submit() { if (!await formRef.value?.validate().catch(() => false
|
||||
watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWarehouses(), load()]) })
|
||||
</script>
|
||||
|
||||
<template><AdminPage as="div" class="domestic-containers-page admin-density">
|
||||
<template><AdminPage as="div" class="domestic-containers-page admin-density admin-workbench">
|
||||
<LoadErrorState v-if="warehouseError" compact title="国内仓候选加载失败" :message="warehouseError.message" :loading="warehouseLoading" @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><section id="domestic-container-results" ><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'">
|
||||
<template #toolbar-start><h1>整柜业务</h1></template>
|
||||
<template #filters><div class="business-query-panel integrated-query-group"><div class="status-tabs density-status-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)">{{ tab.label }}</button></div><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><section id="domestic-container-results" ><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'">
|
||||
<template #toolbar-start><div class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)">{{ tab.label }}</button></div></template>
|
||||
<template #filters><div class="business-query-panel integrated-query-group"><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template>
|
||||
<template #notices><template v-if="loadError"><LoadErrorState title="整柜业务加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template></template>
|
||||
<template #before-results><div v-if="activeTab === 'inventory'" class="inventory-summary"><div><span>入库箱数</span><strong>{{ summary.inboundBoxes }}</strong></div><div><span>已出库箱数</span><strong>{{ summary.outboundBoxes }}</strong></div><div><span>当前库存</span><strong>{{ summary.availableBoxes }}</strong></div><div><span>累计入库体积</span><strong>{{ summary.inboundVolumeCbm.toFixed(2) }} m³</strong></div></div></template>
|
||||
<template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button v-if="canViewInventory(row as ContainerOrder | InventoryRecord)" class="density-action" size="small" @click="viewInventory(row as ContainerOrder | InventoryRecord)">查看</el-button><span v-else>—</span></div></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog admin-density-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit">
|
||||
<template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button v-if="canViewInventory(row as ContainerOrder | InventoryRecord)" class="density-action" size="small" @click="viewInventory(row as ContainerOrder | InventoryRecord)">查看</el-button><span v-else>—</span></div></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></div></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog admin-density-dialog admin-workbench is-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit">
|
||||
<LoadErrorState v-if="submitError" compact title="创建失败" :message="submitError.message" :show-retry="false"><template #details><ApiErrorMeta :details="submitError" /></template></LoadErrorState>
|
||||
<el-form ref="formRef" :model="currentForm" :rules="rules" label-position="left" label-width="100px" class="density-form">
|
||||
<table class="density-table density-table--editable container-entry-table" :aria-label="dialogType === 'inbound' ? '整柜入库资料' : '整柜出库资料'">
|
||||
<div class="workbench-evidence"><table class="density-table density-table--editable container-entry-table" :aria-label="dialogType === 'inbound' ? '整柜入库资料' : '整柜出库资料'">
|
||||
<tbody>
|
||||
<template v-if="dialogType === 'inbound'">
|
||||
<tr>
|
||||
@@ -117,8 +118,8 @@ watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWa
|
||||
<tr><td colspan="2"><el-form-item label="状态" prop="status"><el-select v-model="currentForm.status"><el-option :label="dialogType === 'inbound' ? '已入库' : '已出库'" value="COMPLETED" /><el-option :label="dialogType === 'inbound' ? '待入库' : '待出库'" value="PENDING" /></el-select></el-form-item></td></tr>
|
||||
<tr><td colspan="2"><el-form-item label="备注"><el-input v-model="currentForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
</el-form>
|
||||
</AppDialog></AdminPage></template>
|
||||
|
||||
<style scoped>.domestic-containers-page{min-width:0}.business-query-panel{min-width:0}.business-query-toolbar{border:0}.inventory-summary{display:flex;align-items:center;gap:0;margin:0 var(--admin-space-6, 16px) var(--admin-space-5, 12px);border:1px solid var(--cc-border);background:var(--admin-label-bg, #f8fafc)}.inventory-summary>div{display:flex;align-items:baseline;gap:var(--admin-space-3, 8px);min-width:0;padding:9px var(--admin-space-6, 16px);border-right:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-right:0}.inventory-summary span{color:var(--cc-muted);font-size:var(--admin-font-caption, 12px)}.inventory-summary strong{font-size:15px}.container-entry-table{table-layout:fixed}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:720px){.inventory-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-summary>div:nth-child(2){border-right:0}.inventory-summary>div:nth-child(-n+2){border-bottom:1px solid var(--cc-border)}}@media(max-width:360px){.inventory-summary{grid-template-columns:1fr}.inventory-summary>div{border-right:0;border-bottom:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-bottom:0}}</style>
|
||||
<style scoped>.domestic-containers-page{min-width:0}.business-query-panel{min-width:0}.business-query-toolbar{border:0}.inventory-summary{display:flex;align-items:center;gap:0;margin:0 var(--admin-space-6, 16px) var(--admin-space-5, 12px);border:1px solid var(--workbench-border);background:var(--admin-label-bg, #f8fafc)}.inventory-summary>div{display:flex;align-items:baseline;gap:var(--admin-space-3, 8px);min-width:0;padding:9px var(--admin-space-6, 16px);border-right:1px solid var(--workbench-border)}.inventory-summary>div:last-child{border-right:0}.inventory-summary span{color:var(--workbench-muted);font-size:var(--admin-font-caption, 12px)}.inventory-summary strong{font-size:15px}.container-entry-table{table-layout:fixed}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:720px){.inventory-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-summary>div:nth-child(2){border-right:0}.inventory-summary>div:nth-child(-n+2){border-bottom:1px solid var(--workbench-border)}}@media(max-width:360px){.inventory-summary{grid-template-columns:1fr}.inventory-summary>div{border-right:0;border-bottom:1px solid var(--workbench-border)}.inventory-summary>div:last-child{border-bottom:0}}</style>
|
||||
|
||||
Reference in New Issue
Block a user