@@ -0,0 +1,298 @@
< template >
< div class = "bot-kpi-page" >
<!-- 页面Header -- >
< div class = "bot-header" >
< div class = "header-left" >
< h3 > 🤖 财务Bot KPI看板 < / h3 >
< el-tag type = "warning" size = "small" effect = "plain" > 2026 年7月 < / el-tag >
< / div >
< div class = "header-right" >
< el-button size = "small" @click ="refresh" :loading = "loading" > 刷新 < / el-button >
< / div >
< / div >
<!-- 综合得分 -- >
< div class = "overall-card" >
< div class = "overall-score" >
< div class = "score-ring" :class = "overallStatus" >
< span class = "score-num" > { { overallScore != null ? overallScore : '-' } } < / span >
< span class = "score-label" > 综合得分 < / span >
< / div >
< div class = "overall-info" >
< div class = "info-title" > 🏆 财务Bot · 2026 年7月 < / div >
< div class = "info-desc" > 基于11项KPI的五档评分引擎 , 满分5 .0 < / div >
< div class = "info-status" >
< el-tag v-if = "overallScore != null" :type="overallStatus === 'success' ? 'success' : overallStatus === 'warning' ? 'warning' : 'danger'" size="small" >
{{ overallScore > = 4 ? '表现优秀' : overallScore >= 3 ? '需要关注' : '亟需改善' } }
< / el-tag >
< / div >
< / div >
< / div >
< / div >
<!-- 三区 : 核心产出 / 质量监控 / 用户反馈 -- >
< div class = "kpi-groups" >
<!-- 核心产出 -- >
< div class = "group-section" >
< div class = "group-title" >
< el-icon :size = "20" > < DataBoard / > < / el-icon >
< span > 核心产出 KPI ( 月度 ) < / span >
< el-tag size = "small" type = "primary" > { { coreKpis . length } } < / el-tag >
< / div >
< div class = "kpi-card-grid" >
< div v-for = "k in coreKpis" :key="k.id" class="kpi-card" :class="'status-' + k.status" >
< div class = "card-head" >
< span class = "card-name" > { { k . kpi _name } } < / span >
< el-tag size = "small" : type = "k.status === 'success' ? 'success' : k.status === 'warning' ? 'warning' : 'danger'" effect = "plain" >
{ { k . score != null ? k . score + '分' : '--' } }
< / el-tag >
< / div >
< div class = "card-values" >
< div class = "cv-row" >
< span class = "cv-label" > 目标 < / span >
< span class = "cv-val target" > { { formatVal ( k . target _value , k . unit ) } } < / span >
< / div >
< div class = "cv-row" >
< span class = "cv-label" > 当前 < / span >
< span class = "cv-val actual" :class = "k.status" > { { k . current _value != null ? formatVal ( k . current _value , k . unit ) : '待统计' } } < / span >
< / div >
< / div >
<!-- 进度条 -- >
< div class = "progress-bar-wrap" v-if = "k.target_value && k.current_value != null" >
< div class = "progress-fill" :class = "k.status"
: style = "{ width: calcProgress(k.current_value, k.target_value, k.kpi_code) + '%' }" >
< / div >
< / div >
< div class = "card-weight" >
< span > 权重 { { k . weight } } % < / span >
< span class = "formula-hint" :title = "k.formula" > 📐 < / span >
< / div >
< / div >
< / div >
< / div >
<!-- 质量监控 -- >
< div class = "group-section" >
< div class = "group-title" >
< el-icon :size = "20" > < WarningFilled / > < / el-icon >
< span > 质量监控 KPI ( 月度 ) < / span >
< el-tag size = "small" type = "primary" > { { qualityKpis . length } } < / el-tag >
< / div >
< div class = "kpi-card-grid" >
< div v-for = "k in qualityKpis" :key="k.id" class="kpi-card" :class="'status-' + k.status" >
< div class = "card-head" >
< span class = "card-name" > { { k . kpi _name } } < / span >
< el-tag size = "small" : type = "k.status === 'success' ? 'success' : k.status === 'warning' ? 'warning' : 'danger'" effect = "plain" >
{ { k . score != null ? k . score + '分' : '--' } }
< / el-tag >
< / div >
< div class = "card-values" >
< div class = "cv-row" >
< span class = "cv-label" > 目标 < / span >
< span class = "cv-val target" > { { formatVal ( k . target _value , k . unit ) } } < / span >
< / div >
< div class = "cv-row" >
< span class = "cv-label" > 当前 < / span >
< span class = "cv-val actual" :class = "k.status" > { { k . current _value != null ? formatVal ( k . current _value , k . unit ) : '待统计' } } < / span >
< / div >
< / div >
< div class = "progress-bar-wrap" v-if = "k.target_value && k.current_value != null" >
< div class = "progress-fill" :class = "k.status"
: style = "{ width: calcProgress(k.current_value, k.target_value, k.kpi_code) + '%' }" >
< / div >
< / div >
< div class = "card-weight" >
< span > 权重 { { k . weight } } % < / span >
< / div >
< / div >
< / div >
< / div >
<!-- 用户反馈 -- >
< div class = "group-section" >
< div class = "group-title" >
< el-icon :size = "20" > < User / > < / el-icon >
< span > 用户反馈 KPI ( 季度 ) < / span >
< el-tag size = "small" type = "primary" > { { feedbackKpis . length } } < / el-tag >
< / div >
< div class = "kpi-card-grid" >
< div v-for = "k in feedbackKpis" :key="k.id" class="kpi-card" :class="'status-' + k.status" >
< div class = "card-head" >
< span class = "card-name" > { { k . kpi _name } } < / span >
< el-tag size = "small" : type = "k.status === 'success' ? 'success' : k.status === 'warning' ? 'warning' : 'danger'" effect = "plain" >
{ { k . score != null ? k . score + '分' : '--' } }
< / el-tag >
< / div >
< div class = "card-values" >
< div class = "cv-row" >
< span class = "cv-label" > 目标 < / span >
< span class = "cv-val target" > { { formatVal ( k . target _value , k . unit ) } } < / span >
< / div >
< div class = "cv-row" >
< span class = "cv-label" > 当前 < / span >
< span class = "cv-val actual" :class = "k.status" > { { k . current _value != null ? formatVal ( k . current _value , k . unit ) : '待统计' } } < / span >
< / div >
< / div >
< div class = "progress-bar-wrap" v-if = "k.target_value && k.current_value != null" >
< div class = "progress-fill" :class = "k.status"
: style = "{ width: calcProgress(k.current_value, k.target_value, k.kpi_code) + '%' }" >
< / div >
< / div >
< div class = "card-weight" >
< span > 权重 { { k . weight } } % < / span >
< el-tag size = "small" type = "info" effect = "plain" > 季度 < / el-tag >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 预警 / 待改进 -- >
< div class = "bottom-summary" >
< el-alert
:title = "alertsInfo"
: type = "hasAlerts ? 'warning' : 'success'"
: description = "hasAlerts ? '以下KPI当前值为空或未达标:' + alertKpiNames : '所有KPI暂无预警'"
show -icon
:closable = "false"
/ >
< / div >
< / div >
< / template >
< script setup lang = "ts" >
import { ref , computed , onMounted } from 'vue'
import { botKpiApi } from '../api/index'
import { DataBoard , WarningFilled , User } from '@element-plus/icons-vue'
const loading = ref ( false )
const source = 'finance-bot'
const kpis = ref < any [ ] > ( [ ] )
const groups = ref < any > ( { } )
const overallScore = ref < number | null > ( null )
const overallStatus = ref ( 'info' )
// 分区计算
const coreKpis = computed ( ( ) => groups . value ? . core _output ? . kpis || [ ] )
const qualityKpis = computed ( ( ) => groups . value ? . quality ? . kpis || [ ] )
const feedbackKpis = computed ( ( ) => groups . value ? . user _feedback ? . kpis || [ ] )
// 预警信息
const hasAlerts = computed ( ( ) => {
return kpis . value . some ( k => k . current _value == null || ( k . score != null && k . score < 3 ) )
} )
const alertKpiNames = computed ( ( ) => {
return kpis . value
. filter ( k => k . current _value == null || ( k . score != null && k . score < 3 ) )
. map ( k => k . kpi _name )
. join ( '、' )
} )
const alertsInfo = computed ( ( ) => {
const cnt = kpis . value . filter ( k => k . current _value == null || ( k . score != null && k . score < 3 ) ) . length
return cnt > 0 ? ` 🚨 ${ cnt } 个KPI需要关注 ` : '✅ 一切正常'
} )
function formatVal ( val : any , unit : string ) {
if ( val == null ) return '-'
if ( unit === '%' ) return val + '%'
if ( unit === '分钟' ) return val + '分钟'
if ( unit === '分' ) return val . toFixed ( 1 ) + '分'
if ( unit === '次' ) return val + '次/季'
return val + ( unit || '' )
}
function calcProgress ( current : number , target : number , code : string ) {
if ( ! target || target === 0 ) return 0
const rate = current / target * 100
// 反向指标:响应时效
if ( code === 'FB_RESPONSE_TIME' ) {
return Math . min ( 100 , ( target / current ) * 100 )
}
return Math . min ( 100 , rate )
}
async function refresh ( ) {
loading . value = true
try {
const res : any = await botKpiApi . list ( { source } )
kpis . value = res . kpis || [ ]
groups . value = res . groups || { }
overallScore . value = res . overall ? . score ? ? null
overallStatus . value = res . overall ? . status || 'info'
} catch ( e : any ) {
console . error ( '加载Bot KPI失败' , e )
} finally {
loading . value = false
}
}
onMounted ( refresh )
< / script >
< style scoped >
. bot - kpi - page { padding : 0 ; max - width : 1200 px ; margin : 0 auto ; }
. bot - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 20 px ; }
. header - left { display : flex ; align - items : center ; gap : 12 px ; }
. header - left h3 { margin : 0 ; font - size : 20 px ; color : # 303133 ; }
. overall - card {
background : linear - gradient ( 135 deg , # 304156 0 % , # 43627 f 100 % ) ;
border - radius : 12 px ; padding : 24 px ; margin - bottom : 24 px ;
color : # fff ;
}
. overall - score { display : flex ; align - items : center ; gap : 24 px ; }
. score - ring {
width : 100 px ; height : 100 px ; border - radius : 50 % ;
display : flex ; flex - direction : column ; align - items : center ; justify - content : center ;
background : rgba ( 255 , 255 , 255 , 0.15 ) ; border : 3 px solid rgba ( 255 , 255 , 255 , 0.3 ) ;
}
. score - ring . success { border - color : # 67 c23a ; }
. score - ring . warning { border - color : # e6a23c ; }
. score - ring . danger { border - color : # f56c6c ; }
. score - num { font - size : 32 px ; font - weight : bold ; line - height : 1 ; }
. score - label { font - size : 12 px ; opacity : 0.8 ; }
. overall - info { flex : 1 ; }
. info - title { font - size : 18 px ; font - weight : bold ; margin - bottom : 4 px ; }
. info - desc { font - size : 13 px ; opacity : 0.7 ; margin - bottom : 8 px ; }
. group - section { margin - bottom : 28 px ; }
. group - title {
display : flex ; align - items : center ; gap : 8 px ;
font - size : 16 px ; font - weight : 600 ; color : # 303133 ;
margin - bottom : 14 px ; padding - bottom : 8 px ;
border - bottom : 2 px solid # e6e6e6 ;
}
. kpi - card - grid { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 280 px , 1 fr ) ) ; gap : 14 px ; }
. kpi - card {
background : # fff ; border - radius : 10 px ; padding : 16 px ;
box - shadow : 0 1 px 4 px rgba ( 0 , 0 , 0 , 0.06 ) ;
border - left : 4 px solid # e6e6e6 ;
transition : all 0.2 s ;
}
. kpi - card : hover { box - shadow : 0 2 px 12 px rgba ( 0 , 0 , 0 , 0.1 ) ; transform : translateY ( - 1 px ) ; }
. kpi - card . status - success { border - left - color : # 67 c23a ; }
. kpi - card . status - warning { border - left - color : # e6a23c ; }
. kpi - card . status - danger { border - left - color : # f56c6c ; }
. kpi - card . status - info { border - left - color : # 909399 ; }
. card - head { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 12 px ; }
. card - name { font - size : 14 px ; font - weight : 600 ; color : # 303133 ; }
. card - values { margin - bottom : 10 px ; }
. cv - row { display : flex ; justify - content : space - between ; align - items : center ; padding : 3 px 0 ; font - size : 13 px ; }
. cv - label { color : # 909399 ; }
. cv - val . target { font - weight : 600 ; color : # 409 EFF ; }
. cv - val . actual { font - weight : 600 ; color : # 303133 ; }
. cv - val . actual . success { color : # 67 c23a ; }
. cv - val . actual . warning { color : # e6a23c ; }
. cv - val . actual . danger { color : # f56c6c ; }
. progress - bar - wrap {
height : 6 px ; background : # ebeef5 ; border - radius : 3 px ;
margin : 8 px 0 ; overflow : hidden ;
}
. progress - fill { height : 100 % ; border - radius : 3 px ; transition : width 0.5 s ; }
. progress - fill . success { background : # 67 c23a ; }
. progress - fill . warning { background : # e6a23c ; }
. progress - fill . danger { background : # f56c6c ; }
. progress - fill . info { background : # 909399 ; }
. card - weight { display : flex ; justify - content : space - between ; align - items : center ; font - size : 12 px ; color : # 909399 ; }
. formula - hint { cursor : help ; }
. bottom - summary { margin - top : 20 px ; }
< / style >