feat(phase3): complete loading workbenches
This commit is contained in:
@@ -101,7 +101,7 @@ function tableButtonByText(text: string) {
|
||||
}
|
||||
|
||||
async function clickMoreAction(text: string) {
|
||||
document.querySelector<HTMLButtonElement>('.el-table__body button[aria-label="更多客户操作"]')?.click()
|
||||
document.querySelector<HTMLButtonElement>('.el-table__body button[aria-label^="更多客户操作:"]')?.click()
|
||||
await flushPromises()
|
||||
const item = [...document.querySelectorAll<HTMLElement>('.el-dropdown-menu__item')]
|
||||
.find((candidate) => candidate.textContent?.trim() === text && !candidate.classList.contains('is-disabled'))
|
||||
@@ -455,7 +455,8 @@ describe('CustomersView', () => {
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
await clickMoreAction('重置密码')
|
||||
tableButtonByText('重置密码')?.click()
|
||||
await flushPromises()
|
||||
|
||||
const password = document.querySelector<HTMLInputElement>('input[autocomplete="new-password"]')
|
||||
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="请再次输入新密码"]')
|
||||
@@ -479,11 +480,10 @@ describe('CustomersView', () => {
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
vi.mocked(archivePlatformTenant).mockResolvedValue({ ...fullTenant, status: 'DISABLED' })
|
||||
|
||||
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
const dropdowns = wrapper.findAllComponents({ name: 'ElDropdown' })
|
||||
expect(dropdowns.length).toBeGreaterThan(0)
|
||||
dropdowns.at(-1)!.vm.$emit('command', 'delete')
|
||||
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).toBeNull()
|
||||
tableButtonByText('删除')?.click()
|
||||
await flushPromises()
|
||||
|
||||
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="TEN8"]')
|
||||
@@ -519,9 +519,53 @@ describe('CustomersView', () => {
|
||||
|
||||
expect(tableButtonByText('详情')).toBeDefined()
|
||||
expect(tableButtonByText('编辑')).toBeDefined()
|
||||
expect(document.querySelector('.el-table__body button[aria-label="更多客户操作"]')).toBeTruthy()
|
||||
expect(tableButtonByText('重置密码')).toBeUndefined()
|
||||
await clickMoreAction('重置密码')
|
||||
expect(document.querySelector('input[autocomplete="new-password"]')).toBeTruthy()
|
||||
expect(document.querySelector('.el-table__body button[aria-label="更多客户操作:柏悦物流"]')).toBeTruthy()
|
||||
expect(tableButtonByText('重置密码')).toBeDefined()
|
||||
expect(tableButtonByText('跳转登录')).toBeUndefined()
|
||||
await clickMoreAction('跳转登录')
|
||||
expect(document.body.textContent).toContain('代登录原因')
|
||||
})
|
||||
|
||||
it('uses stable unique customer identities for multiple more-action buttons', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({
|
||||
records: [
|
||||
{ ...fullTenant, id: '8', tenantName: '客户甲', tenantCode: 'TEN-A', status: 'ACTIVE' },
|
||||
{ ...fullTenant, id: '9', tenantName: '', tenantCode: 'TEN-B', status: 'ACTIVE' },
|
||||
{ ...fullTenant, id: '10', tenantName: '', tenantCode: '', status: 'ACTIVE' },
|
||||
],
|
||||
page: 1, size: 10, total: 3, pages: 1,
|
||||
})
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const buttons = [...document.querySelectorAll<HTMLButtonElement>('.el-table__body button[aria-label^="更多客户操作:"]')]
|
||||
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')))
|
||||
})
|
||||
|
||||
it('falls back to the current customer when all row identifiers are missing', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({
|
||||
records: [{ ...fullTenant, id: null, tenantName: '', tenantCode: '', status: 'ACTIVE' } as unknown as PlatformTenantResource],
|
||||
page: 1, size: 10, total: 1, pages: 1,
|
||||
})
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const button = document.querySelector<HTMLButtonElement>('.el-table__body button[aria-label^="更多客户操作:"]')
|
||||
expect(button?.getAttribute('aria-label')).toBe('更多客户操作:当前客户')
|
||||
expect(button?.getAttribute('title')).toBe('更多客户操作:当前客户')
|
||||
expect(button?.getAttribute('aria-label')).not.toMatch(/undefined|null/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Building2, 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'
|
||||
@@ -633,8 +633,60 @@ function canChangeStatus(row: PlatformTenantResource) {
|
||||
function canArchive() {
|
||||
return access.can('platform:customer:delete')
|
||||
}
|
||||
function hasMoreActions(row: PlatformTenantResource) {
|
||||
return canResetPassword(row) || canImpersonate(row) || canChangeStatus(row) || canArchive()
|
||||
type CustomerRowAction = 'reset-password' | 'impersonate' | 'status' | 'delete'
|
||||
const customerRowActionIcons: Record<CustomerRowAction, Component> = {
|
||||
'reset-password': KeyRound,
|
||||
impersonate: LogIn,
|
||||
status: Power,
|
||||
delete: Trash2,
|
||||
}
|
||||
function hasReviewAction(row: PlatformTenantResource) {
|
||||
return pendingReviewByTenantId.value.has(String(row.id))
|
||||
&& access.can('platform:customer:review')
|
||||
&& (access.can('platform:customer:approve') || access.can('platform:customer:reject'))
|
||||
}
|
||||
function customerPrimaryActionCount(row: PlatformTenantResource) {
|
||||
return Number(access.can('platform:customer:view'))
|
||||
+ Number(access.can('platform:customer:view') && access.can('platform:customer:edit'))
|
||||
+ Number(hasReviewAction(row))
|
||||
}
|
||||
function customerSecondaryActions(row: PlatformTenantResource): CustomerRowAction[] {
|
||||
return [
|
||||
...(canResetPassword(row) ? ['reset-password' as const] : []),
|
||||
...(canImpersonate(row) ? ['impersonate' as const] : []),
|
||||
...(canChangeStatus(row) ? ['status' as const] : []),
|
||||
...(canArchive() ? ['delete' as const] : []),
|
||||
]
|
||||
}
|
||||
function directCustomerActions(row: PlatformTenantResource) {
|
||||
return customerSecondaryActions(row).slice(0, Math.max(0, 3 - customerPrimaryActionCount(row)))
|
||||
}
|
||||
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]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find(Boolean)
|
||||
return `更多客户操作:${identifier || '当前客户'}`
|
||||
}
|
||||
function customerActionLabel(row: PlatformTenantResource, action: CustomerRowAction) {
|
||||
if (action === 'reset-password') return '重置密码'
|
||||
if (action === 'impersonate') return '跳转登录'
|
||||
if (action === 'status') return row.status === 'ACTIVE' ? '停用' : '启用'
|
||||
return '删除'
|
||||
}
|
||||
function customerActionType(row: PlatformTenantResource, action: CustomerRowAction) {
|
||||
if (action === 'delete' || (action === 'status' && row.status === 'ACTIVE')) return 'danger'
|
||||
if (action === 'status') return 'success'
|
||||
return 'primary'
|
||||
}
|
||||
function customerActionDisabled(action: CustomerRowAction) {
|
||||
if (action === 'reset-password') return passwordSubmitting.value
|
||||
if (action === 'impersonate') return impersonationSubmitting.value
|
||||
if (action === 'delete') return archiveSubmitting.value
|
||||
return false
|
||||
}
|
||||
function handleMoreAction(row: PlatformTenantResource, command: string) {
|
||||
if (command === 'reset-password') openResetPassword(row)
|
||||
@@ -917,15 +969,13 @@ onMounted(() => { void loadCustomers() })
|
||||
<div class="table-actions">
|
||||
<el-button v-if="access.can('platform:customer:view')" link type="primary" @click="showDetail(row as PlatformTenantResource)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="access.can('platform:customer:view') && access.can('platform:customer:edit')" link type="primary" @click="openEdit(row as PlatformTenantResource)"><Pencil :size="15" />编辑</el-button>
|
||||
<el-button v-if="pendingReviewByTenantId.has(String((row as PlatformTenantResource).id)) && access.can('platform:customer:review') && (access.can('platform:customer:approve') || access.can('platform:customer:reject'))" link type="warning" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, access.can('platform:customer:approve') ? 'approve' : 'reject')"><ShieldCheck :size="15" />审核</el-button>
|
||||
<el-dropdown v-if="hasMoreActions(row as PlatformTenantResource)" trigger="click" :disabled="rowActionId === String((row as PlatformTenantResource).id)" popper-class="customer-action-menu" @command="handleMoreAction(row as PlatformTenantResource, String($event))">
|
||||
<el-button link type="primary" aria-label="更多客户操作" title="更多操作"><MoreHorizontal :size="16" /></el-button>
|
||||
<el-button v-if="hasReviewAction(row as PlatformTenantResource)" link type="warning" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, access.can('platform:customer:approve') ? 'approve' : 'reject')"><ShieldCheck :size="15" />审核</el-button>
|
||||
<el-button v-for="action in directCustomerActions(row as PlatformTenantResource)" :key="action" link :type="customerActionType(row as PlatformTenantResource, action)" :disabled="customerActionDisabled(action)" @click="handleMoreAction(row as PlatformTenantResource, action)"><component :is="customerRowActionIcons[action]" :size="15" />{{ customerActionLabel(row as PlatformTenantResource, action) }}</el-button>
|
||||
<el-dropdown v-if="moreCustomerActions(row as PlatformTenantResource).length" trigger="click" :disabled="rowActionId === String((row as PlatformTenantResource).id)" popper-class="customer-action-menu" @command="handleMoreAction(row as PlatformTenantResource, String($event))">
|
||||
<el-button link type="primary" :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-if="canResetPassword(row as PlatformTenantResource)" command="reset-password" :disabled="passwordSubmitting"><KeyRound :size="15" />重置密码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canImpersonate(row as PlatformTenantResource)" command="impersonate" :disabled="impersonationSubmitting"><LogIn :size="15" />跳转登录</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canChangeStatus(row as PlatformTenantResource)" command="status"><Power :size="15" />{{ (row as PlatformTenantResource).status === 'ACTIVE' ? '停用' : '启用' }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canArchive()" command="delete" :disabled="archiveSubmitting" divided class="customer-action-menu__danger"><Trash2 :size="15" />删除</el-dropdown-item>
|
||||
<el-dropdown-item v-for="action in moreCustomerActions(row as PlatformTenantResource)" :key="action" :command="action" :disabled="customerActionDisabled(action)" :divided="action === 'delete'" :class="{ 'customer-action-menu__danger': action === 'delete' }"><component :is="customerRowActionIcons[action]" :size="15" />{{ customerActionLabel(row as PlatformTenantResource, action) }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
Reference in New Issue
Block a user