This commit is contained in:
2026-08-13 12:22:41 +08:00
parent 835ac0ad12
commit 8a8c4fa190
26 changed files with 572 additions and 148 deletions
@@ -0,0 +1,26 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import CustomerOperatingMetricCard from './CustomerOperatingMetricCard.vue'
describe('CustomerOperatingMetricCard', () => {
const props = { title: '累计应收', value: '12.3456', unit: '万元', detail: '未结清 0.125 万元', tone: 'green' as const }
it('keeps the supplied decimal precision', () => {
const wrapper = mount(CustomerOperatingMetricCard, { props })
expect(wrapper.text()).toContain('12.3456万元')
expect(wrapper.text()).toContain('未结清 0.125 万元')
})
it('shows a dash instead of zero for failures', () => {
const wrapper = mount(CustomerOperatingMetricCard, { props: { ...props, failed: true } })
expect(wrapper.text()).toContain('—')
expect(wrapper.text()).toContain('加载失败')
expect(wrapper.text()).not.toContain('12.3456')
})
it('shows the permission state without exposing a value', () => {
const wrapper = mount(CustomerOperatingMetricCard, { props: { ...props, unauthorized: true } })
expect(wrapper.text()).toContain('暂无查看权限')
expect(wrapper.text()).not.toContain('12.3456')
})
})
@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
title: string
value: string
unit: string
detail: string
loading?: boolean
failed?: boolean
unauthorized?: boolean
tone: 'blue' | 'green' | 'amber'
}>()
</script>
<template>
<article class="operating-metric-card" :aria-label="title" :aria-busy="loading || undefined">
<span class="metric-icon" :class="`is-${tone}`" aria-hidden="true"><slot name="icon" /></span>
<div class="metric-content">
<p>{{ title }}</p>
<strong v-if="loading" class="metric-loading" aria-label="加载中">—</strong>
<strong v-else-if="failed || unauthorized">—</strong>
<strong v-else>{{ value }}<small>{{ unit }}</small></strong>
<span :class="{ 'is-error': failed }">{{ unauthorized ? '暂无查看权限' : failed ? '加载失败' : loading ? '加载中…' : detail }}</span>
</div>
</article>
</template>
<style scoped>
.operating-metric-card { display:flex; min-width:0; align-items:center; gap:14px; padding:22px 24px; border-right:1px solid var(--cc-border); }
.metric-icon { display:grid; width:44px; height:44px; flex:0 0 auto; place-items:center; border-radius:12px; }
.metric-icon.is-blue { background:linear-gradient(135deg,rgba(102,126,234,.15),rgba(118,75,162,.1)); color:#667eea; }
.metric-icon.is-green { background:linear-gradient(135deg,rgba(34,197,94,.15),rgba(22,163,74,.1)); color:#16a34a; }
.metric-icon.is-amber { background:linear-gradient(135deg,rgba(245,158,11,.15),rgba(234,88,12,.1)); color:#ea580c; }
.metric-content { min-width:0; }
p,span { margin:0; color:var(--cc-muted); font-size:12px; }
strong { display:block; min-width:0; margin:5px 0 4px; color:#172033; font-size:26px; line-height:1; overflow-wrap:anywhere; }
small { margin-left:5px; color:#475467; font-size:12px; font-weight:600; white-space:nowrap; }
.is-error { color:#d92d20; }
.metric-loading { color:#98a2b3; }
@media (max-width:720px) { .operating-metric-card { border-right:0; border-bottom:1px solid var(--cc-border); } }
@media (max-width:360px) { .operating-metric-card { gap:10px; padding:18px 14px; }.metric-icon { width:40px; height:40px; }strong { font-size:23px; } }
</style>