优化
This commit is contained in:
@@ -105,7 +105,7 @@ const queryFields: QueryField[] = [
|
||||
]
|
||||
|
||||
const emptyForm = (): CreatePlatformTenantRequest => ({
|
||||
tenantName: '', legalName: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
||||
tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
||||
contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 0,
|
||||
enterpriseType: '', establishedDate: null, registeredCapital: null, registeredCapitalCurrency: 'CNY',
|
||||
registeredAddress: '', internalRemark: '',
|
||||
@@ -114,6 +114,7 @@ const form = ref(emptyForm())
|
||||
const rules: FormRules = {
|
||||
tenantName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
legalName: [{ required: true, message: '请输入营业执照上的企业名称', trigger: 'blur' }],
|
||||
legalRepresentative: [{ required: true, message: '请输入法定代表人姓名', trigger: 'blur' }],
|
||||
unifiedSocialCreditCode: [
|
||||
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
|
||||
{ pattern: /^[0-9A-Z]{18}$/, message: '请输入 18 位大写字母或数字', trigger: 'blur' },
|
||||
@@ -174,6 +175,8 @@ const customerColumns: DataTableColumn[] = [
|
||||
|
||||
let loadSequence = 0
|
||||
let reviewLoadSequence = 0
|
||||
let editorLoadSequence = 0
|
||||
let detailLoadSequence = 0
|
||||
const editorTitle = computed(() => editingId.value ? '编辑客户' : '新增客户')
|
||||
const canUploadQualifications = computed(() => !editorStatus.value || ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value))
|
||||
const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value || ''))
|
||||
@@ -191,6 +194,13 @@ function statusMeta(status: PlatformTenantStatus) {
|
||||
return { label: '已停用', type: 'info' as const }
|
||||
}
|
||||
|
||||
function qualificationStatusMeta(status: PlatformTenantQualification['status']) {
|
||||
if (status === 'APPROVED') return { label: '已通过', type: 'success' as const }
|
||||
if (status === 'REJECTED') return { label: '已驳回', type: 'danger' as const }
|
||||
if (status === 'SUPERSEDED') return { label: '已替换', type: 'info' as const }
|
||||
return { label: '待审核', type: 'warning' as const }
|
||||
}
|
||||
|
||||
async function loadPendingReviews() {
|
||||
const sequence = ++reviewLoadSequence
|
||||
if (!access.can('platform:customer:review')) {
|
||||
@@ -227,6 +237,7 @@ function reviewToCustomer(review: EnterpriseReview): PlatformTenantResource {
|
||||
tenantName: review.tenantName?.trim() || `待审核企业(${tenantId})`,
|
||||
status: 'PENDING',
|
||||
legalName: null,
|
||||
legalRepresentative: null,
|
||||
unifiedSocialCreditCode: null,
|
||||
contactName: review.contactName ?? null,
|
||||
contactPhone: review.contactPhone ?? null,
|
||||
@@ -308,12 +319,13 @@ function resetEditor() {
|
||||
form.value = emptyForm(); qualifications.value = newQualificationDrafts(); qualificationLoadError.value = ''; savedFormSnapshot.value = ''
|
||||
}
|
||||
function openCreate() {
|
||||
editorLoadSequence += 1
|
||||
editingId.value = null; editorStatus.value = null; resetEditor(); editorOpen.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
function mapTenantToForm(row: PlatformTenantResource): CreatePlatformTenantRequest {
|
||||
return {
|
||||
tenantName: row.tenantName, legalName: row.legalName || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
||||
tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
||||
contactName: row.contactName || '', contactPhone: row.contactPhone || '', contactEmail: row.contactEmail || '',
|
||||
contactWechat: row.contactWechat || '', servicePackage: row.servicePackage || '基础版', salesOwnerName: row.salesOwnerName || '',
|
||||
paymentTermDays: Number(row.paymentTermDays ?? 0), enterpriseType: row.enterpriseType || '', establishedDate: row.establishedDate || null,
|
||||
@@ -322,12 +334,16 @@ function mapTenantToForm(row: PlatformTenantResource): CreatePlatformTenantReque
|
||||
internalRemark: row.internalRemark || '',
|
||||
}
|
||||
}
|
||||
async function loadEditorQualifications(tenantId: number | string) {
|
||||
async function loadEditorQualifications(tenantId: number | string, sequence = editorLoadSequence) {
|
||||
qualificationLoadError.value = ''
|
||||
try {
|
||||
const records = await listTenantQualifications(tenantId)
|
||||
if (sequence !== editorLoadSequence || !editorOpen.value || editingId.value !== String(tenantId)) return
|
||||
const acceptedStatus = ['ACTIVE', 'DISABLED'].includes(editorStatus.value || '') ? 'APPROVED' : 'PENDING'
|
||||
for (const draft of qualifications.value) {
|
||||
const item = [...records].reverse().find((record) => record.qualificationType === draft.type && record.status !== 'REJECTED')
|
||||
const item = [...records].reverse().find((record) => (
|
||||
record.qualificationType === draft.type && record.status === acceptedStatus
|
||||
))
|
||||
if (item) {
|
||||
draft.uploaded = true
|
||||
draft.validTo = item.validTo || ''
|
||||
@@ -341,21 +357,23 @@ async function loadEditorQualifications(tenantId: number | string) {
|
||||
} catch (error) { qualificationLoadError.value = getApiErrorMessage(error, '资质信息加载失败,可稍后重试') }
|
||||
}
|
||||
async function openEdit(row: PlatformTenantResource) {
|
||||
editingId.value = String(row.id); editorStatus.value = row.status; resetEditor(); form.value = mapTenantToForm(row)
|
||||
const id = String(row.id)
|
||||
const sequence = ++editorLoadSequence
|
||||
editingId.value = id; editorStatus.value = row.status; resetEditor(); form.value = mapTenantToForm(row)
|
||||
editorOpen.value = true; editorLoading.value = true
|
||||
try {
|
||||
const tenant = await getPlatformTenant(row.id)
|
||||
if (editingId.value !== String(row.id)) return
|
||||
if (sequence !== editorLoadSequence || editingId.value !== id || !editorOpen.value) return
|
||||
editorStatus.value = tenant.status
|
||||
form.value = mapTenantToForm(tenant)
|
||||
savedFormSnapshot.value = JSON.stringify(form.value)
|
||||
await loadEditorQualifications(row.id)
|
||||
await loadEditorQualifications(row.id, sequence)
|
||||
await nextTick(() => formRef.value?.clearValidate())
|
||||
} catch (error) {
|
||||
if (editingId.value === String(row.id)) editorOpen.value = false
|
||||
if (sequence === editorLoadSequence && editingId.value === id) editorOpen.value = false
|
||||
ElMessage.error(getApiErrorMessage(error, '客户完整资料加载失败,请稍后重试'))
|
||||
} finally {
|
||||
if (editingId.value === String(row.id)) editorLoading.value = false
|
||||
if (sequence === editorLoadSequence && editingId.value === id) editorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +385,7 @@ function selectQualificationFile(event: Event, draft: QualificationDraft) {
|
||||
const extensionValid = /\.(pdf|jpe?g|png)$/i.test(file.name)
|
||||
const mimeValid = ['application/pdf', 'image/jpeg', 'image/png'].includes(file.type)
|
||||
if (!extensionValid || !mimeValid) { draft.error = '仅支持 PDF、JPG、PNG 文件'; input.value = ''; return }
|
||||
if (file.size > 10 * 1024 * 1024) { draft.error = '单个文件不能超过 10MB'; input.value = ''; return }
|
||||
if (file.size > 5 * 1024 * 1024) { draft.error = '单个文件不能超过 5MB'; input.value = ''; return }
|
||||
draft.file = file
|
||||
draft.previewKind = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf') ? 'pdf' : 'image'
|
||||
draft.previewUrl = URL.createObjectURL(file)
|
||||
@@ -545,10 +563,13 @@ async function submitReview() {
|
||||
}
|
||||
|
||||
async function showDetail(row: PlatformTenantResource) {
|
||||
const sequence = ++detailLoadSequence
|
||||
const rowId = String(row.id)
|
||||
clearDetailQualificationPreviews()
|
||||
detail.value = row; detailQualifications.value = []; detailOpen.value = true; detailLoading.value = true
|
||||
try {
|
||||
const [tenant, records] = await Promise.all([getPlatformTenant(row.id), listTenantQualifications(row.id)])
|
||||
if (sequence !== detailLoadSequence || !detailOpen.value || String(detail.value?.id) !== rowId) return
|
||||
detail.value = tenant
|
||||
detailQualifications.value = records
|
||||
detailQualificationPreviews.value = Object.fromEntries(records.map((item) => {
|
||||
@@ -558,8 +579,11 @@ async function showDetail(row: PlatformTenantResource) {
|
||||
for (const item of records) {
|
||||
if (detailQualificationPreviews.value[String(item.id)]?.kind === 'image') void loadDetailQualificationPreview(item, false)
|
||||
}
|
||||
} catch (error) { ElMessage.error(getApiErrorMessage(error, '客户详情加载失败')) }
|
||||
finally { detailLoading.value = false }
|
||||
} catch (error) {
|
||||
if (sequence === detailLoadSequence) ElMessage.error(getApiErrorMessage(error, '客户详情加载失败'))
|
||||
} finally {
|
||||
if (sequence === detailLoadSequence) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
async function toggleStatus(row: PlatformTenantResource) {
|
||||
const next = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'; const verb = next === 'ACTIVE' ? '启用' : '停用'
|
||||
@@ -601,7 +625,12 @@ async function submitDecision() {
|
||||
}
|
||||
decisionOpen.value = false
|
||||
detailOpen.value = false
|
||||
ElMessage.success(decision.type === 'approve' ? '企业资料已审核通过' : '企业资料已驳回')
|
||||
const successMessage = decision.type === 'reject'
|
||||
? '企业资料已驳回'
|
||||
: approved?.administratorCreated
|
||||
? '企业资料已审核通过,管理员账号已创建'
|
||||
: '企业资料已审核通过,注册账号继续作为企业管理员'
|
||||
ElMessage.success(successMessage)
|
||||
if (approved?.administratorCreated) {
|
||||
approvalResult.value = approved
|
||||
approvalResultOpen.value = true
|
||||
@@ -637,8 +666,18 @@ async function exportCustomers() {
|
||||
}
|
||||
|
||||
watch([page, pageSize], () => { void loadCustomers() })
|
||||
watch(editorOpen, (open) => { if (!open) clearQualificationPreviews() })
|
||||
watch(detailOpen, (open) => { if (!open) clearDetailQualificationPreviews() })
|
||||
watch(editorOpen, (open) => {
|
||||
if (!open) {
|
||||
editorLoadSequence += 1
|
||||
clearQualificationPreviews()
|
||||
}
|
||||
})
|
||||
watch(detailOpen, (open) => {
|
||||
if (!open) {
|
||||
detailLoadSequence += 1
|
||||
clearDetailQualificationPreviews()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => [access.can('platform:customer:approve'), access.can('platform:customer:reject')],
|
||||
([canApprove, canReject]) => {
|
||||
@@ -655,7 +694,7 @@ onMounted(loadCustomers)
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户筛选" :fields="queryFields" @search="applyFilters" />
|
||||
<section v-if="loadError" class="surface-panel customers-error" role="alert"><div><strong>客户列表暂时无法加载</strong><p>{{ loadError.message }}</p><ApiErrorMeta :details="loadError" /></div><el-button type="primary" :loading="loading" @click="loadCustomers">重新加载</el-button></section>
|
||||
<section v-else class="surface-panel table-panel" :aria-busy="loading">
|
||||
<div class="customers-table-header"><div class="customers-table-heading"><h2>客户列表</h2><span>共 {{ result.total }} 个客户</span></div><div class="customers-table-actions"><el-button v-if="access.can('platform:customer:export')" :loading="exportSubmitting" :disabled="loading" @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></div>
|
||||
<div class="customers-table-header"><div class="customers-table-heading"><h2>客户列表</h2><span>共 {{ result.total }} 个客户</span></div><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></div>
|
||||
<div v-if="reviewLoadError" class="review-sync-error" role="alert"><div><ShieldX :size="17" /><span>{{ reviewLoadError.message }}</span></div><el-button link type="primary" @click="reloadPendingReviews">重新加载待审核客户</el-button></div>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="customerColumns" :loading="loading" :total="result.total" aria-label="客户列表" row-key="id" empty-text="暂无符合筛选条件的客户">
|
||||
<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:approve')" link type="success" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, 'approve')"><ShieldCheck :size="15" />通过</el-button><el-button v-if="pendingReviewByTenantId.has(String((row as PlatformTenantResource).id)) && access.can('platform:customer:reject')" link type="danger" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, 'reject')"><ShieldX :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>
|
||||
@@ -670,6 +709,7 @@ onMounted(loadCustomers)
|
||||
<div class="form-section"><h3>基础资料</h3><div class="form-grid two-cols">
|
||||
<el-form-item label="公司名称" prop="tenantName" required><el-input v-model="form.tenantName" placeholder="请输入工商登记公司名称" maxlength="128" /></el-form-item>
|
||||
<el-form-item label="营业执照企业名称" prop="legalName" required><el-input v-model="form.legalName" placeholder="请输入营业执照上的企业名称" maxlength="128" /></el-form-item>
|
||||
<el-form-item label="法定代表人" prop="legalRepresentative" required><el-input v-model="form.legalRepresentative" placeholder="请输入法定代表人姓名" maxlength="64" /></el-form-item>
|
||||
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode" required><el-input v-model="form.unifiedSocialCreditCode" :disabled="Boolean(editingId)" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" /></el-form-item>
|
||||
<el-form-item label="企业类型" prop="enterpriseType"><el-input v-model="form.enterpriseType" placeholder="如:有限责任公司" maxlength="64" /></el-form-item>
|
||||
<el-form-item label="成立日期" prop="establishedDate"><el-date-picker v-model="form.establishedDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择成立日期" :disabled-date="(date: Date) => date.getTime() > Date.now()" /></el-form-item>
|
||||
@@ -703,7 +743,7 @@ onMounted(loadCustomers)
|
||||
<div role="cell"><el-date-picker v-model="draft.validTo" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期" :disabled="!canUploadQualifications || draft.uploaded" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="qualification-hint">支持 PDF、JPG、PNG,单个文件不超过 10MB;<span>*</span> 为提交审核必填项。</p>
|
||||
<p class="qualification-hint">支持 PDF、JPG、PNG,单个文件不超过 5MB;<span>*</span> 为提交审核必填项。</p>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer><div class="drawer-footer customer-editor-footer"><span>保存资料时会一并上传已选资质;进入审核仍需点击“提交审核”并再次确认</span><div><el-button :disabled="editorLoading || saving || submitting || uploadingQualifications" @click="editorOpen = false">关闭</el-button><el-button :loading="saving" :disabled="editorLoading || submitting || uploadingQualifications" @click="saveCustomer">保存资料和资质</el-button><el-button v-if="canSubmitReview" type="primary" :loading="submitting" :disabled="editorLoading || saving || uploadingQualifications" @click="submitReview">提交审核</el-button></div></div></template>
|
||||
@@ -740,6 +780,8 @@ onMounted(loadCustomers)
|
||||
<section class="customer-detail-section" aria-labelledby="customer-profile-heading">
|
||||
<h4 id="customer-profile-heading">企业资料</h4>
|
||||
<dl class="customer-detail-grid">
|
||||
<div class="customer-detail-item is-wide"><dt>营业执照企业名称</dt><dd>{{ displayText(detail.legalName) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>法定代表人</dt><dd>{{ displayText(detail.legalRepresentative) }}</dd></div>
|
||||
<div class="customer-detail-item is-wide"><dt>统一社会信用代码</dt><dd>{{ displayText(detail.unifiedSocialCreditCode) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>联系人</dt><dd>{{ displayText(detail.contactName) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>联系电话</dt><dd>{{ displayText(detail.contactPhone) }}</dd></div>
|
||||
@@ -763,7 +805,7 @@ onMounted(loadCustomers)
|
||||
<small v-if="detailQualificationPreviews[String(item.id)]?.kind === 'pdf'">PDF</small>
|
||||
</span>
|
||||
<span class="detail-qualification-copy"><strong>{{ qualifications.find((draft) => draft.type === item.qualificationType)?.label || item.qualificationType }}</strong><small class="detail-qualification-file-name">{{ detailQualificationName(item) }}</small><small>{{ item.validTo ? `有效期至 ${item.validTo}` : '长期有效或未设置有效期' }}</small></span>
|
||||
<span class="detail-qualification-meta"><el-tag :type="item.status === 'APPROVED' ? 'success' : item.status === 'REJECTED' ? 'danger' : 'warning'" size="small" effect="light">{{ item.status === 'APPROVED' ? '已通过' : item.status === 'REJECTED' ? '已驳回' : '待审核' }}</el-tag><span><Eye :size="14" />{{ detailQualificationPreviews[String(item.id)]?.loading ? '读取中' : (detailQualificationPreviews[String(item.id)]?.failed ? '重试' : '查看') }}</span></span>
|
||||
<span class="detail-qualification-meta"><el-tag :type="qualificationStatusMeta(item.status).type" size="small" effect="light">{{ qualificationStatusMeta(item.status).label }}</el-tag><span><Eye :size="14" />{{ detailQualificationPreviews[String(item.id)]?.loading ? '读取中' : (detailQualificationPreviews[String(item.id)]?.failed ? '重试' : '查看') }}</span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="detail-empty-qualification"><FileCheck2 :size="20" /><span>暂未上传资质文件</span></div>
|
||||
|
||||
Reference in New Issue
Block a user