优化
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const pages = [
|
||||
['src/modules/container-loading/pages/ContainersView.vue', "key: 'cabinetNo'"],
|
||||
['src/modules/loading-lists/components/LoadingListsView.vue', "key: 'preloadNo'"],
|
||||
['src/modules/ocean-bookings/pages/OceanBookingsView.vue', "prop: 'bookingNo'"],
|
||||
['src/modules/port-schedules/pages/PortSchedulesView.vue', "prop: 'schedule.scheduleNo'"],
|
||||
['src/modules/trucking/pages/TruckAppointmentsView.vue', "key: 'appointmentNo'"],
|
||||
['src/modules/customs/pages/CustomsCasesView.vue', "prop: 'customsNo'"],
|
||||
['src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue', "prop: 'orderNo'"],
|
||||
] as const
|
||||
|
||||
function source(path: string) {
|
||||
return readFileSync(resolve(process.cwd(), path), 'utf8')
|
||||
}
|
||||
|
||||
describe('domestic outbound list responsive contracts', () => {
|
||||
it.each(pages)('%s keeps the operation column first and fixed left', (path, firstBusinessColumn) => {
|
||||
const content = source(path)
|
||||
const firstAction = content.indexOf("label: '操作'")
|
||||
expect(firstAction).toBeGreaterThan(-1)
|
||||
expect(firstAction).toBeLessThan(content.indexOf(firstBusinessColumn, firstAction))
|
||||
const actionObjects = [...content.matchAll(/\{[^{}]*label: '操作'[^{}]*\}/g)].map((match) => match[0])
|
||||
expect(actionObjects.length).toBeGreaterThan(0)
|
||||
for (const column of actionObjects) expect(column).toContain("fixed: 'left'")
|
||||
})
|
||||
|
||||
it.each(pages)('%s gives every business column a stable width constraint', (path) => {
|
||||
const content = source(path)
|
||||
const columns = [...content.matchAll(/\{[^{}]*(?:key|prop): '[^']+'[^{}]*label: '[^']+'[^{}]*\}/g)]
|
||||
.map((match) => match[0])
|
||||
.filter((column) => !column.includes("type: 'input'") && !column.includes("type: 'select'") && !column.includes("type: 'daterange'"))
|
||||
expect(columns.length).toBeGreaterThan(1)
|
||||
for (const column of columns) expect(column).toMatch(/\b(?:width|minWidth):\s*\d+/)
|
||||
})
|
||||
|
||||
it('keeps shared 320px degradation rules for filters, actions and pagination', () => {
|
||||
const queryToolbar = source('src/shared/components/QueryToolbar.vue')
|
||||
const dataTable = source('src/shared/components/DataTable.vue')
|
||||
const listLayout = source('src/shared/components/ListPageLayout.vue')
|
||||
|
||||
expect(queryToolbar).toContain('@media (max-width: 720px)')
|
||||
expect(queryToolbar).toContain('.query-toolbar__field { width: 100%; }')
|
||||
expect(queryToolbar).toContain('grid-template-columns: repeat(auto-fit, minmax(120px, 1fr))')
|
||||
expect(dataTable).toContain('@media (max-width: 720px)')
|
||||
expect(dataTable).toContain('flex-direction: column')
|
||||
expect(dataTable).toContain('.data-table__page-buttons { flex-wrap: wrap; }')
|
||||
expect(listLayout).toContain('min-width: 0')
|
||||
})
|
||||
})
|
||||
@@ -324,7 +324,7 @@ watch(orderPage, (page, previousPage) => {
|
||||
<header class="detail-header">
|
||||
<el-button class="back-button" @click="router.push({ name: 'customers' })"><ArrowLeft :size="17" />返回列表</el-button>
|
||||
<div class="company-mark" aria-hidden="true"><Building2 :size="23" /></div>
|
||||
<PageToolbar class="detail-page-toolbar" as="section" :title="customer?.tenantName || '客户详情'" :description="customer?.tenantCode || tenantId">
|
||||
<PageToolbar class="detail-page-toolbar" as="section" :title="customer?.tenantName || '客户详情'" :description="customer?.tenantCode || '客户资料'">
|
||||
<template #actions>
|
||||
<div v-if="customer" class="header-actions">
|
||||
<el-tag :type="statusMeta(customer.status).type" effect="light" round>{{ statusMeta(customer.status).label }}</el-tag>
|
||||
|
||||
@@ -546,7 +546,7 @@ describe('CustomersView', () => {
|
||||
expect(buttons.map((button) => button.getAttribute('aria-label'))).toEqual([
|
||||
'更多客户操作:客户甲',
|
||||
'更多客户操作:TEN-B',
|
||||
'更多客户操作:10',
|
||||
'更多客户操作:当前客户',
|
||||
])
|
||||
expect(buttons.map((button) => button.getAttribute('title'))).toEqual(buttons.map((button) => button.getAttribute('aria-label')))
|
||||
})
|
||||
|
||||
@@ -190,13 +190,13 @@ const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', minWidth: 280, slotName: 'actions' },
|
||||
{ key: 'tenantCode', prop: 'tenantCode', label: '客户编号', minWidth: 170 },
|
||||
{ key: 'company', label: '公司名称', minWidth: 230, slotName: 'company' },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', minWidth: 205, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', minWidth: 205, defaultVisible: false, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
{ key: 'contactName', prop: 'contactName', label: '联系人', minWidth: 120, formatter: (row: PlatformTenantResource) => displayText(row.contactName) },
|
||||
{ key: 'contactPhone', prop: 'contactPhone', label: '电话', minWidth: 150, formatter: (row: PlatformTenantResource) => displayText(row.contactPhone) },
|
||||
{ key: 'salesOwnerName', prop: 'salesOwnerName', label: '销售人员', minWidth: 120, formatter: (row: PlatformTenantResource) => displayText(row.salesOwnerName) },
|
||||
{ key: 'paymentTermDays', prop: 'paymentTermDays', label: '账期(天)', width: 105, formatter: (row: PlatformTenantResource) => row.paymentTermDays ?? 0 },
|
||||
{ key: 'salesOwnerName', prop: 'salesOwnerName', label: '销售人员', minWidth: 120, defaultVisible: false, formatter: (row: PlatformTenantResource) => displayText(row.salesOwnerName) },
|
||||
{ key: 'paymentTermDays', prop: 'paymentTermDays', label: '账期(天)', width: 105, defaultVisible: false, formatter: (row: PlatformTenantResource) => row.paymentTermDays ?? 0 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'orderCount', label: '运单数', width: 100, slotName: 'orderCount' },
|
||||
{ key: 'orderCount', label: '运单数', width: 100, slotName: 'orderCount', defaultVisible: false },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '入驻时间', minWidth: 165, formatter: (row: PlatformTenantResource) => formatDate(row.createdAt) },
|
||||
]
|
||||
|
||||
@@ -291,8 +291,8 @@ function reviewToCustomer(review: EnterpriseReview): PlatformTenantResource {
|
||||
const tenantId = String(review.tenantId)
|
||||
return {
|
||||
id: tenantId,
|
||||
tenantCode: review.tenantCode?.trim() || tenantId,
|
||||
tenantName: review.tenantName?.trim() || `待审核企业(${tenantId})`,
|
||||
tenantCode: review.tenantCode?.trim() || '待生成',
|
||||
tenantName: review.tenantName?.trim() || '待审核企业',
|
||||
status: 'PENDING',
|
||||
legalName: null,
|
||||
legalRepresentative: null,
|
||||
@@ -665,8 +665,7 @@ function moreCustomerActions(row: PlatformTenantResource) {
|
||||
return customerSecondaryActions(row).slice(Math.max(0, 3 - customerPrimaryActionCount(row)))
|
||||
}
|
||||
function customerMoreActionsLabel(row: PlatformTenantResource) {
|
||||
const compatibleTenantId = (row as PlatformTenantResource & { tenantId?: unknown }).tenantId
|
||||
const identifier = [row.tenantName, row.tenantCode, row.id, compatibleTenantId]
|
||||
const identifier = [row.tenantName, row.tenantCode]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find(Boolean)
|
||||
return `更多客户操作:${identifier || '当前客户'}`
|
||||
@@ -1053,7 +1052,7 @@ onMounted(() => { void loadCustomers() })
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="decisionOpen" :title="decision.type === 'approve' ? '确认审核通过' : '确认驳回企业资料'" width="min(520px, calc(100vw - 28px))" class="review-decision-dialog" :close-on-click-modal="false">
|
||||
<div v-if="selectedReview" class="decision-summary"><strong>{{ selectedReview.tenantName || '当前客户' }}</strong><span>审核单 {{ selectedReview.id }}</span></div>
|
||||
<div v-if="selectedReview" class="decision-summary"><strong>{{ selectedReview.tenantName || '当前客户' }}</strong><span>客户资料审核</span></div>
|
||||
<el-radio-group v-if="access.can('platform:customer:approve') && access.can('platform:customer:reject')" v-model="decision.type" class="decision-type" aria-label="审核决定"><el-radio-button label="approve">通过</el-radio-button><el-radio-button label="reject">驳回</el-radio-button></el-radio-group>
|
||||
<el-form ref="decisionFormRef" :model="decision" :rules="decisionRules" label-position="top">
|
||||
<el-form-item prop="comment" :label="decision.type === 'approve' ? '审核说明' : '驳回原因'" required><el-input v-model="decision.comment" type="textarea" :rows="4" maxlength="300" show-word-limit :placeholder="decision.type === 'approve' ? '请填写审核通过说明' : '请明确说明需要企业修改的内容'" /></el-form-item>
|
||||
|
||||
@@ -68,6 +68,8 @@ describe('DownloadCenterView', () => {
|
||||
|
||||
expect(document.body.textContent).toContain('客户列表')
|
||||
expect(document.querySelector('.data-table')?.textContent).not.toContain('失败原因')
|
||||
expect(document.querySelector('.data-table')?.textContent).not.toContain('完成时间')
|
||||
expect(document.querySelector('.data-table')?.textContent).not.toContain('过期时间')
|
||||
expect(document.querySelector('.failure-cell')).toBeNull()
|
||||
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
|
||||
button?.click()
|
||||
|
||||
@@ -62,11 +62,11 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'type', label: '任务类型', width: 120, slotName: 'type' },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'progress', label: '数据量', width: 120, slotName: 'progress' },
|
||||
{ key: 'format', prop: 'format', label: '格式', width: 80 },
|
||||
{ key: 'format', prop: 'format', label: '格式', width: 80, defaultVisible: false },
|
||||
{ key: 'requester', label: '申请人', minWidth: 130, slotName: 'requester' },
|
||||
{ key: 'requestedAt', label: '申请时间', width: 170, slotName: 'requestedAt' },
|
||||
{ key: 'completedAt', label: '完成时间', width: 170, slotName: 'completedAt' },
|
||||
{ key: 'expiresAt', label: '过期时间', width: 170, slotName: 'expiresAt' },
|
||||
{ key: 'completedAt', label: '完成时间', width: 170, slotName: 'completedAt', defaultVisible: false },
|
||||
{ key: 'expiresAt', label: '过期时间', width: 170, slotName: 'expiresAt', defaultVisible: false },
|
||||
]
|
||||
|
||||
const hasActiveTasks = computed(() => result.value.records.some((task) => ['PENDING', 'RUNNING'].includes(task.status)))
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('CarrierRatesView price-book source contract', () => {
|
||||
expect(source).toContain('PENDING_CONFIRMATION')
|
||||
expect(source).toContain('SUPERSEDED')
|
||||
expect(source).not.toContain('<span>ID {{ (row as PlatformPriceBookVersion).id }}</span>')
|
||||
expect(source).toContain("key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true, defaultVisible: false")
|
||||
})
|
||||
|
||||
it('uses only the parsed price-book workflow instead of legacy carrier-rate editing', () => {
|
||||
|
||||
@@ -97,7 +97,7 @@ const queryFields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'version', label: '价格表版本', minWidth: 180, slotName: 'version' },
|
||||
{ key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true },
|
||||
{ key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true, defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'contents', label: '解析结果', minWidth: 155, slotName: 'contents' },
|
||||
{ key: 'issues', label: '校验结果', minWidth: 150, slotName: 'issues' },
|
||||
|
||||
@@ -118,8 +118,8 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'pickup', label: '提货', minWidth: 160, slotName: 'pickup' },
|
||||
{ key: 'warehouse', label: '仓库', minWidth: 160, slotName: 'warehouse' },
|
||||
{ key: 'allocationFee', label: '分摊费', width: 100, slotName: 'allocationFee' },
|
||||
{ key: 'description', label: '说明', minWidth: 220, slotName: 'description' },
|
||||
{ key: 'updatedAt', label: '更新时间', minWidth: 155, slotName: 'updatedAt' },
|
||||
{ key: 'description', label: '说明', minWidth: 220, slotName: 'description', defaultVisible: false },
|
||||
{ key: 'updatedAt', label: '更新时间', minWidth: 155, slotName: 'updatedAt', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '../api/platformIam'
|
||||
import PlatformTenantSelector from './PlatformTenantSelector.vue'
|
||||
|
||||
vi.mock('../api/platformIam', () => ({ listPlatformTenants: vi.fn() }))
|
||||
|
||||
const customer = {
|
||||
id: '190000000000000008',
|
||||
tenantCode: 'CUS-008',
|
||||
tenantName: '柏悦物流',
|
||||
status: 'ACTIVE',
|
||||
} as PlatformTenantResource
|
||||
|
||||
function mountSelector(props: { modelValue?: string; label?: string } = {}) {
|
||||
return mount(PlatformTenantSelector, {
|
||||
props: { modelValue: props.modelValue ?? '', ...(props.label ? { label: props.label } : {}) },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
}
|
||||
|
||||
describe('PlatformTenantSelector customer labels', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
|
||||
})
|
||||
|
||||
it('shows business identity and status without exposing the internal id', async () => {
|
||||
const wrapper = mountSelector({ modelValue: String(customer.id) })
|
||||
await flushPromises()
|
||||
|
||||
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200 })
|
||||
expect(wrapper.text()).toContain('目标客户')
|
||||
expect(document.body.textContent).toContain('柏悦物流(CUS-008) · 启用')
|
||||
expect(document.body.textContent).not.toContain(String(customer.id))
|
||||
})
|
||||
|
||||
it('keeps the internal id only as the emitted contract value', async () => {
|
||||
const wrapper = mountSelector()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[String(customer.id)]])
|
||||
expect(wrapper.text()).not.toContain(String(customer.id))
|
||||
})
|
||||
|
||||
it('safely degrades labels when customer name or code is missing', async () => {
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({
|
||||
records: [
|
||||
{ ...customer, id: '1', tenantName: '', tenantCode: 'CUS-001', status: 'PENDING' },
|
||||
{ ...customer, id: '2', tenantName: '远航供应链', tenantCode: '', status: 'REJECTED' },
|
||||
{ ...customer, id: '3', tenantName: '', tenantCode: '', status: 'UNKNOWN' },
|
||||
],
|
||||
page: 1, size: 200, total: 3, pages: 1,
|
||||
})
|
||||
mountSelector({ modelValue: '1' })
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.textContent).toContain('CUS-001 · 待审核')
|
||||
expect(document.body.textContent).toContain('远航供应链 · 已驳回')
|
||||
expect(document.body.textContent).toContain('未命名客户 · UNKNOWN')
|
||||
expect(document.body.textContent).not.toMatch(/[((][123] ·/)
|
||||
})
|
||||
|
||||
it('uses a custom business label for both visible and accessible names', async () => {
|
||||
const wrapper = mountSelector({ modelValue: String(customer.id), label: '客户企业' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('客户企业')
|
||||
expect(wrapper.get('[aria-label="客户企业"]').attributes('aria-label')).toBe('客户企业')
|
||||
})
|
||||
|
||||
it('shows an actionable customer-list error and retries', async () => {
|
||||
vi.mocked(listPlatformTenants)
|
||||
.mockRejectedValueOnce('unavailable')
|
||||
.mockResolvedValueOnce({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
|
||||
const wrapper = mountSelector()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('[role="alert"]').text()).toContain('客户列表加载失败,请重试')
|
||||
await wrapper.get('[role="alert"] button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(listPlatformTenants).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ const props = withDefaults(defineProps<{
|
||||
label?: string
|
||||
}>(), {
|
||||
disabled: false,
|
||||
label: '目标企业',
|
||||
label: '目标客户',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -36,7 +36,10 @@ const statusLabels: Record<string, string> = {
|
||||
|
||||
function tenantLabel(tenant: PlatformTenantResource) {
|
||||
const status = statusLabels[tenant.status] || tenant.status
|
||||
return `${tenant.tenantName}(${tenant.id} · ${status})`
|
||||
const name = tenant.tenantName?.trim()
|
||||
const code = tenant.tenantCode?.trim()
|
||||
const identity = name && code ? `${name}(${code})` : name || code || '未命名客户'
|
||||
return status ? `${identity} · ${status}` : identity
|
||||
}
|
||||
|
||||
async function loadTenants() {
|
||||
@@ -50,7 +53,7 @@ async function loadTenants() {
|
||||
emit('update:modelValue', String(preferred.id))
|
||||
}
|
||||
} catch (reason) {
|
||||
error.value = reason instanceof Error ? reason.message : '企业列表加载失败,请重试'
|
||||
error.value = reason instanceof Error ? reason.message : '客户列表加载失败,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -67,8 +70,8 @@ onMounted(() => void loadTenants())
|
||||
class="platform-tenant-selector-input"
|
||||
filterable
|
||||
:disabled="disabled || loading || tenants.length === 0"
|
||||
:placeholder="loading ? '正在加载企业' : '请选择目标企业'"
|
||||
aria-label="目标企业"
|
||||
:placeholder="loading ? '正在加载客户' : '请选择目标客户'"
|
||||
:aria-label="label"
|
||||
>
|
||||
<el-option v-for="tenant in tenants" :key="String(tenant.id)" :label="tenantLabel(tenant)" :value="String(tenant.id)" />
|
||||
</el-select>
|
||||
|
||||
@@ -181,8 +181,8 @@ describe('PermissionsView IAM mode', () => {
|
||||
const wrapper = mountServerView(allActionPermissions)
|
||||
await flushPromises()
|
||||
|
||||
const moreButton = wrapper.get('[data-row-id="5100010"] [aria-label="5100010的更多操作"]')
|
||||
expect(moreButton.attributes('title')).toBe('5100010的更多操作')
|
||||
const moreButton = wrapper.get('[data-row-id="5100010"] [aria-label="当前权限的更多操作"]')
|
||||
expect(moreButton.attributes('title')).toBe('当前权限的更多操作')
|
||||
expect(moreButton.attributes('aria-label')).not.toMatch(/undefined|null/)
|
||||
})
|
||||
|
||||
|
||||
@@ -87,10 +87,10 @@ const parentOptions = computed(() => permissions.value.filter((item) => (
|
||||
const permissionColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'name', label: '权限名称', minWidth: 260, slotName: 'name' },
|
||||
{ key: 'code', label: '权限编码', minWidth: 220, slotName: 'code' },
|
||||
{ key: 'code', label: '权限编码', minWidth: 220, slotName: 'code', defaultVisible: false },
|
||||
{ key: 'type', prop: 'type', label: '类型', width: 80 },
|
||||
{ key: 'endpoint', label: '路由 / 接口', minWidth: 260, slotName: 'endpoint' },
|
||||
{ key: 'sort', prop: 'sort', label: '排序', width: 72, align: 'right' },
|
||||
{ key: 'endpoint', label: '路由 / 接口', minWidth: 260, slotName: 'endpoint', defaultVisible: false },
|
||||
{ key: 'sort', prop: 'sort', label: '排序', width: 72, align: 'right', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 88, slotName: 'status' },
|
||||
]
|
||||
|
||||
@@ -256,7 +256,7 @@ function showDeleteMore(row: Permission) {
|
||||
return canDeleteRow(row) && permissionActionCount(row) > 3
|
||||
}
|
||||
function moreActionsLabel(row: Permission) {
|
||||
const resourceLabel = [row.name, row.code, row.id]
|
||||
const resourceLabel = [row.name]
|
||||
.find((value) => typeof value === 'string' && value.trim())
|
||||
?.trim() || '当前权限'
|
||||
return `${resourceLabel}的更多操作`
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('PartnersView real API integration', () => {
|
||||
expect(wrapper.text()).toContain('深圳示例贸易有限公司')
|
||||
expect(wrapper.text()).toContain('CUS-001')
|
||||
expect(wrapper.text()).toContain('contact@example.com')
|
||||
expect(wrapper.text()).toContain('广东省 深圳市 宝安区 航城街道示例路 1 号')
|
||||
expect(wrapper.find('.data-table').text()).not.toContain('广东省 深圳市 宝安区 航城街道示例路 1 号')
|
||||
expect(wrapper.text()).not.toContain('往来单位总数')
|
||||
expect(wrapper.text()).not.toContain('往来单位档案')
|
||||
expect(wrapper.find('.query-toolbar').exists()).toBe(true)
|
||||
@@ -123,6 +123,10 @@ describe('PartnersView real API integration', () => {
|
||||
expect(wrapper.find('.data-table__toolbar-start').exists()).toBe(false)
|
||||
expect(wrapper.find('.data-table__pagination-left').text()).toContain('平台公共数据 · 共 1 个往来单位')
|
||||
expect(wrapper.find('.list-page-layout__state').exists()).toBe(false)
|
||||
const detailButton = wrapper.findAll('button').find((button) => button.text().trim() === '详情')
|
||||
await detailButton?.trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('广东省 深圳市 宝安区 航城街道示例路 1 号')
|
||||
})
|
||||
|
||||
it('opens the create form without selecting an enterprise', async () => {
|
||||
|
||||
@@ -154,9 +154,9 @@ const partnerColumns: DataTableColumn[] = [
|
||||
{ key: 'partner', label: '往来单位', minWidth: 210, slotName: 'partner' },
|
||||
{ key: 'type', label: '类型', minWidth: 115, slotName: 'type' },
|
||||
{ key: 'contact', label: '联系人', minWidth: 165, slotName: 'contact' },
|
||||
{ key: 'address', label: '地址', minWidth: 240, slotName: 'address' },
|
||||
{ key: 'address', label: '地址', minWidth: 240, slotName: 'address', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 140, formatter: (row: PlatformPartnerResource) => formatUpdatedAt(row.updatedAt) },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 140, defaultVisible: false, formatter: (row: PlatformPartnerResource) => formatUpdatedAt(row.updatedAt) },
|
||||
]
|
||||
|
||||
watch(
|
||||
|
||||
@@ -122,7 +122,7 @@ const expressCompanyColumns: DataTableColumn[] = [
|
||||
{ key: 'company', label: '快递公司', minWidth: 210, slotName: 'company' },
|
||||
{ key: 'servicePhone', prop: 'servicePhone', label: '客服电话', minWidth: 130, formatter: (row: PlatformExpressCompanyResource) => row.servicePhone || '—' },
|
||||
{ key: 'website', label: '官方网站', minWidth: 210, slotName: 'website' },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, formatter: (row: PlatformExpressCompanyResource) => formatUpdatedAt(row.updatedAt) },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, defaultVisible: false, formatter: (row: PlatformExpressCompanyResource) => formatUpdatedAt(row.updatedAt) },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
'feeReviewStatus', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded', 'fullContainer',
|
||||
'measurementReviewStatus', 'fbaCode', 'createdRange',
|
||||
]) expect(source).toContain(`key: '${key}'`)
|
||||
for (const label of ['定位仓库', '交货仓', '费用审核状态', '是否已配载', '是否整柜', '测量数据审核状态', '商品 FBA 编码']) {
|
||||
for (const label of ['定位仓库', '交货仓', '费用审核状态', '是否已配载', '是否整柜', '测量数据审核状态', '商品 SKU 编码']) {
|
||||
expect(source).toContain(`label: '${label}'`)
|
||||
}
|
||||
expect(source).toContain('listAllPlatformWarehouses')
|
||||
@@ -57,6 +57,21 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('preference-key="unified-orders-query-v2"')
|
||||
})
|
||||
|
||||
it('keeps unified filters aligned with the formal query contract and does not expose hold filters', () => {
|
||||
const filterSource = source.slice(source.indexOf('const primaryQueryFields'), source.indexOf('const columns:'))
|
||||
for (const key of ['expectedArrivalDate', 'feeReviewStatus', 'measurementReviewStatus', 'fbaCode', 'loaded', 'fullContainer']) {
|
||||
expect(filterSource).toContain(`key: '${key}'`)
|
||||
}
|
||||
expect(filterSource).not.toContain("key: 'holdCargo'")
|
||||
expect(filterSource).not.toContain("key: 'holdReason'")
|
||||
expect(filterSource).toContain("key: 'fbaCode', type: 'input', width: 220, label: '商品 SKU 编码'")
|
||||
expect(filterSource).not.toContain("label: '商品 FBA 编码'")
|
||||
expect(source).toContain('expectedArrivalDate: activeFilters.expectedArrivalDate || undefined')
|
||||
expect(source).toContain('feeReviewStatus: activeFilters.feeReviewStatus || undefined')
|
||||
expect(source).toContain('measurementReviewStatus: activeFilters.measurementReviewStatus || undefined')
|
||||
expect(source).toContain('fbaCode: trimmed(activeFilters.fbaCode)')
|
||||
})
|
||||
|
||||
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
|
||||
expect(source).toContain('mainStatus: selectedStatus.value || undefined')
|
||||
expect(source).toContain('<template #actions>')
|
||||
@@ -92,6 +107,8 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
|
||||
expect(columnSource).toContain("{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false }")
|
||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(8)
|
||||
expect(source).toContain('.table-card { overflow: hidden; }')
|
||||
expect(source).toContain('slotName: \'measurementReviewStatus\'')
|
||||
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
|
||||
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注'")
|
||||
|
||||
@@ -189,7 +189,7 @@ const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ value: 'UNRECORDED', label: '未录入' }, { value: 'PENDING', label: '待审核' },
|
||||
{ value: 'APPROVED', label: '审核通过' }, { value: 'REJECTED', label: '审核驳回' },
|
||||
], props: { 'aria-label': '测量数据审核状态' } },
|
||||
{ key: 'fbaCode', type: 'input', width: 220, label: '商品 FBA 编码', placeholder: '请输入商品 FBA 编码', props: { 'aria-label': '商品 FBA 编码' } },
|
||||
{ key: 'fbaCode', type: 'input', width: 220, label: '商品 SKU 编码', placeholder: '请输入商品 SKU 编码', props: { 'aria-label': '商品 SKU 编码' } },
|
||||
{ key: 'createdRange', type: 'daterange', width: 300, label: '创建日期', placeholder: '创建日期范围', props: { 'aria-label': '创建日期范围' } },
|
||||
])
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('LocationsView', () => {
|
||||
expect(wrapper.text()).toContain('义乌收货仓')
|
||||
expect(wrapper.text()).toContain('A1-01')
|
||||
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
|
||||
'操作', '库位编码', '所属仓库', '库区', '库位类型', '最大承重(kg)', '状态', '备注',
|
||||
'操作', '库位编码', '所属仓库', '库区', '库位类型', '状态',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -277,7 +277,7 @@ describe('LocationsView', () => {
|
||||
expect(row.find('button[aria-label="A1-01的更多操作"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('uses unique fallback labels for more actions across rows', async () => {
|
||||
it('uses business labels without exposing internal ids in more actions', async () => {
|
||||
vi.mocked(listPlatformLocations).mockResolvedValue({
|
||||
records: [
|
||||
{ ...availableLocation, id: 'location-1', locationCode: '', locationName: '东侧库位', status: 'DISABLED' },
|
||||
@@ -293,10 +293,10 @@ describe('LocationsView', () => {
|
||||
const moreButtons = wrapper.findAll('button[aria-label$="的更多操作"]')
|
||||
const labels = moreButtons.map((button) => button.attributes('aria-label'))
|
||||
|
||||
expect(labels).toEqual(['东侧库位的更多操作', 'location-2的更多操作', '当前库位的更多操作'])
|
||||
expect(labels).toEqual(['东侧库位的更多操作', '当前库位的更多操作', '当前库位的更多操作'])
|
||||
expect(moreButtons.map((button) => button.attributes('title'))).toEqual(labels)
|
||||
expect(new Set(labels).size).toBe(labels.length)
|
||||
expect(labels.every((label) => Boolean(label) && !/undefined|null/.test(label ?? ''))).toBe(true)
|
||||
expect(labels.join(' ')).not.toContain('location-2')
|
||||
})
|
||||
|
||||
it('does not offer editing for a location outside a domestic warehouse', async () => {
|
||||
|
||||
@@ -181,9 +181,9 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'warehouse', label: '所属仓库', minWidth: 210, slotName: 'warehouse' },
|
||||
{ key: 'zone', label: '库区', minWidth: 150, slotName: 'zone' },
|
||||
{ key: 'locationType', label: '库位类型', width: 120, slotName: 'locationType' },
|
||||
{ key: 'maxWeightKg', label: '最大承重(kg)', minWidth: 140, slotName: 'maxWeightKg' },
|
||||
{ key: 'maxWeightKg', label: '最大承重(kg)', minWidth: 140, slotName: 'maxWeightKg', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark' },
|
||||
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark', defaultVisible: false },
|
||||
]
|
||||
|
||||
function apiId(value: string) {
|
||||
@@ -226,7 +226,7 @@ function hasMoreLocationActions(row: PlatformLocationResource) {
|
||||
}
|
||||
|
||||
function locationMoreActionsLabel(row: PlatformLocationResource) {
|
||||
const identifier = [row.locationCode, row.locationName, row.id]
|
||||
const identifier = [row.locationCode, row.locationName]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find(Boolean) || '当前库位'
|
||||
return `${identifier}的更多操作`
|
||||
|
||||
@@ -149,10 +149,13 @@ describe('PalletsView', () => {
|
||||
page: 1, size: 20,
|
||||
})
|
||||
expect(wrapper.text()).toContain('TP-001')
|
||||
expect(wrapper.text()).toContain('义乌收货仓')
|
||||
expect(wrapper.text()).toContain('CN-YW-01')
|
||||
expect(wrapper.text()).toContain('木托盘')
|
||||
expect(wrapper.text()).toContain('1.2 m³')
|
||||
expect(wrapper.text()).toContain('8 kg')
|
||||
expect(wrapper.text()).toContain('黄色')
|
||||
expect(wrapper.find('.data-table').text()).not.toContain('黄色')
|
||||
expect(wrapper.find('.data-table').text()).not.toContain('标准川字托盘')
|
||||
expect(wrapper.text()).not.toContain('更新时间')
|
||||
const tableToolbar = wrapper.find('.table-panel .data-table__toolbar')
|
||||
expect(tableToolbar.exists()).toBe(true)
|
||||
@@ -168,7 +171,9 @@ describe('PalletsView', () => {
|
||||
expect(wrapper.text()).not.toContain('租户')
|
||||
expect(wrapper.find('.data-table').exists()).toBe(true)
|
||||
expect(wrapper.find('.table-actions.pallet-row-actions').exists()).toBe(true)
|
||||
expect(wrapper.findAll('thead th').map((item) => item.text().trim())[0]).toBe('操作')
|
||||
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
|
||||
'操作', '托盘编码', '所属仓库', '托盘类型', '托盘容积(m³)', '托盘重量(kg)',
|
||||
])
|
||||
})
|
||||
|
||||
it('queries all customers explicitly without adding a customer id', async () => {
|
||||
|
||||
@@ -128,11 +128,12 @@ const { captureBaseline } = useUnsavedWorkRegistration({
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', minWidth: 224, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'pallet', label: '托盘编码', minWidth: 190, slotName: 'pallet' },
|
||||
{ key: 'warehouse', label: '所属仓库', minWidth: 210, slotName: 'warehouse' },
|
||||
{ key: 'palletType', prop: 'palletType', label: '托盘类型', minWidth: 140 },
|
||||
{ key: 'capacity', label: '托盘容积(m³)', minWidth: 160, slotName: 'capacity' },
|
||||
{ key: 'weight', label: '托盘重量(kg)', minWidth: 160, slotName: 'weight' },
|
||||
{ key: 'color', prop: 'color', label: '颜色', minWidth: 120 },
|
||||
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark' },
|
||||
{ key: 'color', prop: 'color', label: '颜色', minWidth: 120, defaultVisible: false },
|
||||
{ key: 'remark', label: '备注', minWidth: 220, slotName: 'remark', defaultVisible: false },
|
||||
]
|
||||
|
||||
function apiId(value: string) {
|
||||
@@ -434,6 +435,9 @@ onMounted(() => {
|
||||
<template #pallet="{ row }">
|
||||
<div class="pallet-cell"><span><Boxes :size="18" /></span><strong>{{ (row as PlatformPalletResource).palletCode }}</strong></div>
|
||||
</template>
|
||||
<template #warehouse="{ row }">
|
||||
<div class="stacked-value"><strong>{{ warehouseFor((row as PlatformPalletResource).warehouseId)?.warehouseName || '—' }}</strong><small>{{ warehouseFor((row as PlatformPalletResource).warehouseId)?.warehouseCode || '—' }}</small></div>
|
||||
</template>
|
||||
<template #capacity="{ row }"><strong>{{ formatCapacity((row as PlatformPalletResource).capacityCbm) }}</strong></template>
|
||||
<template #weight="{ row }"><strong>{{ formatWeight((row as PlatformPalletResource).weightKg) }}</strong></template>
|
||||
<template #remark="{ row }"><span class="remark-text">{{ (row as PlatformPalletResource).remark || '—' }}</span></template>
|
||||
@@ -551,6 +555,9 @@ onMounted(() => {
|
||||
.pallet-row-actions { gap: 8px; white-space: nowrap; }
|
||||
.pallet-row-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.pallet-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.stacked-value { display: grid; min-width: 0; gap: 3px; }
|
||||
.stacked-value strong, .stacked-value small { overflow-wrap: anywhere; }
|
||||
.stacked-value small { color: var(--cc-muted); font-size: 11px; }
|
||||
.pallet-cell > span, .detail-hero > span { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 8px; background: var(--el-color-primary-light-9); color: var(--cc-primary); }
|
||||
.pallet-cell > span { width: 34px; height: 34px; }
|
||||
.remark-text { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.55; }
|
||||
|
||||
@@ -194,12 +194,17 @@ describe('WarehousesView', () => {
|
||||
})
|
||||
expect(getPlatformWarehouse).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('义乌收货仓')
|
||||
expect(wrapper.text()).toContain('浙江省义乌市北苑街道 1 号库')
|
||||
expect(wrapper.find('.data-table').text()).not.toContain('浙江省义乌市北苑街道 1 号库')
|
||||
expect(wrapper.text()).toContain('国内收货仓')
|
||||
expect(wrapper.text()).toContain('中国 CN')
|
||||
expect(wrapper.text()).toContain('UTC+8')
|
||||
expect(wrapper.find('.data-table').text()).not.toContain('UTC+8')
|
||||
expect(wrapper.text()).toContain('共 1 个公共仓库')
|
||||
expect(wrapper.find('.data-table').exists()).toBe(true)
|
||||
const editButton = wrapper.findAll('.table-actions button').find((button) => button.text().trim() === '编辑')
|
||||
await editButton?.trigger('click')
|
||||
await flushPromises()
|
||||
expect((wrapper.find('textarea[placeholder*="街道"]').element as HTMLTextAreaElement).value).toBe('真实物流园 1 号库')
|
||||
expect(document.body.textContent).toContain('UTC+8')
|
||||
})
|
||||
|
||||
it('submits filters and pagination to the API instead of filtering rows locally', async () => {
|
||||
|
||||
@@ -170,10 +170,10 @@ const warehouseColumns: DataTableColumn[] = [
|
||||
{ key: 'warehouse', label: '仓库', minWidth: 190, slotName: 'warehouse' },
|
||||
{ key: 'warehouseType', label: '仓库类型', width: 120, slotName: 'warehouseType' },
|
||||
{ key: 'countryCode', label: '国家/地区', width: 140, slotName: 'countryCode' },
|
||||
{ key: 'timezone', prop: 'timezone', label: '时区', width: 110 },
|
||||
{ key: 'timezone', prop: 'timezone', label: '时区', width: 110, defaultVisible: false },
|
||||
{ key: 'city', label: '所在城市', minWidth: 140, slotName: 'city' },
|
||||
{ key: 'address', label: '地址', minWidth: 280, slotName: 'address' },
|
||||
{ key: 'contact', label: '联系人', minWidth: 150, slotName: 'contact' },
|
||||
{ key: 'address', label: '地址', minWidth: 280, slotName: 'address', defaultVisible: false },
|
||||
{ key: 'contact', label: '联系人', minWidth: 150, slotName: 'contact', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user