优化
This commit is contained in:
@@ -95,6 +95,34 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('LP-99')
|
||||
})
|
||||
|
||||
it('explains simulated empty cabinets instead of presenting an internal id as a container number', async () => {
|
||||
vi.mocked(listContainers).mockResolvedValue({
|
||||
records: [{
|
||||
...container,
|
||||
cabinetNo: '2088475410134773762',
|
||||
containerNo: null,
|
||||
channelName: '美森正班海卡包税',
|
||||
warehouseName: '义乌国内仓',
|
||||
containerType: '40HQ',
|
||||
}],
|
||||
page: 1,
|
||||
size: 100,
|
||||
total: 1,
|
||||
pages: 1,
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const option = wrapper.find('.container-option')
|
||||
expect(option.text()).toContain('模拟柜次')
|
||||
expect(option.text()).toContain('模拟柜次:真实柜号待录入')
|
||||
expect(option.text()).toContain('真实柜号:待录入')
|
||||
expect(option.text()).toContain('渠道:美森正班海卡包税')
|
||||
expect(option.text()).toContain('仓库:义乌国内仓')
|
||||
expect(option.text()).toContain('柜型:40HQ')
|
||||
expect(option.text()).not.toContain('2088475410134773762')
|
||||
})
|
||||
|
||||
it('groups containers by channel and reloads candidates when the channel changes', async () => {
|
||||
const other = {
|
||||
...container,
|
||||
@@ -318,6 +346,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).toContain('overflow-wrap: anywhere;')
|
||||
expect(source).toContain('.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }')
|
||||
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
|
||||
expect(source).toContain('grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr)')
|
||||
expect(source).toContain('min-height: 112px')
|
||||
})
|
||||
|
||||
it('uses overflow-aware channel tabs with a compact right-aligned summary', () => {
|
||||
|
||||
@@ -142,7 +142,17 @@ function displayContainerNo(container: ContainerBookingResource | null | undefin
|
||||
if (!container) return '柜号待生成'
|
||||
const actual = String(container.containerNo || '').trim()
|
||||
const internalReference = /^(?:MAN|SPLIT)-/i.test(actual)
|
||||
return actual && !internalReference ? actual : String(container.cabinetNo || '').trim() || '柜号待生成'
|
||||
if (actual && !internalReference) return actual
|
||||
const cabinetReference = String(container.cabinetNo || '').trim()
|
||||
return /^\d{15,}$/.test(cabinetReference) ? '模拟柜次' : cabinetReference || '柜号待生成'
|
||||
}
|
||||
function hasRealContainerNo(container: ContainerBookingResource | null | undefined) {
|
||||
if (!container) return false
|
||||
const value = String(container.containerNo || '').trim()
|
||||
return Boolean(value) && !/^(?:MAN|SPLIT)-/i.test(value)
|
||||
}
|
||||
function hasOpaqueCabinetReference(container: ContainerBookingResource | null | undefined) {
|
||||
return Boolean(container && /^\d{15,}$/.test(String(container.cabinetNo || '').trim()))
|
||||
}
|
||||
function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
|
||||
@@ -445,7 +455,18 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
:class="{ selected: item.id === selectedContainerId }"
|
||||
@click="selectContainer(item.id)"
|
||||
>
|
||||
<span class="container-option__top"><strong>{{ displayContainerNo(item) }}</strong></span>
|
||||
<span class="container-option__top">
|
||||
<strong>{{ hasRealContainerNo(item) ? displayContainerNo(item) : '模拟柜次' }}</strong>
|
||||
<el-tag size="small" :type="hasRealContainerNo(item) ? 'success' : 'info'">
|
||||
{{ hasRealContainerNo(item) ? '已录入柜号' : '空柜' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="container-option__identity">
|
||||
{{ hasRealContainerNo(item) ? `柜号:${displayContainerNo(item)}` : (hasOpaqueCabinetReference(item) ? '模拟柜次:真实柜号待录入' : `柜次参考:${displayContainerNo(item)}`) }}
|
||||
</span>
|
||||
<span>渠道:{{ item.channelName || item.channelCode || '渠道待补充' }}</span>
|
||||
<span>仓库:{{ item.warehouseName || item.warehouseId || '仓库待补充' }} · 柜型:{{ item.containerType || '柜型待补充' }}</span>
|
||||
<span v-if="!hasRealContainerNo(item)" class="container-option__hint">真实柜号:待录入</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -518,7 +539,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.compact-summary dd { margin: 0; color: var(--cc-text); font-size: 12px; }
|
||||
.compact-summary strong { color: var(--el-color-primary); font-size: 14px; }
|
||||
.pane-header small,.container-option span,.global-loading-page small { color: var(--cc-muted); font-size: 12px; }
|
||||
.dispatch-grid { display: grid; grid-template-columns: minmax(390px, 1.1fr) 270px minmax(460px, 1.35fr); height: clamp(520px, 68vh, 720px); min-height: 0; overflow-x: auto; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.dispatch-grid { display: grid; grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr); height: clamp(520px, 68vh, 720px); min-height: 0; overflow-x: auto; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.embedded-smart-loading .dispatch-grid { min-height: 520px; height: min(62vh, 700px); }
|
||||
.dispatch-pane { display: flex; min-width: 0; min-height: 0; flex-direction: column; }
|
||||
.dispatch-pane + .dispatch-pane { border-left: 1px solid var(--cc-border); }
|
||||
@@ -528,10 +549,12 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.candidate-pane :deep(.el-table),.plan-pane :deep(.el-table) { height: 100%; }
|
||||
.container-pane { background: #fafafa; }
|
||||
.container-list { display: grid; min-height: 0; flex: 1; grid-auto-rows: max-content; align-content: start; gap: 8px; padding: 10px; overflow-y: auto; }
|
||||
.container-option { display: grid; width: 100%; min-height: 0; height: auto; align-content: start; gap: 5px; padding: 10px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; color: var(--cc-text); line-height: 1.35; text-align: left; cursor: pointer; }
|
||||
.container-option { display: grid; width: 100%; min-height: 112px; height: auto; align-content: start; gap: 6px; padding: 14px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; color: var(--cc-text); line-height: 1.4; text-align: left; cursor: pointer; }
|
||||
.container-option:hover,.container-option:focus-visible { border-color: var(--el-color-primary); outline: none; }
|
||||
.container-option.selected { border-color: var(--el-color-primary); box-shadow: inset 3px 0 0 var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
||||
.container-option__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--cc-text) !important; }
|
||||
.container-option__identity { color: var(--cc-text); font-size: 13px; font-weight: 600; }
|
||||
.container-option__hint { color: var(--el-color-warning); font-weight: 600; }
|
||||
.container-option span,.container-option strong { min-width: 0; overflow-wrap: anywhere; }
|
||||
.plan-header { flex-wrap: wrap; }
|
||||
.plan-actions,.row-actions,.meter-title { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
@@ -559,7 +559,7 @@ describe('CustomersView', () => {
|
||||
expect(document.querySelector('.el-table__fixed')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps every permitted customer action visible without a more menu', async () => {
|
||||
it('keeps three high-frequency actions visible and moves overflow actions into a menu', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
@@ -570,11 +570,10 @@ describe('CustomersView', () => {
|
||||
|
||||
expect(tableButtonByText('详情')).toBeDefined()
|
||||
expect(tableButtonByText('编辑')).toBeDefined()
|
||||
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).toBeNull()
|
||||
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).not.toBeNull()
|
||||
expect(tableButtonByText('重置密码')).toBeDefined()
|
||||
expect(tableButtonByText('跳转登录')).toBeDefined()
|
||||
tableButtonByText('跳转登录')?.click()
|
||||
await flushPromises()
|
||||
expect(tableButtonByText('跳转登录')).toBeUndefined()
|
||||
await clickMoreAction('跳转登录')
|
||||
expect(document.body.textContent).toContain('代登录原因')
|
||||
})
|
||||
|
||||
@@ -596,7 +595,7 @@ describe('CustomersView', () => {
|
||||
expect(tableButtonByText('删除')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('renders all permitted actions directly for multiple customers', async () => {
|
||||
it('renders one overflow menu per customer when permitted actions exceed three', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
@@ -612,8 +611,8 @@ describe('CustomersView', () => {
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(document.querySelectorAll('.el-table__body button[aria-label^="更多客户操作:"]').length).toBe(0)
|
||||
expect(document.querySelectorAll('.el-table__body button').length).toBeGreaterThan(9)
|
||||
expect(document.querySelectorAll('.el-table__body button[aria-label^="更多客户操作:"]').length).toBe(3)
|
||||
expect(document.querySelectorAll('.el-table__body .table-actions > .el-button').length).toBe(9)
|
||||
})
|
||||
|
||||
it('keeps the action row usable when all customer identifiers are missing', async () => {
|
||||
@@ -628,7 +627,7 @@ describe('CustomersView', () => {
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).toBeNull()
|
||||
expect(document.querySelector('.el-table__body button[aria-label="更多客户操作:当前客户"]')).not.toBeNull()
|
||||
expect(tableButtonByText('详情')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CheckCircle2, Download, Eye, FileText, FileUp, KeyRound, LogIn, Pencil, Plus, Power, Save, ShieldCheck, ShieldX, Trash2 } from '@lucide/vue'
|
||||
import { CheckCircle2, Download, Eye, FileText, FileUp, KeyRound, LogIn, MoreHorizontal, Pencil, Plus, Power, Save, ShieldCheck, ShieldX, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
@@ -194,7 +194,7 @@ const creditCodeEstablished = ref(false)
|
||||
const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/
|
||||
|
||||
const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 500, slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 330, slotName: 'actions' },
|
||||
{ key: 'tenantCode', prop: 'tenantCode', label: '客户编号', width: 220, className: 'customer-code-column' },
|
||||
{ key: 'company', label: '公司名称', width: 250, slotName: 'company' },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', width: 220, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
@@ -668,6 +668,21 @@ function customerSecondaryActions(row: PlatformTenantResource): CustomerRowActio
|
||||
...(canArchive(row) ? ['delete' as const] : []),
|
||||
]
|
||||
}
|
||||
function customerPrimaryActionCount(row: PlatformTenantResource) {
|
||||
return Number(access.can('platform:customer:list'))
|
||||
+ Number(access.can('platform:customer:edit') && row.status !== 'ARCHIVED')
|
||||
+ Number(hasReviewAction(row))
|
||||
}
|
||||
function customerDirectSecondaryActions(row: PlatformTenantResource) {
|
||||
return customerSecondaryActions(row).slice(0, Math.max(0, 3 - customerPrimaryActionCount(row)))
|
||||
}
|
||||
function customerOverflowActions(row: PlatformTenantResource) {
|
||||
return customerSecondaryActions(row).slice(Math.max(0, 3 - customerPrimaryActionCount(row)))
|
||||
}
|
||||
function customerMoreActionsLabel(row: PlatformTenantResource) {
|
||||
const identity = row.tenantName || row.tenantCode || String(row.id || '当前客户')
|
||||
return `更多客户操作:${identity}`
|
||||
}
|
||||
function customerActionLabel(row: PlatformTenantResource, action: CustomerRowAction) {
|
||||
if (action === 'reset-password') return '重置密码'
|
||||
if (action === 'impersonate') return '跳转登录'
|
||||
@@ -1019,7 +1034,11 @@ onMounted(() => { void loadCustomers() })
|
||||
<el-button v-if="access.can('platform:customer:list')" class="customer-action-button customer-action-detail" size="small" @click="showDetail(row as PlatformTenantResource)"><Eye :size="14" />详情</el-button>
|
||||
<el-button v-if="access.can('platform:customer:edit') && (row as PlatformTenantResource).status !== 'ARCHIVED'" class="customer-action-button customer-action-edit" size="small" @click="openEdit(row as PlatformTenantResource)"><Pencil :size="14" />编辑</el-button>
|
||||
<el-button v-if="hasReviewAction(row as PlatformTenantResource)" class="customer-action-button customer-action-review" size="small" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, access.can('platform:customer:approve') ? 'approve' : 'reject')"><ShieldCheck :size="14" />审核</el-button>
|
||||
<el-button v-for="action in customerSecondaryActions(row as PlatformTenantResource)" :key="action" :class="[`customer-action-button`, `customer-action-${action}`, { 'is-enable': action === 'status' && (row as PlatformTenantResource).status !== 'ACTIVE', 'is-disable': action === 'status' && (row as PlatformTenantResource).status === 'ACTIVE' }]" size="small" :disabled="customerActionDisabled(action)" @click="handleMoreAction(row as PlatformTenantResource, action)"><component :is="customerRowActionIcons[action]" :size="14" />{{ customerActionLabel(row as PlatformTenantResource, action) }}</el-button>
|
||||
<el-button v-for="action in customerDirectSecondaryActions(row as PlatformTenantResource)" :key="action" :class="[`customer-action-button`, `customer-action-${action}`, { 'is-enable': action === 'status' && (row as PlatformTenantResource).status !== 'ACTIVE', 'is-disable': action === 'status' && (row as PlatformTenantResource).status === 'ACTIVE' }]" size="small" :disabled="customerActionDisabled(action)" @click="handleMoreAction(row as PlatformTenantResource, action)"><component :is="customerRowActionIcons[action]" :size="14" />{{ customerActionLabel(row as PlatformTenantResource, action) }}</el-button>
|
||||
<el-dropdown v-if="customerOverflowActions(row as PlatformTenantResource).length" trigger="click" popper-class="customer-action-menu" @command="handleMoreAction(row as PlatformTenantResource, $event as string)">
|
||||
<el-button class="customer-action-button customer-action-more" size="small" plain :aria-label="customerMoreActionsLabel(row as PlatformTenantResource)" :title="customerMoreActionsLabel(row as PlatformTenantResource)"><MoreHorizontal :size="16" /></el-button>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item v-for="action in customerOverflowActions(row as PlatformTenantResource)" :key="action" :command="action" :disabled="customerActionDisabled(action)" :class="{ 'customer-action-menu__danger': customerActionType(row as PlatformTenantResource, action) === 'danger' }"><component :is="customerRowActionIcons[action]" :size="15" />{{ customerActionLabel(row as PlatformTenantResource, action) }}</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template #company="{ row }"><strong class="customer-company-name">{{ (row as PlatformTenantResource).tenantName }}</strong></template>
|
||||
|
||||
Reference in New Issue
Block a user