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

688 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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 class="kb-page">
<!-- Tab切换 -->
<el-tabs v-model="activeTab" class="kb-tabs">
<!-- Tab 1: 知识库 -->
<el-tab-pane label="📖 知识库" name="knowledge">
<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">
<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>
<!-- 详情弹窗 -->
<el-dialog v-model="showDetail" :title="detailItem?.title" width="700" top="5vh">
<div v-if="detailItem" class="detail-content" v-html="detailItem.content"></div>
</el-dialog>
</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>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
import { ElMessage } from "element-plus"
import { ethicsQuizApi } from "../api/index"
// ── Tab ──
const activeTab = ref("knowledge")
// ── 原有知识库逻辑 ──
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>KPIKey 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>BSCBalanced 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(() => {
if (!search.value.trim()) return []
const q = search.value.toLowerCase()
const results: any[] = []
for (const cat of categories.value) {
for (const item of cat.items) {
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) {
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
return map[key] || key
}
function openItem(item: any) {
detailItem.value = item
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>
<style scoped>
.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 h3 { margin: 0; font-size: 20px; }
.kb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.kb-category { background: #fff; border-radius: 10px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.cat-head { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #f0f0f0; }
.cat-icon { font-size: 20px; }
.cat-title { font-size: 16px; font-weight: 600; flex: 1; }
.cat-count { font-size: 12px; color: #999; background: #f5f5f5; padding: 2px 8px; border-radius: 10px; }
.cat-body { display: flex; flex-direction: column; gap: 6px; }
.kb-item { padding: 10px; border-radius: 8px; cursor: pointer; transition: background 0.2s; }
.kb-item:hover { background: #f5f7fa; }
.kb-item-title { display: block; font-size: 14px; font-weight: 500; margin-bottom: 2px; }
.kb-item-summary { display: block; font-size: 12px; color: #999; }
.search-results { margin-bottom: 20px; }
.search-results h4 { margin: 0 0 12px 0; font-size: 15px; color: #666; }
.search-item { display: flex; align-items: center; gap: 8px; padding: 12px; border-radius: 8px; cursor: pointer; background: #fff; margin-bottom: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
.search-item:hover { background: #f5f7fa; }
.search-cat-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: #ecf5ff; color: #409eff; 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; }
.empty-search { text-align: center; color: #999; padding: 40px; }
.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; }
.detail-content ul, .detail-content ol { padding-left: 20px; }
.detail-content li { margin-bottom: 4px; }
.detail-content code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.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; }
/* ── 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>