feat(phase3): complete loading workbenches

This commit is contained in:
2026-08-10 17:42:17 +08:00
parent 820432a381
commit 1595fbcae2
34 changed files with 1352 additions and 159 deletions
@@ -102,20 +102,20 @@ const occupiedLocation: PlatformLocationResource = {
version: 5,
}
async function mountView() {
async function mountView(permissions = [
'platform:location:list',
'platform:location:create',
'platform:location:edit',
'platform:location:status',
'platform:location:delete',
]) {
vi.stubGlobal('localStorage', createMemoryStorage())
vi.stubGlobal('sessionStorage', createMemoryStorage())
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = [
'platform:location:list',
'platform:location:create',
'platform:location:edit',
'platform:location:status',
'platform:location:delete',
]
auth.permissions = permissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(LocationsView, {
attachTo: document.body,
@@ -254,6 +254,51 @@ describe('LocationsView', () => {
expect(bodyRows[1]?.text()).not.toContain('更多')
})
it('shows all available actions directly when a row has no more than three', async () => {
const wrapper = await mountView(['platform:location:list', 'platform:location:status'])
const row = wrapper.find('tbody tr')
expect(row.text()).toContain('详情')
expect(row.text()).toContain('停用')
expect(row.text()).not.toContain('更多')
})
it('keeps the first three actions visible and moves only the fourth action into more', async () => {
vi.mocked(listPlatformLocations).mockResolvedValue({
records: [{ ...availableLocation, status: 'DISABLED' }], page: 1, size: 20, total: 1, pages: 1,
})
const wrapper = await mountView()
const row = wrapper.find('tbody tr')
expect(row.text()).toContain('详情')
expect(row.text()).toContain('编辑')
expect(row.text()).toContain('启用')
expect(row.text()).not.toContain('删除')
expect(row.find('button[aria-label="A1-01的更多操作"]').exists()).toBe(true)
})
it('uses unique fallback labels for more actions across rows', async () => {
vi.mocked(listPlatformLocations).mockResolvedValue({
records: [
{ ...availableLocation, id: 'location-1', locationCode: '', locationName: '东侧库位', status: 'DISABLED' },
{ ...availableLocation, id: 'location-2', locationCode: '', locationName: '', status: 'DISABLED' },
{ ...availableLocation, id: undefined, locationCode: '', locationName: '', status: 'DISABLED' } as unknown as PlatformLocationResource,
],
page: 1,
size: 20,
total: 3,
pages: 1,
})
const wrapper = await mountView()
const moreButtons = wrapper.findAll('button[aria-label$="的更多操作"]')
const labels = moreButtons.map((button) => button.attributes('aria-label'))
expect(labels).toEqual(['东侧库位的更多操作', 'location-2的更多操作', '当前库位的更多操作'])
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)
})
it('does not offer editing for a location outside a domestic warehouse', async () => {
vi.mocked(listPlatformLocations).mockResolvedValue({
records: [{ ...availableLocation, warehouseId: fbaWarehouse.id }, availableLocation], page: 1, size: 20, total: 2, pages: 1,
@@ -221,6 +221,17 @@ function canDelete(row: PlatformLocationResource) {
return access.can('platform:location:delete') && !row.sharedOverflow && row.status === 'DISABLED'
}
function hasMoreLocationActions(row: PlatformLocationResource) {
return 1 + Number(canEdit(row)) + Number(canChangeStatus(row)) + Number(canDelete(row)) > 3
}
function locationMoreActionsLabel(row: PlatformLocationResource) {
const identifier = [row.locationCode, row.locationName, row.id]
.map((value) => String(value ?? '').trim())
.find(Boolean) || '当前库位'
return `${identifier}的更多操作`
}
function actionError(error: unknown, fallback: string) {
const details = getApiErrorDetails(error, fallback)
ElMessage.error(details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message)
@@ -562,19 +573,31 @@ onMounted(() => {
:disabled="loading"
@click="loadDetail(row as PlatformLocationResource, 'edit')"
><Pencil :size="15" />编辑</el-button>
<el-button
v-if="canChangeStatus(row as PlatformLocationResource)"
link
:type="(row as PlatformLocationResource).status === 'AVAILABLE' ? 'danger' : 'success'"
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
@click="toggleStatus(row as PlatformLocationResource)"
><Power :size="15" />{{ (row as PlatformLocationResource).status === 'AVAILABLE' ? '停用' : '启用' }}</el-button>
<el-button
v-if="canDelete(row as PlatformLocationResource) && !hasMoreLocationActions(row as PlatformLocationResource)"
link
type="danger"
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
@click="removeLocation(row as PlatformLocationResource)"
><Trash2 :size="15" />删除</el-button>
<el-dropdown
v-if="canChangeStatus(row as PlatformLocationResource) || canDelete(row as PlatformLocationResource)"
v-if="hasMoreLocationActions(row as PlatformLocationResource)"
trigger="click"
popper-class="location-action-menu"
:disabled="loading || actionLoadingIds.has(String((row as PlatformLocationResource).id))"
@command="handleMoreAction(row as PlatformLocationResource, String($event))"
>
<el-button link type="primary" aria-label="更多库位操作" title="更多库位操作"><MoreHorizontal :size="16" /></el-button>
<el-button link type="primary" :aria-label="locationMoreActionsLabel(row as PlatformLocationResource)" :title="locationMoreActionsLabel(row as PlatformLocationResource)"><MoreHorizontal :size="16" /></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canChangeStatus(row as PlatformLocationResource)" command="status">
<Power :size="15" />{{ (row as PlatformLocationResource).status === 'AVAILABLE' ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item v-if="canDelete(row as PlatformLocationResource)" command="delete" divided>
<el-dropdown-item v-if="canDelete(row as PlatformLocationResource)" command="delete" class="location-action-menu__danger">
<Trash2 :size="15" />删除
</el-dropdown-item>
</el-dropdown-menu>
@@ -794,6 +817,7 @@ onMounted(() => {
.readonly-status { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 12px; background: #f7f9fc; }
.readonly-status > span { color: var(--cc-text); font-size: 13px; font-weight: 600; }
.readonly-status small { color: var(--cc-muted); font-size: 11px; }
:global(.location-action-menu .location-action-menu__danger:not(.is-disabled)) { color: var(--el-color-danger); }
@media (max-width: 720px) {
.detail-identity { grid-template-columns: 44px minmax(0, 1fr); }