feat: CMA P1+P2 Round2 — 风险矩阵+COSO+预算方法

This commit is contained in:
Hermes CI Fix
2026-07-16 17:09:32 +08:00
parent 82919b1616
commit 9eded3e4fe
7 changed files with 750 additions and 5 deletions
+94 -4
View File
@@ -1,14 +1,21 @@
<template>
<div style="padding:0 16px;">
<!-- 顶部标题+操作 -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h3 style="margin:0;">行动方案库</h3>
<div style="display:flex;gap:8px;align-items:center;">
<!-- 顶部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"
@@ -249,7 +256,66 @@
<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">
@@ -260,6 +326,7 @@ 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[]>([])
@@ -496,6 +563,29 @@ function statusLabel(status: string) {
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()