refactor: 收拢客户列表次要操作

This commit is contained in:
2026-08-08 11:21:56 +08:00
parent 4835904507
commit cb0792963c
2 changed files with 75 additions and 7 deletions
+43 -3
View File
@@ -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%; }