wip(strategy-map): V1+V3 合版 + v2/v3 口径(完备度/C1 hold/57 归档组)

This commit is contained in:
Hermes CI Fix
2026-10-19 10:33:40 +08:00
parent 0fc2c6109e
commit 4d20c27de4
16 changed files with 2116 additions and 89 deletions
@@ -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>