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

612 lines
26 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 style="padding:0 16px;">
<!-- 顶部tabs -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div>
<el-radio-group v-model="activeTab" size="small">
<el-radio-button value="plans">行动方案</el-radio-button>
<el-radio-button value="coso">COSO内控自检</el-radio-button>
</el-radio-group>
</div>
<div v-if="activeTab === 'plans'" style="display:flex;gap:8px;align-items:center;">
<el-input v-model="searchKeyword" placeholder="搜索标题/KPI名称/编码" clearable style="width:240px;" @clear="loadData" @keyup.enter="loadData" />
<el-button type="primary" @click="openCreate">+ 新建行动方案</el-button>
</div>
</div>
<!-- ===== 行动方案列表 ===== -->
<template v-if="activeTab === 'plans'">
<!-- 状态统计卡片 -->
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;">
<div v-for="card in statCards" :key="card.key"
:class="['stat-card', { active: filterStatus === card.key }]"
@click="filterStatus = card.key; page = 1; loadData()"
style="cursor:pointer;min-width:100px;padding:14px 20px;border-radius:8px;border:2px solid transparent;transition:all .2s;background:#f5f7fa;text-align:center;">
<div style="font-size:12px;color:#909399;">{{ card.label }}</div>
<div style="font-size:24px;font-weight:600;margin-top:4px;" :style="{ color: card.color }">{{ stats[card.key] ?? '-' }}</div>
</div>
</div>
<!-- 筛选栏 -->
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
<el-select v-model="filterDimension" placeholder="维度" clearable style="width:100px;" @change="page=1;loadData()">
<el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-select v-model="filterKpi" placeholder="关联KPI" clearable filterable style="width:180px;" @change="page=1;loadData()">
<el-option v-for="k in kpiOptions" :key="k.id" :label="`${k.kpi_code} ${k.kpi_name}`" :value="k.id" />
</el-select>
<el-select v-model="filterAssignee" placeholder="负责人" clearable filterable style="width:120px;" @change="page=1;loadData()">
<el-option v-for="u in userOptions" :key="u" :label="u" :value="u" />
</el-select>
<el-select v-model="filterPriority" placeholder="优先级" clearable style="width:100px;" @change="page=1;loadData()">
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
<el-button v-if="hasFilter" size="small" @click="clearFilter">清除筛选</el-button>
</div>
<!-- 列表 -->
<el-table :data="list" v-loading="loading" border stripe size="small" style="width:100%;" @selection-change="(sel: any[]) => selectedRows = sel">
<el-table-column type="selection" width="40" />
<el-table-column label="行动方案" min-width="200">
<template #default="{ row }">
<div>
<span style="font-weight:500;cursor:pointer;color:#409eff;" @click="viewDetail(row)">{{ row.title }}</span>
<div v-if="row.target_value" style="font-size:11px;color:#67c23a;margin-top:2px;display:flex;align-items:center;gap:4px;">
<span style="font-size:10px;border:1px solid #67c23a;border-radius:2px;padding:0 3px;line-height:14px;">S</span>
{{ row.target_value }}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="关联KPI" width="180">
<template #default="{ row }">
<div style="font-size:12px;line-height:1.4;">
<span style="color:#666;">{{ row.kpi_code }}</span><br>
<span>{{ row.kpi_name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="维度" width="80">
<template #default="{ row }">
<el-tag size="small" :type="dimTagType(row.kpi_dimension)">{{ dimLabel(row.kpi_dimension) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="负责人" width="90" prop="assignee" />
<el-table-column label="优先级" width="80">
<template #default="{ row }">
<el-tag size="small" :type="row.priority === 'high' ? 'danger' : row.priority === 'low' ? 'info' : 'warning'">
{{ row.priority === 'high' ? '高' : row.priority === 'low' ? '低' : '中' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="进度" width="120">
<template #default="{ row }">
<div style="display:flex;align-items:center;gap:6px;">
<el-progress :percentage="row.progress || 0" :stroke-width="8" style="flex:1;" />
<span style="font-size:11px;color:#999;min-width:28px;">{{ row.progress || 0 }}%</span>
</div>
</template>
</el-table-column>
<el-table-column label="截止日" width="100" prop="due_date">
<template #default="{ row }">
<span v-if="!row.due_date" style="color:#ccc;"></span>
<span v-else :style="{ color: isOverdue(row) ? '#f56c6c' : '#666', fontWeight: isOverdue(row) ? 600 : 400 }">
{{ row.due_date?.slice(0, 10) }}
<el-tooltip v-if="isOverdue(row)" content="已逾期" placement="top"><span style="margin-left:2px;"></span></el-tooltip>
</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag size="small" :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="editPlan(row)">编辑</el-button>
<el-button size="small" link type="danger" @click="deletePlan(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作栏 -->
<div v-if="selectedRows.length > 0" style="margin-top:8px;display:flex;gap:8px;align-items:center;padding:8px 12px;background:#f0f9ff;border-radius:6px;">
<span style="font-size:13px;color:#409eff;">已选 {{ selectedRows.length }} </span>
<el-button size="small" @click="batchSetStatus('in_progress')">置为进行中</el-button>
<el-button size="small" type="success" @click="batchSetStatus('completed')">置为已完成</el-button>
<el-button size="small" @click="clearSelected">取消选择</el-button>
</div>
<!-- 分页 -->
<div style="display:flex;justify-content:center;margin-top:16px;">
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev, pager, next, total" @current-change="loadData" background small />
</div>
<!-- 新建/编辑弹窗 -->
<MyDialog v-model="showForm" :title="formTitle" :width="550">
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<el-tag size="small" type="success">S 具体的</el-tag>
<el-tag size="small" type="warning">M 可衡量</el-tag>
<el-tag size="small">A 可实现</el-tag>
<el-tag size="small" type="primary">R 相关已关联KPI</el-tag>
<el-tag size="small" type="danger">T 有时限截止日</el-tag>
</div>
<el-form :model="form" label-width="80px" size="small">
<el-form-item label="行动标题" required>
<el-input v-model="form.title" placeholder="请输入行动方案标题" />
</el-form-item>
<el-form-item label="目标值" help="SMART:具体、可衡量的成功标准">
<div style="display:flex;gap:4px;flex-direction:column;width:100%;">
<el-input v-model="form.target_value" placeholder="例:满意度从85%提升至92%、不良率降低50%以下" />
<span style="font-size:11px;color:#999;line-height:1.4;">设置可量化的成功标准完成后对照核验</span>
</div>
</el-form-item>
<el-form-item label="关联KPI" required>
<el-select v-model="form.kpi_id" placeholder="选择关联KPI" filterable style="width:100%;" :teleported="false">
<el-option v-for="k in kpiOptions" :key="k.id" :label="`${k.kpi_code} ${k.kpi_name}`" :value="k.id" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="详细描述(可选)" />
</el-form-item>
<el-form-item label="负责人">
<el-select v-model="form.assignee" placeholder="选择负责人" filterable allow-create style="width:100%;" :teleported="false">
<el-option v-for="u in userOptions" :key="u" :label="u" :value="u" />
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-radio-group v-model="form.priority">
<el-radio value="high"></el-radio>
<el-radio value="medium"></el-radio>
<el-radio value="low"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="截止日期">
<el-date-picker v-model="form.due_date" type="date" placeholder="选择截止日期" style="width:100%;" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item v-if="editingId" label="状态">
<el-select v-model="form.status" style="width:100%;" :teleported="false">
<el-option label="待处理" value="pending" />
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item v-if="editingId" label="进度 %">
<el-slider v-model="form.progress" :min="0" :max="100" :step="5" show-input style="width:100%;" />
</el-form-item>
<el-form-item v-if="editingId && form.status === 'completed'" label="改善结果">
<el-input v-model="form.result" type="textarea" :rows="3" placeholder="描述改善结果,对照目标值核验" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showForm = false">取消</el-button>
<el-button type="primary" @click="savePlan" :loading="saving">{{ editingId ? '保存修改' : '创建' }}</el-button>
</template>
</MyDialog>
<!-- 详情弹窗 -->
<MyDialog v-model="showDetail" :title="detailTitle" :width="600">
<div v-if="detailPlan">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div>
<div style="font-size:12px;color:#999;">关联KPI</div>
<div style="margin-top:2px;">{{ detailPlan.kpi_code }} {{ detailPlan.kpi_name }}</div>
</div>
<div>
<div style="font-size:12px;color:#999;">维度</div>
<div style="margin-top:2px;">
<el-tag size="small" :type="dimTagType(detailPlan.kpi_dimension)">{{ dimLabel(detailPlan.kpi_dimension) }}</el-tag>
</div>
</div>
<div>
<div style="font-size:12px;color:#999;">负责人</div>
<div style="margin-top:2px;">{{ detailPlan.assignee || '未指定' }}</div>
</div>
<div>
<div style="font-size:12px;color:#999;">优先级</div>
<div style="margin-top:2px;">
<el-tag size="small" :type="detailPlan.priority === 'high' ? 'danger' : detailPlan.priority === 'low' ? 'info' : 'warning'">
{{ detailPlan.priority === 'high' ? '高' : detailPlan.priority === 'low' ? '低' : '中' }}
</el-tag>
</div>
</div>
<div>
<div style="font-size:12px;color:#999;">截止日期</div>
<div style="margin-top:2px;" :class="{ 'overdue-text': isOverdue(detailPlan) }">
{{ detailPlan.due_date?.slice(0, 10) || '未设置' }}
<span v-if="isOverdue(detailPlan)" style="color:#f56c6c;font-size:12px;margin-left:4px;">已逾期</span>
</div>
</div>
<div>
<div style="font-size:12px;color:#999;">状态</div>
<div style="margin-top:2px;">
<el-tag size="small" :type="statusTagType(detailPlan.status)">{{ statusLabel(detailPlan.status) }}</el-tag>
</div>
</div>
</div>
<div style="margin-top:12px;">
<div style="font-size:12px;color:#999;">进度</div>
<el-progress :percentage="detailPlan.progress || 0" :stroke-width="12" style="margin-top:4px;" />
</div>
<!-- 目标值 -->
<div v-if="detailPlan.target_value" style="margin-top:12px;padding:10px 14px;background:#f0f9eb;border-radius:6px;border-left:3px solid #67c23a;">
<div style="font-size:12px;color:#67c23a;font-weight:500;display:flex;align-items:center;gap:4px;">
<span style="font-size:10px;border:1px solid #67c23a;border-radius:2px;padding:0 3px;line-height:14px;">SMART</span>
目标值 / 成功标准
</div>
<div style="margin-top:4px;line-height:1.6;font-size:14px;">{{ detailPlan.target_value }}</div>
</div>
<div v-if="detailPlan.description" style="margin-top:12px;">
<div style="font-size:12px;color:#999;">详细描述</div>
<div style="margin-top:4px;line-height:1.6;background:#f9f9f9;padding:8px 12px;border-radius:4px;">{{ detailPlan.description }}</div>
</div>
<div v-if="detailPlan.result" style="margin-top:12px;">
<div style="font-size:12px;color:#999;">改善结果</div>
<div style="margin-top:4px;line-height:1.6;background:#f0f9eb;padding:8px 12px;border-radius:4px;">{{ detailPlan.result }}</div>
</div>
<div style="margin-top:12px;font-size:12px;color:#ccc;display:flex;gap:16px;">
<span>创建人: {{ detailPlan.created_by }}</span>
<span>创建时间: {{ detailPlan.created_at?.slice(0, 16) }}</span>
</div>
</div>
<template #footer>
<el-button @click="showDetail = false">关闭</el-button>
</template>
</MyDialog>
</template>
</div>
<!-- ===== COSO内控自检 ===== -->
<template v-if="activeTab === 'coso'">
<div>
<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center;">
<span style="font-weight:500;">COSO内控自检表 CMA P1内部控制五要素</span>
<el-select v-model="cosoEntity" size="small" style="width:160px;" @change="loadCosoChecklist">
<el-option label="陕西酣客(白酒经销)" value="hanke" />
<el-option label="陕西博海科技(IT服务)" value="bohai" />
</el-select>
<el-button size="small" @click="loadCosoChecklist" :loading="cosoLoading">刷新</el-button>
<el-button v-if="cosoData" type="primary" size="small" style="margin-left:12px;">生成改善行动</el-button>
</div>
<div v-if="cosoData">
<!-- 总体评分 -->
<div style="margin-bottom:16px;padding:16px 20px;background:#f5f7fa;border-radius:8px;display:flex;align-items:center;gap:20px;">
<div>
<div style="font-size:12px;color:#909399;">总体内控评分</div>
<div style="font-size:36px;font-weight:700;margin-top:2px;"
:style="{ color: cosoData.total_score < 50 ? '#f56c6c' : cosoData.total_score < 70 ? '#e6a23c' : '#67c23a' }">
{{ cosoData.total_score }}/{{ cosoData.max_score }}
</div>
</div>
<div>
<el-tag :type="cosoData.risk_level === 'high' ? 'danger' : cosoData.risk_level === 'medium' ? 'warning' : 'success'" size="large" effect="dark" style="font-size:16px;padding:6px 16px;">
{{ cosoData.risk_label }}
</el-tag>
</div>
<div style="flex:1;display:flex;gap:12px;flex-wrap:wrap;">
<div v-for="el in cosoData.elements" :key="el.id" style="text-align:center;min-width:80px;">
<div style="font-size:20px;font-weight:600;" :style="{ color: el.score >= 60 ? '#67c23a' : el.score >= 40 ? '#e6a23c' : '#f56c6c' }">{{ el.score }}</div>
<div style="font-size:11px;color:#999;">{{ el.name }}</div>
</div>
</div>
</div>
<!-- 五要素折叠面板 -->
<div v-for="el in cosoData.elements" :key="el.id" style="margin-bottom:8px;border:1px solid #ebeef5;border-radius:8px;overflow:hidden;">
<div class="coso-element-header" @click="toggleCosoElement(el.id)" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:12px 16px;background:#fafafa;border-left:4px solid"
:style="{ borderLeftColor: el.score >= 60 ? '#67c23a' : el.score >= 40 ? '#e6a23c' : '#f56c6c' }">
<span style="flex:1;font-weight:500;">{{ el.name }}{{ el.name_en }}</span>
<el-progress :percentage="el.score" :stroke-width="14" :status="el.score >= 60 ? 'success' : el.score >= 40 ? 'warning' : 'exception'" style="width:160px;" />
<span :class="{ 'is-open': cosoExpanded[el.id] }" style="color:#999;transition:transform .2s;"></span>
</div>
<div v-show="cosoExpanded[el.id]" style="padding:8px 16px 12px;">
<div v-for="item in el.items" :key="item.id" style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid #f5f5f5;">
<span :style="{color: item.passed ? '#67c23a' : '#f56c6c', fontSize: '16px', fontWeight: 'bold'}">{{ item.passed ? '✅' : '❌' }}</span>
<span style="flex:1;font-size:14px;">{{ item.text }}</span>
<span style="font-size:12px;color:#666;">{{ item.detail }}</span>
</div>
</div>
</div>
</div>
<div v-else-if="!cosoLoading" style="padding:60px 0;text-align:center;color:#999;">暂无COSO自检数据</div>
</div>
</template>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { actionPlanApi, kpiApi, userApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
const route = useRoute()
const activeTab = ref((route.query?.tab as string) || 'plans')
// ── 数据 ──
const list = ref<any[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const stats = ref<Record<string, number>>({})
const selectedRows = ref<any[]>([])
const kpiOptions = ref<any[]>([])
const userOptions = ref<string[]>([])
// ── 筛选 ──
const filterStatus = ref('')
const filterDimension = ref('')
const filterKpi = ref<number | null>(null)
const filterAssignee = ref('')
const filterPriority = ref('')
const searchKeyword = ref('')
const dimensions = [
{ value: 'finance', label: '财务' },
{ value: 'customer', label: '客户' },
{ value: 'process', label: '内部流程' },
{ value: 'learning', label: '学习成长' },
]
const hasFilter = computed(() =>
filterStatus.value || filterDimension.value || filterKpi.value ||
filterAssignee.value || filterPriority.value || searchKeyword.value
)
function clearFilter() {
filterStatus.value = ''
filterDimension.value = ''
filterKpi.value = null
filterAssignee.value = ''
filterPriority.value = ''
searchKeyword.value = ''
page.value = 1
loadData()
}
function clearSelected() {
selectedRows.value = []
}
// ── 统计卡片 ──
const statCards = [
{ key: '', label: '全部', color: '#409eff' },
{ key: 'pending', label: '待处理', color: '#909399' },
{ key: 'in_progress', label: '进行中', color: '#e6a23c' },
{ key: 'completed', label: '已完成', color: '#67c23a' },
{ key: 'overdue', label: '已逾期', color: '#f56c6c' },
]
// ── 加载数据 ──
async function loadData() {
loading.value = true
try {
const params: any = { page: page.value, page_size: pageSize.value }
if (filterStatus.value) params.status = filterStatus.value
if (filterDimension.value) params.dimension = filterDimension.value
if (filterKpi.value) params.kpi_id = filterKpi.value
if (filterAssignee.value) params.assignee = filterAssignee.value
if (filterPriority.value) params.priority = filterPriority.value
if (searchKeyword.value) params.keyword = searchKeyword.value
const r: any = await actionPlanApi.list(params)
const d = r.data || []
list.value = Array.isArray(d) ? d : []
total.value = r.total || list.value.length
} catch { ElMessage.error('加载行动方案失败') }
loading.value = false
}
async function loadStats() {
try {
const r: any = await actionPlanApi.stats()
stats.value = r
} catch {}
}
async function loadKpis() {
try {
const r: any = await kpiApi.list({ page_size: 500 })
const d = r.data || r || []
kpiOptions.value = Array.isArray(d) ? d : []
} catch {}
}
async function loadUsers() {
try {
const r: any = await userApi.list()
const d = r.data || []
if (Array.isArray(d)) {
userOptions.value = d.map((u: any) => u.name || u.username).filter(Boolean)
}
} catch {}
}
// ── 新建/编辑 ──
const showForm = ref(false)
const formTitle = ref('')
const editingId = ref<number | null>(null)
const saving = ref(false)
const form = ref({
title: '',
target_value: '',
kpi_id: null as number | null,
description: '',
assignee: '',
priority: 'medium',
due_date: null as string | null,
status: 'pending',
progress: 0,
result: '',
})
function openCreate() {
editingId.value = null
formTitle.value = '新建行动方案'
form.value = { title: '', target_value: '', kpi_id: null, description: '', assignee: '', priority: 'medium', due_date: null, status: 'pending', progress: 0, result: '' }
// 如果路由携带 kpi_id 参数,自动填入
if (route.query.kpi_id) {
form.value.kpi_id = Number(route.query.kpi_id)
}
showForm.value = true
}
function editPlan(row: any) {
editingId.value = row.id
formTitle.value = '编辑行动方案'
form.value = {
title: row.title,
target_value: row.target_value || '',
kpi_id: row.kpi_id,
description: row.description || '',
assignee: row.assignee || '',
priority: row.priority || 'medium',
due_date: row.due_date?.slice(0, 10) || null,
status: row.status || 'pending',
progress: row.progress || 0,
result: row.result || '',
}
showForm.value = true
}
async function savePlan() {
if (!form.value.title || !form.value.kpi_id) {
ElMessage.warning('请填写标题并选择关联KPI')
return
}
saving.value = true
try {
const data = { ...form.value }
if (editingId.value) {
await actionPlanApi.update(editingId.value, data)
ElMessage.success('已更新')
} else {
await actionPlanApi.create(data)
ElMessage.success('已创建')
}
showForm.value = false
loadData()
loadStats()
} catch { ElMessage.error('保存失败') }
saving.value = false
}
// ── 删除 ──
async function deletePlan(id: number) {
try {
await ElMessageBox.confirm('确定删除此行动方案?')
await actionPlanApi.delete(id)
ElMessage.success('已删除')
loadData()
loadStats()
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
// ── 批量操作 ──
async function batchSetStatus(status: string) {
const ids = selectedRows.value.map(r => r.id)
if (ids.length === 0) return
try {
await ElMessageBox.confirm(`确定将选中的 ${ids.length} 项置为「${status === 'completed' ? '已完成' : '进行中'}」?`)
for (const id of ids) {
await actionPlanApi.update(id, { status })
}
ElMessage.success(`已更新 ${ids.length} 项`)
selectedRows.value = []
loadData()
loadStats()
} catch (e) { if (e !== 'cancel') ElMessage.error('批量操作失败') }
}
// ── 详情 ──
const showDetail = ref(false)
const detailTitle = ref('')
const detailPlan = ref<any>(null)
function viewDetail(row: any) {
detailPlan.value = row
detailTitle.value = row.title
showDetail.value = true
}
// ── 辅助函数 ──
function isOverdue(row: any) {
if (!row.due_date) return false
if (row.status === 'completed' || row.status === 'cancelled') return false
return new Date(row.due_date.slice(0, 10)) < new Date(new Date().toLocaleDateString())
}
function dimTagType(dim: string) {
const map: Record<string, string> = { finance: 'primary', customer: 'success', process: 'warning', learning: 'danger' }
return map[dim] || 'info'
}
function dimLabel(dim: string) {
const map: Record<string, string> = { finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长' }
return map[dim] || dim || '-'
}
function statusTagType(status: string) {
const map: Record<string, string> = { pending: 'info', in_progress: 'warning', completed: 'success', cancelled: 'info' }
return map[status] || 'info'
}
function statusLabel(status: string) {
const map: Record<string, string> = { pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' }
return map[status] || status
}
// ── COSO内控自检 ──
const cosoEntity = ref('hanke')
const cosoData = ref<any>(null)
const cosoLoading = ref(false)
const cosoExpanded = ref<Record<string, boolean>>({})
function toggleCosoElement(id: string) {
cosoExpanded.value[id] = !cosoExpanded.value[id]
}
async function loadCosoChecklist() {
cosoLoading.value = true
try {
const r: any = await actionPlanApi.cosoChecklist({ entity: cosoEntity.value })
cosoData.value = r
// 默认展开第一个
if (r?.elements?.length > 0) {
cosoExpanded.value[r.elements[0].id] = true
}
} catch { ElMessage.error('加载COSO自检数据失败') }
cosoLoading.value = false
}
// ── 初始化 ──
onMounted(() => {
loadStats()
loadData()
loadKpis()
loadUsers()
})
</script>
<style scoped>
.stat-card:hover {
border-color: #dcdfe6;
background: #fff;
}
.stat-card.active {
border-color: #409eff;
background: #ecf5ff;
}
.overdue-text {
color: #f56c6c;
font-weight: 600;
}
</style>