wip(strategy-map): V1+V3 合版 + v2/v3 口径(完备度/C1 hold/57 归档组)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<section class="map-summary" aria-label="战略地图摘要">
|
||||
<div class="sum-card">
|
||||
<div class="sum-label">营业收入</div>
|
||||
<div class="sum-value mono" :class="{ muted: revenue == null }">
|
||||
<template v-if="revenue != null">{{ fmtWan(revenue) }}<span class="sum-unit">万元</span></template>
|
||||
<template v-else>无数据</template>
|
||||
</div>
|
||||
<div class="sum-sub">{{ period ? period + ' 实际值' : '财务分析接口未返回期间' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="sum-card">
|
||||
<div class="sum-label">毛利率</div>
|
||||
<div class="sum-value mono" :class="{ muted: grossMargin == null }">
|
||||
<template v-if="grossMargin != null">{{ grossMargin.toFixed(1) }}<span class="sum-unit">%</span></template>
|
||||
<template v-else>无数据</template>
|
||||
</div>
|
||||
<div class="sum-sub">{{ period ? period + ' 实际值' : '未取到财务分析数据' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="sum-card">
|
||||
<div class="sum-label">战略目标达成率</div>
|
||||
<div class="sum-value mono" :class="achievement == null ? 'muted' : 'lv-' + achievementLevel">
|
||||
<template v-if="achievement != null">{{ Math.round(achievement) }}<span class="sum-unit">%</span></template>
|
||||
<template v-else>无数据</template>
|
||||
</div>
|
||||
<div class="sum-sub">
|
||||
<template v-if="achievement != null">{{ achievementCount }} 个目标有实测值 · 其余无数据</template>
|
||||
<template v-else>所有目标均无可用 KPI 实测值</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sum-card">
|
||||
<div class="sum-label">风险目标</div>
|
||||
<div class="sum-value mono" :class="riskCount == null ? 'muted' : (riskCount > 0 ? 'lv-red' : 'lv-green')">
|
||||
<template v-if="riskCount != null">{{ riskCount }}</template>
|
||||
<template v-else>无数据</template>
|
||||
</div>
|
||||
<div class="sum-sub">
|
||||
<template v-if="summary">达标 {{ summary.green }} · 关注 {{ summary.yellow }} · 共 {{ summary.total }} 个目标</template>
|
||||
<template v-else>战略回顾数据未加载</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
finance: { total_sales: number | null; gross_profit_rate: number | null; period?: string } | null
|
||||
achievement: number | null
|
||||
achievementCount: number
|
||||
summary: { total: number; green: number; yellow: number; red: number; health_score?: number } | null
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const revenue = computed<number | null>(() => {
|
||||
const v = props.finance?.total_sales
|
||||
return v == null || Number.isNaN(Number(v)) ? null : Number(v)
|
||||
})
|
||||
const grossMargin = computed<number | null>(() => {
|
||||
const v = props.finance?.gross_profit_rate
|
||||
return v == null || Number.isNaN(Number(v)) ? null : Number(v)
|
||||
})
|
||||
const period = computed(() => props.finance?.period || '')
|
||||
const riskCount = computed<number | null>(() => (props.summary ? props.summary.red : null))
|
||||
const achievementLevel = computed(() => {
|
||||
const a = props.achievement
|
||||
if (a == null) return 'gray'
|
||||
if (a >= 90) return 'green'
|
||||
if (a >= 60) return 'yellow'
|
||||
return 'red'
|
||||
})
|
||||
|
||||
/** 金额统一折算为万元(保留 1 位小数),不做任何兜底造数 */
|
||||
function fmtWan(v: number): string {
|
||||
return (v / 10000).toLocaleString('zh-CN', { maximumFractionDigits: 1, minimumFractionDigits: 0 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sum-card {
|
||||
background: var(--bh-bg-card, #fff);
|
||||
border: 1px solid var(--bh-border, #E5E7EB);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
.sum-label {
|
||||
font-size: 12px;
|
||||
color: var(--bh-text-secondary, #64748B);
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.sum-value {
|
||||
margin-top: 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--bh-text-primary, #0F172A);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 3px;
|
||||
}
|
||||
.sum-value.muted { font-size: 15px; color: var(--bh-text-muted, #94A3B8); font-weight: 500; }
|
||||
.sum-unit { font-size: 11px; font-weight: 500; color: var(--bh-text-secondary, #64748B); }
|
||||
.sum-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--bh-text-muted, #94A3B8);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mono { font-family: 'IBM Plex Mono', Consolas, 'SFMono-Regular', monospace; font-variant-numeric: tabular-nums; }
|
||||
.lv-green { color: #00C896; }
|
||||
.lv-yellow { color: #F59E0B; }
|
||||
.lv-red { color: #EF4444; }
|
||||
@media (max-width: 1200px) {
|
||||
.map-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user