|
|
@@ -0,0 +1,634 @@
|
|
|
+<template>
|
|
|
+ <view class="container" :style="{ '--font-scale': fontScale }">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <text class="nav-title">培训认证中心</text>
|
|
|
+ <view class="nav-sub">按类别独立认证,通过后方可接单</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <BaseLoading :loading="loading" text="加载认证状态..." />
|
|
|
+
|
|
|
+ <template v-if="!loading">
|
|
|
+ <!-- 类别Tab -->
|
|
|
+ <view class="tab-bar" v-if="certifications.length > 0">
|
|
|
+ <scroll-view scroll-x :enhanced="true" show-scrollbar="false" class="tab-scroll">
|
|
|
+ <view class="tab-inner">
|
|
|
+ <view
|
|
|
+ v-for="(cert, idx) in certifications"
|
|
|
+ :key="cert.categoryId"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: activeTabIdx === idx }"
|
|
|
+ @click="switchTab(idx)">
|
|
|
+ <text class="tab-text">{{ cert.categoryName }}</text>
|
|
|
+ <view v-if="cert.isBase" class="base-badge">基础</view>
|
|
|
+ <view v-if="cert.canService" class="service-badge">可接单</view>
|
|
|
+ <view v-else-if="cert.canExam" class="exam-badge">可考核</view>
|
|
|
+ <view v-else class="learning-badge">学习中</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 当前类别详情 -->
|
|
|
+ <view v-if="currentCert" class="cert-detail">
|
|
|
+ <!-- 进度概览卡片 -->
|
|
|
+ <view class="progress-card">
|
|
|
+ <view class="progress-header">
|
|
|
+ <text class="progress-title">{{ currentCert.categoryName }} 认证进度</text>
|
|
|
+ <view class="progress-status" :class="statusClass">
|
|
|
+ {{ statusText }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="progress-bars">
|
|
|
+ <!-- 课时进度 -->
|
|
|
+ <view class="progress-item">
|
|
|
+ <view class="progress-label">
|
|
|
+ <text>课时进度</text>
|
|
|
+ <text>{{ formatHours(currentCert.completedHours) }} / {{ formatHours(currentCert.requiredHours) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="progress-bar-track">
|
|
|
+ <view class="progress-bar-fill" :style="{ width: hoursPercent + '%' }"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 考核状态 -->
|
|
|
+ <view class="progress-item" v-if="currentCert.examStatus !== 'none'">
|
|
|
+ <view class="progress-label">
|
|
|
+ <text>考核状态</text>
|
|
|
+ <text>{{ examStatusText }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 线上课程 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">
|
|
|
+ <text>📚 线上课程(小鹅通)</text>
|
|
|
+ </view>
|
|
|
+ <view class="course-list" v-if="courses.length > 0">
|
|
|
+ <view class="course-item" v-for="course in courses" :key="course.id" @click="goToCourse(course)">
|
|
|
+ <view class="course-info">
|
|
|
+ <text class="course-title">{{ course.title }}</text>
|
|
|
+ <view class="course-meta">
|
|
|
+ <text v-if="course.isRequired" class="required-tag">必修</text>
|
|
|
+ <text>{{ formatHours(course.requiredHours) }} 课时</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="course-progress">
|
|
|
+ <text class="course-open-tag">去学习</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="empty-state" v-else>
|
|
|
+ <text>暂无线上课程</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 线下培训活动 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">
|
|
|
+ <text>🏫 线下培训课程</text>
|
|
|
+ </view>
|
|
|
+ <view class="activity-list" v-if="activities.length > 0">
|
|
|
+ <view class="activity-item" v-for="act in activities" :key="act.id">
|
|
|
+ <view class="activity-info">
|
|
|
+ <text class="activity-title">{{ act.title }}</text>
|
|
|
+ <view class="activity-meta">
|
|
|
+ <text>{{ formatTime(act.startTime) }} · {{ act.location }}</text>
|
|
|
+ <text class="hours-tag">{{ formatHours(act.durationHours) }} 课时</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="activity-actions">
|
|
|
+ <button
|
|
|
+ class="btn-checkin"
|
|
|
+ v-if="!act.checkedIn"
|
|
|
+ @click.stop="doCheckIn(act)"
|
|
|
+ :loading="checkingIn === act.id">
|
|
|
+ 扫码签到
|
|
|
+ </button>
|
|
|
+ <view v-else class="checked-tag">已签到</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="empty-state" v-else>
|
|
|
+ <text>暂无线下培训课程</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 考核入口 -->
|
|
|
+ <view class="section exam-section">
|
|
|
+ <view class="section-title">
|
|
|
+ <text>📝 考核测试</text>
|
|
|
+ </view>
|
|
|
+ <view class="exam-card">
|
|
|
+ <view class="exam-status" v-if="currentCert.examStatus !== 'none'">
|
|
|
+ <text>{{ examStatusText }}</text>
|
|
|
+ <text v-if="currentCert.examScore != null" class="exam-score">分数:{{ currentCert.examScore }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="exam-actions">
|
|
|
+ <button
|
|
|
+ class="btn-exam"
|
|
|
+ v-if="currentCert.canExam && currentCert.examStatus !== 'exam_passed'"
|
|
|
+ @click="startExam"
|
|
|
+ :loading="examLoading">
|
|
|
+ {{ currentCert.examStatus === 'exam_failed' ? '重新考核' : '开始考核' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn-result"
|
|
|
+ v-if="currentCert.examStatus === 'exam_passed'"
|
|
|
+ @click="viewResult">
|
|
|
+ 查看证书
|
|
|
+ </button>
|
|
|
+ <view v-else-if="!currentCert.canExam" class="exam-locked">
|
|
|
+ 完成课时要求({{ formatHours(currentCert.completedHours) }}/{{ formatHours(currentCert.requiredHours) }})后开放考核
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 证书展示 -->
|
|
|
+ <view class="section" v-if="currentCert.status === 'certified' && currentCert.certNo">
|
|
|
+ <view class="section-title">
|
|
|
+ <text>🏆 证书信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="cert-card">
|
|
|
+ <view class="cert-row">
|
|
|
+ <text class="cert-label">证书编号</text>
|
|
|
+ <text class="cert-value">{{ currentCert.certNo }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="cert-row">
|
|
|
+ <text class="cert-label">获得时间</text>
|
|
|
+ <text class="cert-value">{{ formatTime(currentCert.certifiedAt) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="cert-row" v-if="currentCert.expireAt">
|
|
|
+ <text class="cert-label">有效期至</text>
|
|
|
+ <text class="cert-value">{{ formatTime(currentCert.expireAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空态 -->
|
|
|
+ <view class="empty-main" v-else>
|
|
|
+ <text class="empty-icon">📋</text>
|
|
|
+ <text class="empty-text">暂无认证类别,请联系管理员配置</text>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 考核弹窗 -->
|
|
|
+ <view class="modal-mask" v-if="showExamModal" @click="closeExamModal">
|
|
|
+ <view class="modal-exam" @click.stop>
|
|
|
+ <view class="modal-header">
|
|
|
+ <text class="modal-title">{{ currentExamPaper.title || '考核测试' }}</text>
|
|
|
+ <view class="modal-timer" v-if="examTimeLeft > 0">
|
|
|
+ ⏱ 剩余 {{ formatExamTime(examTimeLeft) }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="exam-questions">
|
|
|
+ <view v-for="(q, idx) in examQuestions" :key="q.id" class="question-item">
|
|
|
+ <view class="question-header">
|
|
|
+ <text class="question-num">{{ idx + 1 }}.</text>
|
|
|
+ <text class="question-score">({{ q.score }}分)</text>
|
|
|
+ </view>
|
|
|
+ <text class="question-text">{{ q.question }}</text>
|
|
|
+ <view class="options">
|
|
|
+ <view
|
|
|
+ v-for="(opt, oi) in parseOptions(q.options)"
|
|
|
+ :key="oi"
|
|
|
+ class="option"
|
|
|
+ :class="{ selected: userAnswers[q.id] === opt.value }"
|
|
|
+ @click="selectOption(q.id, opt.value)">
|
|
|
+ <text class="opt-label">{{ opt.label }}.</text>
|
|
|
+ <text class="opt-text">{{ opt.text }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="modal-footer">
|
|
|
+ <button class="btn-submit" @click="submitExam" :loading="submittingExam" :disabled="!canSubmit">
|
|
|
+ 提交试卷
|
|
|
+ </button>
|
|
|
+ <button class="btn-cancel" @click="closeExamModal">取消</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 证书查看弹窗 -->
|
|
|
+ <view class="modal-mask" v-if="showCertModal" @click="closeCertModal">
|
|
|
+ <view class="modal-cert" @click.stop>
|
|
|
+ <view class="cert-modal-header">
|
|
|
+ <text class="cert-modal-title">{{ currentCert.categoryName }} 认证证书</text>
|
|
|
+ </view>
|
|
|
+ <view class="cert-modal-body">
|
|
|
+ <view class="cert-modal-no">{{ currentCert.certNo }}</view>
|
|
|
+ <view class="cert-modal-name">{{ userNickname }}</view>
|
|
|
+ <view class="cert-modal-date">颁发日期:{{ formatTime(currentCert.certifiedAt) }}</view>
|
|
|
+ </view>
|
|
|
+ <button class="btn-close-modal" @click="closeCertModal">关闭</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
+import BaseLoading from '@/components/BaseLoading.vue'
|
|
|
+import {
|
|
|
+ getPlannerCertifications,
|
|
|
+ getPlannerCourses,
|
|
|
+ getPlannerTrainingActivities,
|
|
|
+ plannerTrainingCheckIn,
|
|
|
+ plannerExamStart,
|
|
|
+ plannerExamSubmit
|
|
|
+} from '@/utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ mixins: [fontScaleMixin],
|
|
|
+ components: {
|
|
|
+ BaseLoading
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: true,
|
|
|
+ certifications: [],
|
|
|
+ activeTabIdx: 0,
|
|
|
+ courses: [],
|
|
|
+ activities: [],
|
|
|
+ currentExamPaper: null,
|
|
|
+ examQuestions: [],
|
|
|
+ userAnswers: {},
|
|
|
+ examTimeLeft: 0,
|
|
|
+ examTimer: null,
|
|
|
+ showExamModal: false,
|
|
|
+ showCertModal: false,
|
|
|
+ examLoading: false,
|
|
|
+ submittingExam: false,
|
|
|
+ checkingIn: null,
|
|
|
+ userNickname: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ currentCert() {
|
|
|
+ return this.certifications[this.activeTabIdx] || null
|
|
|
+ },
|
|
|
+ statusClass() {
|
|
|
+ if (!this.currentCert) return ''
|
|
|
+ const status = this.currentCert.status
|
|
|
+ if (status === 'certified') return 'status-certified'
|
|
|
+ if (status === 'learning_completed') return 'status-ready-exam'
|
|
|
+ if (status === 'exam_failed') return 'status-exam-failed'
|
|
|
+ return 'status-learning'
|
|
|
+ },
|
|
|
+ statusText() {
|
|
|
+ if (!this.currentCert) return ''
|
|
|
+ const map = {
|
|
|
+ 'pending_cert': '待认证',
|
|
|
+ 'pending_learning': '学习中',
|
|
|
+ 'learning_completed': '可参加考核',
|
|
|
+ 'certified': '已认证',
|
|
|
+ 'suspended': '已暂停',
|
|
|
+ 'exam_failed': '考核未通过'
|
|
|
+ }
|
|
|
+ return map[this.currentCert.status] || this.currentCert.status
|
|
|
+ },
|
|
|
+ examStatusText() {
|
|
|
+ if (!this.currentCert) return ''
|
|
|
+ const map = {
|
|
|
+ 'none': '未参加',
|
|
|
+ 'exam_failed': '未通过',
|
|
|
+ 'exam_passed': '已通过'
|
|
|
+ }
|
|
|
+ return map[this.currentCert.examStatus] || this.currentCert.examStatus
|
|
|
+ },
|
|
|
+ hoursPercent() {
|
|
|
+ if (!this.currentCert || !this.currentCert.requiredHours) return 0
|
|
|
+ const completed = parseFloat(this.currentCert.completedHours) || 0
|
|
|
+ const required = parseFloat(this.currentCert.requiredHours) || 1
|
|
|
+ return Math.min(100, Math.round((completed / required) * 100))
|
|
|
+ },
|
|
|
+ canSubmit() {
|
|
|
+ return Object.keys(this.userAnswers).length === this.examQuestions.length
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.loadUserInfo()
|
|
|
+ this.loadCertifications()
|
|
|
+ },
|
|
|
+ onShow() {
|
|
|
+ this.loadUserInfo()
|
|
|
+ },
|
|
|
+ onUnload() {
|
|
|
+ this.clearExamTimer()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loadUserInfo() {
|
|
|
+ const userInfo = uni.getStorageSync('userInfo')
|
|
|
+ this.userNickname = (userInfo && userInfo.nickname) || ''
|
|
|
+ },
|
|
|
+ async loadCertifications() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await getPlannerCertifications()
|
|
|
+ if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
+ this.certifications = res.data
|
|
|
+ this.activeTabIdx = 0
|
|
|
+ await this.loadCategoryData(this.certifications[0])
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载认证状态失败', e)
|
|
|
+ uni.showToast({ title: '加载失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadCategoryData(cert) {
|
|
|
+ if (!cert) return
|
|
|
+ // 并行加载课程和活动
|
|
|
+ await Promise.all([
|
|
|
+ this.loadCourses(cert.categoryId),
|
|
|
+ this.loadActivities()
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ async loadCourses(categoryId) {
|
|
|
+ try {
|
|
|
+ const res = await getPlannerCourses(categoryId)
|
|
|
+ if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
+ this.courses = res.data
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载课程失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadActivities() {
|
|
|
+ try {
|
|
|
+ const res = await getPlannerTrainingActivities()
|
|
|
+ if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
+ this.activities = res.data
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载活动失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ switchTab(idx) {
|
|
|
+ if (idx === this.activeTabIdx) return
|
|
|
+ this.activeTabIdx = idx
|
|
|
+ this.loadCategoryData(this.certifications[idx])
|
|
|
+ },
|
|
|
+ formatHours(val) {
|
|
|
+ if (val == null) return '0'
|
|
|
+ const num = parseFloat(val)
|
|
|
+ return num === Math.floor(num) ? num.toString() : num.toFixed(1)
|
|
|
+ },
|
|
|
+ formatTime(str) {
|
|
|
+ if (!str) return '-'
|
|
|
+ return str.replace('T', ' ').substring(0, 16)
|
|
|
+ },
|
|
|
+ goToCourse(course) {
|
|
|
+ if (course.xiaoeResourceId) {
|
|
|
+ // TODO: 跳转小鹅通小程序/H5
|
|
|
+ uni.showToast({ title: '即将跳转小鹅通', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async doCheckIn(activity) {
|
|
|
+ this.checkingIn = activity.id
|
|
|
+ try {
|
|
|
+ const res = await plannerTrainingCheckIn(activity.id)
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '签到成功,课时已计入', icon: 'success' })
|
|
|
+ activity.checkedIn = true
|
|
|
+ // 刷新当前类别进度
|
|
|
+ const cert = this.certifications[this.activeTabIdx]
|
|
|
+ if (cert && cert.categoryId === activity.categoryId) {
|
|
|
+ await this.loadCategoryData(cert)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '签到失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('签到失败', e)
|
|
|
+ uni.showToast({ title: '签到失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.checkingIn = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async startExam() {
|
|
|
+ if (!this.currentCert) return
|
|
|
+ this.examLoading = true
|
|
|
+ try {
|
|
|
+ const res = await plannerExamStart(this.currentCert.categoryId)
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ const data = res.data
|
|
|
+ this.currentExamPaper = { id: data.paperId, title: data.paperTitle || '考核试卷', durationMinutes: data.durationMinutes, passScore: data.passScore, attemptId: data.attemptId }
|
|
|
+ this.examQuestions = data.questions || []
|
|
|
+ this.userAnswers = {}
|
|
|
+ this.examTimeLeft = (data.durationMinutes || 30) * 60
|
|
|
+ this.showExamModal = true
|
|
|
+ this.startExamTimer()
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '获取试卷失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('开始考核失败', e)
|
|
|
+ uni.showToast({ title: '开始考核失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.examLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ startExamTimer() {
|
|
|
+ this.clearExamTimer()
|
|
|
+ this.examTimer = setInterval(() => {
|
|
|
+ if (this.examTimeLeft > 0) {
|
|
|
+ this.examTimeLeft--
|
|
|
+ } else {
|
|
|
+ this.clearExamTimer()
|
|
|
+ this.submitExam()
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ },
|
|
|
+ clearExamTimer() {
|
|
|
+ if (this.examTimer) {
|
|
|
+ clearInterval(this.examTimer)
|
|
|
+ this.examTimer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ parseOptions(optionsStr) {
|
|
|
+ try {
|
|
|
+ const arr = JSON.parse(optionsStr || '[]')
|
|
|
+ return arr.map((opt, i) => ({ label: String.fromCharCode(65 + i), value: String.fromCharCode(65 + i), text: opt }))
|
|
|
+ } catch (e) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ selectOption(questionId, value) {
|
|
|
+ this.$set(this.userAnswers, questionId, value)
|
|
|
+ },
|
|
|
+ async submitExam() {
|
|
|
+ this.clearExamTimer()
|
|
|
+ if (!this.currentExamPaper) return
|
|
|
+ this.submittingExam = true
|
|
|
+ try {
|
|
|
+ const res = await plannerExamSubmit(
|
|
|
+ this.currentExamPaper.attemptId || this.currentExamPaper.id,
|
|
|
+ JSON.stringify(this.userAnswers)
|
|
|
+ )
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: res.data.passed ? '考核通过!' : '考核未通过,请重新学习', icon: res.data.passed ? 'success' : 'none' })
|
|
|
+ this.showExamModal = false
|
|
|
+ this.examQuestions = []
|
|
|
+ this.userAnswers = {}
|
|
|
+ // 刷新认证状态
|
|
|
+ await this.loadCertifications()
|
|
|
+ if (res.data.passed && this.currentCert) {
|
|
|
+ this.showCertModal = true
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '提交失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('提交考核失败', e)
|
|
|
+ uni.showToast({ title: '提交失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.submittingExam = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ viewResult() {
|
|
|
+ this.showCertModal = true
|
|
|
+ },
|
|
|
+ closeExamModal() {
|
|
|
+ this.clearExamTimer()
|
|
|
+ this.showExamModal = false
|
|
|
+ },
|
|
|
+ closeCertModal() {
|
|
|
+ this.showCertModal = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.container {
|
|
|
+ background: $tint-sky-ghost;
|
|
|
+ min-height: 100vh;
|
|
|
+ padding-bottom: 80rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-bar {
|
|
|
+ background: linear-gradient(135deg, $brand-primary, $brand-secondary);
|
|
|
+ padding: 32rpx 24rpx;
|
|
|
+ color: $app-surface;
|
|
|
+}
|
|
|
+.nav-title { font-size: calc(36rpx * var(--font-scale, 1)); font-weight: bold; display: block; margin-bottom: 8rpx; }
|
|
|
+.nav-sub { font-size: calc(24rpx * var(--font-scale, 1)); opacity: 0.9; }
|
|
|
+
|
|
|
+.tab-bar { background: $app-surface; border-bottom: 1rpx solid $facade-border-soft; position: sticky; top: 0; z-index: 10; }
|
|
|
+.tab-scroll { padding: 0 24rpx; }
|
|
|
+.tab-inner { display: inline-flex; gap: 16rpx; padding: 16rpx 0; }
|
|
|
+.tab-item { display: flex; flex-direction: column; align-items: center; padding: 8rpx 16rpx; border-radius: 20rpx; background: $facade-fill; transition: background-color 0.2s, color 0.2s; min-width: 120rpx; }
|
|
|
+.tab-item.active { background: $brand-primary; color: $app-surface; }
|
|
|
+.tab-text { font-size: calc(26rpx * var(--font-scale, 1)); font-weight: 500; }
|
|
|
+.tab-item .base-badge { font-size: calc(20rpx * var(--font-scale, 1)); padding: 2rpx 8rpx; border-radius: 10rpx; background: $status-warning-soft; color: $status-warning-deep; margin-top: 4rpx; }
|
|
|
+.tab-item .service-badge { font-size: calc(20rpx * var(--font-scale, 1)); padding: 2rpx 8rpx; border-radius: 10rpx; background: $status-success-tint; color: $status-success-deep; margin-top: 4rpx; }
|
|
|
+.tab-item .exam-badge { font-size: calc(20rpx * var(--font-scale, 1)); padding: 2rpx 8rpx; border-radius: 10rpx; background: $status-info-soft; color: $status-info-deep; margin-top: 4rpx; }
|
|
|
+.tab-item .learning-badge { font-size: calc(20rpx * var(--font-scale, 1)); padding: 2rpx 8rpx; border-radius: 10rpx; background: $facade-fill-2; color: $facade-text-muted; margin-top: 4rpx; }
|
|
|
+
|
|
|
+.cert-detail { padding: 24rpx; }
|
|
|
+
|
|
|
+.progress-card { background: $app-surface; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
|
|
|
+.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
|
|
+.progress-title { font-size: calc(30rpx * var(--font-scale, 1)); font-weight: 600; color: $facade-text-strong; }
|
|
|
+.progress-status { font-size: calc(24rpx * var(--font-scale, 1)); padding: 4rpx 16rpx; border-radius: 16rpx; font-weight: 500; }
|
|
|
+.status-certified { background: $status-success-tint; color: $status-success-deep; }
|
|
|
+.status-ready-exam { background: $status-info-soft; color: $status-info-deep; }
|
|
|
+.status-exam-failed { background: $status-danger-tint-2; color: $status-danger-deep; }
|
|
|
+.status-learning { background: $status-warning-soft; color: $status-warning-deep; }
|
|
|
+
|
|
|
+.progress-bars { display: flex; flex-direction: column; gap: 16rpx; }
|
|
|
+.progress-item { }
|
|
|
+.progress-label { display: flex; justify-content: space-between; font-size: calc(24rpx * var(--font-scale, 1)); margin-bottom: 8rpx; color: $facade-text-muted; }
|
|
|
+.progress-bar-track { height: 8rpx; background: $facade-border-soft; border-radius: 4rpx; overflow: hidden; }
|
|
|
+.progress-bar-fill { height: 100%; background: linear-gradient(90deg, $brand-primary, $brand-secondary); border-radius: 4rpx; transition: width 0.3s; }
|
|
|
+.mini-bar { height: 4rpx; background: $facade-border-soft; border-radius: 2rpx; margin-top: 4rpx; overflow: hidden; }
|
|
|
+.mini-fill { height: 100%; background: $brand-primary; border-radius: 2rpx; }
|
|
|
+
|
|
|
+.section { background: $app-surface; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
|
|
|
+.section-title { font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 600; color: $facade-text-strong; margin-bottom: 16rpx; }
|
|
|
+
|
|
|
+.course-list { display: flex; flex-direction: column; gap: 12rpx; }
|
|
|
+.course-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: $facade-fill; border-radius: 12rpx; }
|
|
|
+.course-info { flex: 1; }
|
|
|
+.course-title { display: block; font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 500; color: $facade-text-strong; margin-bottom: 4rpx; }
|
|
|
+.course-meta { display: flex; gap: 12rpx; font-size: calc(22rpx * var(--font-scale, 1)); color: $facade-text-muted; }
|
|
|
+.required-tag { background: $status-warning-soft; color: $status-warning-deep; padding: 2rpx 6rpx; border-radius: 4rpx; font-size: calc(20rpx * var(--font-scale, 1)); }
|
|
|
+.course-open-tag { font-size: calc(24rpx * var(--font-scale, 1)); color: $brand-primary; font-weight: 500; }
|
|
|
+.mini-bar { height: 4rpx; background: $facade-border-soft; border-radius: 2rpx; margin-top: 4rpx; }
|
|
|
+.mini-fill { height: 100%; background: $brand-primary; border-radius: 2rpx; }
|
|
|
+
|
|
|
+.activity-list { display: flex; flex-direction: column; gap: 12rpx; }
|
|
|
+.activity-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: $facade-fill; border-radius: 12rpx; flex-wrap: wrap; gap: 12rpx; }
|
|
|
+.activity-info { flex: 1; min-width: 200rpx; }
|
|
|
+.activity-title { display: block; font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 500; color: $facade-text-strong; margin-bottom: 4rpx; }
|
|
|
+.activity-meta { display: flex; gap: 12rpx; font-size: calc(22rpx * var(--font-scale, 1)); color: $facade-text-muted; flex-wrap: wrap; }
|
|
|
+.hours-tag { background: $brand-primary-soft; color: $brand-primary-deep; padding: 2rpx 8rpx; border-radius: 4rpx; font-size: calc(20rpx * var(--font-scale, 1)); }
|
|
|
+.activity-actions { display: flex; gap: 8rpx; }
|
|
|
+.btn-checkin { background: $brand-primary; color: $app-surface; padding: 10rpx 20rpx; border-radius: 8rpx; font-size: calc(24rpx * var(--font-scale, 1)); border: none; }
|
|
|
+.btn-checkin:disabled { opacity: 0.6; }
|
|
|
+.checked-tag { font-size: calc(24rpx * var(--font-scale, 1)); color: $status-success-deep; background: $status-success-tint; padding: 8rpx 16rpx; border-radius: 8rpx; }
|
|
|
+
|
|
|
+.exam-section { background: $app-surface; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); border-left: 4rpx solid $brand-primary; }
|
|
|
+.exam-card { background: $facade-fill; border-radius: 12rpx; padding: 20rpx; }
|
|
|
+.exam-status { font-size: calc(26rpx * var(--font-scale, 1)); font-weight: 500; margin-bottom: 12rpx; }
|
|
|
+.exam-status .exam-score { margin-left: 16rpx; color: $brand-primary; }
|
|
|
+.exam-actions { display: flex; gap: 12rpx; flex-wrap: wrap; align-items: center; }
|
|
|
+.btn-exam { background: $status-info; color: $app-surface; padding: 14rpx 28rpx; border-radius: 8rpx; font-size: calc(26rpx * var(--font-scale, 1)); font-weight: 500; border: none; }
|
|
|
+.btn-exam:disabled { opacity: 0.6; }
|
|
|
+.btn-result { background: $brand-primary; color: $app-surface; padding: 14rpx 28rpx; border-radius: 8rpx; font-size: calc(26rpx * var(--font-scale, 1)); font-weight: 500; border: none; }
|
|
|
+.exam-locked { font-size: calc(24rpx * var(--font-scale, 1)); color: $facade-text-muted; flex: 1; }
|
|
|
+
|
|
|
+.cert-card { background: linear-gradient(135deg, $brand-primary, $brand-secondary); border-radius: 16rpx; padding: 24rpx; color: $app-surface; }
|
|
|
+.cert-row { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid rgba(255,255,255,0.2); }
|
|
|
+.cert-row:last-child { border-bottom: none; }
|
|
|
+.cert-label { font-size: calc(24rpx * var(--font-scale, 1)); opacity: 0.8; }
|
|
|
+.cert-value { font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 600; }
|
|
|
+
|
|
|
+.empty-state { text-align: center; padding: 40rpx; color: $facade-text-muted; font-size: calc(26rpx * var(--font-scale, 1)); }
|
|
|
+.empty-main { text-align: center; padding: 120rpx 24rpx; }
|
|
|
+.empty-icon { font-size: calc(80rpx * var(--font-scale, 1)); display: block; margin-bottom: 16rpx; }
|
|
|
+.empty-text { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-muted; }
|
|
|
+
|
|
|
+/* 考核弹窗 */
|
|
|
+.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 24rpx; }
|
|
|
+.modal-exam { background: $app-surface; border-radius: 16rpx; width: 100%; max-width: 680rpx; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
|
|
|
+.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 24rpx; border-bottom: 1rpx solid $facade-border-soft; }
|
|
|
+.modal-title { font-size: calc(30rpx * var(--font-scale, 1)); font-weight: 600; }
|
|
|
+.modal-timer { font-size: calc(24rpx * var(--font-scale, 1)); color: $status-danger-deep; background: $status-danger-tint-2; padding: 4rpx 12rpx; border-radius: 12rpx; }
|
|
|
+.exam-questions { flex: 1; overflow-y: auto; padding: 20rpx 24rpx; }
|
|
|
+.question-item { margin-bottom: 24rpx; }
|
|
|
+.question-header { display: flex; align-items: center; gap: 8rpx; margin-bottom: 8rpx; font-size: calc(24rpx * var(--font-scale, 1)); color: $facade-text-muted; }
|
|
|
+.question-num { font-weight: 600; color: $facade-text-strong; }
|
|
|
+.question-text { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-strong; margin-bottom: 12rpx; line-height: 1.5; }
|
|
|
+.options { display: flex; flex-direction: column; gap: 8rpx; }
|
|
|
+.option { display: flex; align-items: center; gap: 12rpx; padding: 16rpx; background: $facade-fill; border-radius: 8rpx; border: 2rpx solid transparent; transition: background-color 0.2s, border-color 0.2s; }
|
|
|
+.option.selected { border-color: $brand-primary; background: $brand-primary-soft; }
|
|
|
+.option:active { transform: scale(0.99); }
|
|
|
+.opt-label { width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid $facade-border-soft; display: flex; align-items: center; justify-content: center; font-size: calc(24rpx * var(--font-scale, 1)); font-weight: bold; color: $facade-text-muted; }
|
|
|
+.option.selected .opt-label { border-color: $brand-primary; background: $brand-primary; color: $app-surface; }
|
|
|
+.opt-text { font-size: calc(26rpx * var(--font-scale, 1)); color: $facade-text-strong; flex: 1; }
|
|
|
+.modal-footer { display: flex; gap: 12rpx; padding: 20rpx 24rpx; border-top: 1rpx solid $facade-border-soft; }
|
|
|
+.btn-submit { flex: 1; background: $brand-primary; color: $app-surface; padding: 16rpx; border-radius: 8rpx; font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 500; border: none; }
|
|
|
+.btn-submit:disabled { opacity: 0.5; }
|
|
|
+.btn-cancel { background: $facade-fill-2; color: $facade-text-strong; padding: 16rpx 28rpx; border-radius: 8rpx; font-size: calc(28rpx * var(--font-scale, 1)); border: none; }
|
|
|
+
|
|
|
+/* 证书弹窗 */
|
|
|
+.modal-cert { background: $app-surface; border-radius: 16rpx; width: 100%; max-width: 560rpx; padding: 32rpx 24rpx; text-align: center; }
|
|
|
+.cert-modal-header { margin-bottom: 24rpx; }
|
|
|
+.cert-modal-title { font-size: calc(32rpx * var(--font-scale, 1)); font-weight: 600; color: $facade-text-strong; }
|
|
|
+.cert-modal-body { background: linear-gradient(135deg, $brand-primary, $brand-secondary); border-radius: 16rpx; padding: 32rpx; color: $app-surface; margin-bottom: 24rpx; }
|
|
|
+.cert-modal-no { font-size: calc(26rpx * var(--font-scale, 1)); opacity: 0.8; margin-bottom: 16rpx; letter-spacing: 2rpx; }
|
|
|
+.cert-modal-name { font-size: calc(40rpx * var(--font-scale, 1)); font-weight: bold; margin-bottom: 16rpx; }
|
|
|
+.cert-modal-date { font-size: calc(24rpx * var(--font-scale, 1)); opacity: 0.8; }
|
|
|
+.btn-close-modal { background: $brand-primary; color: $app-surface; padding: 16rpx 48rpx; border-radius: 8rpx; font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 500; border: none; width: 100%; }
|
|
|
+</style>
|