Release/test 合并上生产 #2
@@ -100,6 +100,16 @@ function tableButtonByText(text: string) {
|
||||
.find((button) => button.textContent?.trim() === text)
|
||||
}
|
||||
|
||||
async function clickMoreAction(text: string) {
|
||||
tableButtonByText('更多')?.click()
|
||||
await flushPromises()
|
||||
const item = [...document.querySelectorAll<HTMLElement>('.el-dropdown-menu__item')]
|
||||
.find((candidate) => candidate.textContent?.trim() === text && !candidate.classList.contains('is-disabled'))
|
||||
expect(item).toBeDefined()
|
||||
item!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
describe('CustomersView', () => {
|
||||
@@ -445,8 +455,7 @@ describe('CustomersView', () => {
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
tableButtonByText('重置密码')?.click()
|
||||
await flushPromises()
|
||||
await clickMoreAction('重置密码')
|
||||
|
||||
const password = document.querySelector<HTMLInputElement>('input[autocomplete="new-password"]')
|
||||
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="请再次输入新密码"]')
|
||||
@@ -470,9 +479,11 @@ describe('CustomersView', () => {
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
vi.mocked(archivePlatformTenant).mockResolvedValue({ ...fullTenant, status: 'DISABLED' })
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
tableButtonByText('删除')?.click()
|
||||
const dropdowns = wrapper.findAllComponents({ name: 'ElDropdown' })
|
||||
expect(dropdowns.length).toBeGreaterThan(0)
|
||||
dropdowns.at(-1)!.vm.$emit('command', 'delete')
|
||||
await flushPromises()
|
||||
|
||||
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="TEN8"]')
|
||||
@@ -496,4 +507,21 @@ describe('CustomersView', () => {
|
||||
|
||||
expect(document.querySelector('.el-table__fixed')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps frequent actions visible and groups secondary customer actions under more', 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, status: 'ACTIVE' }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
|
||||
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(tableButtonByText('详情')).toBeDefined()
|
||||
expect(tableButtonByText('编辑')).toBeDefined()
|
||||
expect(tableButtonByText('更多')).toBeDefined()
|
||||
expect(tableButtonByText('重置密码')).toBeUndefined()
|
||||
await clickMoreAction('重置密码')
|
||||
expect(document.querySelector('input[autocomplete="new-password"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Building2, CheckCircle2, Download, Eye, FileText, FileUp, KeyRound, LogIn, Pencil, Plus, Power, ShieldCheck, ShieldX, Trash2 } from '@lucide/vue'
|
||||
import { Building2, CheckCircle2, Download, Eye, FileText, FileUp, KeyRound, LogIn, MoreHorizontal, Pencil, Plus, Power, 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'
|
||||
@@ -180,7 +180,7 @@ const creditCodeEstablished = ref(false)
|
||||
const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/
|
||||
|
||||
const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', minWidth: 520, slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', minWidth: 280, slotName: 'actions' },
|
||||
{ key: 'tenantCode', prop: 'tenantCode', label: '客户ID', minWidth: 170 },
|
||||
{ key: 'company', label: '公司名称', minWidth: 230, slotName: 'company' },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', minWidth: 205, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
@@ -614,6 +614,27 @@ async function submitReview() {
|
||||
function showDetail(row: PlatformTenantResource) {
|
||||
void router.push({ name: 'customer-detail', params: { tenantId: String(row.id) } })
|
||||
}
|
||||
function canResetPassword(row: PlatformTenantResource) {
|
||||
return access.can('platform:customer:reset-password') && ['ACTIVE', 'DISABLED'].includes(row.status)
|
||||
}
|
||||
function canImpersonate(row: PlatformTenantResource) {
|
||||
return access.can('platform:customer:impersonate') && row.status === 'ACTIVE'
|
||||
}
|
||||
function canChangeStatus(row: PlatformTenantResource) {
|
||||
return access.can('platform:customer:status') && ['ACTIVE', 'DISABLED'].includes(row.status)
|
||||
}
|
||||
function canArchive() {
|
||||
return access.can('platform:customer:delete')
|
||||
}
|
||||
function hasMoreActions(row: PlatformTenantResource) {
|
||||
return canResetPassword(row) || canImpersonate(row) || canChangeStatus(row) || canArchive()
|
||||
}
|
||||
function handleMoreAction(row: PlatformTenantResource, command: string) {
|
||||
if (command === 'reset-password') openResetPassword(row)
|
||||
if (command === 'impersonate') openImpersonation(row)
|
||||
if (command === 'status') void toggleStatus(row)
|
||||
if (command === 'delete') openArchive(row)
|
||||
}
|
||||
async function toggleStatus(row: PlatformTenantResource) {
|
||||
const next = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'; const verb = next === 'ACTIVE' ? '启用' : '停用'
|
||||
try {
|
||||
@@ -881,7 +902,24 @@ onMounted(() => { void loadCustomers() })
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="customerColumns" preference-key="customers-table" :loading="loading" :total="result.total" aria-label="客户列表" row-key="id" empty-text="暂无符合筛选条件的客户">
|
||||
<template #toolbar-start><div class="customers-table-heading"><h2>客户列表</h2><span>共 {{ result.total }} 个客户</span></div></template>
|
||||
<template #toolbar-actions><div class="customers-table-actions"><el-button v-if="access.can('platform:customer:export') && access.can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportCustomers"><Download :size="16" />导出Excel</el-button><el-button v-if="access.can('platform:customer:create')" type="primary" :disabled="loading" @click="openCreate"><Plus :size="16" />新增客户</el-button></div></template>
|
||||
<template #actions="{ row }"><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-button v-if="access.can('platform:customer:reset-password') && ['ACTIVE','DISABLED'].includes((row as PlatformTenantResource).status)" link type="primary" :disabled="passwordSubmitting" @click="openResetPassword(row as PlatformTenantResource)"><KeyRound :size="15" />重置密码</el-button><el-button v-if="access.can('platform:customer:impersonate') && (row as PlatformTenantResource).status === 'ACTIVE'" link type="primary" :disabled="impersonationSubmitting" @click="openImpersonation(row as PlatformTenantResource)"><LogIn :size="15" />跳转登录</el-button><el-button v-if="access.can('platform:customer:delete')" link type="danger" :disabled="archiveSubmitting" @click="openArchive(row as PlatformTenantResource)"><Trash2 :size="15" />删除</el-button><el-button v-if="access.can('platform:customer:status') && ['ACTIVE','DISABLED'].includes((row as PlatformTenantResource).status)" link :type="(row as PlatformTenantResource).status === 'ACTIVE' ? 'danger' : 'success'" :loading="rowActionId === String((row as PlatformTenantResource).id)" @click="toggleStatus(row as PlatformTenantResource)"><Power :size="15" />{{ (row as PlatformTenantResource).status === 'ACTIVE' ? '停用' : '启用' }}</el-button></div></template>
|
||||
<template #actions="{ row }">
|
||||
<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="更多客户操作"><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-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template #company="{ row }"><div class="customer-company-cell"><span><Building2 :size="18" /></span><strong>{{ (row as PlatformTenantResource).tenantName }}</strong></div></template>
|
||||
<template #status="{ row }"><el-tag :type="statusMeta((row as PlatformTenantResource).status).type" effect="light">{{ statusMeta((row as PlatformTenantResource).status).label }}</el-tag></template>
|
||||
<template #orderCount="{ row }">{{ access.can('platform:order:list') ? (orderCounts[String((row as PlatformTenantResource).id)] ?? '—') : '—' }}</template>
|
||||
@@ -996,6 +1034,8 @@ onMounted(() => { void loadCustomers() })
|
||||
.customers-table-heading { display:flex; min-width:0; align-items:baseline; gap:10px; } .customers-table-heading h2 { margin:0; color:var(--cc-text); font-size:16px; } .customers-table-heading span { color:var(--cc-muted); font-size:12px; white-space:nowrap; }
|
||||
.customers-table-actions { display:flex; flex-wrap:wrap; gap:8px; } .customers-table-actions :deep(.el-button + .el-button) { margin-left:0; }
|
||||
.table-actions { display:flex; flex-wrap:wrap; align-items:center; gap:2px 8px; } .table-actions :deep(.el-button) { margin-left:0; }
|
||||
:global(.customer-action-menu .el-dropdown-menu__item) { min-height:40px; gap:8px; }
|
||||
:global(.customer-action-menu .customer-action-menu__danger) { color:var(--el-color-danger); }
|
||||
.review-sync-error { display:flex; min-width:0; align-items:center; justify-content:space-between; gap:12px; padding:10px 18px; border-bottom:1px solid #fecdca; background:#fffafa; color:#b42318; font-size:12px; } .review-sync-error > div { display:flex; min-width:0; align-items:center; gap:8px; } .review-sync-error span { overflow-wrap:anywhere; }
|
||||
.customer-company-cell { display:flex; min-width:0; align-items:center; gap:10px; } .customer-company-cell > span { display:grid; width:34px; height:34px; flex:0 0 auto; place-items:center; border-radius:7px; background:#eef4ff; color:var(--cc-primary); } .customer-company-cell strong { overflow:hidden; text-overflow:ellipsis; }
|
||||
.capital-input { display:grid; grid-template-columns:minmax(0,1fr) 130px; width:100%; gap:8px; } .capital-input :deep(.el-input-number),.capital-input :deep(.el-select) { width:100%; }
|
||||
|
||||
Reference in New Issue
Block a user