diff --git a/src/App.test.ts b/src/App.test.ts new file mode 100644 index 00000000..7f423a1d --- /dev/null +++ b/src/App.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import zhCn from 'element-plus/es/locale/lang/zh-cn' + +describe('Admin Element Plus locale', () => { + it('provides simplified Chinese globally', () => { + const source = readFileSync(join(process.cwd(), 'src/App.vue'), 'utf8') + + expect(zhCn.name).toBe('zh-cn') + expect(source).toContain("import zhCn from 'element-plus/es/locale/lang/zh-cn'") + expect(source).toContain('') + expect(source).toMatch(/[\s\S]*<\/ElConfigProvider>/) + }) +}) diff --git a/src/App.vue b/src/App.vue index 4be89653..1210025e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,4 +1,10 @@ - + + diff --git a/src/modules/customers/pages/CustomerDetailView.test.ts b/src/modules/customers/pages/CustomerDetailView.test.ts index c0e1298a..f8e10c08 100644 --- a/src/modules/customers/pages/CustomerDetailView.test.ts +++ b/src/modules/customers/pages/CustomerDetailView.test.ts @@ -9,9 +9,7 @@ import { listTenantQualifications, type PlatformTenantResource, } from '@/modules/iam' -import { getPlatformTenantOrderStatistics, listPlatformFirstLegOrders, type PlatformFirstLegOrderResource } from '@/modules/first-leg-orders' -import { getFinanceCustomerAccountSummary } from '@/modules/finance' -import { getCustomerContainerTransitSummary } from '@/modules/container-loading' +import { listPlatformFirstLegOrders, type PlatformFirstLegOrderResource } from '@/modules/first-leg-orders' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import CustomerDetailView from './CustomerDetailView.vue' @@ -42,17 +40,13 @@ vi.mock('@/modules/iam', () => ({ })) vi.mock('@/modules/first-leg-orders', () => ({ - getPlatformTenantOrderStatistics: vi.fn(), listPlatformFirstLegOrders: vi.fn(), })) vi.mock('@/modules/finance', () => ({ CustomerStatementsPanel: { template: '
财务账单
' }, - getFinanceCustomerAccountSummary: vi.fn(), })) -vi.mock('@/modules/container-loading', () => ({ getCustomerContainerTransitSummary: vi.fn() })) - class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} @@ -96,9 +90,6 @@ describe('CustomerDetailView', () => { membershipId: 'member-1', userId: 'user-1', authUserId: 'auth-1', username: 'merchant_admin', displayName: '张三', phone: '13800138000', email: 'ops@example.com', memberType: 'OWNER', memberNo: null, status: 'ACTIVE', roleIds: [], roleNames: ['企业管理员'], }]) - vi.mocked(getPlatformTenantOrderStatistics).mockResolvedValue({ tenantId: String(tenant.id), totalSubmittedOrders: 12, currentMonthSubmittedOrders: 3, monthStart: '2026-08-01T00:00:00+08:00', nextMonthStart: '2026-09-01T00:00:00+08:00', zoneId: 'Asia/Shanghai', asOf: '2026-08-13T12:00:00+08:00' }) - vi.mocked(getFinanceCustomerAccountSummary).mockResolvedValue({ customerId: String(tenant.id), currency: 'CNY', totalReceivable: '123456.78', receivedAmount: '100000', outstandingReceivable: '23456.78', pendingBilling: '0', outstandingPayable: '0', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null }) - vi.mocked(getCustomerContainerTransitSummary).mockResolvedValue({ customerId: String(tenant.id), containerCount: 2, totalTeu: '2.25', unknownContainerCount: 0, asOf: '2026-08-13T12:00:00', definition: '已开船未到仓' }) vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({ records: [{ id: 'order-1', tenantId: tenant.id, orderNo: 'BY20260807001', customerId: null, customerName: tenant.tenantName, @@ -113,25 +104,21 @@ describe('CustomerDetailView', () => { vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' })) }) - it('renders three operating metrics and keeps enterprise users in detail content', async () => { + it('hides the deferred operating metrics and keeps enterprise users in detail content', async () => { mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.getAttribute('aria-label'))).toEqual([ '基本信息', '业务概况', '运单记录', '财务账单', '计费规则', '操作日志', ]) - expect(document.body.textContent).toContain('累计运单12票本月 3 票') - expect(document.body.textContent).toContain('累计应收12.345678万元未结清 2.345678 万元') - expect(document.body.textContent).toContain('在途柜量2.25TEU2 柜') - expect([...document.querySelectorAll('.overview-strip article')]).toHaveLength(3) - expect(document.querySelector('.overview-strip')?.textContent).not.toContain('企业用户') + expect(document.body.textContent).not.toContain('累计运单') + expect(document.body.textContent).not.toContain('累计应收') + expect(document.body.textContent).not.toContain('在途柜量') + expect(document.querySelector('.overview-strip')).toBeNull() expect(document.body.textContent).toContain('merchant_admin') expect(document.body.textContent).toContain('企业管理员') expect(listPlatformTenantMembers).toHaveBeenCalledWith(tenant.id) expect(document.body.textContent).toContain('已替换') - expect(getPlatformTenantOrderStatistics).toHaveBeenCalledWith(tenant.id) - expect(getFinanceCustomerAccountSummary).toHaveBeenCalledWith(tenant.id) - expect(getCustomerContainerTransitSummary).toHaveBeenCalledWith(tenant.id) const ordersTab = document.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() @@ -205,7 +192,6 @@ describe('CustomerDetailView', () => { vi.mocked(getPlatformTenant).mockImplementation((tenantId) => tenantId === '8' ? firstTenantRequest : Promise.resolve(secondTenant)) vi.mocked(listTenantQualifications).mockResolvedValue([]) vi.mocked(listPlatformTenantMembers).mockResolvedValue([]) - vi.mocked(getPlatformTenantOrderStatistics).mockImplementation(async (tenantId) => ({ tenantId, totalSubmittedOrders: 0, currentMonthSubmittedOrders: 0, monthStart: '2026-08-01', nextMonthStart: '2026-09-01', zoneId: 'Asia/Shanghai', asOf: '2026-08-13' })) vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 }) mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) @@ -222,30 +208,4 @@ describe('CustomerDetailView', () => { expect(document.body.textContent).not.toContain('柏悦物流') }) - it('does not request metrics without their independent permissions', async () => { - const auth = useAuthStore() - auth.permissions = ['platform:customer:list', 'platform:user:list'] - useAccessPreviewStore().initializeAfterLogin() - - mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) - await flushPromises() - - expect(getPlatformTenantOrderStatistics).not.toHaveBeenCalled() - expect(getFinanceCustomerAccountSummary).not.toHaveBeenCalled() - expect(getCustomerContainerTransitSummary).not.toHaveBeenCalled() - expect(document.querySelector('.overview-strip')?.textContent?.match(/暂无查看权限/g)).toHaveLength(3) - }) - - it('keeps metric failures independent and never renders them as zero', async () => { - vi.mocked(getFinanceCustomerAccountSummary).mockRejectedValueOnce(new Error('finance unavailable')) - - mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) - await flushPromises() - - const cards = [...document.querySelectorAll('.overview-strip article')] - expect(cards[0]?.textContent).toContain('12票') - expect(cards[1]?.textContent).toContain('—加载失败') - expect(cards[1]?.textContent).not.toContain('0万元') - expect(cards[2]?.textContent).toContain('2.25TEU') - }) }) diff --git a/src/modules/customers/pages/CustomerDetailView.vue b/src/modules/customers/pages/CustomerDetailView.vue index 6fd42248..e7e0f166 100644 --- a/src/modules/customers/pages/CustomerDetailView.vue +++ b/src/modules/customers/pages/CustomerDetailView.vue @@ -3,7 +3,7 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ArrowLeft, BarChart3, Building2, Eye, - CircleDollarSign, Container, FileCheck2, FileText, Pencil, Power, + FileCheck2, FileText, Pencil, Power, Ship, UserRound, UsersRound, } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' @@ -20,13 +20,10 @@ import { type PlatformTenantStatus, } from '@/modules/iam' import { - getPlatformTenantOrderStatistics, listPlatformFirstLegOrders, type PlatformFirstLegOrderResource, type PlatformFirstLegOrderStatus, } from '@/modules/first-leg-orders' -import { getFinanceCustomerAccountSummary } from '@/modules/finance' -import { getCustomerContainerTransitSummary } from '@/modules/container-loading' import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' @@ -41,11 +38,9 @@ import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBi import CustomerOverseasStorageFeePanel from '@/modules/customers/CustomerOverseasStorageFeePanel.vue' import { CustomerStatementsPanel } from '@/modules/finance' import { CustomerAuditLogsPanel } from '@/modules/audit' -import CustomerOperatingMetricCard from '@/modules/customers/components/CustomerOperatingMetricCard.vue' type DetailTab = 'basic' | 'business' | 'orders' | 'statements' | 'billing' | 'logs' type PreviewState = { url: string; kind: 'image' | 'pdf'; loading: boolean; failed: boolean } -type MetricState = { loading: boolean; failed: boolean; data: T | null } const route = useRoute() const router = useRouter() @@ -58,9 +53,6 @@ const loadError = ref(null) const customer = ref(null) const qualifications = ref([]) const members = ref([]) -const orderStatistics = ref>>>({ loading: false, failed: false, data: null }) -const financeStatistics = ref>>>({ loading: false, failed: false, data: null }) -const transitStatistics = ref>>>({ loading: false, failed: false, data: null }) const customerOrders = ref([]) const customerOrderTotal = ref(0) const orderPage = ref(1) @@ -80,7 +72,7 @@ const tabList = ref(null) const tabs: Array<{ key: DetailTab; label: string; icon: string }> = [ { key: 'basic', label: '基本信息', icon: '📋' }, - { key: 'business', label: '业务概况', icon: '📊' }, + // { key: 'business', label: '业务概况', icon: '📊' }, { key: 'orders', label: '运单记录', icon: '📦' }, { key: 'statements', label: '财务账单', icon: '🧾' }, { key: 'billing', label: '计费规则', icon: '⚖️' }, @@ -139,25 +131,6 @@ function transportLabel(order: PlatformFirstLegOrderResource) { return '陆运' } function formattedDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' } -function decimalText(value: number | string | null | undefined) { - const raw = String(value ?? '').trim() - if (!/^-?\d+(\.\d+)?$/.test(raw)) return '—' - const normalized = raw.replace(/^(-?)0+(?=\d)/, '$1').replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1') - return normalized === '-0' ? '0' : normalized -} -function cnyWan(value: number | string | null | undefined) { - const raw = decimalText(value) - if (raw === '—') return raw - const negative = raw.startsWith('-') - const [integer, fraction = ''] = (negative ? raw.slice(1) : raw).split('.') - const digits = `${integer}${fraction}`.replace(/^0+(?=\d)/, '') || '0' - const scale = fraction.length + 4 - const padded = digits.padStart(scale + 1, '0') - const whole = padded.slice(0, -scale) || '0' - const decimals = padded.slice(-scale).replace(/0+$/, '') - const result = `${negative && digits !== '0' ? '-' : ''}${whole}${decimals ? `.${decimals}` : ''}` - return result -} async function loadCustomerOrders(page = orderPage.value, requestedTenantId = tenantId.value) { if (!access.can('platform:order:list')) return @@ -178,30 +151,6 @@ async function loadCustomerOrders(page = orderPage.value, requestedTenantId = te } } -async function loadOperatingStatistics(requestedTenantId: string, serial: number) { - const tasks: Promise[] = [] - const active = () => !disposed && serial === detailRequestSerial && requestedTenantId === tenantId.value - if (access.can('platform:order:list')) { - orderStatistics.value = { loading: true, failed: false, data: null } - tasks.push(getPlatformTenantOrderStatistics(requestedTenantId).then((data) => { - if (active()) orderStatistics.value = { loading: false, failed: false, data } - }).catch(() => { if (active()) orderStatistics.value = { loading: false, failed: true, data: null } })) - } - if (access.can('platform:finance:customer-account:list')) { - financeStatistics.value = { loading: true, failed: false, data: null } - tasks.push(getFinanceCustomerAccountSummary(requestedTenantId).then((data) => { - if (active()) financeStatistics.value = { loading: false, failed: false, data } - }).catch(() => { if (active()) financeStatistics.value = { loading: false, failed: true, data: null } })) - } - if (access.can('platform:firstleg:container:list')) { - transitStatistics.value = { loading: true, failed: false, data: null } - tasks.push(getCustomerContainerTransitSummary(requestedTenantId).then((data) => { - if (active()) transitStatistics.value = { loading: false, failed: false, data } - }).catch(() => { if (active()) transitStatistics.value = { loading: false, failed: true, data: null } })) - } - await Promise.all(tasks) -} - function clearPreviews() { for (const preview of Object.values(previews.value)) if (preview.url) URL.revokeObjectURL(preview.url) previews.value = {} @@ -247,9 +196,6 @@ async function loadDetail() { customer.value = null qualifications.value = [] members.value = [] - orderStatistics.value = { loading: false, failed: false, data: null } - financeStatistics.value = { loading: false, failed: false, data: null } - transitStatistics.value = { loading: false, failed: false, data: null } customerOrders.value = [] customerOrderTotal.value = 0 customerOrdersError.value = null @@ -267,7 +213,6 @@ async function loadDetail() { url: '', kind: qualificationName(item).toLowerCase().endsWith('.pdf') ? 'pdf' : 'image', loading: false, failed: false, } satisfies PreviewState])) - void loadOperatingStatistics(requestedTenantId, serial) if (access.can('platform:order:list')) { if (orderPage.value !== 1) skipNextOrderPageLoad = true orderPage.value = 1 @@ -382,13 +327,13 @@ watch(orderPage, (page, previousPage) => { - -
+
@@ -490,8 +435,6 @@ watch(orderPage, (page, previousPage) => { .detail-page-toolbar { min-width:0; flex:1; } .header-actions { display:flex; flex:0 0 auto; gap:8px; } .header-actions :deep(.el-button + .el-button) { margin-left:0; } -.overview-strip { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); overflow:hidden; border:1px solid rgba(255,255,255,.5); border-radius:12px; background:rgba(255,255,255,.95); box-shadow:0 3px 12px rgba(15,23,42,.05); } -.overview-strip :deep(.operating-metric-card:last-child) { border-right:0; } .detail-tabs { display:flex; gap:0; overflow-x:auto; overflow-y:hidden; border-bottom:1px solid rgba(228,228,231,.5); border-radius:20px 20px 0 0; background:rgba(255,255,255,.95); box-shadow:0 -4px 16px rgba(0,0,0,.04); scrollbar-width:thin; } .detail-tabs button { display:inline-flex; min-width:max-content; min-height:0; flex:0 0 auto; align-items:center; justify-content:center; gap:8px; padding:16px 28px; border:0; border-bottom:3px solid transparent; background:transparent; color:#71717a; font-size:15px; font-weight:600; white-space:nowrap; cursor:pointer; transition:all .3s cubic-bezier(.4,0,.2,1); } .detail-tab-icon { font-size:18px; line-height:1; } @@ -522,6 +465,6 @@ table { width:100%; min-width:720px; border-collapse:collapse; font-size:12px; } .contract-note { padding:16px 18px; border-radius:10px; background:#f8fafc; }.contract-note strong { color:#344054; font-size:12px; }.contract-note p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; line-height:1.7; } .qualification-preview { display:grid; min-height:320px; max-height:72vh; overflow:auto; place-items:center; border-radius:10px; background:#f8fafc; }.qualification-preview img { display:block; max-width:100%; height:auto; }.qualification-preview iframe { width:100%; min-height:68vh; border:0; background:#fff; } @media (max-width:960px) { .basic-layout { grid-template-columns:1fr; }.contact-panel,.user-panel { grid-column:auto; }.qualification-panel { order:3; }.user-panel { order:4; } } -@media (max-width:720px) { .detail-header { align-items:flex-start; flex-wrap:wrap; }.detail-page-toolbar { width:100%; flex-basis:100%; }.company-mark { width:40px; height:40px; }.header-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }.header-actions :deep(.el-tag) { grid-column:1 / -1; justify-self:start; }.header-actions :deep(.el-button) { min-height:44px; }.overview-strip { grid-template-columns:1fr; }.overview-strip :deep(.operating-metric-card:last-child) { border-bottom:0; }.detail-panel { padding:16px; }.info-grid { grid-template-columns:1fr; }.info-grid > div.wide { grid-column:auto; } } +@media (max-width:720px) { .detail-header { align-items:flex-start; flex-wrap:wrap; }.detail-page-toolbar { width:100%; flex-basis:100%; }.company-mark { width:40px; height:40px; }.header-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }.header-actions :deep(.el-tag) { grid-column:1 / -1; justify-self:start; }.header-actions :deep(.el-button) { min-height:44px; }.detail-panel { padding:16px; }.info-grid { grid-template-columns:1fr; }.info-grid > div.wide { grid-column:auto; } } @media (max-width:420px) { .company-mark { display:none; }.header-actions { grid-template-columns:1fr; }.header-actions :deep(.el-tag) { grid-column:auto; }.qualification-list button { grid-template-columns:36px minmax(0,1fr) auto; }.qualification-list .file-date,.qualification-list button > svg { display:none; } } diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index 947247cc..0d41c3cc 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -172,6 +172,13 @@ describe('CustomersView', () => { expect(customersSource).not.toContain(' { + expect(customersSource).toContain('class="qualification-validity" role="cell"') + expect(customersSource).toContain('grid-template-columns:170px minmax(0,1fr) 220px') + expect(customersSource).toContain('.qualification-validity :deep(.el-date-editor) { width:100%; min-width:0; }') + expect(customersSource).toContain('.qualification-file-name { min-width:0; max-width:140px;') + }) + it('loads the complete customer detail before editing masked list fields', async () => { mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index d0403dfb..8e82b61d 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -1087,7 +1087,7 @@ onMounted(() => { void loadCustomers() })
{{ draft.file?.name || draft.uploadedName || '未选择文件' }}{{ draft.error }}
-
+

支持 PDF、JPG、PNG,单个文件不超过 5MB;* 为提交审核必填项。

@@ -1219,10 +1219,10 @@ onMounted(() => { void loadCustomers() }) .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; justify-content:flex-end; margin-bottom:12px; } .qualification-hint { margin:12px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; } .qualification-hint span,.qualification-label > span { color:var(--el-color-danger); } .qualification-credit-code { max-width:420px; margin-top:16px; } .qualification-credit-code small,.field-hint { display:block; margin-top:6px; color:var(--cc-muted); font-size:11px; line-height:1.5; } -.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; } +.qualification-table { overflow:hidden; margin-top:14px; border:1px solid var(--cc-border); border-radius:8px; } .qualification-row { display:grid; grid-template-columns:170px minmax(0,1fr) 220px; 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; } .qualification-label { gap:5px; font-weight:600; } .qualification-label svg { margin-left:auto; color:var(--el-color-success); } .qualification-optional { padding:2px 5px; border-radius:4px; background:var(--el-fill-color-light); color:var(--cc-muted); font-size:10px; font-weight:500; line-height:1.4; } -.qualification-file { gap:12px; } .qualification-file-control { display:flex; min-width:0; flex:1; flex-wrap:wrap; align-items:center; gap:8px; } .file-picker { position:relative; display:inline-flex; min-height:36px; flex:0 0 auto; align-items:center; gap:7px; overflow:hidden; padding:0 12px; border:1px solid var(--cc-border); border-radius:6px; background:#fff; color:var(--cc-text); cursor:pointer; } .file-picker:focus-within { outline:2px solid color-mix(in srgb,var(--cc-primary) 35%,transparent); outline-offset:2px; } .file-picker.is-disabled { cursor:not-allowed; opacity:.6; } .file-picker input { position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:inherit; } .qualification-file-name { min-width:0; max-width:180px; overflow:hidden; color:var(--cc-muted); font-size:12px; text-overflow:ellipsis; white-space:nowrap; } .qualification-error { width:100%; color:var(--el-color-danger); line-height:1.4; } +.qualification-file { gap:12px; } .qualification-file-control { display:flex; min-width:0; flex:1; flex-wrap:wrap; align-items:center; gap:8px; } .file-picker { position:relative; display:inline-flex; min-height:36px; flex:0 0 auto; align-items:center; gap:7px; overflow:hidden; padding:0 12px; border:1px solid var(--cc-border); border-radius:6px; background:#fff; color:var(--cc-text); cursor:pointer; } .file-picker:focus-within { outline:2px solid color-mix(in srgb,var(--cc-primary) 35%,transparent); outline-offset:2px; } .file-picker.is-disabled { cursor:not-allowed; opacity:.6; } .file-picker input { position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:inherit; } .qualification-file-name { min-width:0; max-width:140px; overflow:hidden; color:var(--cc-muted); font-size:12px; text-overflow:ellipsis; white-space:nowrap; } .qualification-validity :deep(.el-date-editor) { width:100%; min-width:0; } .qualification-error { width:100%; color:var(--el-color-danger); line-height:1.4; } .qualification-preview-trigger { position:relative; display:grid; width:68px; height:68px; flex:0 0 68px; overflow:hidden; place-items:center; padding:0; border:1px solid var(--cc-border); border-radius:6px; background:#f8fafc; color:var(--cc-muted); cursor:pointer; } .qualification-preview-trigger:hover { border-color:var(--cc-primary); color:var(--cc-primary); } .qualification-preview-trigger:focus-visible { outline:2px solid color-mix(in srgb,var(--cc-primary) 42%,transparent); outline-offset:2px; }