This commit is contained in:
2026-09-18 15:21:47 +08:00
parent 0d8c34a2c9
commit aa27d35a47
55 changed files with 2842 additions and 471 deletions
+33 -5
View File
@@ -1,16 +1,16 @@
<template>
<div v-if="scorecard?.length">
<h2 class="section-title">Scorecard</h2>
<h2 class="section-title">{{ t('report.scorecard') }}</h2>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3">
<div
v-for="s in scorecard"
v-for="s in enriched"
:key="s.metric"
:class="['border rounded-lg p-4 transition-colors', cardBg(s.rating)]"
>
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-500">{{ s.label }}</span>
<span class="text-xs text-slate-500">{{ s.labelLocalized }}</span>
<span :class="['text-[10px] font-semibold uppercase px-1.5 py-0.5 rounded', ratingColor(s.rating)]">
{{ s.rating }}
{{ s.ratingLocalized }}
</span>
</div>
<div class="flex items-baseline gap-2">
@@ -18,7 +18,7 @@
<span class="text-xs text-slate-500">/100</span>
<span :class="['text-xs ml-auto', trendColor(s.trend)]">
{{ s.trend === 'up' ? '↑' : s.trend === 'down' ? '↓' : '→' }}
<span class="text-slate-500 ml-0.5">from {{ s.start_value }}</span>
<span class="text-slate-500 ml-0.5">{{ _fromLabel() }} {{ s.start_value }}</span>
</span>
</div>
<!-- Sparkline -->
@@ -56,12 +56,40 @@
</template>
<script setup>
import { computed as _computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({
scorecard: { type: Array, default: () => [] },
metricsHistory: { type: Array, default: () => [] },
forecasts: { type: Object, default: () => ({}) },
})
const { t, tm } = useI18n()
// Look up a metric/rating/trend key in the report.* namespace and fall back
// to the raw enum value if the key isn't recognized. Lets the backend keep
// canonical English enum strings while we render localized labels.
function _metricLabel(key) {
const keyMap = { stability: 'report.metricLabels.stability', prosperity: 'report.metricLabels.prosperity',
trust: 'report.metricLabels.trust', freedom: 'report.metricLabels.freedom',
conflict: 'report.metricLabels.conflict', brand_sentiment: 'report.metricLabels.brandSentiment',
purchase_intent: 'report.metricLabels.purchaseIntent', word_of_mouth: 'report.metricLabels.wordOfMouth',
churn_risk: 'report.metricLabels.churnRisk', adoption_rate: 'report.metricLabels.adoptionRate' }
const i18nKey = keyMap[key]
return i18nKey ? t(i18nKey) : key
}
function _ratingLabel(r) { return t(`report.ratings.${r}`, r) }
function _trendLabel(t_) { return t(`report.trends.${t_}`, t_) }
function _fromLabel() { return t('report.from') }
const enriched = _computed(() => props.scorecard.map(s => ({
...s,
labelLocalized: _metricLabel(s.metric),
ratingLocalized: _ratingLabel(s.rating),
trendLocalized: _trendLabel(s.trend),
})))
function getSparkline(metricKey) {
if (!props.metricsHistory?.length) return []
const hist = props.metricsHistory