refactor: 知识库从硬编码改为API动态加载(24篇文章)
This commit is contained in:
@@ -1,442 +1,100 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="kb-page">
|
<div class="kb-page">
|
||||||
<!-- Tab切换 -->
|
<div class="kb-header">
|
||||||
<el-tabs v-model="activeTab" class="kb-tabs">
|
<h3>CMA知识库</h3>
|
||||||
<!-- Tab 1: 知识库 -->
|
<el-input v-model="search" placeholder="搜索文章标题..." prefix-icon="Search" clearable style="width:300px;" />
|
||||||
<el-tab-pane label="📖 知识库" name="knowledge">
|
</div>
|
||||||
<div class="kb-header">
|
|
||||||
<h3>CMA知识库</h3>
|
|
||||||
<el-input v-model="search" placeholder="搜索术语、计算公式、最佳实践..." prefix-icon="Search" clearable style="width:360px;" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索结果 -->
|
<!-- 搜索结果 -->
|
||||||
<div v-if="search" class="search-results">
|
<div v-if="search" class="search-results">
|
||||||
<h4>搜索结果({{ searchResults.length }}条)</h4>
|
<h4>搜索结果({{ searchResults.length }}条)</h4>
|
||||||
<div v-for="item in searchResults" :key="item.title" class="search-item" @click="openItem(item)">
|
<div v-for="item in searchResults" :key="item.id" class="search-item" @click="openItem(item)">
|
||||||
<span class="search-cat-tag">{{ catLabel(item.category) }}</span>
|
<span class="search-cat-tag">{{ catLabel(item.category) }}</span>
|
||||||
<span class="search-title">{{ item.title }}</span>
|
<span class="search-title">{{ item.title }}</span>
|
||||||
<span class="search-summary">{{ item.summary }}</span>
|
<span class="search-summary">{{ item.summary }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="searchResults.length === 0" class="empty-search">未找到相关内容</div>
|
<div v-if="searchResults.length === 0" class="empty-search">未找到相关内容</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分类展示 -->
|
<!-- 分类展示 -->
|
||||||
<div v-else class="kb-grid">
|
<div v-else-if="!loading" class="kb-grid">
|
||||||
<div v-for="(cat, ci) in categories" :key="ci" class="kb-category">
|
<div v-for="(cat, ci) in categories" :key="ci" class="kb-category">
|
||||||
<div class="cat-head">
|
<div class="cat-head">
|
||||||
<span class="cat-icon">{{ cat.icon }}</span>
|
<span class="cat-icon">{{ cat.icon }}</span>
|
||||||
<span class="cat-title">{{ cat.name }}</span>
|
<span class="cat-title">{{ cat.name }}</span>
|
||||||
<span class="cat-count">{{ cat.items.length }}篇</span>
|
<span class="cat-count">{{ cat.items.length }}篇</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cat-body">
|
<div class="cat-body">
|
||||||
<div v-for="(item, ii) in cat.items" :key="ii" class="kb-item" @click="openItem(item)">
|
<div v-for="(item, ii) in cat.items" :key="ii" class="kb-item" @click="openItem(item)">
|
||||||
<span class="kb-item-title">{{ item.title }}</span>
|
<span class="kb-item-title">{{ item.title }}</span>
|
||||||
<span class="kb-item-summary">{{ item.summary }}</span>
|
<span class="kb-item-summary">{{ item.summary }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="text-align:center;padding:40px;color:#999;">加载中...</div>
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 详情弹窗 -->
|
||||||
<MyDialog v-model="showDetail" :title="detailItem?.title || '文章详情'" :width="700">
|
<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="detailItem.content"></div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showDetail = false">关闭</el-button>
|
<el-button @click="showDetail = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</MyDialog>
|
</MyDialog>
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<!-- Tab 2: 职业道德自测 -->
|
|
||||||
<el-tab-pane label="⚖️ 职业道德自测" name="ethics-quiz">
|
|
||||||
<div v-if="!quizStarted" class="quiz-intro">
|
|
||||||
<div class="quiz-intro-card">
|
|
||||||
<h2>⚖️ CMA职业道德自测</h2>
|
|
||||||
<p>本测试包含 <strong>10道</strong> IMA职业道德情景题,涵盖:</p>
|
|
||||||
<ul class="quiz-standards">
|
|
||||||
<li><span class="std-tag std-integrity">诚信</span> Integrity — 诚实、公正地处理事务</li>
|
|
||||||
<li><span class="std-tag std-objectivity">客观</span> Objectivity — 保持独立性和职业判断</li>
|
|
||||||
<li><span class="std-tag std-confidentiality">保密</span> Confidentiality — 保护公司机密信息</li>
|
|
||||||
<li><span class="std-tag std-competence">胜任</span> Competence — 持续提升专业能力</li>
|
|
||||||
</ul>
|
|
||||||
<el-button type="primary" size="large" @click="startQuiz" class="quiz-start-btn">
|
|
||||||
开始自测
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="quiz-body">
|
|
||||||
<!-- 进度条 -->
|
|
||||||
<div class="quiz-progress">
|
|
||||||
<div class="quiz-progress-text">
|
|
||||||
第 {{ currentQuestion + 1 }} / {{ questions.length }} 题
|
|
||||||
</div>
|
|
||||||
<el-progress :percentage="progressPct" :stroke-width="8" color="#409eff" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 题目卡片 -->
|
|
||||||
<div class="quiz-question-card" v-if="currentQuestion < questions.length">
|
|
||||||
<div class="q-scenario">
|
|
||||||
<span class="q-number">Q{{ currentQuestion + 1 }}</span>
|
|
||||||
<p>{{ questions[currentQuestion].scenario }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-options">
|
|
||||||
<div
|
|
||||||
v-for="opt in questions[currentQuestion].options"
|
|
||||||
:key="opt.id"
|
|
||||||
class="q-option"
|
|
||||||
:class="{
|
|
||||||
'selected': selectedOption?.id === opt.id,
|
|
||||||
'correct': answered && opt.correct,
|
|
||||||
'wrong': answered && selectedOption?.id === opt.id && !opt.correct,
|
|
||||||
'disabled': answered
|
|
||||||
}"
|
|
||||||
@click="selectOption(opt)"
|
|
||||||
>
|
|
||||||
<span class="opt-id">{{ opt.id.toUpperCase() }}</span>
|
|
||||||
<span class="opt-text">{{ opt.text }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 反馈 -->
|
|
||||||
<div v-if="answered" class="q-feedback" :class="selectedOption?.correct ? 'feedback-correct' : 'feedback-wrong'">
|
|
||||||
<div class="feedback-icon">{{ selectedOption?.correct ? '✅' : '❌' }}</div>
|
|
||||||
<div class="feedback-text">
|
|
||||||
<strong>{{ selectedOption?.correct ? '正确!' : '选择有误' }}</strong>
|
|
||||||
<p>{{ feedbackMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="q-actions">
|
|
||||||
<el-button v-if="answered && currentQuestion < questions.length - 1" type="primary" @click="nextQuestion">
|
|
||||||
下一题 →
|
|
||||||
</el-button>
|
|
||||||
<el-button v-if="answered && currentQuestion === questions.length - 1" type="success" @click="showResult">
|
|
||||||
查看成绩 🏆
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 成绩页面 -->
|
|
||||||
<div v-if="showScore" class="quiz-result">
|
|
||||||
<div class="result-card">
|
|
||||||
<div class="result-emoji">{{ scoreEmoji }}</div>
|
|
||||||
<div class="result-title">{{ scoreTitle }}</div>
|
|
||||||
<div class="result-score">{{ correctCount }} / {{ questions.length }}</div>
|
|
||||||
<el-progress :percentage="scorePct" :stroke-width="12" :color="scoreColor" class="result-bar" />
|
|
||||||
<div class="result-detail">
|
|
||||||
<div class="result-stat correct-stat">✅ 正确:{{ correctCount }} 题</div>
|
|
||||||
<div class="result-stat wrong-stat">❌ 错误:{{ questions.length - correctCount }} 题</div>
|
|
||||||
</div>
|
|
||||||
<div class="result-actions">
|
|
||||||
<el-button type="primary" @click="retryQuiz">重新测试</el-button>
|
|
||||||
<el-button @click="backToIntro">返回首页</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 每题回顾 -->
|
|
||||||
<div class="result-review">
|
|
||||||
<h4>逐题回顾</h4>
|
|
||||||
<div v-for="(q, idx) in questions" :key="idx" class="review-item" :class="userAnswers[idx]?.correct ? 'review-correct' : 'review-wrong'">
|
|
||||||
<div class="review-head">
|
|
||||||
<span class="review-icon">{{ userAnswers[idx]?.correct ? '✅' : '❌' }}</span>
|
|
||||||
<span class="review-q">Q{{ idx + 1 }}. {{ q.scenario }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="review-answer">
|
|
||||||
你的选择:<strong>{{ userAnswers[idx]?.text || '未作答' }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="review-correct-answer">
|
|
||||||
正确答案:<strong>{{ q.options.find(o => o.correct)?.text }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="review-feedback" v-if="!userAnswers[idx]?.correct">
|
|
||||||
💡 {{ userAnswers[idx]?.feedback || q.options.find(o => o.correct)?.feedback }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue"
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { ElMessage } from "element-plus"
|
import { knowledgeArticleApi } from '../api/index'
|
||||||
import { ethicsQuizApi } from "../api/index"
|
|
||||||
|
|
||||||
// ── Tab ──
|
const search = ref('')
|
||||||
const activeTab = ref("knowledge")
|
|
||||||
|
|
||||||
// ── 原有知识库逻辑 ──
|
|
||||||
const search = ref("")
|
|
||||||
const showDetail = ref(false)
|
const showDetail = ref(false)
|
||||||
const detailItem = ref<any>(null)
|
const detailItem = ref<any>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
const allArticles = ref<any[]>([])
|
||||||
|
|
||||||
const categories = ref([
|
async function loadArticles() {
|
||||||
{
|
try {
|
||||||
name: "术语解释", icon: "📖", key: "term",
|
const r: any = await knowledgeArticleApi.list()
|
||||||
items: [
|
const articles = r.data || r || []
|
||||||
{
|
allArticles.value = articles
|
||||||
title: "KPI(关键绩效指标)",
|
const groupMap: Record<string, any[]> = {}
|
||||||
summary: "衡量战略目标达成情况的量化指标",
|
for (const a of articles) {
|
||||||
content: `<h4>KPI(Key Performance Indicator)</h4>
|
const cat = a.category || '其他'
|
||||||
<p>关键绩效指标,是衡量企业战略目标达成情况的量化指标。在管理会计OS中,KPI贯穿战略地图、经营看板、预警中心等所有模块。</p>
|
if (!groupMap[cat]) groupMap[cat] = []
|
||||||
<h5>特征</h5>
|
groupMap[cat].push(a)
|
||||||
<ul>
|
}
|
||||||
<li><b>可量化</b>:每个KPI有明确的计算公式和数值</li>
|
const catIcons: Record<string, string> = {
|
||||||
<li><b>有时效</b>:按日/周/月/季/年采集和考核</li>
|
'CMA完整文档': '📘', '管理会计方法': '📊', 'KPI设定': '🎯',
|
||||||
<li><b>有标准</b>:每个KPI设定了目标值和红黄绿灯阈值</li>
|
'OKR': '📋', '术语解释': '📖', '计算公式': '📐',
|
||||||
</ul>
|
'最佳实践': '⭐', '常见问题': '❓', '其他': '📄',
|
||||||
<h5>编码规则</h5>
|
}
|
||||||
<p>KPI编码按 BSC维度_类别_序号 命名,如 <code>F_REVENUE_001</code>:</p>
|
categories.value = Object.entries(groupMap).map(([key, items]) => ({
|
||||||
<ul>
|
name: key, icon: catIcons[key] || '📄', key, items,
|
||||||
<li>F = 财务维度 (Finance)</li>
|
}))
|
||||||
<li>C = 客户维度 (Customer)</li>
|
} catch (e) {
|
||||||
<li>P = 内部流程 (Process)</li>
|
console.error('加载知识库失败', e)
|
||||||
<li>L = 学习成长 (Learning)</li>
|
}
|
||||||
</ul>`,
|
loading.value = false
|
||||||
},
|
}
|
||||||
{
|
|
||||||
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>`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const searchResults = computed(() => {
|
const searchResults = computed(() => {
|
||||||
if (!search.value.trim()) return []
|
if (!search.value.trim()) return []
|
||||||
const q = search.value.toLowerCase()
|
const q = search.value.toLowerCase()
|
||||||
const results: any[] = []
|
return allArticles.value.filter((a: any) =>
|
||||||
for (const cat of categories.value) {
|
(a.title || '').toLowerCase().includes(q) ||
|
||||||
for (const item of cat.items) {
|
(a.summary || '').toLowerCase().includes(q)
|
||||||
if (
|
)
|
||||||
item.title.toLowerCase().includes(q) ||
|
|
||||||
item.summary.toLowerCase().includes(q) ||
|
|
||||||
item.content.toLowerCase().includes(q)
|
|
||||||
) {
|
|
||||||
results.push({ ...item, category: cat.key })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function catLabel(key: string) {
|
function catLabel(key: string) {
|
||||||
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
|
const map: Record<string, string> = {
|
||||||
|
'CMA完整文档': 'CMA', '管理会计方法': '方法', 'KPI设定': 'KPI',
|
||||||
|
'OKR': 'OKR', '术语解释': '术语', '计算公式': '公式',
|
||||||
|
'最佳实践': '实践', '常见问题': 'FAQ',
|
||||||
|
}
|
||||||
return map[key] || key
|
return map[key] || key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,114 +103,11 @@ function openItem(item: any) {
|
|||||||
showDetail.value = true
|
showDetail.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 职业道德自测逻辑 ──
|
onMounted(() => loadArticles())
|
||||||
|
|
||||||
const questions = ref<any[]>([])
|
|
||||||
const quizStarted = ref(false)
|
|
||||||
const currentQuestion = ref(0)
|
|
||||||
const selectedOption = ref<any>(null)
|
|
||||||
const answered = ref(false)
|
|
||||||
const userAnswers = ref<any[]>([])
|
|
||||||
const showScore = ref(false)
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const progressPct = computed(() => {
|
|
||||||
if (questions.value.length === 0) return 0
|
|
||||||
return ((currentQuestion.value + (answered.value ? 1 : 0)) / questions.value.length) * 100
|
|
||||||
})
|
|
||||||
|
|
||||||
const correctCount = computed(() => {
|
|
||||||
return userAnswers.value.filter(a => a?.correct).length
|
|
||||||
})
|
|
||||||
|
|
||||||
const scorePct = computed(() => {
|
|
||||||
if (questions.value.length === 0) return 0
|
|
||||||
return Math.round((correctCount.value / questions.value.length) * 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const scoreEmoji = computed(() => {
|
|
||||||
const pct = scorePct.value
|
|
||||||
if (pct >= 90) return "🏆"
|
|
||||||
if (pct >= 70) return "👍"
|
|
||||||
if (pct >= 50) return "💪"
|
|
||||||
return "📖"
|
|
||||||
})
|
|
||||||
|
|
||||||
const scoreTitle = computed(() => {
|
|
||||||
const pct = scorePct.value
|
|
||||||
if (pct >= 90) return "优秀!你对IMA职业道德准则掌握得很好!"
|
|
||||||
if (pct >= 70) return "良好!建议复习一下出错的部分"
|
|
||||||
if (pct >= 50) return "需加强!请回顾IMA四项基本准则"
|
|
||||||
return "建议重新学习IMA职业道德准则"
|
|
||||||
})
|
|
||||||
|
|
||||||
const scoreColor = computed(() => {
|
|
||||||
const pct = scorePct.value
|
|
||||||
if (pct >= 90) return "#67c23a"
|
|
||||||
if (pct >= 70) return "#409eff"
|
|
||||||
if (pct >= 50) return "#e6a23c"
|
|
||||||
return "#f56c6c"
|
|
||||||
})
|
|
||||||
|
|
||||||
const feedbackMessage = computed(() => {
|
|
||||||
if (!selectedOption.value) return ""
|
|
||||||
return selectedOption.value.feedback || (selectedOption.value.correct ? "回答正确!" : "回答错误")
|
|
||||||
})
|
|
||||||
|
|
||||||
async function startQuiz() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data: any = await ethicsQuizApi.getQuestions()
|
|
||||||
questions.value = data || []
|
|
||||||
quizStarted.value = true
|
|
||||||
currentQuestion.value = 0
|
|
||||||
selectedOption.value = null
|
|
||||||
answered.value = false
|
|
||||||
userAnswers.value = []
|
|
||||||
showScore.value = false
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error("加载题目失败,请重试")
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(opt: any) {
|
|
||||||
if (answered.value) return
|
|
||||||
selectedOption.value = opt
|
|
||||||
answered.value = true
|
|
||||||
userAnswers.value[currentQuestion.value] = opt
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextQuestion() {
|
|
||||||
currentQuestion.value++
|
|
||||||
selectedOption.value = null
|
|
||||||
answered.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResult() {
|
|
||||||
showScore.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryQuiz() {
|
|
||||||
quizStarted.value = false
|
|
||||||
questions.value = []
|
|
||||||
currentQuestion.value = 0
|
|
||||||
selectedOption.value = null
|
|
||||||
answered.value = false
|
|
||||||
userAnswers.value = []
|
|
||||||
showScore.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function backToIntro() {
|
|
||||||
quizStarted.value = false
|
|
||||||
showScore.value = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.kb-page { padding: 20px; height: calc(100vh - 60px); overflow-y: auto; }
|
.kb-page { padding: 20px; height: calc(100vh - 60px); overflow-y: auto; }
|
||||||
.kb-tabs { height: 100%; }
|
|
||||||
.kb-tabs :deep(.el-tabs__content) { height: calc(100% - 40px); overflow-y: auto; }
|
|
||||||
.kb-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
.kb-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
.kb-header h3 { margin: 0; font-size: 20px; }
|
.kb-header h3 { margin: 0; font-size: 20px; }
|
||||||
.kb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
.kb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||||
@@ -574,7 +129,7 @@ function backToIntro() {
|
|||||||
.search-title { font-weight: 500; font-size: 14px; flex-shrink: 0; }
|
.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; }
|
.search-summary { font-size: 12px; color: #999; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.empty-search { text-align: center; color: #999; padding: 40px; }
|
.empty-search { text-align: center; color: #999; padding: 40px; }
|
||||||
.detail-content { line-height: 1.8; font-size: 14px; }
|
.detail-content { line-height: 1.8; font-size: 14px; padding: 10px 0; }
|
||||||
.detail-content h4 { margin: 16px 0 8px; }
|
.detail-content h4 { margin: 16px 0 8px; }
|
||||||
.detail-content h5 { margin: 12px 0 6px; color: #666; }
|
.detail-content h5 { margin: 12px 0 6px; color: #666; }
|
||||||
.detail-content ul, .detail-content ol { padding-left: 20px; }
|
.detail-content ul, .detail-content ol { padding-left: 20px; }
|
||||||
@@ -583,108 +138,4 @@ function backToIntro() {
|
|||||||
.detail-content table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
.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, .detail-content td { border: 1px solid #e0e0e0; padding: 8px; text-align: left; }
|
||||||
.detail-content th { background: #f5f5f5; font-weight: 600; }
|
.detail-content th { background: #f5f5f5; font-weight: 600; }
|
||||||
|
|
||||||
/* ── Quiz Intro ── */
|
|
||||||
.quiz-intro { display: flex; justify-content: center; padding: 40px 0; }
|
|
||||||
.quiz-intro-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 40px;
|
|
||||||
max-width: 560px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
|
||||||
}
|
|
||||||
.quiz-intro-card h2 { margin: 0 0 16px; font-size: 24px; color: #303133; }
|
|
||||||
.quiz-intro-card p { color: #606266; font-size: 15px; margin-bottom: 20px; }
|
|
||||||
.quiz-standards { list-style: none; padding: 0; margin: 0 0 32px; text-align: left; }
|
|
||||||
.quiz-standards li { padding: 10px 16px; margin-bottom: 8px; border-radius: 8px; background: #f5f7fa; font-size: 14px; color: #303133; display: flex; align-items: center; gap: 8px; }
|
|
||||||
.std-tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; color: #fff; flex-shrink: 0; }
|
|
||||||
.std-integrity { background: #409eff; }
|
|
||||||
.std-objectivity { background: #67c23a; }
|
|
||||||
.std-confidentiality { background: #e6a23c; }
|
|
||||||
.std-competence { background: #909399; }
|
|
||||||
.quiz-start-btn { min-width: 200px; font-size: 16px; }
|
|
||||||
|
|
||||||
/* ── Quiz Body ── */
|
|
||||||
.quiz-body { max-width: 700px; margin: 0 auto; padding: 20px 0; }
|
|
||||||
|
|
||||||
/* Progress */
|
|
||||||
.quiz-progress { margin-bottom: 24px; }
|
|
||||||
.quiz-progress-text { font-size: 14px; color: #606266; margin-bottom: 8px; font-weight: 500; }
|
|
||||||
|
|
||||||
/* Question Card */
|
|
||||||
.quiz-question-card { background: #fff; border-radius: 12px; padding: 28px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
|
||||||
.q-scenario { margin-bottom: 24px; }
|
|
||||||
.q-number { display: inline-block; background: #409eff; color: #fff; padding: 4px 12px; border-radius: 20px; font-size: 13px; font-weight: 600; margin-bottom: 12px; }
|
|
||||||
.q-scenario p { font-size: 16px; line-height: 1.7; color: #303133; margin: 0; }
|
|
||||||
|
|
||||||
/* Options */
|
|
||||||
.q-options { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; }
|
|
||||||
.q-option {
|
|
||||||
display: flex; align-items: flex-start; gap: 12px;
|
|
||||||
padding: 14px 16px; border-radius: 10px;
|
|
||||||
border: 2px solid #e4e7ed;
|
|
||||||
cursor: pointer; transition: all 0.2s;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.q-option:hover { border-color: #409eff; background: #f0f7ff; }
|
|
||||||
.q-option.selected { border-color: #409eff; background: #ecf5ff; }
|
|
||||||
.q-option.correct { border-color: #67c23a; background: #f0f9eb; }
|
|
||||||
.q-option.wrong { border-color: #f56c6c; background: #fef0f0; }
|
|
||||||
.q-option.disabled { cursor: default; }
|
|
||||||
.opt-id {
|
|
||||||
flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-weight: 600; font-size: 14px;
|
|
||||||
background: #f0f2f5; color: #606266;
|
|
||||||
}
|
|
||||||
.q-option.correct .opt-id { background: #67c23a; color: #fff; }
|
|
||||||
.q-option.wrong .opt-id { background: #f56c6c; color: #fff; }
|
|
||||||
.opt-text { font-size: 15px; line-height: 1.6; color: #303133; padding-top: 3px; }
|
|
||||||
|
|
||||||
/* Feedback */
|
|
||||||
.q-feedback {
|
|
||||||
display: flex; align-items: flex-start; gap: 12px;
|
|
||||||
padding: 16px; border-radius: 10px; margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.feedback-correct { background: #f0f9eb; border: 1px solid #b7eb8f; }
|
|
||||||
.feedback-wrong { background: #fef0f0; border: 1px solid #fbc4c4; }
|
|
||||||
.feedback-icon { font-size: 24px; flex-shrink: 0; }
|
|
||||||
.feedback-text { flex: 1; }
|
|
||||||
.feedback-text strong { display: block; margin-bottom: 4px; font-size: 15px; }
|
|
||||||
.feedback-text p { margin: 0; font-size: 14px; color: #606266; line-height: 1.6; }
|
|
||||||
|
|
||||||
.q-actions { text-align: right; }
|
|
||||||
|
|
||||||
/* Result */
|
|
||||||
.quiz-result { max-width: 700px; margin: 0 auto; padding: 10px 0; }
|
|
||||||
.result-card {
|
|
||||||
background: #fff; border-radius: 16px; padding: 36px;
|
|
||||||
text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.result-emoji { font-size: 56px; margin-bottom: 12px; }
|
|
||||||
.result-title { font-size: 18px; color: #303133; font-weight: 600; margin-bottom: 16px; }
|
|
||||||
.result-score { font-size: 42px; font-weight: 700; color: #409eff; margin-bottom: 16px; }
|
|
||||||
.result-bar { max-width: 400px; margin: 0 auto 20px; }
|
|
||||||
.result-detail { margin-bottom: 24px; }
|
|
||||||
.result-stat { font-size: 15px; margin-bottom: 6px; }
|
|
||||||
.correct-stat { color: #67c23a; }
|
|
||||||
.wrong-stat { color: #f56c6c; }
|
|
||||||
.result-actions { display: flex; gap: 12px; justify-content: center; }
|
|
||||||
|
|
||||||
/* Review */
|
|
||||||
.result-review { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 1px 6px rgba(0,0,0,0.04); }
|
|
||||||
.result-review h4 { margin: 0 0 16px; font-size: 16px; color: #303133; }
|
|
||||||
.review-item {
|
|
||||||
padding: 16px; border-radius: 8px; margin-bottom: 10px;
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
}
|
|
||||||
.review-correct { background: #f0f9eb; border-left-color: #67c23a; }
|
|
||||||
.review-wrong { background: #fef0f0; border-left-color: #f56c6c; }
|
|
||||||
.review-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
|
|
||||||
.review-icon { flex-shrink: 0; font-size: 16px; }
|
|
||||||
.review-q { font-size: 14px; font-weight: 500; line-height: 1.5; }
|
|
||||||
.review-answer, .review-correct-answer { font-size: 13px; color: #606266; margin-bottom: 4px; }
|
|
||||||
.review-feedback { font-size: 13px; color: #e6a23c; background: #fdf6ec; padding: 8px 12px; border-radius: 6px; margin-top: 6px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,221 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="kb-page">
|
<div class="kb-page">
|
||||||
<div class="kb-header">
|
<!-- Tab切换 -->
|
||||||
<h3>📖 CMA知识库</h3>
|
<el-tabs v-model="activeTab" class="kb-tabs">
|
||||||
<el-input v-model="search" placeholder="搜索术语、计算公式、最佳实践..." prefix-icon="Search" clearable style="width:360px;" />
|
<!-- Tab 1: 知识库 -->
|
||||||
</div>
|
<el-tab-pane label="📖 知识库" name="knowledge">
|
||||||
|
<div class="kb-header">
|
||||||
<!-- 搜索结果 -->
|
<h3>CMA知识库</h3>
|
||||||
<div v-if="search" class="search-results">
|
<el-input v-model="search" placeholder="搜索术语、计算公式、最佳实践..." prefix-icon="Search" clearable style="width:360px;" />
|
||||||
<h4>搜索结果({{ searchResults.length }}条)</h4>
|
|
||||||
<div v-for="item in searchResults" :key="item.title" class="search-item" @click="openItem(item)">
|
|
||||||
<span class="search-cat-tag">{{ catLabel(item.category) }}</span>
|
|
||||||
<span class="search-title">{{ item.title }}</span>
|
|
||||||
<span class="search-summary">{{ item.summary }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchResults.length === 0" class="empty-search">未找到相关内容</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分类展示 -->
|
|
||||||
<div v-else class="kb-grid">
|
|
||||||
<div v-for="(cat, ci) in categories" :key="ci" class="kb-category">
|
|
||||||
<div class="cat-head">
|
|
||||||
<span class="cat-icon">{{ cat.icon }}</span>
|
|
||||||
<span class="cat-title">{{ cat.name }}</span>
|
|
||||||
<span class="cat-count">{{ cat.items.length }}篇</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="cat-body">
|
|
||||||
<div v-for="(item, ii) in cat.items" :key="ii" class="kb-item" @click="openItem(item)">
|
<!-- 搜索结果 -->
|
||||||
<span class="kb-item-title">{{ item.title }}</span>
|
<div v-if="search" class="search-results">
|
||||||
<span class="kb-item-summary">{{ item.summary }}</span>
|
<h4>搜索结果({{ searchResults.length }}条)</h4>
|
||||||
|
<div v-for="item in searchResults" :key="item.title" class="search-item" @click="openItem(item)">
|
||||||
|
<span class="search-cat-tag">{{ catLabel(item.category) }}</span>
|
||||||
|
<span class="search-title">{{ item.title }}</span>
|
||||||
|
<span class="search-summary">{{ item.summary }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="searchResults.length === 0" class="empty-search">未找到相关内容</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分类展示 -->
|
||||||
|
<div v-else class="kb-grid">
|
||||||
|
<div v-for="(cat, ci) in categories" :key="ci" class="kb-category">
|
||||||
|
<div class="cat-head">
|
||||||
|
<span class="cat-icon">{{ cat.icon }}</span>
|
||||||
|
<span class="cat-title">{{ cat.name }}</span>
|
||||||
|
<span class="cat-count">{{ cat.items.length }}篇</span>
|
||||||
|
</div>
|
||||||
|
<div class="cat-body">
|
||||||
|
<div v-for="(item, ii) in cat.items" :key="ii" class="kb-item" @click="openItem(item)">
|
||||||
|
<span class="kb-item-title">{{ item.title }}</span>
|
||||||
|
<span class="kb-item-summary">{{ item.summary }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 详情弹窗 -->
|
||||||
<el-dialog v-model="showDetail" :title="detailItem?.title" width="700" top="5vh">
|
<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="detailItem.content"></div>
|
||||||
</el-dialog>
|
<template #footer>
|
||||||
|
<el-button @click="showDetail = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</MyDialog>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Tab 2: 职业道德自测 -->
|
||||||
|
<el-tab-pane label="⚖️ 职业道德自测" name="ethics-quiz">
|
||||||
|
<div v-if="!quizStarted" class="quiz-intro">
|
||||||
|
<div class="quiz-intro-card">
|
||||||
|
<h2>⚖️ CMA职业道德自测</h2>
|
||||||
|
<p>本测试包含 <strong>10道</strong> IMA职业道德情景题,涵盖:</p>
|
||||||
|
<ul class="quiz-standards">
|
||||||
|
<li><span class="std-tag std-integrity">诚信</span> Integrity — 诚实、公正地处理事务</li>
|
||||||
|
<li><span class="std-tag std-objectivity">客观</span> Objectivity — 保持独立性和职业判断</li>
|
||||||
|
<li><span class="std-tag std-confidentiality">保密</span> Confidentiality — 保护公司机密信息</li>
|
||||||
|
<li><span class="std-tag std-competence">胜任</span> Competence — 持续提升专业能力</li>
|
||||||
|
</ul>
|
||||||
|
<el-button type="primary" size="large" @click="startQuiz" class="quiz-start-btn">
|
||||||
|
开始自测
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="quiz-body">
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div class="quiz-progress">
|
||||||
|
<div class="quiz-progress-text">
|
||||||
|
第 {{ currentQuestion + 1 }} / {{ questions.length }} 题
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="progressPct" :stroke-width="8" color="#409eff" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 题目卡片 -->
|
||||||
|
<div class="quiz-question-card" v-if="currentQuestion < questions.length">
|
||||||
|
<div class="q-scenario">
|
||||||
|
<span class="q-number">Q{{ currentQuestion + 1 }}</span>
|
||||||
|
<p>{{ questions[currentQuestion].scenario }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-options">
|
||||||
|
<div
|
||||||
|
v-for="opt in questions[currentQuestion].options"
|
||||||
|
:key="opt.id"
|
||||||
|
class="q-option"
|
||||||
|
:class="{
|
||||||
|
'selected': selectedOption?.id === opt.id,
|
||||||
|
'correct': answered && opt.correct,
|
||||||
|
'wrong': answered && selectedOption?.id === opt.id && !opt.correct,
|
||||||
|
'disabled': answered
|
||||||
|
}"
|
||||||
|
@click="selectOption(opt)"
|
||||||
|
>
|
||||||
|
<span class="opt-id">{{ opt.id.toUpperCase() }}</span>
|
||||||
|
<span class="opt-text">{{ opt.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 反馈 -->
|
||||||
|
<div v-if="answered" class="q-feedback" :class="selectedOption?.correct ? 'feedback-correct' : 'feedback-wrong'">
|
||||||
|
<div class="feedback-icon">{{ selectedOption?.correct ? '✅' : '❌' }}</div>
|
||||||
|
<div class="feedback-text">
|
||||||
|
<strong>{{ selectedOption?.correct ? '正确!' : '选择有误' }}</strong>
|
||||||
|
<p>{{ feedbackMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="q-actions">
|
||||||
|
<el-button v-if="answered && currentQuestion < questions.length - 1" type="primary" @click="nextQuestion">
|
||||||
|
下一题 →
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="answered && currentQuestion === questions.length - 1" type="success" @click="showResult">
|
||||||
|
查看成绩 🏆
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成绩页面 -->
|
||||||
|
<div v-if="showScore" class="quiz-result">
|
||||||
|
<div class="result-card">
|
||||||
|
<div class="result-emoji">{{ scoreEmoji }}</div>
|
||||||
|
<div class="result-title">{{ scoreTitle }}</div>
|
||||||
|
<div class="result-score">{{ correctCount }} / {{ questions.length }}</div>
|
||||||
|
<el-progress :percentage="scorePct" :stroke-width="12" :color="scoreColor" class="result-bar" />
|
||||||
|
<div class="result-detail">
|
||||||
|
<div class="result-stat correct-stat">✅ 正确:{{ correctCount }} 题</div>
|
||||||
|
<div class="result-stat wrong-stat">❌ 错误:{{ questions.length - correctCount }} 题</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-actions">
|
||||||
|
<el-button type="primary" @click="retryQuiz">重新测试</el-button>
|
||||||
|
<el-button @click="backToIntro">返回首页</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 每题回顾 -->
|
||||||
|
<div class="result-review">
|
||||||
|
<h4>逐题回顾</h4>
|
||||||
|
<div v-for="(q, idx) in questions" :key="idx" class="review-item" :class="userAnswers[idx]?.correct ? 'review-correct' : 'review-wrong'">
|
||||||
|
<div class="review-head">
|
||||||
|
<span class="review-icon">{{ userAnswers[idx]?.correct ? '✅' : '❌' }}</span>
|
||||||
|
<span class="review-q">Q{{ idx + 1 }}. {{ q.scenario }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="review-answer">
|
||||||
|
你的选择:<strong>{{ userAnswers[idx]?.text || '未作答' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="review-correct-answer">
|
||||||
|
正确答案:<strong>{{ q.options.find(o => o.correct)?.text }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="review-feedback" v-if="!userAnswers[idx]?.correct">
|
||||||
|
💡 {{ userAnswers[idx]?.feedback || q.options.find(o => o.correct)?.feedback }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue"
|
import { ref, computed } from "vue"
|
||||||
|
import { ElMessage } from "element-plus"
|
||||||
|
import { ethicsQuizApi } from "../api/index"
|
||||||
|
|
||||||
|
// ── Tab ──
|
||||||
|
const activeTab = ref("knowledge")
|
||||||
|
|
||||||
|
// ── 原有知识库逻辑 ──
|
||||||
const search = ref("")
|
const search = ref("")
|
||||||
const showDetail = ref(false)
|
const showDetail = ref(false)
|
||||||
const detailItem = ref<any>(null)
|
const detailItem = ref<any>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 从API加载知识库文章
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
const allArticles = ref<any[]>([])
|
||||||
|
async function loadArticles() {
|
||||||
|
try {
|
||||||
|
const { knowledgeArticleApi } = await import('../api/index')
|
||||||
|
const r: any = await knowledgeArticleApi.list()
|
||||||
|
const articles = r.data || r || []
|
||||||
|
allArticles.value = articles
|
||||||
|
// 按category分组
|
||||||
|
const groupMap: Record<string, any[]> = {}
|
||||||
|
for (const a of articles) {
|
||||||
|
const cat = a.category || '其他'
|
||||||
|
if (!groupMap[cat]) groupMap[cat] = []
|
||||||
|
groupMap[cat].push(a)
|
||||||
|
}
|
||||||
|
const catIcons: Record<string, string> = {
|
||||||
|
'CMA完整文档': '📘', '管理会计方法': '📊', 'KPI设定': '🎯',
|
||||||
|
'OKR': '📋', '其他': '📄',
|
||||||
|
}
|
||||||
|
categories.value = Object.entries(groupMap).map(([key, items]) => ({
|
||||||
|
name: key, icon: catIcons[key] || '📄', key, items,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载知识库失败', e)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const searchResults = ref<any[]>([])
|
||||||
|
watch(search, (val) => {
|
||||||
|
if (!val) { searchResults.value = []; return }
|
||||||
|
const kw = val.toLowerCase()
|
||||||
|
searchResults.value = allArticles.value.filter((a: any) =>
|
||||||
|
(a.title || '').toLowerCase().includes(kw) || (a.category || '').toLowerCase().includes(kw)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderedContent = computed(() => detailItem.value?.content || '')
|
||||||
|
|
||||||
// ── 分类配置 ──
|
|
||||||
const categories = ref([
|
|
||||||
{
|
{
|
||||||
name: "术语解释", icon: "📖", key: "term",
|
name: "术语解释", icon: "📖", key: "term",
|
||||||
items: [
|
items: [
|
||||||
@@ -202,7 +369,7 @@ const categories = ref([
|
|||||||
<ol>
|
<ol>
|
||||||
<li><b>明确战略主题</b>:确定本年度最重要的3-5个战略方向</li>
|
<li><b>明确战略主题</b>:确定本年度最重要的3-5个战略方向</li>
|
||||||
<li><b>从财务开始</b>:先在财务维度设定最终目标(如"提升销售总额30%")</li>
|
<li><b>从财务开始</b>:先在财务维度设定最终目标(如"提升销售总额30%")</li>
|
||||||
<li><b>追溯驱动因素</b>:思考\"要达成财务目标,客户需要什么?流程需要怎么优化?员工需要什么能力?\"</li>
|
<li><b>追溯驱动因素</b>:思考"要达成财务目标,客户需要什么?流程需要怎么优化?员工需要什么能力?"</li>
|
||||||
<li><b>关联KPI</b>:每个目标绑定1-2个可量化的KPI</li>
|
<li><b>关联KPI</b>:每个目标绑定1-2个可量化的KPI</li>
|
||||||
<li><b>画因果连线</b>:从下到上连接各维度目标,形成因果链</li>
|
<li><b>画因果连线</b>:从下到上连接各维度目标,形成因果链</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -291,7 +458,6 @@ const categories = ref([
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// ── 计算属性 ──
|
|
||||||
const searchResults = computed(() => {
|
const searchResults = computed(() => {
|
||||||
if (!search.value.trim()) return []
|
if (!search.value.trim()) return []
|
||||||
const q = search.value.toLowerCase()
|
const q = search.value.toLowerCase()
|
||||||
@@ -310,7 +476,6 @@ const searchResults = computed(() => {
|
|||||||
return results
|
return results
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── 方法 ──
|
|
||||||
function catLabel(key: string) {
|
function catLabel(key: string) {
|
||||||
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
|
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
|
||||||
return map[key] || key
|
return map[key] || key
|
||||||
@@ -320,10 +485,115 @@ function openItem(item: any) {
|
|||||||
detailItem.value = item
|
detailItem.value = item
|
||||||
showDetail.value = true
|
showDetail.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 职业道德自测逻辑 ──
|
||||||
|
|
||||||
|
const questions = ref<any[]>([])
|
||||||
|
const quizStarted = ref(false)
|
||||||
|
const currentQuestion = ref(0)
|
||||||
|
const selectedOption = ref<any>(null)
|
||||||
|
const answered = ref(false)
|
||||||
|
const userAnswers = ref<any[]>([])
|
||||||
|
const showScore = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const progressPct = computed(() => {
|
||||||
|
if (questions.value.length === 0) return 0
|
||||||
|
return ((currentQuestion.value + (answered.value ? 1 : 0)) / questions.value.length) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const correctCount = computed(() => {
|
||||||
|
return userAnswers.value.filter(a => a?.correct).length
|
||||||
|
})
|
||||||
|
|
||||||
|
const scorePct = computed(() => {
|
||||||
|
if (questions.value.length === 0) return 0
|
||||||
|
return Math.round((correctCount.value / questions.value.length) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const scoreEmoji = computed(() => {
|
||||||
|
const pct = scorePct.value
|
||||||
|
if (pct >= 90) return "🏆"
|
||||||
|
if (pct >= 70) return "👍"
|
||||||
|
if (pct >= 50) return "💪"
|
||||||
|
return "📖"
|
||||||
|
})
|
||||||
|
|
||||||
|
const scoreTitle = computed(() => {
|
||||||
|
const pct = scorePct.value
|
||||||
|
if (pct >= 90) return "优秀!你对IMA职业道德准则掌握得很好!"
|
||||||
|
if (pct >= 70) return "良好!建议复习一下出错的部分"
|
||||||
|
if (pct >= 50) return "需加强!请回顾IMA四项基本准则"
|
||||||
|
return "建议重新学习IMA职业道德准则"
|
||||||
|
})
|
||||||
|
|
||||||
|
const scoreColor = computed(() => {
|
||||||
|
const pct = scorePct.value
|
||||||
|
if (pct >= 90) return "#67c23a"
|
||||||
|
if (pct >= 70) return "#409eff"
|
||||||
|
if (pct >= 50) return "#e6a23c"
|
||||||
|
return "#f56c6c"
|
||||||
|
})
|
||||||
|
|
||||||
|
const feedbackMessage = computed(() => {
|
||||||
|
if (!selectedOption.value) return ""
|
||||||
|
return selectedOption.value.feedback || (selectedOption.value.correct ? "回答正确!" : "回答错误")
|
||||||
|
})
|
||||||
|
|
||||||
|
async function startQuiz() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await ethicsQuizApi.getQuestions()
|
||||||
|
questions.value = data || []
|
||||||
|
quizStarted.value = true
|
||||||
|
currentQuestion.value = 0
|
||||||
|
selectedOption.value = null
|
||||||
|
answered.value = false
|
||||||
|
userAnswers.value = []
|
||||||
|
showScore.value = false
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error("加载题目失败,请重试")
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOption(opt: any) {
|
||||||
|
if (answered.value) return
|
||||||
|
selectedOption.value = opt
|
||||||
|
answered.value = true
|
||||||
|
userAnswers.value[currentQuestion.value] = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextQuestion() {
|
||||||
|
currentQuestion.value++
|
||||||
|
selectedOption.value = null
|
||||||
|
answered.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult() {
|
||||||
|
showScore.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryQuiz() {
|
||||||
|
quizStarted.value = false
|
||||||
|
questions.value = []
|
||||||
|
currentQuestion.value = 0
|
||||||
|
selectedOption.value = null
|
||||||
|
answered.value = false
|
||||||
|
userAnswers.value = []
|
||||||
|
showScore.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToIntro() {
|
||||||
|
quizStarted.value = false
|
||||||
|
showScore.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.kb-page { padding: 20px; height: calc(100vh - 60px); overflow-y: auto; }
|
.kb-page { padding: 20px; height: calc(100vh - 60px); overflow-y: auto; }
|
||||||
|
.kb-tabs { height: 100%; }
|
||||||
|
.kb-tabs :deep(.el-tabs__content) { height: calc(100% - 40px); overflow-y: auto; }
|
||||||
.kb-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
.kb-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
.kb-header h3 { margin: 0; font-size: 20px; }
|
.kb-header h3 { margin: 0; font-size: 20px; }
|
||||||
.kb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
.kb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||||
@@ -354,4 +624,108 @@ function openItem(item: any) {
|
|||||||
.detail-content table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
.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, .detail-content td { border: 1px solid #e0e0e0; padding: 8px; text-align: left; }
|
||||||
.detail-content th { background: #f5f5f5; font-weight: 600; }
|
.detail-content th { background: #f5f5f5; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Quiz Intro ── */
|
||||||
|
.quiz-intro { display: flex; justify-content: center; padding: 40px 0; }
|
||||||
|
.quiz-intro-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.quiz-intro-card h2 { margin: 0 0 16px; font-size: 24px; color: #303133; }
|
||||||
|
.quiz-intro-card p { color: #606266; font-size: 15px; margin-bottom: 20px; }
|
||||||
|
.quiz-standards { list-style: none; padding: 0; margin: 0 0 32px; text-align: left; }
|
||||||
|
.quiz-standards li { padding: 10px 16px; margin-bottom: 8px; border-radius: 8px; background: #f5f7fa; font-size: 14px; color: #303133; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.std-tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; color: #fff; flex-shrink: 0; }
|
||||||
|
.std-integrity { background: #409eff; }
|
||||||
|
.std-objectivity { background: #67c23a; }
|
||||||
|
.std-confidentiality { background: #e6a23c; }
|
||||||
|
.std-competence { background: #909399; }
|
||||||
|
.quiz-start-btn { min-width: 200px; font-size: 16px; }
|
||||||
|
|
||||||
|
/* ── Quiz Body ── */
|
||||||
|
.quiz-body { max-width: 700px; margin: 0 auto; padding: 20px 0; }
|
||||||
|
|
||||||
|
/* Progress */
|
||||||
|
.quiz-progress { margin-bottom: 24px; }
|
||||||
|
.quiz-progress-text { font-size: 14px; color: #606266; margin-bottom: 8px; font-weight: 500; }
|
||||||
|
|
||||||
|
/* Question Card */
|
||||||
|
.quiz-question-card { background: #fff; border-radius: 12px; padding: 28px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
||||||
|
.q-scenario { margin-bottom: 24px; }
|
||||||
|
.q-number { display: inline-block; background: #409eff; color: #fff; padding: 4px 12px; border-radius: 20px; font-size: 13px; font-weight: 600; margin-bottom: 12px; }
|
||||||
|
.q-scenario p { font-size: 16px; line-height: 1.7; color: #303133; margin: 0; }
|
||||||
|
|
||||||
|
/* Options */
|
||||||
|
.q-options { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; }
|
||||||
|
.q-option {
|
||||||
|
display: flex; align-items: flex-start; gap: 12px;
|
||||||
|
padding: 14px 16px; border-radius: 10px;
|
||||||
|
border: 2px solid #e4e7ed;
|
||||||
|
cursor: pointer; transition: all 0.2s;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.q-option:hover { border-color: #409eff; background: #f0f7ff; }
|
||||||
|
.q-option.selected { border-color: #409eff; background: #ecf5ff; }
|
||||||
|
.q-option.correct { border-color: #67c23a; background: #f0f9eb; }
|
||||||
|
.q-option.wrong { border-color: #f56c6c; background: #fef0f0; }
|
||||||
|
.q-option.disabled { cursor: default; }
|
||||||
|
.opt-id {
|
||||||
|
flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-weight: 600; font-size: 14px;
|
||||||
|
background: #f0f2f5; color: #606266;
|
||||||
|
}
|
||||||
|
.q-option.correct .opt-id { background: #67c23a; color: #fff; }
|
||||||
|
.q-option.wrong .opt-id { background: #f56c6c; color: #fff; }
|
||||||
|
.opt-text { font-size: 15px; line-height: 1.6; color: #303133; padding-top: 3px; }
|
||||||
|
|
||||||
|
/* Feedback */
|
||||||
|
.q-feedback {
|
||||||
|
display: flex; align-items: flex-start; gap: 12px;
|
||||||
|
padding: 16px; border-radius: 10px; margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.feedback-correct { background: #f0f9eb; border: 1px solid #b7eb8f; }
|
||||||
|
.feedback-wrong { background: #fef0f0; border: 1px solid #fbc4c4; }
|
||||||
|
.feedback-icon { font-size: 24px; flex-shrink: 0; }
|
||||||
|
.feedback-text { flex: 1; }
|
||||||
|
.feedback-text strong { display: block; margin-bottom: 4px; font-size: 15px; }
|
||||||
|
.feedback-text p { margin: 0; font-size: 14px; color: #606266; line-height: 1.6; }
|
||||||
|
|
||||||
|
.q-actions { text-align: right; }
|
||||||
|
|
||||||
|
/* Result */
|
||||||
|
.quiz-result { max-width: 700px; margin: 0 auto; padding: 10px 0; }
|
||||||
|
.result-card {
|
||||||
|
background: #fff; border-radius: 16px; padding: 36px;
|
||||||
|
text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.result-emoji { font-size: 56px; margin-bottom: 12px; }
|
||||||
|
.result-title { font-size: 18px; color: #303133; font-weight: 600; margin-bottom: 16px; }
|
||||||
|
.result-score { font-size: 42px; font-weight: 700; color: #409eff; margin-bottom: 16px; }
|
||||||
|
.result-bar { max-width: 400px; margin: 0 auto 20px; }
|
||||||
|
.result-detail { margin-bottom: 24px; }
|
||||||
|
.result-stat { font-size: 15px; margin-bottom: 6px; }
|
||||||
|
.correct-stat { color: #67c23a; }
|
||||||
|
.wrong-stat { color: #f56c6c; }
|
||||||
|
.result-actions { display: flex; gap: 12px; justify-content: center; }
|
||||||
|
|
||||||
|
/* Review */
|
||||||
|
.result-review { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 1px 6px rgba(0,0,0,0.04); }
|
||||||
|
.result-review h4 { margin: 0 0 16px; font-size: 16px; color: #303133; }
|
||||||
|
.review-item {
|
||||||
|
padding: 16px; border-radius: 8px; margin-bottom: 10px;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
.review-correct { background: #f0f9eb; border-left-color: #67c23a; }
|
||||||
|
.review-wrong { background: #fef0f0; border-left-color: #f56c6c; }
|
||||||
|
.review-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
|
||||||
|
.review-icon { flex-shrink: 0; font-size: 16px; }
|
||||||
|
.review-q { font-size: 14px; font-weight: 500; line-height: 1.5; }
|
||||||
|
.review-answer, .review-correct-answer { font-size: 13px; color: #606266; margin-bottom: 4px; }
|
||||||
|
.review-feedback { font-size: 13px; color: #e6a23c; background: #fdf6ec; padding: 8px 12px; border-radius: 6px; margin-top: 6px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user