refactor(admin): use dialog for inbound relationships

This commit is contained in:
2026-08-30 18:21:12 +08:00
parent 8c1dc50cf3
commit abdea48e8d
6 changed files with 80 additions and 48 deletions
@@ -12,7 +12,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { listPlatformInbounds } from '../core/api' import { listPlatformInbounds } from '../core/api'
import type { InboundOrderSummary } from '../core/types' import type { InboundOrderSummary } from '../core/types'
@@ -45,8 +45,8 @@ const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
const selectedInbound = ref<InboundOrderSummary | null>(null) const selectedInbound = ref<InboundOrderSummary | null>(null)
const measurementDialogOpen = ref(false) const measurementDialogOpen = ref(false)
const reviewDialogOpen = ref(false) const reviewDialogOpen = ref(false)
const relationshipOpen = ref(false) const relationshipDialogOpen = ref(false)
const relationshipInboundCode = ref('') const relationshipDialogInboundCode = ref('')
const batchReviewLoading = ref(false) const batchReviewLoading = ref(false)
const selectedReviewRows = ref<InboundOrderSummary[]>([]) const selectedReviewRows = ref<InboundOrderSummary[]>([])
let requestSerial = 0 let requestSerial = 0
@@ -196,8 +196,8 @@ function openDetail(row: InboundOrderSummary) {
function openRelationship(row: InboundOrderSummary) { function openRelationship(row: InboundOrderSummary) {
const code = row.inboundCode?.trim() const code = row.inboundCode?.trim()
if (!code) return if (!code) return
relationshipInboundCode.value = code relationshipDialogInboundCode.value = code
relationshipOpen.value = true relationshipDialogOpen.value = true
} }
function handleTaskChanged() { function handleTaskChanged() {
@@ -391,7 +391,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
@submitted="handleMeasurementSubmitted" @submitted="handleMeasurementSubmitted"
@closed="clearSelectedInbound" @closed="clearSelectedInbound"
/> />
<InboundCodeRelationshipDrawer v-if="relationshipOpen" v-model="relationshipOpen" :inbound-code="relationshipInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" /> <InboundCodeRelationshipDialog v-if="relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
</AdminPage> </AdminPage>
</template> </template>
@@ -10,7 +10,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels' import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
@@ -43,8 +43,8 @@ const pageSize = ref(20)
const loading = ref(false) const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null) const loadError = ref<ApiErrorDetails | null>(null)
const printingInboundId = ref('') const printingInboundId = ref('')
const relationshipOpen = ref(false) const relationshipDialogOpen = ref(false)
const relationshipInboundCode = ref('') const relationshipDialogInboundCode = ref('')
const result = ref({ records: [] as InboundOrderSummary[], total: 0 }) const result = ref({ records: [] as InboundOrderSummary[], total: 0 })
const filters = ref<FilterModel>({ customerId: '', inboundCode: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] }) const filters = ref<FilterModel>({ customerId: '', inboundCode: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] })
const appliedFilters = ref<FilterModel>({ ...filters.value, expectedArrivalRange: [] }) const appliedFilters = ref<FilterModel>({ ...filters.value, expectedArrivalRange: [] })
@@ -289,7 +289,7 @@ function openDetail(row: InboundOrderSummary) {
} }
void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } }) void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } })
} }
function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim(); if (!code) return; relationshipInboundCode.value = code; relationshipOpen.value = true } function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim(); if (!code) return; relationshipDialogInboundCode.value = code; relationshipDialogOpen.value = true }
async function printCartonLabels(row: InboundOrderSummary) { async function printCartonLabels(row: InboundOrderSummary) {
if (printingInboundId.value) return if (printingInboundId.value) return
printingInboundId.value = row.id printingInboundId.value = row.id
@@ -460,7 +460,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId +
</DataTable> </DataTable>
</section> </section>
</ListPageLayout> </ListPageLayout>
<InboundCodeRelationshipDrawer v-if="relationshipOpen" v-model="relationshipOpen" :inbound-code="relationshipInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" /> <InboundCodeRelationshipDialog v-if="relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
</AdminPage> </AdminPage>
</template> </template>
@@ -5,11 +5,11 @@ import { useRoute, useRouter } from 'vue-router'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AdminPage from '@/shared/components/AdminPage.vue'; import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.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 AdminPage from '@/shared/components/AdminPage.vue'; import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.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 InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
import { listInventoryLedger } from '../api/inventory'; import { formatOverseasDateTime } from '../lastMilePresentation'; import type { InventoryTransactionListItem } from '../types' import { listInventoryLedger } from '../api/inventory'; import { formatOverseasDateTime } from '../lastMilePresentation'; import type { InventoryTransactionListItem } from '../types'
const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore(); const rows = ref<InventoryTransactionListItem[]>([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const loadError = ref<ApiErrorDetails | null>(null) const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore(); const rows = ref<InventoryTransactionListItem[]>([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const loadError = ref<ApiErrorDetails | null>(null)
const relationshipOpen = ref(false); const relationshipInboundCode = ref('') const relationshipDialogOpen = ref(false); const relationshipDialogInboundCode = ref('')
const filters = ref<QueryModel>({ occurredDate: [], customerName: '', warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundCode: '', inboundOrderNo: '', outboundOrderNo: '' }) const filters = ref<QueryModel>({ occurredDate: [], customerName: '', warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundCode: '', inboundOrderNo: '', outboundOrderNo: '' })
const fields = computed<QueryField[]>(() => [ const fields = computed<QueryField[]>(() => [
{ key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 }, { key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 },
@@ -27,7 +27,7 @@ const columns: DataTableColumn[] = [
{ key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }, { key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 },
] ]
function text(value: unknown) { return String(value ?? '').trim() || '—' } function text(value: unknown) { return String(value ?? '').trim() || '—' }
function openRelationship(row: InventoryTransactionListItem) { if (row.inboundCode === '—') return; relationshipInboundCode.value = row.inboundCode; relationshipOpen.value = true } function openRelationship(row: InventoryTransactionListItem) { if (row.inboundCode === '—') return; relationshipDialogInboundCode.value = row.inboundCode; relationshipDialogOpen.value = true }
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const range = Array.isArray(filters.value.occurredDate) ? filters.value.occurredDate.map(String) : []; loading.value = true; loadError.value = null; try { const result = await listInventoryLedger({ page: page.value, size: pageSize.value, customerName: String(filters.value.customerName || '').trim() || undefined, destinationWarehouseCode: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, businessType: String(filters.value.businessType || '').trim() || undefined, businessOrderNo: String(filters.value.businessOrderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, inboundOrderNo: String(filters.value.inboundOrderNo || '').trim() || undefined, outboundOrderNo: String(filters.value.outboundOrderNo || '').trim() || undefined, occurredFrom: range[0] ? `${range[0]}T00:00:00` : undefined, occurredTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), owner: text(item.customerName || '客户信息待补充'), warehouse: text(item.destinationWarehouseCode), sku: text(item.skuCode), productName: '—', businessType: text(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundCode: text(item.inboundCode), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: item.operatorId ? '操作人名称待补充' : '—', remark: text(item.remark) })); total.value = result.total } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '进销存记录加载失败,请重试') } finally { loading.value = false } } async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const range = Array.isArray(filters.value.occurredDate) ? filters.value.occurredDate.map(String) : []; loading.value = true; loadError.value = null; try { const result = await listInventoryLedger({ page: page.value, size: pageSize.value, customerName: String(filters.value.customerName || '').trim() || undefined, destinationWarehouseCode: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, businessType: String(filters.value.businessType || '').trim() || undefined, businessOrderNo: String(filters.value.businessOrderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, inboundOrderNo: String(filters.value.inboundOrderNo || '').trim() || undefined, outboundOrderNo: String(filters.value.outboundOrderNo || '').trim() || undefined, occurredFrom: range[0] ? `${range[0]}T00:00:00` : undefined, occurredTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), owner: text(item.customerName || '客户信息待补充'), warehouse: text(item.destinationWarehouseCode), sku: text(item.skuCode), productName: '—', businessType: text(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundCode: text(item.inboundCode), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: item.operatorId ? '操作人名称待补充' : '—', remark: text(item.remark) })); total.value = result.total } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '进销存记录加载失败,请重试') } finally { loading.value = false } }
function search(model: QueryModel) { page.value = 1; void load(model) }; function reset(model: QueryModel) { page.value = 1; void load(model) }; function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load) function search(model: QueryModel) { page.value = 1; void load(model) }; function reset(model: QueryModel) { page.value = 1; void load(model) }; function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load)
</script> </script>
@@ -36,6 +36,6 @@ function search(model: QueryModel) { page.value = 1; void load(model) }; functio
<template v-if="loadError" #state><LoadErrorState title="进销存记录加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template> <template v-if="loadError" #state><LoadErrorState title="进销存记录加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
<template v-else #state><p class="read-only-boundary">出库相关字段仅展示既有库存流水事实;本页面不提供出库单跳转、库存扣减或其他离仓操作。当前流水 DTO 未提供产品名称与海外仓名称,对应列显示空值。</p></template> <template v-else #state><p class="read-only-boundary">出库相关字段仅展示既有库存流水事实;本页面不提供出库单跳转、库存扣减或其他离仓操作。当前流水 DTO 未提供产品名称与海外仓名称,对应列显示空值。</p></template>
<section v-if="!loadError" 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="overseas-inventory-transactions" row-key="id" empty-text="暂无进销存记录" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-actions><el-button @click="back"><ArrowLeft :size="16" />返回库存管理</el-button></template><template #actions="{ row }"><el-button link type="primary" :disabled="(row as InventoryTransactionListItem).inboundCode === '—'" @click="openRelationship(row as InventoryTransactionListItem)">关联数据</el-button></template><template #pagination-left>共 {{ total }} 条进销存记录</template></DataTable></section> <section v-if="!loadError" 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="overseas-inventory-transactions" row-key="id" empty-text="暂无进销存记录" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-actions><el-button @click="back"><ArrowLeft :size="16" />返回库存管理</el-button></template><template #actions="{ row }"><el-button link type="primary" :disabled="(row as InventoryTransactionListItem).inboundCode === '—'" @click="openRelationship(row as InventoryTransactionListItem)">关联数据</el-button></template><template #pagination-left>共 {{ total }} 条进销存记录</template></DataTable></section>
<InboundCodeRelationshipDrawer v-if="relationshipOpen" v-model="relationshipOpen" :inbound-code="relationshipInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" /> <InboundCodeRelationshipDialog v-if="relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
</ListPageLayout></AdminPage></template> </ListPageLayout></AdminPage></template>
<style scoped>.read-only-boundary{margin:8px 2px 0;color:var(--cc-muted);font-size:12px;line-height:1.5}</style> <style scoped>.read-only-boundary{margin:8px 2px 0;color:var(--cc-muted);font-size:12px;line-height:1.5}</style>
@@ -36,7 +36,7 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.vue' import LineClampText from '@/shared/components/LineClampText.vue'
import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue' import StatusBadge from '@/shared/components/StatusBadge.vue'
import { import {
@@ -141,8 +141,8 @@ const noteContent = ref('')
const noteOrder = ref<UnifiedOrderResource | null>(null) const noteOrder = ref<UnifiedOrderResource | null>(null)
const relationOpen = ref(false) const relationOpen = ref(false)
const relationOrder = ref<UnifiedOrderResource | null>(null) const relationOrder = ref<UnifiedOrderResource | null>(null)
const inboundRelationshipOpen = ref(false) const inboundRelationshipDialogOpen = ref(false)
const inboundRelationshipCode = ref('') const inboundRelationshipDialogCode = ref('')
const measurementDetail = ref<InboundMeasurementDetail | null>(null) const measurementDetail = ref<InboundMeasurementDetail | null>(null)
const measurementLoading = ref(false) const measurementLoading = ref(false)
const measurementError = ref<ApiErrorDetails | null>(null) const measurementError = ref<ApiErrorDetails | null>(null)
@@ -793,8 +793,8 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
function openInboundRelationship(row: UnifiedOrderResource) { function openInboundRelationship(row: UnifiedOrderResource) {
const code = row.inboundCode?.trim() const code = row.inboundCode?.trim()
if (!code) return if (!code) return
inboundRelationshipCode.value = code inboundRelationshipDialogCode.value = code
inboundRelationshipOpen.value = true inboundRelationshipDialogOpen.value = true
} }
function coLoadGroup(row: UnifiedOrderResource) { function coLoadGroup(row: UnifiedOrderResource) {
@@ -1419,7 +1419,7 @@ onBeforeUnmount(() => {
:inbound-code="relationOrder.inboundCode" :inbound-code="relationOrder.inboundCode"
/> />
</AppDrawer> </AppDrawer>
<InboundCodeRelationshipDrawer v-if="inboundRelationshipOpen" v-model="inboundRelationshipOpen" :inbound-code="inboundRelationshipCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" /> <InboundCodeRelationshipDialog v-if="inboundRelationshipDialogOpen" v-model="inboundRelationshipDialogOpen" :inbound-code="inboundRelationshipDialogCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
<AppDialog v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote"> <AppDialog v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" /> <el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
@@ -1,4 +1,4 @@
import { readFileSync } from 'node:fs' import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { flushPromises, mount } from '@vue/test-utils' import { flushPromises, mount } from '@vue/test-utils'
@@ -9,7 +9,7 @@ import {
getOverseasInboundCodeRelationship, getOverseasInboundCodeRelationship,
getPlatformInboundCodeAssociation, getPlatformInboundCodeAssociation,
} from '@/shared/api/inboundCodeRelations' } from '@/shared/api/inboundCodeRelations'
import InboundCodeRelationshipDrawer from './InboundCodeRelationshipDrawer.vue' import InboundCodeRelationshipDialog from './InboundCodeRelationshipDialog.vue'
vi.mock('@/shared/api/inboundCodeRelations', () => ({ vi.mock('@/shared/api/inboundCodeRelations', () => ({
getLoadingAssociationSummary: vi.fn(), getLoadingAssociationSummary: vi.fn(),
@@ -17,15 +17,15 @@ vi.mock('@/shared/api/inboundCodeRelations', () => ({
getPlatformInboundCodeAssociation: vi.fn(), getPlatformInboundCodeAssociation: vi.fn(),
})) }))
const source = readFileSync(resolve(process.cwd(), 'src/shared/components/InboundCodeRelationshipDrawer.vue'), 'utf8') const source = readFileSync(resolve(process.cwd(), 'src/shared/components/InboundCodeRelationshipDialog.vue'), 'utf8')
const AppDrawerStub = defineComponent({ const AppDialogStub = defineComponent({
props: { modelValue: Boolean, title: String, loading: Boolean }, props: { modelValue: Boolean, title: String, loading: Boolean },
emits: ['update:modelValue'], emits: ['update:modelValue'],
template: '<aside><h2>{{ title }}</h2><slot /></aside>', template: '<aside><h2>{{ title }}</h2><slot /></aside>',
}) })
function mountDrawer() { function mountDialog() {
return mount(InboundCodeRelationshipDrawer, { return mount(InboundCodeRelationshipDialog, {
props: { props: {
modelValue: true, modelValue: true,
inboundCode: 'NYEXX2608300027', inboundCode: 'NYEXX2608300027',
@@ -35,9 +35,9 @@ function mountDrawer() {
}, },
global: { global: {
stubs: { stubs: {
AppDrawer: AppDrawerStub, AppDialog: AppDialogStub,
ApiErrorMeta: true, ApiErrorMeta: true,
LoadErrorState: true, LoadErrorState: { props: ['title', 'message'], template: '<div>{{ title }} · {{ message }}</div>' },
StatusBadge: true, StatusBadge: true,
'el-alert': true, 'el-alert': true,
'el-empty': { props: ['description'], template: '<p>{{ description }}</p>' }, 'el-empty': { props: ['description'], template: '<p>{{ description }}</p>' },
@@ -48,7 +48,7 @@ function mountDrawer() {
}) })
} }
describe('InboundCodeRelationshipDrawer contract', () => { describe('InboundCodeRelationshipDialog contract', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
vi.mocked(getPlatformInboundCodeAssociation).mockResolvedValue({ vi.mocked(getPlatformInboundCodeAssociation).mockResolvedValue({
@@ -73,7 +73,7 @@ describe('InboundCodeRelationshipDrawer contract', () => {
}) })
it('loads all three sections on an initially open mount and reloads after closing and reopening', async () => { it('loads all three sections on an initially open mount and reloads after closing and reopening', async () => {
const wrapper = mountDrawer() const wrapper = mountDialog()
await flushPromises() await flushPromises()
expect(getPlatformInboundCodeAssociation).toHaveBeenCalledWith('NYEXX2608300027') expect(getPlatformInboundCodeAssociation).toHaveBeenCalledWith('NYEXX2608300027')
@@ -106,20 +106,30 @@ describe('InboundCodeRelationshipDrawer contract', () => {
inboundCode: 'NYEXX2608300027', found: false, customer: null, overseasWarehouse: null, inboundCode: 'NYEXX2608300027', found: false, customer: null, overseasWarehouse: null,
overseasReceivingStatus: null, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], overseasReceivingStatus: null, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [],
}) })
const wrapper = mountDrawer() const wrapper = mountDialog()
await flushPromises() await flushPromises()
expect(wrapper.text()).toContain('服务端未找到该入仓码的海外关联数据') expect(wrapper.text()).toContain('服务端未找到该入仓码的海外关联数据')
expect(wrapper.text()).toContain('FL2093975748931063810') expect(wrapper.text()).toContain('FL2093975748931063810')
expect(wrapper.text()).toContain('LP-260830-0016') expect(wrapper.text()).toContain('LP-260830-0016')
}) })
it('keeps a failed section local while the other successful sections stay visible', async () => {
vi.mocked(getOverseasInboundCodeRelationship).mockRejectedValueOnce(new Error('overseas unavailable'))
const wrapper = mountDialog()
await flushPromises()
expect(wrapper.text()).toContain('海外关联加载失败')
expect(wrapper.text()).toContain('FL2093975748931063810')
expect(wrapper.text()).toContain('LP-260830-0016')
expect(wrapper.text()).not.toContain('洛杉矶海外仓')
})
it('distinguishes an unassigned overseas warehouse from a missing warehouse name', async () => { it('distinguishes an unassigned overseas warehouse from a missing warehouse name', async () => {
vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValueOnce({ vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValueOnce({
inboundCode: 'NYEXX2608300027', found: true, inboundCode: 'NYEXX2608300027', found: true,
customer: { customerId: '90071992547409932', customerName: '纽约客户' }, overseasWarehouse: null, customer: { customerId: '90071992547409932', customerName: '纽约客户' }, overseasWarehouse: null,
overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [],
}) })
const unassigned = mountDrawer() const unassigned = mountDialog()
await flushPromises() await flushPromises()
expect(unassigned.text()).toContain('尚未分配海外仓') expect(unassigned.text()).toContain('尚未分配海外仓')
expect(unassigned.text()).not.toContain('仓库名称待补充') expect(unassigned.text()).not.toContain('仓库名称待补充')
@@ -131,7 +141,7 @@ describe('InboundCodeRelationshipDrawer contract', () => {
overseasWarehouse: { warehouseId: '90071992547409939', warehouseCode: 'LAX', warehouseName: '' }, overseasWarehouse: { warehouseId: '90071992547409939', warehouseCode: 'LAX', warehouseName: '' },
overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [],
}) })
const missingName = mountDrawer() const missingName = mountDialog()
await flushPromises() await flushPromises()
expect(missingName.text()).toContain('仓库名称待补充') expect(missingName.text()).toContain('仓库名称待补充')
expect(missingName.text()).not.toContain('尚未分配海外仓') expect(missingName.text()).not.toContain('尚未分配海外仓')
@@ -165,10 +175,11 @@ describe('InboundCodeRelationshipDrawer contract', () => {
expect(source).toContain(':title="display(baseAssociation.inboundCode)"') expect(source).toContain(':title="display(baseAssociation.inboundCode)"')
}) })
it('keeps sections and status badges within the drawer at desktop and narrow widths', () => { it('keeps sections and status badges within the dialog at desktop and narrow widths', () => {
expect(source).toContain('class="inbound-relationship-drawer"') expect(source).toContain('class="inbound-relationship-dialog"')
expect(source).toContain(':global(.inbound-relationship-drawer .el-drawer__body){box-sizing:border-box;min-width:0;max-width:100%;overflow-x:hidden;padding-right:calc(var(--el-drawer-padding-primary) + 8px)}') expect(source).toContain('width="min(1060px, 94vw)"')
expect(source).toContain(':global(.inbound-relationship-drawer .app-drawer__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}') expect(source).toContain(':global(.inbound-relationship-dialog .el-dialog__body){box-sizing:border-box;min-width:0;max-height:calc(90vh - 112px);overflow-x:hidden;overflow-y:auto;padding-right:28px}')
expect(source).toContain(':global(.inbound-relationship-dialog .app-dialog__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}')
expect(source).toContain('.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr)') expect(source).toContain('.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr)')
expect(source).toContain('.relationship-section{box-sizing:border-box;width:100%;min-width:0') expect(source).toContain('.relationship-section{box-sizing:border-box;width:100%;min-width:0')
expect(source).toContain('flex-wrap:wrap') expect(source).toContain('flex-wrap:wrap')
@@ -177,8 +188,8 @@ describe('InboundCodeRelationshipDrawer contract', () => {
expect(source).toContain('.inventory-table{width:100%;max-width:100%;min-width:0') expect(source).toContain('.inventory-table{width:100%;max-width:100%;min-width:0')
}) })
it('mounts the containment class through the real AppDrawer and Element Plus drawer DOM', async () => { it('mounts the containment class through the real AppDialog and Element Plus dialog DOM', async () => {
const wrapper = mount(InboundCodeRelationshipDrawer, { const wrapper = mount(InboundCodeRelationshipDialog, {
attachTo: document.body, attachTo: document.body,
props: { props: {
modelValue: true, modelValue: true,
@@ -191,15 +202,36 @@ describe('InboundCodeRelationshipDrawer contract', () => {
}) })
await flushPromises() await flushPromises()
const drawer = document.querySelector('.inbound-relationship-drawer') const dialog = document.querySelector('.inbound-relationship-dialog')
const elementBody = drawer?.querySelector('.el-drawer__body') const elementBody = dialog?.querySelector('.el-dialog__body')
const appBody = drawer?.querySelector('.app-drawer__body') const appBody = dialog?.querySelector('.app-dialog__body')
expect(drawer).not.toBeNull() expect(dialog).not.toBeNull()
expect(elementBody).not.toBeNull() expect(elementBody).not.toBeNull()
expect(appBody).not.toBeNull() expect(appBody).not.toBeNull()
expect(appBody?.querySelectorAll('.relationship-section')).toHaveLength(3) expect(appBody?.querySelectorAll('.relationship-section')).toHaveLength(3)
expect(appBody?.querySelectorAll('.relationship-status')).toHaveLength(3) expect(appBody?.querySelectorAll('.relationship-status')).toHaveLength(3)
expect(dialog?.querySelector('.el-drawer__body')).toBeNull()
wrapper.unmount() wrapper.unmount()
}) })
it('removes the retired Drawer component and all Admin consumer references', () => {
const root = process.cwd()
expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.vue'))).toBe(false)
expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.test.ts'))).toBe(false)
const consumerFiles = [
'src/modules/unified-orders/pages/UnifiedOrdersView.vue',
'src/modules/inbounds/pages/InboundOrdersView.vue',
'src/modules/inbounds/components/InboundTaskList.vue',
'src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue',
]
const consumerSources = consumerFiles.map((file) => readFileSync(resolve(root, file), 'utf8'))
const consumers = consumerSources.join('\n')
expect(consumers).not.toContain('InboundCodeRelationshipDrawer')
expect(consumers).not.toMatch(/relationshipOpen|relationshipInboundCode|inboundRelationshipOpen/)
consumerSources.forEach((consumer) => {
expect(consumer).toContain("import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'")
expect(consumer).toContain('<InboundCodeRelationshipDialog')
})
})
}) })
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship, type PlatformInboundCodeAssociation } from '@/shared/api/inboundCodeRelations' import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship, type PlatformInboundCodeAssociation } from '@/shared/api/inboundCodeRelations'
import ApiErrorMeta from './ApiErrorMeta.vue' import ApiErrorMeta from './ApiErrorMeta.vue'
import AppDrawer from './AppDrawer.vue' import AppDialog from './AppDialog.vue'
import LoadErrorState from './LoadErrorState.vue' import LoadErrorState from './LoadErrorState.vue'
import StatusBadge from './StatusBadge.vue' import StatusBadge from './StatusBadge.vue'
@@ -50,7 +50,7 @@ watch(() => [props.modelValue, props.inboundCode, props.canReadBase, props.canRe
</script> </script>
<template> <template>
<AppDrawer class="inbound-relationship-drawer" :model-value="modelValue" :title="`入仓码关联数据 · ${normalizedCode || '未提供'}`" size="min(860px, 94vw)" :loading="loading" :show-footer="false" destroy-on-close @update:model-value="close"> <AppDialog class="inbound-relationship-dialog" :model-value="modelValue" :title="`入仓码关联数据 · ${normalizedCode || '未提供'}`" width="min(1060px, 94vw)" :loading="loading" :show-footer="false" destroy-on-close @update:model-value="close">
<el-alert v-if="!normalizedCode" type="warning" :closable="false" show-icon title="缺少入仓码,已停止查询关联数据。" /> <el-alert v-if="!normalizedCode" type="warning" :closable="false" show-icon title="缺少入仓码,已停止查询关联数据。" />
<el-alert v-else-if="!canReadBase && !canReadLoading && !canReadOverseas" type="warning" :closable="false" show-icon title="当前账号无入仓关联数据查看权限。" /> <el-alert v-else-if="!canReadBase && !canReadLoading && !canReadOverseas" type="warning" :closable="false" show-icon title="当前账号无入仓关联数据查看权限。" />
@@ -90,9 +90,9 @@ watch(() => [props.modelValue, props.inboundCode, props.canReadBase, props.canRe
</template> </template>
</section> </section>
</div> </div>
</AppDrawer> </AppDialog>
</template> </template>
<style scoped> <style scoped>
:global(.inbound-relationship-drawer .el-drawer__body){box-sizing:border-box;min-width:0;max-width:100%;overflow-x:hidden;padding-right:calc(var(--el-drawer-padding-primary) + 8px)}:global(.inbound-relationship-drawer .app-drawer__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr);gap:14px}.relationship-section{box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--cc-border);border-radius:10px;padding:14px;background:var(--cc-surface)}.relationship-section>header{display:flex;min-width:0;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 12px;margin-bottom:12px}.relationship-section h3{flex:1 1 auto;min-width:0;margin:0;font-size:15px}.relationship-status{flex:0 0 auto;max-width:100%}.compact-facts{display:grid;min-width:0;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;margin:0;background:var(--cc-border)}.compact-facts>div{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:9px 10px;background:var(--cc-surface)}.compact-facts dt{color:var(--cc-muted)}.compact-facts dd{min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.compact-facts small{display:block;overflow:hidden;color:var(--cc-muted);margin-top:2px;text-overflow:ellipsis;white-space:nowrap}.compact-list{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:9px 10px;border-top:1px solid var(--cc-border)}.compact-list span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inventory-table{width:100%;max-width:100%;min-width:0;margin-top:10px}@media(max-width:720px){.compact-facts{grid-template-columns:1fr}} :global(.inbound-relationship-dialog.el-dialog){width:min(1060px,94vw);max-width:94vw}:global(.inbound-relationship-dialog .el-dialog__body){box-sizing:border-box;min-width:0;max-height:calc(90vh - 112px);overflow-x:hidden;overflow-y:auto;padding-right:28px}:global(.inbound-relationship-dialog .app-dialog__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr);gap:12px}.relationship-section{box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--cc-border);border-radius:8px;padding:12px;background:var(--cc-surface)}.relationship-section>header{display:flex;min-width:0;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 12px;margin-bottom:10px}.relationship-section h3{flex:1 1 auto;min-width:0;margin:0;font-size:15px}.relationship-status{flex:0 0 auto;max-width:100%}.compact-facts{display:grid;min-width:0;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;margin:0;background:var(--cc-border)}.compact-facts>div{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:8px 10px;background:var(--cc-surface)}.compact-facts dt{color:var(--cc-muted)}.compact-facts dd{min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.compact-facts small{display:block;overflow:hidden;color:var(--cc-muted);margin-top:2px;text-overflow:ellipsis;white-space:nowrap}.compact-list{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:8px 10px;border-top:1px solid var(--cc-border)}.compact-list span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inventory-table{width:100%;max-width:100%;min-width:0;margin-top:8px}@media(max-width:720px){:global(.inbound-relationship-dialog .el-dialog__body){padding-right:20px}.compact-facts{grid-template-columns:1fr}}
</style> </style>