Files
cma-management/frontend/src/views/KPIList.vue
T

744 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="kpi-dict-page">
<!-- 页头 -->
<div class="page-header">
<h3>KPI字典</h3>
<div class="header-actions">
<el-button @click="showTemplateSelect = true">📋 从模板创建</el-button>
<el-button type="primary" @click="showForm=true; form={}; editMode=false">+ 新建KPI</el-button>
</div>
</div>
<div class="main-layout">
<!-- 左侧BSC分类导航树 -->
<div class="left-tree">
<div class="tree-header">BSC分类导航</div>
<el-tree
ref="treeRef"
:data="categoryTree"
node-key="key"
:props="{ label: 'label', children: 'children' }"
:default-expanded-keys="defaultExpanded"
:show-checkbox="true"
:check-strictly="true"
@check="onTreeCheck"
highlight-current
>
<template #default="{ node, data }">
<span class="tree-node">
<span class="tree-node-label">{{ data.label }}</span>
<el-tag size="small" :type="data.children && data.children.length > 0 ? '' : 'info'" class="tree-count">{{ data.count }}</el-tag>
</span>
</template>
</el-tree>
</div>
<!-- 右侧搜索+表格 -->
<div class="right-content">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="searchKeyword" placeholder="搜索KPI名称/编码" clearable style="width:220px" @clear="loadKpis" @keyup.enter="loadKpis" />
<el-select v-model="searchDimension" placeholder="维度" clearable style="width:110px" @change="loadKpis">
<el-option label="财务" value="finance" />
<el-option label="客户" value="customer" />
<el-option label="内部流程" value="process" />
<el-option label="学习成长" value="learning" />
</el-select>
<el-select v-model="searchCategory" placeholder="二级类别" clearable style="width:140px" @change="loadKpis">
<el-option v-for="c in allCategories" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
<el-button type="primary" @click="loadKpis">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</div>
<!-- 批量操作栏 -->
<div class="batch-bar" v-if="selectedIds.length > 0">
<span class="selected-info">已选 {{ selectedIds.length }} </span>
<el-button size="small" @click="batchDelete">批量删除</el-button>
<el-button size="small" @click="batchExport">导出CSV</el-button>
<el-button size="small" @click="selectedIds=[]">取消选择</el-button>
</div>
<!-- 表格 -->
<el-table
:data="kpis"
v-loading="loading"
style="width:100%"
@selection-change="onSelectionChange"
@row-click="(row) => $router.push('/kpis/' + row.id)"
border
stripe
size="small"
>
<el-table-column type="selection" width="40" />
<el-table-column prop="kpi_code" label="编码" width="120">
<template #default="{ row }">
<span class="kpi-code">{{ row.kpi_code }}</span>
</template>
</el-table-column>
<el-table-column prop="kpi_name" label="名称" min-width="170" />
<el-table-column prop="dimension" label="维度" width="70">
<template #default="{ row }">
<el-tag :type="dimTagType(row.dimension)" size="small">{{ dimLabel(row.dimension) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="二级类别" width="90">
<template #default="{ row }">{{ catLabel(row.category) }}</template>
</el-table-column>
<el-table-column prop="target_value" label="目标值" width="90">
<template #default="{ row }">{{ formatTarget(row.target_value, row.unit) }}</template>
</el-table-column>
<el-table-column prop="frequency" label="频率" width="70">
<template #default="{ row }">{{ freqLabel(row.frequency) }}</template>
</el-table-column>
<el-table-column prop="data_source_type" label="数据源" width="70">
<template #default="{ row }">{{ sourceLabel(row.data_source_type) }}</template>
</el-table-column>
<el-table-column label="所属地图" width="120">
<template #default="{ row }">
<span v-if="row.map_title" style="color:#409eff;font-size:12px;">{{ row.map_title }}</span>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="当前值" width="100">
<template #default="{ row }">
<span v-if="row.actual_value !== null" :class="'status-' + (row.alert_level || 'none')">
{{ row.actual_value }}{{ row.unit }}
</span>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="来源" width="70">
<template #default="{ row }">
<el-tag v-if="row.is_system" size="small" type="info">系统</el-tag>
<el-tag v-else size="small" type="success">自定义</el-tag>
</template>
</el-table-column>
<el-table-column label="预警" width="70">
<template #default="{ row }">
<el-tag v-if="row.alert_level && row.alert_level !== 'none'" :type="row.alert_level === 'red' ? 'danger' : row.alert_level === 'yellow' ? 'warning' : 'success'" size="small">
{{ row.alert_level === 'red' ? '红灯' : row.alert_level === 'yellow' ? '黄灯' : '绿灯' }}
</el-tag>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" text type="primary" @click.stop="$router.push('/kpis/' + row.id)">编辑</el-button>
<el-button size="small" text type="danger" @click.stop="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-bar">
<span class="total-info">共 {{ total }} 条</span>
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="loadKpis"
/>
</div>
</div>
</div>
<!-- 新建/编辑对话框 -->
<MyDialog v-model="showForm" :title="editMode ? '编辑KPI' : '新建KPI'" :width="640">
<el-form :model="form" label-width="110px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="KPI编码"><el-input v-model="form.kpi_code" :disabled="editMode" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="KPI名称"><el-input v-model="form.kpi_name" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="维度">
<el-select v-model="form.dimension" style="width:100%" @change="onDimensionChange">
<el-option label="财务" value="finance" />
<el-option label="客户" value="customer" />
<el-option label="内部流程" value="process" />
<el-option label="学习成长" value="learning" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="二级类别">
<el-select v-model="form.category" style="width:100%" :disabled="!form.dimension">
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="目标值"><el-input-number v-model="form.target_value" :min="0" style="width:100%" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位"><el-input v-model="form.unit" placeholder="%, , , ..." /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="频率">
<el-select v-model="form.frequency" style="width:100%">
<el-option label="月度" value="monthly" />
<el-option label="季度" value="quarterly" />
<el-option label="年度" value="yearly" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据源">
<el-select v-model="form.data_source_type" style="width:100%">
<el-option label="ERP自动" value="erp" />
<el-option label="Excel导入" value="excel" />
<el-option label="手工填报" value="manual" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="负责部门"><el-input v-model="form.responsible_dept" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人"><el-input v-model="form.responsible_user" /></el-form-item>
</el-col>
</el-row>
<el-form-item label="计算公式"><el-input v-model="form.formula" type="textarea" :rows="2" /></el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="绿灯阈值">
<el-input v-model="form.threshold_green" placeholder=" >=80" />
<div class="threshold-hint">超过此值视为正常</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="黄灯阈值">
<el-input v-model="form.threshold_yellow" placeholder=" >=60" />
<div class="threshold-hint">超过此值预警</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="红灯阈值">
<el-input v-model="form.threshold_red" placeholder=" <60" />
<div class="threshold-hint">触发此值紧急</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属Epic">
<el-select v-model="form.epic" style="width:100%">
<el-option v-for="i in 10" :key="i" :label="'Epic ' + i" :value="'Epic' + i" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showForm=false">取消</el-button>
<el-button type="primary" @click="saveKPI">{{ editMode ? '保存' : '创建' }}</el-button>
</template>
</MyDialog>
<!-- 从模板创建弹窗 -->
<el-dialog v-model="showTemplateSelect" title="从模板创建KPI" width="760" :close-on-click-modal="false">
<div v-loading="templateLoading">
<!-- 筛选栏 -->
<div class="search-bar" style="margin-bottom:12px;">
<el-input v-model="templateKeyword" placeholder="搜索模板名称/编码" clearable style="width:200px" />
<el-select v-model="templateDimFilter" placeholder="维度" clearable style="width:110px">
<el-option label="财务" value="finance" />
<el-option label="客户" value="customer" />
<el-option label="内部流程" value="process" />
<el-option label="学习成长" value="learning" />
</el-select>
<el-button @click="loadTemplates">查询</el-button>
</div>
<!-- 模板列表 -->
<el-table :data="templates" style="width:100%" border stripe size="small"
:highlight-current-row="true"
@current-change="onTemplateSelect"
>
<el-table-column type="index" width="40" />
<el-table-column prop="kpi_code" label="编码" width="120">
<template #default="{ row }"><span class="kpi-code">{{ row.kpi_code }}</span></template>
</el-table-column>
<el-table-column prop="kpi_name" label="名称" min-width="140" />
<el-table-column prop="dimension" label="维度" width="70">
<template #default="{ row }"><el-tag :type="dimTagType(row.dimension)" size="small">{{ dimLabel(row.dimension) }}</el-tag></template>
</el-table-column>
<el-table-column label="类别" width="80">
<template #default="{ row }">{{ catLabel(row.category) }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="60" />
<el-table-column prop="target_value" label="建议目标" width="80">
<template #default="{ row }">{{ row.target_value ?? '-' }}</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="120" show-overflow-tooltip />
<el-table-column label="来源" width="60">
<template #default="{ row }">
<el-tag v-if="row.is_system" size="small" type="info">系统</el-tag>
<el-tag v-else size="small" type="success">自定义</el-tag>
</template>
</el-table-column>
</el-table>
<div v-if="!selectedTemplate" style="color:#999;font-size:13px;margin-top:8px;">请选择一个模板然后填写实例化信息</div>
<!-- 实例化表单 -->
<div v-if="selectedTemplate" class="instantiate-form" style="margin-top:12px;border-top:1px solid #eee;padding-top:12px;">
<div style="font-size:14px;font-weight:500;margin-bottom:8px;">{{ selectedTemplate.kpi_name }}创建KPI实例</div>
<el-form :model="instantiateForm" label-width="90px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="KPI编码"><el-input v-model="instantiateForm.kpi_code" :placeholder="selectedTemplate.kpi_code" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="KPI名称"><el-input v-model="instantiateForm.kpi_name" :placeholder="selectedTemplate.kpi_name" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="目标值"><el-input-number v-model="instantiateForm.target_value" :min="0" style="width:100%" :placeholder="String(selectedTemplate.target_value ?? '')" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据源">
<el-select v-model="instantiateForm.data_source_type" style="width:100%">
<el-option label="ERP自动" value="erp" />
<el-option label="Excel导入" value="excel" />
<el-option label="手工填报" value="manual" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="负责部门"><el-input v-model="instantiateForm.responsible_dept" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人"><el-input v-model="instantiateForm.responsible_user" /></el-form-item>
</el-col>
</el-row>
</el-form>
<div style="color:#999;font-size:12px;margin-top:4px;">* 编码和名称不填则使用模板默认值后续可在KPI字典中修改</div>
</div>
</div>
<template #footer>
<el-button @click="showTemplateSelect=false">取消</el-button>
<el-button type="primary" :disabled="!selectedTemplate" @click="doInstantiate">从模板创建</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { kpiApi, templateApi, dashboardApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
// ── 数据 ──
const kpis = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const pageSize = 20
// ── 搜索筛选 ──
const searchKeyword = ref('')
const searchDimension = ref('')
const searchCategory = ref('')
// ── 分类树 ──
const categoryTree = ref<any[]>([])
const defaultExpanded = ref<string[]>([])
const treeRef = ref<any>(null)
const treeFilterDims = ref<string[]>([])
const treeFilterCats = ref<string[]>([])
// ── 多选 ──
const selectedIds = ref<number[]>([])
// ── 新建/编辑表单 ──
const showForm = ref(false)
const editMode = ref(false)
const form = ref<any>({})
// ── 维度映射 ──
const DIM_MAP: Record<string, string> = { finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长' }
const CAT_MAP: Record<string, string> = {
revenue_growth: '收入增长', profitability: '盈利水平', cost_control: '成本费用',
asset_efficiency: '资产效率', cash_risk: '现金流风控',
customer_scale: '客户规模', customer_concentration: '客户集中度', customer_satisfaction: '客户满意',
supply_chain: '供应链效率', delivery_quality: '交付质量',
talent_pipeline: '人才梯队', employee_engagement: '员工敬业', innovation: '创新改善',
}
const FREQ_MAP: Record<string, string> = { daily: '日', weekly: '周', monthly: '月', quarterly: '季', yearly: '年' }
const SOURCE_MAP: Record<string, string> = { erp: 'ERP', excel: 'Excel', manual: '手工' }
// 维度→二级类别 对照映射
const DIM_CAT_MAP: Record<string, string[]> = {
finance: ['revenue_growth', 'profitability', 'cost_control', 'asset_efficiency', 'cash_risk'],
customer: ['customer_scale', 'customer_concentration', 'customer_satisfaction'],
process: ['supply_chain', 'delivery_quality'],
learning: ['talent_pipeline', 'employee_engagement', 'innovation'],
}
// 二级类别选项(联动维度)
const categoryOptions = computed(() => {
const dim = form.value?.dimension
if (!dim) return []
const catKeys = DIM_CAT_MAP[dim] || []
return catKeys.map(key => ({ value: key, label: CAT_MAP[key] || key }))
})
// 所有二级类别(搜索栏用)
const allCategories = computed(() => {
const seen = new Set<string>()
const result: { value: string; label: string }[] = []
for (const key in DIM_CAT_MAP) {
for (const cat of DIM_CAT_MAP[key]) {
if (!seen.has(cat)) {
seen.add(cat)
result.push({ value: cat, label: CAT_MAP[cat] || cat })
}
}
}
return result
})
function dimLabel(d: string) { return DIM_MAP[d] || d }
function catLabel(c: string) { return CAT_MAP[c] || c }
function freqLabel(f: string) { return FREQ_MAP[f] || f }
function sourceLabel(s: string) { return SOURCE_MAP[s] || s }
function dimTagType(d: string) { return ({ finance: '', customer: 'success', process: 'warning', learning: 'info' } as any)[d] || '' }
function formatTarget(val: number, unit: string) {
if (val === null || val === undefined) return '-'
if (val >= 10000) return (val / 10000).toFixed(0) + '万' + (unit || '')
return val + (unit || '')
}
// ── 加载分类树 ──
async function loadCategories() {
try {
const r: any = await kpiApi.listCategories()
categoryTree.value = [
{ key: '_all', label: '全部', count: r.total, children: r.tree || [] }
]
defaultExpanded.value = ['_all', ...(r.tree || []).map((n: any) => n.key)]
} catch (e) {
console.error('加载分类树失败', e)
}
}
// ── 树节点勾选(支持多选)──
function onTreeCheck(data: any, { checkedKeys }: any) {
// 从checkedKeys中分离出维度和分类
const dimKeys = ['finance', 'customer', 'process', 'learning']
const checkedDims = checkedKeys.filter((k: string) => dimKeys.includes(k))
const checkedCats = checkedKeys.filter((k: string) => !dimKeys.includes(k) && k !== '_all')
if (data.key === '_all' || checkedKeys.includes('_all')) {
// 勾选全部=清空过滤
treeFilterDims.value = []
treeFilterCats.value = []
} else {
treeFilterDims.value = checkedDims
treeFilterCats.value = checkedCats
}
loadKpis()
}
// ── 加载KPI列表 ──
async function loadKpis() {
loading.value = true
try {
const params: any = { page: page.value, page_size: pageSize }
if (searchKeyword.value) params.keyword = searchKeyword.value
if (searchDimension.value) params.dimension = searchDimension.value
if (searchCategory.value) params.category = searchCategory.value
if (treeFilterDims.value.length > 0 && !params.dimension) params.dimension = treeFilterDims.value.join(',')
if (treeFilterCats.value.length > 0 && !params.category) params.category = treeFilterCats.value.join(',')
const r: any = await kpiApi.list(params)
kpis.value = r.data || []
total.value = r.total || 0
} catch (e) {
console.error('加载KPI失败', e)
}
loading.value = false
}
// 获取当前值
async function loadActualValues() {
try {
const r: any = await dashboardApi.kpis({ role: 'ceo', period: 'month' })
const vals = r.data || []
const valMap: Record<number, any> = {}
for (const v of vals) {
valMap[v.id] = v
}
for (const kpi of kpis.value) {
if (valMap[kpi.id]) {
kpi.actual_value = valMap[kpi.id].actual_value
kpi.alert_level = valMap[kpi.id].alert_level
}
}
} catch (e) {
// 静默失败
}
}
function resetSearch() {
searchKeyword.value = ''
searchDimension.value = ''
searchCategory.value = ''
treeFilterDims.value = []
treeFilterCats.value = []
if (treeRef.value) treeRef.value.setCheckedKeys([])
page.value = 1
loadKpis()
}
// ── 多选 ──
function onSelectionChange(rows: any[]) {
selectedIds.value = rows.map((r: any) => r.id)
}
function batchDelete() {
ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.length} 条KPI`, '提示').then(async () => {
for (const id of selectedIds.value) {
try { await kpiApi.delete(id) } catch (e) {}
}
ElMessage.success('批量删除完成')
selectedIds.value = []
loadKpis()
loadCategories()
}).catch(() => {})
}
function batchExport() {
const header = '编码,名称,维度,二级类别,目标值,频率,数据源,负责人,计算公式'
const rows = kpis.value.map((k: any) =>
[k.kpi_code, k.kpi_name, dimLabel(k.dimension), catLabel(k.category), k.target_value, freqLabel(k.frequency), sourceLabel(k.data_source_type), k.responsible_user, (k.formula || '')].join(',')
)
const bom = '\uFEFF' // Excel UTF-8 BOM
const blob = new Blob([bom + header + '\n' + rows.join('\n')], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = 'KPI字典_' + new Date().toISOString().slice(0, 10) + '.csv'
a.click()
URL.revokeObjectURL(url)
}
// ── 新建/保存 ──
function onDimensionChange(val: string) {
form.value.category = ''
}
async function saveKPI() {
try {
if (editMode.value) {
await kpiApi.update(form.value.id, form.value)
ElMessage.success('保存成功')
} else {
await kpiApi.create(form.value)
ElMessage.success('创建成功')
}
showForm.value = false
loadKpis()
loadCategories()
} catch (e) {
ElMessage.error(editMode.value ? '保存失败' : '创建失败')
}
}
function doDelete(id: number) {
ElMessageBox.confirm('确定删除此KPI', '提示').then(async () => {
try { await kpiApi.delete(id); ElMessage.success('已删除'); loadKpis(); loadCategories() }
catch (e: any) { ElMessage.error(e?.response?.data?.detail || '删除失败') }
}).catch(() => {})
}
// ── 从模板创建 ──
const showTemplateSelect = ref(false)
const templateLoading = ref(false)
const templates = ref<any[]>([])
const templateKeyword = ref('')
const templateDimFilter = ref('')
const selectedTemplate = ref<any>(null)
const instantiateForm = ref<any>({})
async function loadTemplates() {
templateLoading.value = true
try {
const params: any = {}
if (templateKeyword.value) params.keyword = templateKeyword.value
if (templateDimFilter.value) params.dimension = templateDimFilter.value
const r: any = await templateApi.list(params)
templates.value = r.data || []
} catch (e) {
console.error('加载模板失败', e)
}
templateLoading.value = false
}
function onTemplateSelect(tpl: any) {
selectedTemplate.value = tpl
instantiateForm.value = {
kpi_code: '',
kpi_name: '',
target_value: undefined,
data_source_type: 'manual',
responsible_dept: '',
responsible_user: '',
}
}
async function doInstantiate() {
if (!selectedTemplate.value) return
try {
const data: any = {}
if (instantiateForm.value.kpi_code) data.kpi_code = instantiateForm.value.kpi_code
if (instantiateForm.value.kpi_name) data.kpi_name = instantiateForm.value.kpi_name
if (instantiateForm.value.target_value !== undefined && instantiateForm.value.target_value !== null)
data.target_value = instantiateForm.value.target_value
if (instantiateForm.value.data_source_type) data.data_source_type = instantiateForm.value.data_source_type
if (instantiateForm.value.responsible_dept) data.responsible_dept = instantiateForm.value.responsible_dept
if (instantiateForm.value.responsible_user) data.responsible_user = instantiateForm.value.responsible_user
await templateApi.instantiate(selectedTemplate.value.id, data)
ElMessage.success(`已从模板「${selectedTemplate.value.kpi_name}」创建KPI`)
showTemplateSelect.value = false
selectedTemplate.value = null
instantiateForm.value = {}
loadKpis()
loadCategories()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '创建失败')
}
}
onMounted(() => {
loadCategories()
loadKpis()
})
</script>
<style scoped>
.kpi-dict-page {
height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.page-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 8px;
}
.main-layout {
display: flex;
gap: 12px;
flex: 1;
overflow: hidden;
}
.left-tree {
width: 260px;
min-width: 260px;
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
padding: 8px 0;
overflow-y: auto;
}
.tree-header {
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 4px;
}
.tree-node {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
padding-right: 8px;
}
.tree-node-label {
font-size: 13px;
}
.tree-count {
font-size: 11px;
min-width: 20px;
text-align: center;
}
.right-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
align-items: center;
flex-wrap: wrap;
}
.batch-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #ecf5ff;
border-radius: 4px;
margin-bottom: 8px;
}
.selected-info {
font-size: 13px;
color: #409eff;
font-weight: 500;
}
.kpi-code {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #606266;
}
.threshold-hint {
font-size: 11px;
color: #909399;
line-height: 1.4;
margin-top: 2px;
}
.status-red { color: #f56c6c; font-weight: 500; }
.status-yellow { color: #e6a23c; font-weight: 500; }
.status-green { color: #67c23a; font-weight: 500; }
.status-none { color: #606266; }
.pagination-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
.total-info {
font-size: 13px;
color: #909399;
}
:deep(.el-tree-node__content) {
height: 32px;
}
</style>