feat: Round3 职业道德自测题 — 10道IMA准则情景题
This commit is contained in:
@@ -154,3 +154,105 @@ def trigger_monthly_summary(db: Session = Depends(get_db)):
|
||||
except Exception as e:
|
||||
logger.exception("月度摘要生成失败")
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
# ── CMA P1+P2 Round 3: 职业道德自测题 ──
|
||||
|
||||
ETHICS_QUIZ_DATA = [
|
||||
{
|
||||
"id": 1,
|
||||
"scenario": '你发现公司财务数据异常(现金比率仅0.6%),但总经理说"先别声张,等融资款到账再说",你应该:',
|
||||
"options": [
|
||||
{"id": "a", "text": "听从老板,暂时隐瞒", "correct": False, "feedback": "违反IMA诚信准则。财务人员有义务如实记录和报告"},
|
||||
{"id": "b", "text": "如实记录异常并向上级或审计委员会报告", "correct": True, "feedback": "诚信+客观准则。IMA要求财务人员如实反映财务状况"},
|
||||
{"id": "c", "text": "立即辞职,一走了之", "correct": False, "feedback": "可行但非最优解。应先通过正规渠道报告问题"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"scenario": "竞争对手公司的高管在行业会议上向你打探你们公司的成本数据,你应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "告知对方,反正行业数据都差不多", "correct": False, "feedback": "违反保密准则。成本数据属于公司机密"},
|
||||
{"id": "b", "text": "断然拒绝,并告知这是公司机密", "correct": True, "feedback": "保密准则。财务人员必须保护公司机密信息"},
|
||||
{"id": "c", "text": "用编造的假数据糊弄对方", "correct": False, "feedback": "违反诚信准则。即使对竞争对手也不应提供虚假信息"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"scenario": "你被要求分析一项复杂的金融衍生品对公司的影响,但你没有相关专业知识经验,应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "凭感觉估算一个数值交差", "correct": False, "feedback": "违反胜任准则。不能在自己不擅长的领域出具专业意见"},
|
||||
{"id": "b", "text": "如实告知自己缺乏经验,建议聘请外部专家", "correct": True, "feedback": "胜任准则。IMA要求持续学习并在能力范围内执业"},
|
||||
{"id": "c", "text": "花一个晚上学习后立即出报告", "correct": False, "feedback": "短期内突击学习难以达到专业标准,建议请专家"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"scenario": "供应商送你价值5000元的高档礼品,并暗示希望在招标中获得倾斜,你应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "收下礼物,在招标中适当照顾", "correct": False, "feedback": "违反客观准则。礼品影响职业判断的独立性"},
|
||||
{"id": "b", "text": "拒收礼品并报告公司合规部门", "correct": True, "feedback": "客观准则。财务人员必须保持独立性和客观判断"},
|
||||
{"id": "c", "text": "收下但不做任何特殊照顾", "correct": False, "feedback": "即使不特殊照顾,收礼本身已违反公司廉洁规定"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"scenario": '销售总监要求你将下季度已签约但未发货的订单提前确认为本季度收入,以"完成业绩指标",你应该:',
|
||||
"options": [
|
||||
{"id": "a", "text": "照做,反正迟早会确认", "correct": False, "feedback": "违反诚信准则。收入确认必须符合会计准则的确认条件"},
|
||||
{"id": "b", "text": "拒绝并解释收入确认应遵循权责发生制", "correct": True, "feedback": "诚信+客观准则。IMA要求如实反映交易实质"},
|
||||
{"id": "c", "text": "折中处理,先确认一半", "correct": False, "feedback": "部分确认依然违反会计准则和IMA诚信准则"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"scenario": "你的亲戚是股票投资者,向你打听公司即将发布的季度盈利数据(尚未公开),你应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "暗示性地透露一点,让亲戚自己去判断", "correct": False, "feedback": "违反保密准则。内幕信息在任何情况下都不能泄露"},
|
||||
{"id": "b", "text": "明确拒绝并告知这是未公开的内部信息", "correct": True, "feedback": "保密准则。IMA要求保护信息机密性,禁止内幕交易"},
|
||||
{"id": "c", "text": "告知所有数据需等正式发布后才能讨论", "correct": False, "feedback": "虽然拒绝了但未明确说明信息性质,应更直接拒绝"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"scenario": "你在编制税务筹划方案时,发现最新的税法已经更新,但你还没参加过相关培训,应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "按老法规继续编制,客户不会发现", "correct": False, "feedback": "违反胜任准则。必须掌握最新知识才能提供专业服务"},
|
||||
{"id": "b", "text": "先学习新税法,再基于最新法规编制方案", "correct": True, "feedback": "胜任准则。IMA要求持续提升专业能力,保持知识更新"},
|
||||
{"id": "c", "text": "请客户另请高明", "correct": False, "feedback": "回避问题不可取。应先通过学习解决问题"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"scenario": "月度经营分析会上,CEO要求你只汇报好的指标,不提及存在的风险问题,你应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "听从CEO安排,只报喜不报忧", "correct": False, "feedback": "违反客观准则。选择性披露误导决策"},
|
||||
{"id": "b", "text": "完整披露所有指标,包括存在风险和问题", "correct": True, "feedback": "客观+诚信准则。IMA要求全面、公正地呈现信息"},
|
||||
{"id": "c", "text": "在会上只口头上提一下风险但不出现在书面报告里", "correct": False, "feedback": "口头提及无记录,核心问题仍需书面报告"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"scenario": "为达成KPI目标获得年终奖,部门经理要求你修改部分实际运营数据,你应该:",
|
||||
"options": [
|
||||
{"id": "a", "text": "修改数据,反正金额不大", "correct": False, "feedback": "违反诚信准则。任何金额的数据造假都不可接受"},
|
||||
{"id": "b", "text": "坚决拒绝并向上级或审计委员会举报", "correct": True, "feedback": "诚信准则。诚实是财务人员的首要职业道德要求"},
|
||||
{"id": "c", "text": "假装答应但不执行", "correct": False, "feedback": "不明确的回应可能导致后续施压,应明确拒绝"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"scenario": "你准备离职去竞争对手公司,离职前应该如何处理手头的公司财务数据文件?",
|
||||
"options": [
|
||||
{"id": "a", "text": "拷贝一份带走,作为新公司的参考", "correct": False, "feedback": "违反保密准则。离职不得带走公司机密数据"},
|
||||
{"id": "b", "text": "全部归还公司,并确认不保留任何副本", "correct": True, "feedback": "保密准则。离职后仍须承担保密义务"},
|
||||
{"id": "c", "text": "只带走非敏感性的一般统计资料", "correct": False, "feedback": "任何公司未公开数据均不得外传"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/ethics-quiz")
|
||||
def get_ethics_quiz():
|
||||
"""返回10道IMA职业道德情景自测题"""
|
||||
return ETHICS_QUIZ_DATA
|
||||
|
||||
@@ -220,4 +220,8 @@ export const biReportApi = {
|
||||
exportReport: (data: any) => api.post('/bi-reports/export', data),
|
||||
}
|
||||
|
||||
export const ethicsQuizApi = {
|
||||
getQuestions: () => api.get('/knowledge/ethics-quiz'),
|
||||
}
|
||||
|
||||
export default api
|
||||
|
||||
@@ -1,53 +1,176 @@
|
||||
<template>
|
||||
<div class="kb-page">
|
||||
<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>
|
||||
<!-- 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 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 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>
|
||||
</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-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",
|
||||
@@ -202,7 +325,7 @@ const categories = ref([
|
||||
<ol>
|
||||
<li><b>明确战略主题</b>:确定本年度最重要的3-5个战略方向</li>
|
||||
<li><b>从财务开始</b>:先在财务维度设定最终目标(如"提升销售总额30%")</li>
|
||||
<li><b>追溯驱动因素</b>:思考\"要达成财务目标,客户需要什么?流程需要怎么优化?员工需要什么能力?\"</li>
|
||||
<li><b>追溯驱动因素</b>:思考"要达成财务目标,客户需要什么?流程需要怎么优化?员工需要什么能力?"</li>
|
||||
<li><b>关联KPI</b>:每个目标绑定1-2个可量化的KPI</li>
|
||||
<li><b>画因果连线</b>:从下到上连接各维度目标,形成因果链</li>
|
||||
</ol>
|
||||
@@ -291,7 +414,6 @@ const categories = ref([
|
||||
},
|
||||
])
|
||||
|
||||
// ── 计算属性 ──
|
||||
const searchResults = computed(() => {
|
||||
if (!search.value.trim()) return []
|
||||
const q = search.value.toLowerCase()
|
||||
@@ -310,7 +432,6 @@ const searchResults = computed(() => {
|
||||
return results
|
||||
})
|
||||
|
||||
// ── 方法 ──
|
||||
function catLabel(key: string) {
|
||||
const map: Record<string, string> = { term: "术语", formula: "公式", practice: "实践", faq: "FAQ" }
|
||||
return map[key] || key
|
||||
@@ -320,10 +441,115 @@ 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; }
|
||||
@@ -354,4 +580,108 @@ function openItem(item: any) {
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user