This commit is contained in:
2026-08-10 16:24:54 +08:00
parent e119b48662
commit 820432a381
52 changed files with 1015 additions and 705 deletions
@@ -14,7 +14,7 @@ describe('InboundOrderDetailView', () => {
expect(source).toContain('缺少所属企业信息,请从入库单列表重新打开')
expect(source).not.toContain('PlatformTenantSelector')
expect(source).not.toContain('租户')
expect(source).toContain('<h1>入库单详情</h1>')
expect(source).toContain('<PageToolbar title="入库单详情"')
expect(source).toContain('货品与测量、费用信息、状态历史及其他记录')
expect(source).toContain('class="surface-panel detail-page"')
expect(source).toContain('class="detail-page-actions"')
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@lucide/vue'
import { ArrowLeft, RefreshCw } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import FormSection from '@/shared/components/FormSection.vue'
import PageToolbar from '@/shared/components/PageToolbar.vue'
import InboundWorkflowWorkspace from '../components/InboundWorkflowWorkspace.vue'
const route = useRoute()
@@ -21,19 +25,17 @@ function backToList() {
</script>
<template>
<div class="business-page inbound-detail-page">
<section class="surface-panel detail-page">
<header class="detail-page-header">
<div class="detail-page-heading">
<h1>入库单详情</h1>
<p>查看基本信息、货品与测量、费用信息、状态历史及其他记录。</p>
</div>
<div class="detail-page-actions">
<el-button @click="backToList"><ArrowLeft :size="16" />返回入库单列表</el-button>
</div>
</header>
<AdminPage as="div" class="business-page inbound-detail-page">
<DetailPageLayout class="surface-panel detail-page" aria-label="入库单详情">
<template #header>
<PageToolbar title="入库单详情" description="查看基本信息、货品与测量、费用信息、状态历史及其他记录。">
<div class="detail-page-actions">
<el-button @click="backToList"><ArrowLeft :size="16" />返回入库单列表</el-button>
</div>
</PageToolbar>
</template>
<div class="detail-page-content">
<FormSection title="入库处理工作区" :bordered="false">
<InboundWorkflowWorkspace
v-if="inboundId && customerId"
:customer-id="customerId"
@@ -43,23 +45,19 @@ function backToList() {
v-else
:description="inboundId ? '缺少所属企业信息,请从入库单列表重新打开' : '缺少有效的入库单标识'"
/>
</div>
</section>
</div>
</FormSection>
</DetailPageLayout>
</AdminPage>
</template>
<style scoped>
.inbound-detail-page { min-width: 0; }
.detail-page { overflow: hidden; }
.detail-page-header { display: flex; min-height: 88px; align-items: center; justify-content: space-between; gap: 18px; padding: 20px 24px; border-bottom: 1px solid var(--cc-border); }
.detail-page-heading { min-width: 0; }
.detail-page-heading h1 { margin: 0; color: var(--cc-text); font-size: 20px; line-height: 1.35; }
.detail-page-heading p { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.detail-page { overflow: hidden; padding: 20px 24px; }
.detail-page-actions { display: flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 12px; }
.detail-page-content { min-width: 0; }
@media (max-width: 720px) {
.detail-page-header { align-items: flex-start; flex-direction: column; padding: 16px; }
.detail-page { padding: 16px; }
.detail-page-actions { width: 100%; align-items: stretch; flex-direction: column; }
.detail-page-actions :deep(.el-button) { width: 100%; min-height: 44px; }
}
@media (max-width: 360px) { .detail-page { padding: 12px; } }
</style>
@@ -107,7 +107,7 @@ describe('InboundOrdersView', () => {
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200 })
expect(document.body.textContent).toContain('RK-001')
expect(document.body.textContent).toContain('查看')
expect(document.body.textContent).toContain('所属企业 示例企业有限公司')
expect(document.body.textContent).toContain('示例企业有限公司')
expect(document.body.textContent).not.toContain('租户')
expect(wrapper.find('.inbound-tabs .is-active').text()).toContain('全部')
})
@@ -209,10 +209,9 @@ describe('InboundOrdersView', () => {
expect(toolbarRows[0].classList.contains('table-tabs-row')).toBe(true)
expect(toolbarRows[1].classList.contains('inbound-stats-row')).toBe(true)
expect(toolbarRows).toHaveLength(2)
expect(wrapper.find('.inbound-stats-row').text()).toContain('当前显示 1 条 · 共 1 条')
const dataTableToolbar = wrapper.findComponent(DataTable).find('.data-table__toolbar')
expect(dataTableToolbar.text()).toContain('所属企业 全部客户')
expect(dataTableToolbar.text()).toContain('刷新')
expect(wrapper.findComponent(DataTable).find('.data-table__pagination-left').text()).toContain('当前显示 1 条 · 共 1 条')
expect(wrapper.findComponent(DataTable).find('.data-table__toolbar-start').text()).not.toContain('入库单列表')
expect(wrapper.text()).not.toContain('刷新')
expect(wrapper.text()).not.toContain('全部入库单')
expect(tabButtons[0].attributes('aria-selected')).toBe('true')
expect(tabButtons[0].attributes('tabindex')).toBe('0')
@@ -4,9 +4,13 @@ import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ChevronDown, ChevronUp, ClipboardCheck, Eye, Printer, RefreshCw, SlidersHorizontal } from '@lucide/vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
@@ -400,25 +404,32 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
</script>
<template>
<div class="business-page inbound-orders-page">
<QueryToolbar v-model="filters" class="surface-panel inbound-query-toolbar" ariaLabel="入库单筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="inbound-orders-query" :search-on-reset="false" @search="applyFilters" @reset="resetFilters">
<template #actions>
<el-button :aria-expanded="showAdvancedFilters" @click="toggleAdvancedFilters">
<SlidersHorizontal :size="16" />
{{ showAdvancedFilters ? '收起筛选' : '更多筛选' }}
<ChevronUp v-if="showAdvancedFilters" :size="15" />
<ChevronDown v-else :size="15" />
</el-button>
<AdminPage as="div" class="business-page inbound-orders-page">
<ListPageLayout ariaLabel="入库单管理">
<template #toolbar>
<QueryToolbar v-model="filters" class="surface-panel inbound-query-toolbar" ariaLabel="入库单筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="inbound-orders-query" :search-on-reset="false" @search="applyFilters" @reset="resetFilters">
<template #actions>
<el-button :aria-expanded="showAdvancedFilters" @click="toggleAdvancedFilters">
<SlidersHorizontal :size="16" />
{{ showAdvancedFilters ? '收起筛选' : '更多筛选' }}
<ChevronUp v-if="showAdvancedFilters" :size="15" />
<ChevronDown v-else :size="15" />
</el-button>
</template>
</QueryToolbar>
</template>
</QueryToolbar>
<el-alert v-if="customerLoadError" type="warning" :closable="false" show-icon role="alert">
<template #title><div class="api-error-alert-title"><span>{{ customerLoadError.message }}</span><ApiErrorMeta :details="customerLoadError" /></div></template>
<el-button link type="warning" :loading="customersLoading" @click="loadCustomers">重新加载客户列表</el-button>
</el-alert>
<template v-if="customerLoadError || loadError" #state>
<div class="inbound-state-stack">
<el-alert v-if="customerLoadError" type="warning" :closable="false" show-icon role="alert">
<template #title><div class="api-error-alert-title"><span>{{ customerLoadError.message }}</span><ApiErrorMeta :details="customerLoadError" /></div></template>
<el-button link type="warning" :loading="customersLoading" @click="loadCustomers"><RefreshCw :size="15" />重新加载客户列表</el-button>
</el-alert>
<LoadErrorState v-if="loadError" :message="loadError.message" title="入库单暂时无法加载" @retry="loadInbounds"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
</div>
</template>
<LoadErrorState v-if="loadError" class="inbound-load-error" :message="loadError.message" title="入库单暂时无法加载" @retry="loadInbounds"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<section v-else class="surface-panel table-panel" :aria-busy="loading">
<section v-if="!loadError" class="surface-panel table-panel" :aria-busy="loading">
<header class="table-toolbar">
<div class="table-tabs-row">
<nav class="inbound-tabs" role="tablist" aria-label="入库单处理状态">
@@ -426,7 +437,6 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
</nav>
</div>
<div class="inbound-stats-row" aria-label="入库单统计">
<span class="selected-info">当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</span>
<span class="total-info">
<b>总箱数 {{ formatAggregate(aggregateStats.packages, 0) }}</b>
<i aria-hidden="true" />
@@ -439,8 +449,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
</div>
</header>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="columns" preference-key="inbound-orders-table" :loading="loading" :total="result.total" :page-sizes="[20, 50, 100]" row-key="id" :empty-text="listEmptyText" aria-label="入库单列表">
<template #toolbar-start><div><h2>入库单列表</h2><p>所属企业 {{ selectedCustomerName }}</p></div></template>
<template #toolbar-actions><el-button :loading="loading" @click="loadInbounds"><RefreshCw :size="16" />刷新</el-button></template>
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div></template>
<template #inbound="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong><span>订单 {{ text((row as InboundOrderSummary).orderNo) }}</span></div></template>
<template #orderStatus="{ row }"><div class="status-stack"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag><span>订单:{{ text((row as InboundOrderSummary).orderStatusSnapshot) }}</span></div></template>
@@ -481,13 +490,19 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<template #createdAt="{ row }">{{ formatDate((row as InboundOrderSummary).createdAt) }}</template>
<template #empty><el-empty :description="listEmptyText" /></template>
</DataTable>
</section>
</section>
</ListPageLayout>
<AppDrawer v-model="detailOpen" :title="detail ? `入库单详情 · ${detail.summary.inboundCode || detail.summary.id}` : '入库单详情'" size="min(720px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail">
<section v-if="detailError" class="detail-error" role="alert"><strong>入库单详情暂时无法加载</strong><p>{{ detailError.message }}</p><ApiErrorMeta :details="detailError" /><el-button type="primary" :disabled="!detailTarget" @click="detailTarget && openDetail(detailTarget)">重新加载</el-button></section>
<div v-else-if="detail" class="detail-content"><InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" /><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="null" /></div>
<DetailPageLayout aria-label="入库单详情抽屉">
<section v-if="detailError" class="detail-error" role="alert"><strong>入库单详情暂时无法加载</strong><p>{{ detailError.message }}</p><ApiErrorMeta :details="detailError" /><el-button type="primary" :disabled="!detailTarget" @click="detailTarget && openDetail(detailTarget)"><RefreshCw :size="16" />重新加载</el-button></section>
<div v-else-if="detail" class="detail-content">
<FormSection title="入库概览"><InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" /></FormSection>
<FormSection title="资源与状态记录" :bordered="false"><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="null" /></FormSection>
</div>
</DetailPageLayout>
</AppDrawer>
</div>
</AdminPage>
</template>
<style scoped>
@@ -499,7 +514,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
.inbound-tab:hover { color: var(--cc-primary); background: #f8fbff; }
.inbound-tab:focus-visible { outline: 3px solid rgba(102, 126, 234, .2); outline-offset: 1px; }
.inbound-tab.is-active { border-bottom-color: var(--cc-primary); background: #fff; color: var(--cc-primary); font-weight: 650; }
.inbound-load-error { margin-top: 12px; }
.inbound-state-stack { display: grid; min-width: 0; gap: 12px; }
.row-actions { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.table-toolbar { display: grid; min-width: 0; border-bottom: 1px solid var(--cc-border); }
@@ -521,4 +536,5 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
.detail-error :deep(.el-button) { margin-top: 14px; }
@media (max-width: 900px) { .inbound-tabs { width: 100%; } }
@media (max-width: 720px) { .inbound-tab { padding-inline: 14px; } .table-tabs-row { padding: 0 10px; } .inbound-stats-row { min-height: 0; align-items: stretch; flex-direction: column; gap: 10px; padding: 10px; } .inbound-stats-row .total-info { justify-content: flex-start; } }
@media (max-width: 360px) { .inbound-tab { padding-inline: 12px; } .row-actions { align-items: flex-start; flex-direction: column; } }
</style>