This commit is contained in:
2026-08-04 13:32:08 +08:00
parent 6354f5ca66
commit 2192aed125
17 changed files with 711 additions and 354 deletions
+20 -194
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Building2, CheckCircle2, Download, Eye, FileCheck2, FileText, FileUp, Pencil, Plus, Power, ShieldCheck, ShieldX } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import { Building2, CheckCircle2, Download, Eye, FileText, FileUp, Pencil, Plus, Power, ShieldCheck, ShieldX } 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'
@@ -52,14 +52,9 @@ type QualificationDraft = {
uploading: boolean
error: string
}
type DetailQualificationPreview = {
url: string
kind: 'image' | 'pdf'
loading: boolean
failed: boolean
}
const access = useAccessPreviewStore()
const route = useRoute()
const router = useRouter()
const page = ref(1)
const pageSize = ref(10)
@@ -86,11 +81,6 @@ const savedFormSnapshot = ref('')
const editingId = ref<string | null>(null)
const editorStatus = ref<PlatformTenantStatus | null>(null)
const formRef = ref<FormInstance>()
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref<PlatformTenantResource | null>(null)
const detailQualifications = ref<PlatformTenantQualification[]>([])
const detailQualificationPreviews = ref<Record<string, DetailQualificationPreview>>({})
const qualificationLoadError = ref('')
const qualificationPreviewOpen = ref(false)
const qualificationPreviewName = ref('')
@@ -179,7 +169,6 @@ 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 || ''))
@@ -197,12 +186,6 @@ 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
@@ -435,65 +418,6 @@ function handleQualificationPreviewError(draft: QualificationDraft) {
if (qualificationPreviewUrl.value === draft.previewUrl) qualificationPreviewOpen.value = false
}
function detailQualificationName(item: PlatformTenantQualification) {
try { return qualificationFileName(item.documentUrl) }
catch { return `${qualifications.value.find((draft) => draft.type === item.qualificationType)?.label || '资质'}文件` }
}
function clearDetailQualificationPreviews() {
for (const preview of Object.values(detailQualificationPreviews.value)) {
if (preview.url) URL.revokeObjectURL(preview.url)
}
detailQualificationPreviews.value = {}
qualificationPreviewOpen.value = false
}
async function loadDetailQualificationPreview(item: PlatformTenantQualification, showError = true) {
const key = String(item.id)
const preview = detailQualificationPreviews.value[key]
const tenantId = detail.value?.id
if (!preview || !tenantId || !item.documentUrl || preview.loading) return false
preview.loading = true
try {
const blob = await downloadTenantQualification(tenantId, item.documentUrl)
if (!blob.size) throw new Error('资质文件内容为空')
if (!detailOpen.value || detailQualificationPreviews.value[key] !== preview) return false
if (preview.url) URL.revokeObjectURL(preview.url)
preview.url = URL.createObjectURL(blob)
preview.kind = blob.type === 'application/pdf' || detailQualificationName(item).toLowerCase().endsWith('.pdf') ? 'pdf' : 'image'
preview.failed = false
return true
} catch (error) {
preview.failed = true
if (showError) ElMessage.error(getApiErrorMessage(error, '资质文件预览加载失败,请稍后重试'))
return false
} finally {
if (detailQualificationPreviews.value[key] === preview) preview.loading = false
}
}
async function openDetailQualificationPreview(item: PlatformTenantQualification) {
const preview = detailQualificationPreviews.value[String(item.id)]
if (!preview) return
if (preview.failed) {
if (preview.url) URL.revokeObjectURL(preview.url)
preview.url = ''
preview.failed = false
}
if (!preview.url) await loadDetailQualificationPreview(item)
if (!preview.url) return
qualificationPreviewName.value = detailQualificationName(item)
qualificationPreviewUrl.value = preview.url
qualificationPreviewKind.value = preview.kind
qualificationPreviewOpen.value = true
}
function handleDetailQualificationPreviewError(item: PlatformTenantQualification) {
const preview = detailQualificationPreviews.value[String(item.id)]
if (!preview) return
preview.failed = true
if (qualificationPreviewUrl.value === preview.url) qualificationPreviewOpen.value = false
}
async function uploadSelectedQualifications(tenantId: string) {
const failures: string[] = []
@@ -565,28 +489,8 @@ async function submitReview() {
} finally { submitting.value = false }
}
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) => {
const kind = detailQualificationName(item).toLowerCase().endsWith('.pdf') ? 'pdf' : 'image'
return [String(item.id), { url: '', kind, loading: false, failed: false } satisfies DetailQualificationPreview]
}))
for (const item of records) {
if (detailQualificationPreviews.value[String(item.id)]?.kind === 'image') void loadDetailQualificationPreview(item, false)
}
} catch (error) {
if (sequence === detailLoadSequence) ElMessage.error(getApiErrorMessage(error, '客户详情加载失败'))
} finally {
if (sequence === detailLoadSequence) detailLoading.value = false
}
function showDetail(row: PlatformTenantResource) {
void router.push({ name: 'customer-detail', params: { tenantId: String(row.id) } })
}
async function toggleStatus(row: PlatformTenantResource) {
const next = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'; const verb = next === 'ACTIVE' ? '启用' : '停用'
@@ -627,7 +531,6 @@ async function submitDecision() {
await rejectEnterpriseReview(review.id, decision.comment.trim())
}
decisionOpen.value = false
detailOpen.value = false
const successMessage = decision.type === 'reject'
? '企业资料已驳回'
: approved?.administratorCreated
@@ -682,12 +585,6 @@ watch(editorOpen, (open) => {
clearQualificationPreviews()
}
})
watch(detailOpen, (open) => {
if (!open) {
detailLoadSequence += 1
clearDetailQualificationPreviews()
}
})
watch(
() => [access.can('platform:customer:approve'), access.can('platform:customer:reject')],
([canApprove, canReject]) => {
@@ -695,8 +592,18 @@ watch(
},
)
onBeforeUnmount(clearQualificationPreviews)
onBeforeUnmount(clearDetailQualificationPreviews)
onMounted(loadCustomers)
onMounted(async () => {
await loadCustomers()
const editId = typeof route.query.edit === 'string' ? route.query.edit : ''
if (!editId || !access.can('platform:customer:edit')) return
const row = result.value.records.find((item) => String(item.id) === editId)
if (row) await openEdit(row)
else {
try { await openEdit(await getPlatformTenant(editId)) }
catch (error) { ElMessage.error(getApiErrorMessage(error, '客户资料加载失败')) }
}
void router.replace({ name: 'customers' })
})
</script>
<template>
@@ -778,55 +685,8 @@ onMounted(loadCustomers)
<template #footer><el-button type="primary" @click="closeApprovalResult">我已保存,关闭</el-button></template>
</el-dialog>
<el-drawer v-model="detailOpen" title="客户详情" size="620px" class="business-drawer">
<div v-loading="detailLoading" class="customer-detail">
<template v-if="detail">
<div class="detail-title">
<span><Building2 :size="22" /></span>
<div><h3>{{ detail.tenantName }}</h3><p>{{ detail.tenantCode }}</p></div>
<el-tag :type="statusMeta(detail.status).type" effect="light">{{ statusMeta(detail.status).label }}</el-tag>
</div>
<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>
<div class="customer-detail-item is-wide"><dt>联系邮箱</dt><dd>{{ displayText(detail.contactEmail) }}</dd></div>
<div class="customer-detail-item"><dt>客户套餐</dt><dd>{{ displayText(detail.servicePackage) }}</dd></div>
<div class="customer-detail-item"><dt>账期</dt><dd>{{ detail.paymentTermDays ?? 0 }} 天</dd></div>
<div class="customer-detail-item"><dt>销售人员</dt><dd>{{ displayText(detail.salesOwnerName) }}</dd></div>
<div class="customer-detail-item"><dt>入驻时间</dt><dd>{{ formatDate(detail.createdAt) }}</dd></div>
<div class="customer-detail-item is-wide"><dt>注册地址</dt><dd>{{ displayText(detail.registeredAddress) }}</dd></div>
<div class="customer-detail-item is-wide"><dt>内部备注</dt><dd>{{ displayText(detail.internalRemark) }}</dd></div>
</dl>
</section>
<section class="customer-detail-section detail-qualification-list" aria-labelledby="customer-qualification-heading">
<div class="detail-section-heading"><h4 id="customer-qualification-heading">资质文件</h4><span>共 {{ detailQualifications.length }} 份</span></div>
<div v-if="detailQualifications.length" class="detail-qualification-items">
<button v-for="item in detailQualifications" :key="String(item.id)" type="button" class="detail-qualification-item" :disabled="detailQualificationPreviews[String(item.id)]?.loading" :aria-label="`查看${qualifications.find((draft) => draft.type === item.qualificationType)?.label || item.qualificationType}:${detailQualificationName(item)}`" @click="openDetailQualificationPreview(item)">
<span class="detail-qualification-icon">
<img v-if="detailQualificationPreviews[String(item.id)]?.url && detailQualificationPreviews[String(item.id)]?.kind === 'image' && !detailQualificationPreviews[String(item.id)]?.failed" :src="detailQualificationPreviews[String(item.id)]?.url" :alt="`${qualifications.find((draft) => draft.type === item.qualificationType)?.label || item.qualificationType}缩略图`" @error="handleDetailQualificationPreviewError(item)" />
<FileText v-else :size="20" />
<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="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>
</section>
</template>
</div>
<template #footer><div class="drawer-footer"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="detail && pendingReviewByTenantId.has(String(detail.id)) && access.can('platform:customer:reject')" type="danger" plain @click="openDecision(detail, 'reject')">驳回</el-button><el-button v-if="detail && pendingReviewByTenantId.has(String(detail.id)) && access.can('platform:customer:approve')" type="success" @click="openDecision(detail, 'approve')">审核通过</el-button></div></template>
</el-drawer>
<el-dialog v-model="decisionOpen" :title="decision.type === 'approve' ? '确认审核通过' : '确认驳回企业资料'" width="min(520px, calc(100vw - 28px))" class="review-decision-dialog" :close-on-click-modal="false">
<div v-if="selectedReview" class="decision-summary"><strong>{{ selectedReview.tenantName || detail?.tenantName || '当前客户' }}</strong><span>审核单 {{ selectedReview.id }}</span></div>
<div v-if="selectedReview" class="decision-summary"><strong>{{ selectedReview.tenantName || '当前客户' }}</strong><span>审核单 {{ selectedReview.id }}</span></div>
<el-form ref="decisionFormRef" :model="decision" :rules="decisionRules" label-position="top">
<el-form-item prop="comment" :label="decision.type === 'approve' ? '审核说明' : '驳回原因'" required><el-input v-model="decision.comment" type="textarea" :rows="4" maxlength="300" show-word-limit :placeholder="decision.type === 'approve' ? '请填写审核通过说明' : '请明确说明需要企业修改的内容'" /></el-form-item>
</el-form>
@@ -843,7 +703,7 @@ onMounted(loadCustomers)
.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; }
.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,.detail-title > 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; }
.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%; }
.qualification-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; } .qualification-heading h3 { margin-bottom:4px; } .qualification-heading p,.qualification-hint { margin:0; color:var(--cc-muted); font-size:12px; line-height:1.6; } .qualification-hint { margin-top:12px; } .qualification-hint span,.qualification-label > span { color:var(--el-color-danger); }
.qualification-table { overflow:hidden; margin-top:14px; border:1px solid var(--cc-border); border-radius:8px; } .qualification-row { display:grid; grid-template-columns:180px minmax(300px,1fr) 180px; min-width:0; border-top:1px solid var(--cc-border); } .qualification-row:first-child { border-top:0; } .qualification-row > * { display:flex; min-width:0; min-height:68px; align-items:center; padding:12px 14px; } .qualification-header { min-height:44px; background:#f7f8fc; color:var(--cc-muted); font-size:12px; font-weight:600; } .qualification-header > * { min-height:44px; }
@@ -864,42 +724,8 @@ onMounted(loadCustomers)
.customer-editor :deep(.el-drawer__body) { min-height:0; overflow-y:auto; overscroll-behavior:contain; }
.customer-editor :deep(.el-drawer__footer) { flex:0 0 auto; }
.customer-editor-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; } .customer-editor-footer > span { color:var(--cc-muted); font-size:12px; text-align:left; } .customer-editor-footer > div { display:flex; flex:0 0 auto; gap:8px; } .customer-editor-footer :deep(.el-button + .el-button) { margin-left:0; }
.customer-detail { min-height:200px; padding:20px 24px 24px; }
.detail-title { display:grid; grid-template-columns:44px minmax(0,1fr) auto; align-items:center; gap:12px; margin-bottom:24px; padding:4px 0 20px; border-bottom:1px solid var(--cc-border); }
.detail-title > span { width:44px; height:44px; border-radius:10px; }
.detail-title h3 { margin:0; overflow:hidden; color:var(--cc-text); font-size:20px; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }
.detail-title p { margin:4px 0 0; color:var(--cc-muted); font-size:12px; letter-spacing:.02em; }
.customer-detail-section + .customer-detail-section { margin-top:24px; }
.customer-detail-section h4 { margin:0 0 12px; color:var(--cc-text); font-size:14px; }
.customer-detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); overflow:hidden; margin:0; border:1px solid var(--cc-border); border-radius:10px; background:#fff; }
.customer-detail-item { min-width:0; padding:13px 16px; border-bottom:1px solid var(--cc-border); }
.customer-detail-item:nth-child(odd):not(.is-wide) { border-right:1px solid var(--cc-border); }
.customer-detail-item.is-wide { grid-column:1 / -1; }
.customer-detail-item:last-child { border-bottom:0; }
.customer-detail-item dt { margin-bottom:5px; color:var(--cc-muted); font-size:12px; line-height:1.4; }
.customer-detail-item dd { margin:0; color:var(--cc-text); font-size:14px; font-weight:600; line-height:1.55; overflow-wrap:anywhere; }
.decision-summary { display:flex; min-width:0; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; padding:12px 14px; border-radius:8px; background:#f8fafc; } .decision-summary > * { min-width:0; overflow-wrap:anywhere; } .decision-summary strong { color:var(--cc-text); font-size:13px; } .decision-summary span { color:var(--cc-muted); font-size:11px; }
.approval-result-warning { display:flex; align-items:flex-start; gap:10px; padding:12px 14px; border:1px solid #fecdca; border-radius:8px; background:#fffafa; color:#b42318; } .approval-result-warning strong,.approval-result-warning span { display:block; } .approval-result-warning strong { font-size:14px; } .approval-result-warning span { margin-top:4px; font-size:12px; line-height:1.5; } .approval-credentials { margin:18px 0 0; border:1px solid var(--cc-border); border-radius:8px; } .approval-credentials > div { display:grid; grid-template-columns:90px minmax(0,1fr); gap:12px; padding:12px 14px; } .approval-credentials > div + div { border-top:1px solid var(--cc-border); } .approval-credentials dt { color:var(--cc-muted); font-size:12px; } .approval-credentials dd { display:flex; min-width:0; align-items:center; gap:10px; margin:0; } .approval-credentials code { min-width:0; flex:1; overflow-wrap:anywhere; color:var(--cc-text); font-size:14px; font-weight:700; }
.detail-section-heading { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
.detail-section-heading h4 { margin-bottom:12px; }
.detail-section-heading span { color:var(--cc-muted); font-size:12px; }
.detail-qualification-items { display:flex; flex-direction:column; gap:8px; }
.detail-qualification-item { display:grid; width:100%; grid-template-columns:52px minmax(0,1fr) auto; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--cc-border); border-radius:8px; background:#fff; color:inherit; text-align:left; cursor:pointer; }
.detail-qualification-item:hover { border-color:color-mix(in srgb,var(--cc-primary) 48%,var(--cc-border)); background:#fbfdff; }
.detail-qualification-item:focus-visible { outline:2px solid color-mix(in srgb,var(--cc-primary) 42%,transparent); outline-offset:2px; }
.detail-qualification-item:disabled { cursor:wait; opacity:.75; }
.detail-qualification-icon { display:grid; width:52px; height:52px; overflow:hidden; place-items:center; border-radius:8px; background:#eef4ff; color:var(--cc-primary); }
.detail-qualification-icon img { width:100%; height:100%; object-fit:contain; background:#fff; }
.detail-qualification-icon small { font-size:9px; font-weight:700; }
.detail-qualification-copy { min-width:0; }
.detail-qualification-item strong,.detail-qualification-item small { display:block; }
.detail-qualification-item strong { color:var(--cc-text); font-size:13px; }
.detail-qualification-item small { margin-top:4px; color:var(--cc-muted); font-size:11px; line-height:1.4; }
.detail-qualification-file-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.detail-qualification-meta { display:flex; align-items:flex-end; gap:8px; flex-direction:column; }
.detail-qualification-meta > span { display:flex; align-items:center; gap:4px; color:var(--cc-primary); font-size:11px; font-weight:600; }
.detail-empty-qualification { display:flex; min-height:88px; align-items:center; justify-content:center; gap:8px; border:1px dashed var(--cc-border); border-radius:8px; background:#fafbfc; color:var(--cc-muted); font-size:12px; }
@media (max-width:720px) { .customers-error,.customers-table-header,.customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .customer-detail { padding:16px; } .customer-detail-grid { grid-template-columns:1fr; } .customer-detail-item,.customer-detail-item:nth-child(odd):not(.is-wide) { grid-column:1; border-right:0; } .customer-detail-item:last-child { border-bottom:0; } .decision-summary { align-items:flex-start; flex-direction:column; } }
@media (max-width:480px) { .detail-qualification-item { grid-template-columns:44px minmax(0,1fr); } .detail-qualification-icon { width:44px; height:44px; } .detail-qualification-meta { grid-column:2; align-items:center; flex-direction:row; justify-content:space-between; } }
@media (max-width:720px) { .customers-error,.customers-table-header,.customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .decision-summary { align-items:flex-start; flex-direction:column; } }
@media (max-width:360px) { .customers-table-actions,.capital-input { grid-template-columns:1fr; } }
</style>