feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限

This commit is contained in:
Hermes CI Fix
2026-07-12 17:46:08 +08:00
parent cdf00efd69
commit ee25d5fa1d
8871 changed files with 1778433 additions and 0 deletions
+738
View File
@@ -0,0 +1,738 @@
<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="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) {
// 简单模式:只支持单选过滤
if (data.key === '_all') {
// 勾选全部=清空过滤
treeFilterDims.value = []
treeFilterCats.value = []
} else if (data.children) {
// 维度节点
treeFilterDims.value = checkedKeys.includes(data.key) ? [data.key] : []
treeFilterCats.value = []
} else {
// 类别节点
treeFilterCats.value = checkedKeys.includes(data.key) ? [data.key] : []
treeFilterDims.value = []
}
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[0]
if (treeFilterCats.value.length > 0 && !params.category) params.category = treeFilterCats.value[0]
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>