feat(docs-center): CMAKnowledge reads md single source via new /api/cma/docs
ci/woodpecker/push/main Pipeline failed

New router app/api/doc_center.py: GET /api/cma/docs (list, ?with_content) and GET /api/cma/docs/{slug}; runtime disk read, role-gated (ceo/finance/business/it), no existing API semantics changed.

Content dir /srv/hermes/shared/cma-content (docs/*.md + categories.json). Placed outside the repo on purpose: the deploy tree equals the committed tree (deploy.sh checkout -f + clean -fdx), so in-repo content would be reset on every deploy and content edits would fail the deploy-tree dirty check.

CMAKnowledge.vue: 20 hardcoded items migrated to md, page now fetches at runtime and renders md with the existing marked dep (no new deps); table borders restored via :deep() since scoped styles never reach v-html content.

Tests: backend/tests/test_doc_center_20260924.py (20 cases). Full suite 1448 passed (baseline 1428 + 20).
This commit is contained in:
Hermes CI Fix
2026-09-24 01:07:29 +08:00
parent 72d4d97c83
commit ede0524c22
4 changed files with 512 additions and 286 deletions
+62 -285
View File
@@ -9,6 +9,10 @@
<el-input v-model="search" placeholder="搜索术语、计算公式、最佳实践..." prefix-icon="Search" clearable style="width:360px;" />
</div>
<div v-if="docsLoading" class="kb-hint">文档加载中...</div>
<div v-else-if="docsError" class="kb-hint kb-hint-error">{{ docsError }}</div>
<div v-else-if="categories.length === 0" class="kb-hint">暂无文档内容</div>
<!-- 搜索结果 -->
<div v-if="search" class="search-results">
<h4>搜索结果({{ searchResults.length }}条)</h4>
@@ -39,7 +43,7 @@
<!-- 详情弹窗 -->
<MyDialog v-model="showDetail" :title="detailItem?.title || '文章详情'" :width="700">
<div v-if="detailItem" class="detail-content" v-html="detailItem.content"></div>
<div v-if="detailItem" class="detail-content" v-html="detailHtml"></div>
<template #footer>
<el-button @click="showDetail = false">关闭</el-button>
</template>
@@ -166,6 +170,7 @@ import { ref, computed } from "vue"
import { ElMessage } from "element-plus"
import { ethicsQuizApi } from "../api/index"
import api from "../api/index"
import { marked } from "marked"
// ── Tab ──
const activeTab = ref("knowledge")
@@ -175,288 +180,48 @@ const search = ref("")
const showDetail = ref(false)
const detailItem = ref<any>(null)
const categories = ref([
{
name: "术语解释", icon: "📖", key: "term",
items: [
{
title: "KPI(关键绩效指标)",
summary: "衡量战略目标达成情况的量化指标",
content: `<h4>KPI(Key Performance Indicator)</h4>
<p>关键绩效指标,是衡量企业战略目标达成情况的量化指标。在管理会计OS中,KPI贯穿战略地图、经营看板、预警中心等所有模块。</p>
<h5>特征</h5>
<ul>
<li><b>可量化</b>:每个KPI有明确的计算公式和数值</li>
<li><b>有时效</b>:按日/周/月/季/年采集和考核</li>
<li><b>有标准</b>:每个KPI设定了目标值和红黄绿灯阈值</li>
</ul>
<h5>编码规则</h5>
<p>KPI编码按 BSC维度_类别_序号 命名,如 <code>F_REVENUE_001</code>:</p>
<ul>
<li>F = 财务维度 (Finance)</li>
<li>C = 客户维度 (Customer)</li>
<li>P = 内部流程 (Process)</li>
<li>L = 学习成长 (Learning)</li>
</ul>`,
},
{
title: "BSC(平衡计分卡)",
summary: "从财务、客户、内部流程、学习成长四个维度衡量企业绩效",
content: `<h4>BSC(Balanced Scorecard)</h4>
<p>平衡计分卡,由Kaplan和Norton提出,从四个维度衡量企业绩效:</p>
<ul>
<li><b>财务维度</b>:最终结果指标,如收入、利润、成本</li>
<li><b>客户维度</b>:市场反馈指标,如客户数量、满意度</li>
<li><b>内部流程</b>:运营效率指标,如交付及时率、库存周转</li>
<li><b>学习成长</b>:驱动因素指标,如培训完成率、人才梯队</li>
</ul>
<p>四个维度形成因果链:<b>学习成长 → 内部流程 → 客户 → 财务</b></p>`,
},
{
title: "红黄绿灯预警",
summary: "KPI达成情况的颜色标识:绿=正常 黄=预警 红=异常",
content: `<h4>红黄绿灯预警机制</h4>
<p>系统自动根据KPI实际值与目标值的比率,生成三种预警等级:</p>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;width:100%;margin:12px 0;">
<tr style="background:#f5f5f5;"><th>颜色</th><th>等级</th><th>判定标准</th><th>建议动作</th></tr>
<tr><td style="color:#67c23a;font-weight:bold;">🟢 绿色</td><td>正常</td><td>达成率 ≥ 90%</td><td>保持现状</td></tr>
<tr><td style="color:#e6a23c;font-weight:bold;">🟡 黄色</td><td>预警</td><td>70% ≤ 达成率 < 90%</td><td>关注趋势,制定改善计划</td></tr>
<tr><td style="color:#f56c6c;font-weight:bold;">🔴 红色</td><td>异常</td><td>达成率 < 70%</td><td>立即分析原因,启动改善行动</td></tr>
</table>`,
},
{
title: "战略地图",
summary: "用BSC四维框架可视化展示战略目标与因果关系的工具",
content: `<h4>战略地图</h4>
<p>战略地图是平衡计分卡的图形化表达,将战略目标按BSC四个维度排列,并用箭头展示因果驱动关系。</p>
<h5>使用方法</h5>
<ol>
<li>在「战略地图」页面创建一张新地图</li>
<li>在每个维度下添加战略目标</li>
<li>给目标关联KPI,用于量化衡量</li>
<li>从低维度向高维度画因果连线(如 人才培养→流程优化→客户满意→收入增长)</li>
<li>发布后可在「战略回顾会」中查看执行情况</li>
</ol>`,
},
{
title: "改善行动计划(CAP)",
summary: "针对预警问题制定的改进措施,跟踪执行和效果",
content: `<h4>改善行动计划</h4>
<p>当KPI出现黄色或红色预警时,需要制定改善行动计划来解决问题。</p>
<h5>流程</h5>
<ol>
<li>在预警中心识别异常KPI</li>
<li>创建改善行动计划:指定标题、负责人、截止日期、优先级</li>
<li>执行过程中更新进度(0-100%)</li>
<li>完成后填写改善结果</li>
<li>系统会持续跟踪逾期和进度</li>
</ol>`,
},
{
title: "PDCA双循环",
summary: "计划(Plan)→执行(Do)→检查(Check)→改进(Act)的持续改进循环",
content: `<h4>PDCA双循环</h4>
<p>PDCA是计划→执行→检查→改进的持续改进循环,由戴明提出,是CMA管理控制体系的核心框架。系统菜单已按PDCA四阶段分组。</p>
<h5>双循环模型</h5>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;margin:8px 0;">
<tr style="background:#f5f5f5;"><th>循环</th><th>频率</th><th>内容</th><th>系统对应</th></tr>
<tr><td><b>战略循环</b></td><td>年度/季度</td><td>战略地图更新·O调整</td><td>战略回顾会</td></tr>
<tr><td><b>执行循环</b></td><td>月度/周度</td><td>KR追踪·KPI考核·改善</td><td>经营看板+差异分析</td></tr>
</table>`,
},
{
title: "SMART原则",
summary: "设定KPI的五项原则:具体的、可衡量的、可实现的、相关的、有时限的",
content: `<h4>SMART原则</h4>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;margin:8px 0;">
<tr style="background:#f5f5f5;"><th>字母</th><th>含义</th><th>好KPI</th><th>差KPI</th></tr>
<tr><td><b>S</b></td><td>具体的</td><td>库存周转从45天降到30天</td><td>提升效率</td></tr>
<tr><td><b>M</b></td><td>可衡量的</td><td>渠补率从82.8%降到75%</td><td>改善渠道关系</td></tr>
<tr><td><b>A</b></td><td>可实现的</td><td>管理费从91万降到85万</td><td>降到0(不现实)</td></tr>
<tr><td><b>R</b></td><td>相关的</td><td>仓库KPI→库存周转率</td><td>仓库KPI→客户满意度</td></tr>
<tr><td><b>T</b></td><td>有时限的</td><td>Q3前完成TOP5谈判</td><td>尽快完成</td></tr>
</table>`,
},
{
title: "OKR",
summary: "目标与关键结果:O是定性方向,KR是定量衡量",
content: `<h4>OKR——目标与关键结果</h4>
<p><b>O</b>(目标):我们要去哪里?——定性、有方向感</p>
<p><b>KR</b>(关键结果):怎么知道到了?——定量、可衡量</p>
<h5>OKR vs KPI</h5>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;margin:8px 0;">
<tr style="background:#f5f5f5;"><th>维度</th><th>OKR</th><th>KPI</th></tr>
<tr><td>本质</td><td>设定目标+衡量结果</td><td>持续监控健康状况</td></tr>
<tr><td>频率</td><td>季度设定</td><td>持续追踪</td></tr>
<tr><td>场景</td><td>战略落地、创新突破</td><td>日常运营、绩效评估</td></tr>
</table>
<p>CMA系统已内置OKR模板库(<a href="/okr-templates">/okr-templates</a>),每个O可预设3-5个KR。</p>`,
},
],
},
{
name: "计算公式", icon: "📐", key: "formula",
items: [
{
title: "达成率",
summary: "实际值 ÷ 目标值 × 100%",
content: `<h4>达成率</h4>
<p><b>公式</b>:达成率 = 实际值 / 目标值 × 100%</p>
<h5>示例</h5>
<p>销售总额目标2000万,实际1640万:1640/2000 = <b>82%</b></p>
<h5>说明</h5>
<ul>
<li>达成率 ≥ 90% 为绿灯</li>
<li>70% ≤ 达成率 < 90% 为黄灯</li>
<li>达成率 < 70% 为红灯</li>
<li>部分反向指标(如成本控制率)需取相反判断</li>
</ul>`,
},
{
title: "偏差率",
summary: "(实际值 - 目标值)÷ 目标值 × 100%",
content: `<h4>偏差率</h4>
<p><b>公式</b>:偏差率 = (实际值 - 目标值) / 目标值 × 100%</p>
<h5>示例</h5>
<p>预算100万,实际支出120万:(120-100)/100 = <b>+20%</b>(超支)</p>
<h5>说明</h5>
<ul>
<li>正偏差 = 超过目标(可能好也可能坏,取决于指标性质)</li>
<li>负偏差 = 低于目标</li>
</ul>`,
},
{
title: "销售毛利率",
summary: "(销售收入 - 销售成本)÷ 销售收入 × 100%",
content: `<h4>销售毛利率</h4>
<p><b>公式</b>:毛利率 = (销售收入 - 销售成本) / 销售收入 × 100%</p>
<h5>示例</h5>
<p>收入1000万,成本700万:(1000-700)/1000 = <b>30%</b></p>
<h5>说明</h5>
<p>毛利率是衡量企业盈利能力的核心指标,越高代表产品附加值越大。</p>`,
},
{
title: "库存周转率",
summary: "销售成本 ÷ 平均库存金额",
content: `<h4>库存周转率</h4>
<p><b>公式</b>:库存周转率 = 销售成本 / 平均库存金额</p>
<h5>示例</h5>
<p>年销售成本1.2亿,平均库存1500万:1.2亿/1500万 = <b>8次/年</b></p>
<h5>说明</h5>
<p>库存周转率反映企业存货管理效率,越高说明资金占用越少。</p>`,
},
{
title: "达成率 vs 偏差率 对照",
summary: "两个指标的区别与使用场景",
content: `<h4>达成率 vs 偏差率</h4>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;width:100%;margin:12px 0;">
<tr style="background:#f5f5f5;"><th></th><th>达成率</th><th>偏差率</th></tr>
<tr><td><b>公式</b></td><td>实际/目标 × 100%</td><td>(实际-目标)/目标 × 100%</td></tr>
<tr><td><b>理想值</b></td><td>≥ 100%</td><td>接近0%</td></tr>
<tr><td><b>适用场景</b></td><td>正向指标(收入、满意度)</td><td>预算控制、成本差异</td></tr>
<tr><td><b>红黄绿灯</b></td><td>≥90%绿,≥70%黄,<70%红</td><td>按偏差幅度自定义</td></tr>
</table>`,
},
],
},
{
name: "最佳实践", icon: "⭐", key: "practice",
items: [
{
title: "如何绘制一张好的战略地图",
summary: "从设定目标到画因果链的实操指南",
content: `<h4>绘制战略地图的5个步骤</h4>
<ol>
<li><b>明确战略主题</b>:确定本年度最重要的3-5个战略方向</li>
<li><b>从财务开始</b>:先在财务维度设定最终目标(如"提升销售总额30%")</li>
<li><b>追溯驱动因素</b>:思考"要达成财务目标,客户需要什么?流程需要怎么优化?员工需要什么能力?"</li>
<li><b>关联KPI</b>:每个目标绑定1-2个可量化的KPI</li>
<li><b>画因果连线</b>:从下到上连接各维度目标,形成因果链</li>
</ol>
<h5>常见错误</h5>
<ul>
<li>❌ 每个维度设太多目标(建议不超过4个)</li>
<li>❌ 目标不关联KPI(无法量化跟踪)</li>
<li>❌ 因果链混乱(学习→流程→客户→财务,不要跳层)</li>
</ul>`,
},
{
title: "预算编制的10个原则",
summary: "预算管理模块使用的最佳实践",
content: `<h4>预算编制最佳实践</h4>
<ol>
<li><b>自上而下 vs 自下而上</b>:先定总盘子,再分解到各部门</li>
<li><b>零基预算</b>:每年从零开始编制,不以上年基数为准</li>
<li><b>弹性预算</b>:按业务量设定不同层级的预算标准</li>
<li><b>滚动预算</b>:每季度更新一次全年预算</li>
<li><b>差异分析</b>:每月对比实际与预算,找出偏差原因</li>
</ol>`,
},
{
title: "预警阈值设置的技巧",
summary: "如何设置合理的红黄绿灯阈值",
content: `<h4>阈值设置建议</h4>
<p>合理的红黄绿灯阈值应该:</p>
<ul>
<li><b>不要过严</b>:绿灯设90%而不是95%,否则大部分KPI都是黄色</li>
<li><b>区分指标性质</b>:正向指标(收入)和反向指标(成本)的绿/红判断逻辑不同</li>
<li><b>结合历史数据</b>:参考过去6个月的实际值来设定合理目标</li>
<li><b>动态调整</b>:每季度review一次阈值,随业务发展调整</li>
</ul>`,
},
{
title: "每日工作台的使用方法",
summary: "如何高效使用我的工作台进行日常管理",
content: `<h4>工作台使用三部曲</h4>
<ol>
<li><b>早上看KPI</b>:登录先看一眼我的KPI红黄绿灯,了解整体状态</li>
<li><b>处理待办</b>:查看逾期改善行动和待审批事项</li>
<li><b>跟踪异常</b>:红色预警的KPI立即查看详情,安排改善行动</li>
</ol>`,
},
],
},
{
name: "常见问题", icon: "❓", key: "faq",
items: [
{
title: "数据不同步怎么办?",
summary: "ERP数据同步失败的排查步骤",
content: `<h4>数据同步问题排查</h4>
<ol>
<li>检查ERP API网关是否正常运行(端口8300)</li>
<li>在「数据管理」页面查看最近一次同步时间</li>
<li>手动触发同步:数据管理 → 点击「手动同步」</li>
<li>如仍失败,联系IT管理员查看后端日志</li>
</ol>`,
},
{
title: "为什么看不到某些KPI?",
summary: "角色权限和KPI可见性的说明",
content: `<h4>KPI可见性规则</h4>
<ul>
<li><b>CEO</b>:全量KPI可见</li>
<li><b>财务</b>:财务维度KPI + 通用KPI</li>
<li><b>业务</b>:客户、流程、学习维度KPI</li>
<li><b>IT</b>:全量KPI可见(运维用途)</li>
</ul>
<p>如需调整可见范围,请联系管理员修改权限配置。</p>`,
},
{
title: "如何删除一个战略目标?",
summary: "画布上目标的编辑和删除操作",
content: `<h4>删除战略目标</h4>
<ol>
<li>进入战略地图画布页面</li>
<li>点击要删除的目标卡片(会弹出编辑弹窗)</li>
<li>点击弹窗底部的「删除」按钮</li>
<li>确认删除</li>
<li>点击「保存」使改动生效</li>
</ol>`,
},
],
},
])
// ── 知识库内容:md 单一来源 ─────────────────────────────────────────────
// 口径(任总 2026-09-24 00:54):指标进 UI、结论出 md、机器走 API。
// 内容源 = 后端 /api/cma/docs(运行期实时读 md 目录)⇒ 加/改一篇内容只改一个 md 文件,
// 前端无需改码、无需 rebuild,刷新页面即生效。
const categories = ref<any[]>([])
const docsLoading = ref(false)
const docsError = ref("")
const detailHtml = ref("")
async function loadDocs() {
docsLoading.value = true
try {
const body: any = (await api.get("/docs", { params: { with_content: true } })) || {}
const list: any[] = Array.isArray(body.data) ? body.data : (Array.isArray(body) ? body : [])
const cats: any[] = Array.isArray(body.categories) ? body.categories : []
const grouped: Record<string, any[]> = {}
for (const d of list) {
const k = d.category || "其他"
if (!grouped[k]) grouped[k] = []
grouped[k].push({
title: d.title,
summary: d.summary || "",
content: d.content || "",
slug: d.slug,
kind: "md",
})
}
const view: any[] = cats
.map((c: any) => ({ name: c.name || c.key, icon: c.icon || "", key: c.key, items: grouped[c.key] || [] }))
.filter((c: any) => c.items.length > 0)
// 兜底:categories.json 未登记的分类也照常展示,不丢内容
for (const k of Object.keys(grouped)) {
if (!view.find((c: any) => c.key === k)) view.push({ name: k, icon: "", key: k, items: grouped[k] })
}
categories.value = view
docsError.value = ""
} catch (e) {
docsError.value = "文档加载失败,请稍后重试"
console.error("加载文档中心失败", e)
}
docsLoading.value = false
}
const searchResults = computed(() => {
if (!search.value.trim()) return []
@@ -533,9 +298,12 @@ async function loadDynamicArticles() {
}
}
// 页面加载时拉取动态知识库
// 页面加载:先取 md 单一来源文档,再追加 DB 动态知识库(合并语义与改动前一致)
import { onMounted } from "vue"
onMounted(() => { loadDynamicArticles() })
onMounted(async () => {
await loadDocs()
loadDynamicArticles()
})
function catLabel(key: string) {
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
@@ -544,6 +312,8 @@ function catLabel(key: string) {
function openItem(item: any) {
detailItem.value = item
// md 单一来源的内容用页面同一渲染器渲染;DB 动态知识库文章保持既有 HTML 直出
detailHtml.value = item?.kind === "md" ? (marked(item.content || "") as string) : (item?.content || "")
showDetail.value = true
}
@@ -676,6 +446,8 @@ function backToIntro() {
.search-title { font-weight: 500; font-size: 14px; flex-shrink: 0; }
.search-summary { font-size: 12px; color: #999; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-search { text-align: center; color: #999; padding: 40px; }
.kb-hint { text-align: center; color: #999; padding: 24px; font-size: 14px; }
.kb-hint-error { color: #f56c6c; }
.detail-content { line-height: 1.8; font-size: 14px; }
.detail-content h4 { margin: 16px 0 8px; }
.detail-content h5 { margin: 12px 0 6px; color: #666; }
@@ -685,6 +457,11 @@ function backToIntro() {
.detail-content table { width: 100%; border-collapse: collapse; margin: 12px 0; }
.detail-content th, .detail-content td { border: 1px solid #e0e0e0; padding: 8px; text-align: left; }
.detail-content th { background: #f5f5f5; font-weight: 600; }
/* v-html 内容不带 scoped 属性(旧版靠 HTML 内联 border/cellpadding 出边框);
md 单一来源的表格是纯 markdown,改用 :deep 让既有表格样式真正生效(2026-09-24) */
.detail-content :deep(table) { width: 100%; border-collapse: collapse; margin: 12px 0; }
.detail-content :deep(th), .detail-content :deep(td) { border: 1px solid #e0e0e0; padding: 8px; text-align: left; }
.detail-content :deep(th) { background: #f5f5f5; font-weight: 600; }
/* ── Quiz Intro ── */
.quiz-intro { display: flex; justify-content: center; padding: 40px 0; }