|
@@ -0,0 +1,523 @@
|
|
|
|
|
+<!-- 学习课件 - 小程序webview版 -->
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="learningMaterials">
|
|
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
|
|
+ <div class="nav-bar">
|
|
|
|
|
+ <p class="nav-title">{{ newData.coursewareName }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 课件展示区 -->
|
|
|
|
|
+ <div class="courseware-box">
|
|
|
|
|
+ <!-- 视频 -->
|
|
|
|
|
+ <video
|
|
|
|
|
+ v-if="officeType === 'video'"
|
|
|
|
|
+ class="video-player"
|
|
|
|
|
+ :src="lookUrl"
|
|
|
|
|
+ controls
|
|
|
|
|
+ playsinline
|
|
|
|
|
+ webkit-playsinline
|
|
|
|
|
+ x5-playsinline
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ <!-- 富文本/文章 -->
|
|
|
|
|
+ <div v-else-if="officeType === 'text'" class="rich-text-box" v-html="lookUrl"></div>
|
|
|
|
|
+ <!-- 文档(docx/excel/pdf) -->
|
|
|
|
|
+ <div v-else-if="officeType === 'docx' || officeType === 'excel' || officeType === 'pdf'" class="office-box">
|
|
|
|
|
+ <vue-office-docx
|
|
|
|
|
+ v-if="officeType === 'docx' && !officeNullType"
|
|
|
|
|
+ :src="lookUrl"
|
|
|
|
|
+ @rendered="renderedHandler"
|
|
|
|
|
+ @error="errorHandler"
|
|
|
|
|
+ />
|
|
|
|
|
+ <vue-office-excel
|
|
|
|
|
+ v-if="officeType === 'excel' && !officeNullType"
|
|
|
|
|
+ :src="lookUrl"
|
|
|
|
|
+ @rendered="renderedHandler"
|
|
|
|
|
+ @error="errorHandler"
|
|
|
|
|
+ />
|
|
|
|
|
+ <vue-office-pdf
|
|
|
|
|
+ v-if="officeType === 'pdf' && !officeNullType"
|
|
|
|
|
+ :src="lookUrl"
|
|
|
|
|
+ @rendered="renderedHandler"
|
|
|
|
|
+ @error="errorHandler"
|
|
|
|
|
+ />
|
|
|
|
|
+ <p v-if="officeNullType" class="load-fail-text">加载失败...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 空态 -->
|
|
|
|
|
+ <div v-else class="empty-box">
|
|
|
|
|
+ <p>课件加载中...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 底部学习信息 -->
|
|
|
|
|
+ <div class="info-box">
|
|
|
|
|
+ <div class="info-meta">
|
|
|
|
|
+ <span>{{ secondsToMinutes(newData.minLearningDuration) }}分钟</span>
|
|
|
|
|
+ <span class="dot">·</span>
|
|
|
|
|
+ <span>{{ newData.creditHours }}学时</span>
|
|
|
|
|
+ <span class="dot">·</span>
|
|
|
|
|
+ <span>{{ newData.points }}积分</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!--<div class="progress-row">-->
|
|
|
|
|
+ <!--<span class="progress-label">学习进度</span>-->
|
|
|
|
|
+ <!--<div class="progress-bar-wrap">-->
|
|
|
|
|
+ <!--<div class="progress-bar-track">-->
|
|
|
|
|
+ <!--<div class="progress-bar-fill" :style="{ width: (newData.progress || 0) + '%' }"></div>-->
|
|
|
|
|
+ <!--</div>-->
|
|
|
|
|
+ <!--<span class="progress-pct">{{ newData.progress || 0 }}%</span>-->
|
|
|
|
|
+ <!--</div>-->
|
|
|
|
|
+ <!--</div>-->
|
|
|
|
|
+ <div class="time-row">
|
|
|
|
|
+ <span class="time-text">
|
|
|
|
|
+ 已学:{{ formatTime(currentLearnedDuration) }} / {{ formatTime(newData.minLearningDuration) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span
|
|
|
|
|
+ :class="['status-tag',
|
|
|
|
|
+ learnStatus === 2 ? 'status-done' :
|
|
|
|
|
+ learnStatus === 1 ? 'status-ing' : 'status-none'
|
|
|
|
|
+ ]"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ learnStatus === 2 ? '已学完' : (learnStatus === 1 ? '学习中' : '未学完') }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 确认完成按钮 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="confirm-btn"
|
|
|
|
|
+ v-if="learnStatus !== 2 && currentLearnedDuration >= newData.minLearningDuration && newData.minLearningDuration > 0"
|
|
|
|
|
+ @click="finishStudying"
|
|
|
|
|
+ >
|
|
|
|
|
+ 若已完成学习,请点此确认
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+<script>
|
|
|
|
|
+ import {judgmentNetworkReturnAddress} from "@/utils/ruoyi";
|
|
|
|
|
+ import VueOfficeDocx from '@vue-office/docx'
|
|
|
|
|
+ import '@vue-office/docx/lib/index.css'
|
|
|
|
|
+ import VueOfficeExcel from '@vue-office/excel'
|
|
|
|
|
+ import '@vue-office/excel/lib/index.css'
|
|
|
|
|
+ import VueOfficePdf from '@vue-office/pdf'
|
|
|
|
|
+ import { Toast, Dialog } from 'vant'
|
|
|
|
|
+ import {
|
|
|
|
|
+ examUserCoursewareLearningDetail,
|
|
|
|
|
+ examUserCoursewareLearningReportProgress,
|
|
|
|
|
+ examUserCoursewareLearningConfirmCompletion,
|
|
|
|
|
+ } from '@/api/index.js'
|
|
|
|
|
+
|
|
|
|
|
+ export default {
|
|
|
|
|
+ name: 'learningMaterials',
|
|
|
|
|
+ props:{
|
|
|
|
|
+ learningMaterialsData:{},
|
|
|
|
|
+ },
|
|
|
|
|
+ components: {
|
|
|
|
|
+ VueOfficeDocx,
|
|
|
|
|
+ VueOfficeExcel,
|
|
|
|
|
+ VueOfficePdf,
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ coursewareId: null,
|
|
|
|
|
+ newData: {
|
|
|
|
|
+ coursewareName: '',
|
|
|
|
|
+ minLearningDuration: 0,
|
|
|
|
|
+ creditHours: 0,
|
|
|
|
|
+ points: 0,
|
|
|
|
|
+ progress: 0,
|
|
|
|
|
+ attachmentPath: '',
|
|
|
|
|
+ attachmentType: '',
|
|
|
|
|
+ materialType: null,
|
|
|
|
|
+ articleContent: '',
|
|
|
|
|
+ learnedDuration: 0,
|
|
|
|
|
+ learnStatus: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ // 课件展示
|
|
|
|
|
+ officeType: null,
|
|
|
|
|
+ lookUrl: '',
|
|
|
|
|
+ officeNullType: false,
|
|
|
|
|
+ // 学习状态
|
|
|
|
|
+ learnStatus: 0,
|
|
|
|
|
+ currentLearnedDuration: 0,
|
|
|
|
|
+ // 定时器
|
|
|
|
|
+ studyTimer: null,
|
|
|
|
|
+ heartbeatValue: 30,
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ document.title = "";
|
|
|
|
|
+ this.initialization()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ // 初始化
|
|
|
|
|
+ initialization() {
|
|
|
|
|
+ // 从 URL 参数获取 coursewareId(小程序 webview 传参方式)
|
|
|
|
|
+ let text = decodeURIComponent(window.location.href)
|
|
|
|
|
+ let search = text.split('?')[1] || ''
|
|
|
|
|
+ let params = {}
|
|
|
|
|
+ search.split('&').forEach(item => {
|
|
|
|
|
+ let kv = item.split('=')
|
|
|
|
|
+ if (kv[0]) params[kv[0]] = kv[1]
|
|
|
|
|
+ })
|
|
|
|
|
+ this.$set(this, 'coursewareId', params.coursewareId)
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.loadDetail()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ // 获取课件详情
|
|
|
|
|
+ loadDetail() {
|
|
|
|
|
+ examUserCoursewareLearningDetail({ coursewareId: this.coursewareId }).then(response => {
|
|
|
|
|
+ let data = response.data
|
|
|
|
|
+ this.$set(this, 'newData', data)
|
|
|
|
|
+ this.$set(this, 'learnStatus', data.learnStatus)
|
|
|
|
|
+ this.$set(this, 'currentLearnedDuration', data.learnedDuration || 0)
|
|
|
|
|
+ this.renderCourseware(data)
|
|
|
|
|
+ this.initTimer()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ // 课件渲染
|
|
|
|
|
+ renderCourseware(data) {
|
|
|
|
|
+ this.$set(this, 'officeNullType', false)
|
|
|
|
|
+ if (data.materialType === 1) {
|
|
|
|
|
+ this.$set(this, 'officeType', 'video')
|
|
|
|
|
+ this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.attachmentPath)
|
|
|
|
|
+ } else if (data.materialType === 3) {
|
|
|
|
|
+ let ext = data.attachmentType
|
|
|
|
|
+ if (ext === '.docx') {
|
|
|
|
|
+ this.$set(this, 'officeType', 'docx')
|
|
|
|
|
+ } else if (ext === '.xlsx') {
|
|
|
|
|
+ this.$set(this, 'officeType', 'excel')
|
|
|
|
|
+ } else if (ext === '.pdf') {
|
|
|
|
|
+ this.$set(this, 'officeType', 'pdf')
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.attachmentPath)
|
|
|
|
|
+ } else if (data.materialType === 4) {
|
|
|
|
|
+ this.$set(this, 'officeType', 'text')
|
|
|
|
|
+ this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.articleContent)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 更新学习状态为"学习中"
|
|
|
|
|
+ if (this.learnStatus === 0) {
|
|
|
|
|
+ this.$set(this, 'learnStatus', 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ renderedHandler() {},
|
|
|
|
|
+ errorHandler() {
|
|
|
|
|
+ this.$set(this, 'officeNullType', true)
|
|
|
|
|
+ },
|
|
|
|
|
+ // 返回 - 小程序 navigateBack
|
|
|
|
|
+ backPage() {
|
|
|
|
|
+ if (window.wx && window.wx.miniProgram) {
|
|
|
|
|
+ window.wx.miniProgram.navigateBack({ delta: 1 })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.$router.back()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 点击确认完成
|
|
|
|
|
+ finishStudying() {
|
|
|
|
|
+ Dialog.confirm({
|
|
|
|
|
+ title: '提示',
|
|
|
|
|
+ message: '确认完成课件学习?',
|
|
|
|
|
+ confirmButtonText: '确认',
|
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
|
+ }).then(() => {
|
|
|
|
|
+ this.confirmCompletion()
|
|
|
|
|
+ }).catch(() => {})
|
|
|
|
|
+ },
|
|
|
|
|
+ // 确认完成接口
|
|
|
|
|
+ confirmCompletion() {
|
|
|
|
|
+ examUserCoursewareLearningConfirmCompletion({
|
|
|
|
|
+ coursewareId: this.coursewareId,
|
|
|
|
|
+ courseId: 0,
|
|
|
|
|
+ examId: 0,
|
|
|
|
|
+ }).then(response => {
|
|
|
|
|
+ this.$set(this, 'learnStatus', 2)
|
|
|
|
|
+ this.$set(this.newData, 'progress', 100)
|
|
|
|
|
+ Toast(response.message || '学习完成')
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ // 心跳上报
|
|
|
|
|
+ reportProgress() {
|
|
|
|
|
+ examUserCoursewareLearningReportProgress({
|
|
|
|
|
+ coursewareId: this.coursewareId,
|
|
|
|
|
+ courseId: 0,
|
|
|
|
|
+ examId: 0,
|
|
|
|
|
+ duration: this.heartbeatValue,
|
|
|
|
|
+ }).then(() => {})
|
|
|
|
|
+ },
|
|
|
|
|
+ // 初始化定时器
|
|
|
|
|
+ initTimer() {
|
|
|
|
|
+ if (this.newData.learnedDuration >= this.newData.minLearningDuration) return
|
|
|
|
|
+ let self = this
|
|
|
|
|
+ let initialSeconds = this.newData.learnedDuration || 0
|
|
|
|
|
+ let maxSeconds = this.newData.minLearningDuration
|
|
|
|
|
+ let elapsedTime = initialSeconds
|
|
|
|
|
+ let isRunning = false
|
|
|
|
|
+ let isFinished = false
|
|
|
|
|
+ let timerId = null
|
|
|
|
|
+ let startTimeStamp = 0
|
|
|
|
|
+
|
|
|
|
|
+ const handleVisibilityChange = () => {
|
|
|
|
|
+ if (isFinished) return
|
|
|
|
|
+ if (document.hidden) {
|
|
|
|
|
+ pause()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ resume()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
+
|
|
|
|
|
+ const resume = () => {
|
|
|
|
|
+ if (isRunning || isFinished) return
|
|
|
|
|
+ isRunning = true
|
|
|
|
|
+ startTimeStamp = Date.now() - elapsedTime * 1000
|
|
|
|
|
+ timerId = setInterval(() => {
|
|
|
|
|
+ let now = Date.now()
|
|
|
|
|
+ let rawElapsed = (now - startTimeStamp) / 1000
|
|
|
|
|
+ if (rawElapsed >= maxSeconds) {
|
|
|
|
|
+ elapsedTime = maxSeconds
|
|
|
|
|
+ self.$set(self, 'currentLearnedDuration', elapsedTime)
|
|
|
|
|
+ self.reportProgress()
|
|
|
|
|
+ pause()
|
|
|
|
|
+ isFinished = true
|
|
|
|
|
+ document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ elapsedTime = Math.floor(rawElapsed)
|
|
|
|
|
+ self.$set(self, 'currentLearnedDuration', elapsedTime)
|
|
|
|
|
+ if (elapsedTime % self.heartbeatValue === 0 && elapsedTime > 0) {
|
|
|
|
|
+ self.reportProgress()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pause = () => {
|
|
|
|
|
+ if (!isRunning) return
|
|
|
|
|
+ isRunning = false
|
|
|
|
|
+ clearInterval(timerId)
|
|
|
|
|
+ timerId = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.studyTimer = { pause, resume, destroy: () => {
|
|
|
|
|
+ pause()
|
|
|
|
|
+ document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ resume()
|
|
|
|
|
+ },
|
|
|
|
|
+ // 时间格式化
|
|
|
|
|
+ formatTime(totalSeconds) {
|
|
|
|
|
+ if (typeof totalSeconds !== 'number' || totalSeconds <= 0) return '0秒'
|
|
|
|
|
+ let minutes = Math.floor(totalSeconds / 60)
|
|
|
|
|
+ let seconds = totalSeconds % 60
|
|
|
|
|
+ if (minutes > 0 && seconds > 0) return `${minutes}分${seconds}秒`
|
|
|
|
|
+ if (minutes > 0) return `${minutes}分`
|
|
|
|
|
+ return `${seconds}秒`
|
|
|
|
|
+ },
|
|
|
|
|
+ secondsToMinutes(totalSeconds) {
|
|
|
|
|
+ if (typeof totalSeconds !== 'number' || totalSeconds <= 0) return 1
|
|
|
|
|
+ return Math.ceil(totalSeconds / 60)
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ if (this.studyTimer) {
|
|
|
|
|
+ this.studyTimer.destroy()
|
|
|
|
|
+ this.studyTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+</script>
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+ .learningMaterials {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ .nav-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ padding: 0 12px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ .nav-back {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 60px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #0183fa;
|
|
|
|
|
+ .back-arrow {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ margin-right: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .nav-title {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .courseware-box {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #000;
|
|
|
|
|
+
|
|
|
|
|
+ .video-player {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ background: #000;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .rich-text-box {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ ::v-deep img {
|
|
|
|
|
+ max-width: 100% !important;
|
|
|
|
|
+ height: auto !important;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin: 8px auto;
|
|
|
|
|
+ }
|
|
|
|
|
+ ::v-deep table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ border-collapse: collapse;
|
|
|
|
|
+ }
|
|
|
|
|
+ ::v-deep p {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .office-box {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ .load-fail-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ line-height: 200px;
|
|
|
|
|
+ color: #ccc;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .empty-box {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-box {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ padding: 12px 16px 40px 16px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-top: 1px solid #eee;
|
|
|
|
|
+
|
|
|
|
|
+ .info-meta {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ .dot {
|
|
|
|
|
+ margin: 0 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .progress-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ .progress-label {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .progress-bar-wrap {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ .progress-bar-track {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ background: #eee;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ .progress-bar-fill {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: #0183fa;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: width 0.3s;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .progress-pct {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #0183fa;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ min-width: 32px;
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .time-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ .time-text {
|
|
|
|
|
+ font-size: 26px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ }
|
|
|
|
|
+ .status-tag {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: #999;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ &.status-done {
|
|
|
|
|
+ background: #0183fa;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.status-ing {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #0183fa;
|
|
|
|
|
+ color: #0183fa;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .confirm-btn {
|
|
|
|
|
+ background: #13ce66;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ height: 60px;
|
|
|
|
|
+ line-height: 64px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+</style>
|