feat: Round3 职业道德自测题 — 10道IMA准则情景题

This commit is contained in:
Hermes CI Fix
2026-07-16 17:30:35 +08:00
parent 9eded3e4fe
commit 75f3f0df77
3 changed files with 473 additions and 37 deletions
+367 -37
View File
@@ -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>