- DataQuality / ProductMatrix / AlertList / ActionPlanLibrary / views.GrowthQuality / PredictAccuracy: drop entity el-select (including the "all entities" option), show read-only session entity tag (not clickable), same style as the top bar tag - drop page-level entity param from those requests; backend resolves entity from the session token (assert_entity_scope / get_entity_id) - utils/entity.ts: add sessionEntityName() (cma_entity_name else id map) - P3: ReportCenter header emoji removed; components/GrowthQuality detail drill-down el-dialog -> MyDialog (cma-entity-selectors-converge-6pages-20260914)
239 lines
8.0 KiB
Vue
239 lines
8.0 KiB
Vue
<template>
|
||
<div class="growth-quality-page">
|
||
<div class="page-header">
|
||
<h2>📈 增长质量诊断</h2>
|
||
<div class="header-right">
|
||
<!-- 企业只读标识(不可点):账套由登录窗口选定,页内无切换入口 -->
|
||
<el-tag size="small" type="primary" effect="plain">{{ entityLabel }}</el-tag>
|
||
<el-select v-model="period" size="small" style="width:140px" @change="loadData" :loading="loadingPeriods">
|
||
<el-option v-for="p in periods" :key="p" :value="p" :label="p" />
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
|
||
<el-row :gutter="16" v-loading="loading">
|
||
<!-- 总体评分 -->
|
||
<el-col :span="8">
|
||
<el-card class="score-card" shadow="never">
|
||
<div class="overall-score" :style="{ color: overallColor }">
|
||
{{ overall != null ? overall.toFixed(1) : '-' }}
|
||
</div>
|
||
<div class="overall-label">综合增长质量评分</div>
|
||
<el-tag :type="overallTag" size="small" style="margin-top:8px">{{ level }}</el-tag>
|
||
<div class="diagnosis-text">{{ diagnosis }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 雷达图 -->
|
||
<el-col :span="8">
|
||
<el-card shadow="never" header="五维雷达图">
|
||
<div ref="radarRef" style="height:300px"></div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 跨期对比 -->
|
||
<el-col :span="8">
|
||
<el-card shadow="never" header="跨期对比">
|
||
<div ref="compareRef" style="height:300px"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 五维明细 -->
|
||
<el-row :gutter="16" style="margin-top:16px">
|
||
<el-col :span="24">
|
||
<el-card shadow="never">
|
||
<template #header>五维明细</template>
|
||
<el-table :data="dimensionRows" size="small">
|
||
<el-table-column prop="name" label="维度" width="140" />
|
||
<el-table-column prop="score" label="评分" width="90">
|
||
<template #default="{ row }">
|
||
<span :style="{ color: row.color, fontWeight: 600 }">{{ row.score }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="level" label="等级" width="80" />
|
||
<el-table-column label="关键指标" min-width="300">
|
||
<template #default="{ row }">
|
||
<el-tag v-for="ind in row.indicators" :key="ind.label" size="small" style="margin:2px" :type="ind.status || 'info'">
|
||
{{ ind.label }}: {{ ind.value }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="suggestion" label="建议" min-width="220" />
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 趋势 -->
|
||
<el-row style="margin-top:16px">
|
||
<el-col :span="24">
|
||
<el-card shadow="never" header="12个月趋势">
|
||
<div ref="trendRef" style="height:260px"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||
import * as echarts from 'echarts'
|
||
import api from '@/api'
|
||
import { sessionEntityName } from '@/utils/entity'
|
||
|
||
// 企业只读标识(账套由登录窗口选定,页内无切换入口)
|
||
const entityLabel = ref(sessionEntityName())
|
||
const period = ref('2026-06')
|
||
const periods = ref<string[]>([])
|
||
const loading = ref(false)
|
||
const loadingPeriods = ref(false)
|
||
const overall = ref<number | null>(null)
|
||
const level = ref('')
|
||
const levelType = ref('info')
|
||
const diagnosis = ref('')
|
||
const dimensions = ref<any>({})
|
||
const comparison = ref<any>({})
|
||
const trend = ref<any[]>([])
|
||
|
||
const radarRef = ref<HTMLElement>()
|
||
const compareRef = ref<HTMLElement>()
|
||
const trendRef = ref<HTMLElement>()
|
||
|
||
const overallTag = computed(() => levelType.value as any)
|
||
const overallColor = computed(() => {
|
||
if (levelType.value === 'success') return '#67c23a'
|
||
if (levelType.value === 'warning') return '#e6a23c'
|
||
return '#f56c6c'
|
||
})
|
||
|
||
const dimensionRows = computed(() => {
|
||
const meta: Record<string, { name: string }> = {
|
||
revenueGrowth: { name: '营收增长' },
|
||
profitQuality: { name: '利润质量' },
|
||
cashQuality: { name: '现金质量' },
|
||
growthEfficiency: { name: '增长效率' },
|
||
orgHealth: { name: '组织健康' },
|
||
}
|
||
return Object.entries(dimensions.value || {}).map(([key, v]: any) => ({
|
||
key,
|
||
name: meta[key]?.name || key,
|
||
score: v?.score ?? 0,
|
||
level: v?.level || (v?.score >= 80 ? '好' : v?.score >= 60 ? '中' : '差'),
|
||
color: v?.score >= 80 ? '#67c23a' : v?.score >= 60 ? '#e6a23c' : '#f56c6c',
|
||
indicators: v?.indicators || [],
|
||
suggestion: v?.suggestion || '-',
|
||
}))
|
||
})
|
||
|
||
async function loadPeriods() {
|
||
loadingPeriods.value = true
|
||
try {
|
||
const r: any = await api.get('/growth-quality/periods')
|
||
periods.value = r.data || r || []
|
||
if (periods.value.length && !periods.value.includes(period.value)) {
|
||
period.value = periods.value[periods.value.length - 1]
|
||
}
|
||
} catch (_) {}
|
||
loadingPeriods.value = false
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const r: any = await api.get('/growth-quality/diagnosis', { params: { period: period.value } })
|
||
const d = r.data || r || {}
|
||
overall.value = d.overall ?? null
|
||
level.value = d.level || ''
|
||
levelType.value = d.level_type || 'info'
|
||
diagnosis.value = d.diagnosis || ''
|
||
dimensions.value = d.dimensions || {}
|
||
comparison.value = d.comparison || {}
|
||
trend.value = d.trend || []
|
||
await nextTick()
|
||
renderCharts()
|
||
} catch (e: any) {
|
||
console.error('load growth quality failed', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function renderCharts() {
|
||
// 雷达图
|
||
if (radarRef.value) {
|
||
const chart = echarts.init(radarRef.value)
|
||
const dims = dimensionRows.value
|
||
chart.setOption({
|
||
radar: {
|
||
indicator: dims.map((d: any) => ({ name: d.name, max: 100 })),
|
||
radius: '65%',
|
||
},
|
||
series: [{
|
||
type: 'radar',
|
||
data: [{
|
||
value: dims.map((d: any) => d.score),
|
||
name: period.value,
|
||
areaStyle: { color: 'rgba(64,158,255,0.2)' },
|
||
lineStyle: { color: '#409EFF' },
|
||
itemStyle: { color: '#409EFF' },
|
||
}],
|
||
}],
|
||
})
|
||
}
|
||
// 跨期对比
|
||
if (compareRef.value) {
|
||
const chart = echarts.init(compareRef.value)
|
||
const dims = dimensionRows.value
|
||
const cur = comparison.value?.current || {}
|
||
const prev = comparison.value?.previous || {}
|
||
chart.setOption({
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['本期', '上期'], bottom: 0 },
|
||
radar: {
|
||
indicator: dims.map((d: any) => ({ name: d.name, max: 100 })),
|
||
radius: '60%',
|
||
},
|
||
series: [{
|
||
type: 'radar',
|
||
data: [
|
||
{ value: dims.map((d: any) => cur[d.key]?.score ?? d.score), name: '本期', areaStyle: { opacity: 0.2 } },
|
||
{ value: dims.map((d: any) => prev[d.key]?.score ?? 0), name: '上期' },
|
||
],
|
||
}],
|
||
})
|
||
}
|
||
// 趋势
|
||
if (trendRef.value) {
|
||
const chart = echarts.init(trendRef.value)
|
||
chart.setOption({
|
||
tooltip: { trigger: 'axis' },
|
||
xAxis: { type: 'category', data: trend.value.map((t: any) => t.period) },
|
||
yAxis: { type: 'value', min: 0, max: 100 },
|
||
series: [{
|
||
type: 'line',
|
||
data: trend.value.map((t: any) => t.overall),
|
||
smooth: true,
|
||
areaStyle: { opacity: 0.15 },
|
||
itemStyle: { color: '#409EFF' },
|
||
}],
|
||
})
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadPeriods()
|
||
await loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.growth-quality-page { max-width: 1400px; margin: 0 auto; padding: 16px; }
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||
.header-right { display: flex; gap: 8px; }
|
||
.score-card { text-align: center; min-height: 300px; }
|
||
.overall-score { font-size: 56px; font-weight: 700; margin: 20px 0 4px; }
|
||
.overall-label { font-size: 14px; color: #888; }
|
||
.diagnosis-text { font-size: 13px; color: #666; text-align: left; margin-top: 16px; line-height: 1.6; padding: 8px; background: #f8f8f8; border-radius: 6px; }
|
||
</style>
|