|
|
@@ -0,0 +1,584 @@
|
|
|
+<template>
|
|
|
+ <ele-modal
|
|
|
+ :visible.sync="visible"
|
|
|
+ title="开始学习"
|
|
|
+ width="90vw"
|
|
|
+ :fullscreen="true"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :maxable="true"
|
|
|
+ @close="cancel"
|
|
|
+ >
|
|
|
+ <div v-loading="loading" class="preview-container">
|
|
|
+ <!-- 左侧树形 -->
|
|
|
+ <div class="left-tree">
|
|
|
+ <div class="tree-header">{{ treeTitle }}</div>
|
|
|
+ <div class="tree-meta">
|
|
|
+ <span>总时长:{{ totalMinutes }} 分钟</span>
|
|
|
+ <span>已学时长:{{ totalLearnedMinutes }} 分钟</span>
|
|
|
+ </div>
|
|
|
+ <el-tree
|
|
|
+ ref="treeRef"
|
|
|
+ :data="treeData"
|
|
|
+ :props="defaultProps"
|
|
|
+ node-key="id"
|
|
|
+ default-expand-all
|
|
|
+ highlight-current
|
|
|
+ @node-click="handleNodeClick"
|
|
|
+ >
|
|
|
+ <span slot-scope="{ node, data }" class="custom-tree-node">
|
|
|
+ <span :class="{ 'not-learned': data.isLeaf }">{{
|
|
|
+ node.label
|
|
|
+ }}</span>
|
|
|
+ <span v-if="data.isMaterial" class="node-meta">
|
|
|
+ 应学 {{ data.requiredMinutes }} 分钟 / 已学
|
|
|
+ {{ data.completedMinutes }} 分钟
|
|
|
+ </span>
|
|
|
+ <span v-else-if="data.durationMinutes" class="node-duration">{{
|
|
|
+ data.duration
|
|
|
+ }}</span>
|
|
|
+ </span>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧内容 -->
|
|
|
+ <div class="right-content">
|
|
|
+ <div class="content-title">{{ currentTitle }}</div>
|
|
|
+ <div class="content-meta">
|
|
|
+ <span>本节时长:{{ currentDuration }} 分钟</span>
|
|
|
+ <span>已学时长:{{ learnedMinutes }} 分钟</span>
|
|
|
+ <span>剩余时长:{{ remainMinutes }} 分钟</span>
|
|
|
+ </div>
|
|
|
+ <div class="iframe-wrapper">
|
|
|
+ <!-- 附件预览:按 视频 / 图片 / 文档 分别渲染,学习中不提供新窗口打开与下载 -->
|
|
|
+ <file-preview
|
|
|
+ ref="filePreviewRef"
|
|
|
+ :ids="currentFileIds"
|
|
|
+ :show-actions="false"
|
|
|
+ class="preview-files"
|
|
|
+ @play="handleMediaPlay"
|
|
|
+ @pause="handleMediaPause"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 开始学习遮罩 -->
|
|
|
+ <div v-if="showOverlay" class="start-overlay">
|
|
|
+ <el-button type="primary" @click="handleStartLearn">
|
|
|
+ 开始学习
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-slot:footer>
|
|
|
+ <el-button @click="cancel">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </ele-modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import FilePreview from '@/components/FilePreview/index.vue';
|
|
|
+ import { learn, getLearnDetail } from '@/api/onlineTrainingPlan/index.js';
|
|
|
+
|
|
|
+ export default {
|
|
|
+ components: {
|
|
|
+ FilePreview
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ visible: false,
|
|
|
+ treeTitle: '',
|
|
|
+ currentTitle: '',
|
|
|
+ // 当前选中培训资源的附件id集合
|
|
|
+ currentFileIds: [],
|
|
|
+ showOverlay: true,
|
|
|
+ defaultProps: {
|
|
|
+ children: 'children',
|
|
|
+ label: 'label'
|
|
|
+ },
|
|
|
+ treeData: [],
|
|
|
+ // 培训任务ID
|
|
|
+ currentTaskId: null,
|
|
|
+ // 当前学习的培训资料ID(learn接口必填)
|
|
|
+ currentMaterialId: null,
|
|
|
+ // 学习详情加载中
|
|
|
+ loading: false,
|
|
|
+ // 每分钟上报学习进度的定时器
|
|
|
+ learnTimer: null,
|
|
|
+ // 学习倒计时:总时长(秒) / 剩余时长(秒)
|
|
|
+ totalSeconds: 0,
|
|
|
+ // 应学总时长(秒),所有资料应学时长之和,用于判断是否完成
|
|
|
+ totalRequiredSeconds: 0,
|
|
|
+ // 是否已提示过学习完成
|
|
|
+ completedNotified: false,
|
|
|
+ remainingSeconds: 0,
|
|
|
+ // 倒计时定时器
|
|
|
+ countdownTimer: null,
|
|
|
+ // 总时长(分钟),所有资料应学时长之和
|
|
|
+ totalMinutesValue: 0,
|
|
|
+ // 总已学时长(秒),所有资料已完成时长之和
|
|
|
+ totalCompletedSeconds: 0,
|
|
|
+ // 当前选中资料的已学时长(秒)
|
|
|
+ currentCompletedSeconds: 0,
|
|
|
+ // 当前选中节点的时长(分钟),取自 suggestDuration
|
|
|
+ currentDuration: 0
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 总时长(分钟)
|
|
|
+ totalMinutes() {
|
|
|
+ return this.totalMinutesValue;
|
|
|
+ },
|
|
|
+ // 总已学时长(分钟),所有资料已完成时长之和
|
|
|
+ totalLearnedMinutes() {
|
|
|
+ return this.toMinutes(this.totalCompletedSeconds);
|
|
|
+ },
|
|
|
+ // 当前选中资料的已学时长(分钟)
|
|
|
+ learnedMinutes() {
|
|
|
+ return this.toMinutes(this.currentCompletedSeconds);
|
|
|
+ },
|
|
|
+ // 剩余时长(分钟),本节时长减已学时长,最小为0
|
|
|
+ remainMinutes() {
|
|
|
+ return Math.max(this.currentDuration - this.learnedMinutes, 0);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ document.removeEventListener(
|
|
|
+ 'visibilitychange',
|
|
|
+ this.handleVisibilityChange
|
|
|
+ );
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.stopCountdown();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async open(row) {
|
|
|
+ this.visible = true;
|
|
|
+ this.currentTaskId = row && row.id ? row.id : null;
|
|
|
+ this.loading = true;
|
|
|
+ let detail = row || {};
|
|
|
+ // 优先取学习详情(含资料及各资料学习进度),失败时回退到行数据
|
|
|
+ if (this.currentTaskId) {
|
|
|
+ try {
|
|
|
+ detail = await getLearnDetail(this.currentTaskId);
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || '获取学习详情失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.buildTree(detail);
|
|
|
+ this.loading = false;
|
|
|
+ },
|
|
|
+ // 资料节点展示应学/已学时长,资源节点展示建议时长
|
|
|
+ buildTree(detail) {
|
|
|
+ const materialList = detail.materialList || [];
|
|
|
+ const treeData = materialList.map((material, mIndex) => {
|
|
|
+ const resources = material.trainingResourceList || [];
|
|
|
+ const children = resources.map((item, index) => ({
|
|
|
+ id: item.id || `material${mIndex}-resource${index}`,
|
|
|
+ label: item.resourceName || `培训资源${index + 1}`,
|
|
|
+ duration: item.suggestDuration
|
|
|
+ ? `总时长: ${item.suggestDuration} 分钟`
|
|
|
+ : '',
|
|
|
+ durationMinutes: Number(item.suggestDuration) || 0,
|
|
|
+ // 资源继承所属资料的ID,上报学习进度时使用
|
|
|
+ materialId: material.materialId || null,
|
|
|
+ attachment: this.parseAttachment(item.attachment),
|
|
|
+ isLeaf: true
|
|
|
+ }));
|
|
|
+ return {
|
|
|
+ id: material.taskMaterialId || `material-${mIndex}`,
|
|
|
+ label: material.materialName || `培训资料${mIndex + 1}`,
|
|
|
+ // 资料节点:应学/已学时长,秒转分钟
|
|
|
+ isMaterial: true,
|
|
|
+ // 培训资料ID,上报学习进度时必填
|
|
|
+ materialId: material.materialId || null,
|
|
|
+ requiredMinutes: this.toMinutes(material.requiredDurationSeconds),
|
|
|
+ completedMinutes: this.toMinutes(material.completedDurationSeconds),
|
|
|
+ // 资料已学时长同时作为该资料下「本节时长」的参考
|
|
|
+ completedSeconds: Number(material.completedDurationSeconds) || 0,
|
|
|
+ children
|
|
|
+ };
|
|
|
+ });
|
|
|
+ this.treeTitle = detail.trainingName || '培训资料';
|
|
|
+ // 总时长为所有资料应学时长之和,已学时长为所有资料已完成时长之和
|
|
|
+ this.totalRequiredSeconds = materialList.reduce(
|
|
|
+ (sum, material) =>
|
|
|
+ sum + (Number(material.requiredDurationSeconds) || 0),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ this.totalMinutesValue = this.toMinutes(this.totalRequiredSeconds);
|
|
|
+ this.totalCompletedSeconds = materialList.reduce(
|
|
|
+ (sum, material) =>
|
|
|
+ sum + (Number(material.completedDurationSeconds) || 0),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ this.treeData = treeData;
|
|
|
+ this.totalSeconds = this.totalRequiredSeconds;
|
|
|
+ this.remainingSeconds = this.totalSeconds;
|
|
|
+ this.showOverlay = true;
|
|
|
+ // 默认选中第一个资源节点
|
|
|
+ const firstChild = treeData.length ? treeData[0].children[0] : null;
|
|
|
+ if (firstChild) {
|
|
|
+ const firstMaterial = treeData[0];
|
|
|
+ this.currentTitle = firstChild.label;
|
|
|
+ this.currentFileIds = firstChild.attachment;
|
|
|
+ this.currentDuration = firstChild.durationMinutes;
|
|
|
+ this.currentCompletedSeconds = firstMaterial.completedSeconds;
|
|
|
+ this.currentMaterialId = firstChild.materialId || null;
|
|
|
+ } else {
|
|
|
+ this.currentTitle = this.treeTitle;
|
|
|
+ this.currentFileIds = [];
|
|
|
+ this.currentDuration = 0;
|
|
|
+ this.currentCompletedSeconds = 0;
|
|
|
+ }
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (firstChild) {
|
|
|
+ this.$refs.treeRef.setCurrentKey(firstChild.id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 打开时若已完成学习,直接提示
|
|
|
+ this.checkLearnComplete();
|
|
|
+ },
|
|
|
+ // 秒转分钟,向上取整
|
|
|
+ toMinutes(seconds) {
|
|
|
+ const value = Number(seconds);
|
|
|
+ if (!value || value <= 0) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return Math.ceil(value / 60);
|
|
|
+ },
|
|
|
+ // 附件id标准化,兼容字符串和数组
|
|
|
+ parseAttachment(attachment) {
|
|
|
+ if (!attachment) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ const list = Array.isArray(attachment)
|
|
|
+ ? attachment
|
|
|
+ : String(attachment).split(',');
|
|
|
+ return list
|
|
|
+ .map((item) => String(item || '').trim())
|
|
|
+ .filter((item) => item);
|
|
|
+ },
|
|
|
+ handleNodeClick(data) {
|
|
|
+ this.currentTitle = data.label;
|
|
|
+ this.currentFileIds = data.attachment || [];
|
|
|
+ // 本节时长取当前选中节点的 suggestDuration
|
|
|
+ this.currentDuration = Number(data.durationMinutes) || 0;
|
|
|
+ // 已学时长取所属资料的已完成时长(秒),以资料节点为唯一数据源
|
|
|
+ const material = data.materialId
|
|
|
+ ? this.treeData.find((node) => node.materialId === data.materialId)
|
|
|
+ : null;
|
|
|
+ this.currentCompletedSeconds = material
|
|
|
+ ? material.completedSeconds
|
|
|
+ : data.completedSeconds || 0;
|
|
|
+ // 只在选中具体资源时记录资料ID(learn接口materialId必填)
|
|
|
+ this.currentMaterialId = data.isLeaf
|
|
|
+ ? data.materialId || null
|
|
|
+ : null;
|
|
|
+ // 切换资源后停止计时,等待重新开始学习
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.stopCountdown();
|
|
|
+ this.showOverlay = true;
|
|
|
+ },
|
|
|
+ // 点击「开始学习」后才开始计时与上报
|
|
|
+ handleStartLearn() {
|
|
|
+ this.showOverlay = false;
|
|
|
+ // 重新开始学习时,倒计时归位到总时长
|
|
|
+ if (this.countdownTimer) {
|
|
|
+ this.remainingSeconds = this.totalSeconds;
|
|
|
+ }
|
|
|
+ // 视频类附件在播放时才开始计时
|
|
|
+ if (!this.isCurrentVideo()) {
|
|
|
+ this.startLearnTimer();
|
|
|
+ this.startCountdown();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 当前附件是否为视频
|
|
|
+ isCurrentVideo() {
|
|
|
+ const preview = this.$refs.filePreviewRef;
|
|
|
+ return !!(
|
|
|
+ preview &&
|
|
|
+ preview.currentFile &&
|
|
|
+ preview.currentFile.category === 'video'
|
|
|
+ );
|
|
|
+ },
|
|
|
+ // 视频开始播放:开始计时与上报
|
|
|
+ handleMediaPlay() {
|
|
|
+ if (!this.isCurrentVideo()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.startLearnTimer();
|
|
|
+ this.startCountdown();
|
|
|
+ },
|
|
|
+ // 视频暂停或播放结束:停止计时与上报
|
|
|
+ handleMediaPause() {
|
|
|
+ if (!this.isCurrentVideo()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.stopCountdown();
|
|
|
+ },
|
|
|
+ // 浏览器标签页不可见(切到其它标签页/最小化)时停止计时并开启学习遮罩
|
|
|
+ handleVisibilityChange() {
|
|
|
+ if (document.hidden) {
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.stopCountdown();
|
|
|
+ this.showOverlay = true;
|
|
|
+ // 暂停正在播放的视频
|
|
|
+ this.pauseVideo();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 暂停预览中的视频
|
|
|
+ pauseVideo() {
|
|
|
+ const preview = this.$refs.filePreviewRef;
|
|
|
+ if (preview && typeof preview.pauseVideo === 'function') {
|
|
|
+ preview.pauseVideo();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 每分钟上报一次学习进度
|
|
|
+ startLearnTimer() {
|
|
|
+ this.stopLearnTimer();
|
|
|
+ if (!this.currentTaskId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.learnTimer = setInterval(() => {
|
|
|
+ this.reportLearn();
|
|
|
+ }, 60 * 1000);
|
|
|
+ },
|
|
|
+ stopLearnTimer() {
|
|
|
+ if (this.learnTimer) {
|
|
|
+ clearInterval(this.learnTimer);
|
|
|
+ this.learnTimer = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 上报学习进度:任务ID + 培训资料ID + 本次学习秒数 + 事件ID(幂等)
|
|
|
+ async reportLearn() {
|
|
|
+ // materialId 为必填,未选中具体资源时跳过本次上报
|
|
|
+ if (!this.currentTaskId || !this.currentMaterialId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const watchedSeconds = 60;
|
|
|
+ try {
|
|
|
+ await learn({
|
|
|
+ taskId: this.currentTaskId,
|
|
|
+ materialId: this.currentMaterialId,
|
|
|
+ watchedSeconds,
|
|
|
+ eventId: this.createEventId()
|
|
|
+ });
|
|
|
+ // 上报成功后本地同步已学时长,无需等待下次重新查询
|
|
|
+ this.addLocalProgress(watchedSeconds);
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || '学习进度上报失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 生成学习事件ID,用于接口重试幂等(优先使用浏览器原生 randomUUID)
|
|
|
+ createEventId() {
|
|
|
+ if (window.crypto && typeof window.crypto.randomUUID === 'function') {
|
|
|
+ return window.crypto.randomUUID();
|
|
|
+ }
|
|
|
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
|
|
|
+ },
|
|
|
+ // 上报成功后本地累加已学时长,并同步树上的资料节点显示
|
|
|
+ addLocalProgress(watchedSeconds) {
|
|
|
+ this.totalCompletedSeconds += watchedSeconds;
|
|
|
+ this.currentCompletedSeconds += watchedSeconds;
|
|
|
+ const material = this.currentMaterialId
|
|
|
+ ? this.treeData.find(
|
|
|
+ (node) => node.materialId === this.currentMaterialId
|
|
|
+ )
|
|
|
+ : null;
|
|
|
+ if (material) {
|
|
|
+ material.completedSeconds += watchedSeconds;
|
|
|
+ material.completedMinutes = this.toMinutes(material.completedSeconds);
|
|
|
+ }
|
|
|
+ this.checkLearnComplete();
|
|
|
+ },
|
|
|
+ // 已学时长达到应学总时长时提示用户已完成培训
|
|
|
+ checkLearnComplete() {
|
|
|
+ if (this.completedNotified || !this.totalRequiredSeconds) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.totalCompletedSeconds >= this.totalRequiredSeconds) {
|
|
|
+ this.completedNotified = true;
|
|
|
+ this.$message.success('恭喜,您已完成本次培训');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 启动学习倒计时(每秒递减)
|
|
|
+ startCountdown() {
|
|
|
+ this.stopCountdown();
|
|
|
+ if (!this.totalSeconds) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.countdownTimer = setInterval(() => {
|
|
|
+ this.remainingSeconds -= 1;
|
|
|
+ if (this.remainingSeconds <= 0) {
|
|
|
+ this.remainingSeconds = 0;
|
|
|
+ this.handleCountdownEnd();
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+ stopCountdown() {
|
|
|
+ if (this.countdownTimer) {
|
|
|
+ clearInterval(this.countdownTimer);
|
|
|
+ this.countdownTimer = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 倒计时结束:停止计时并提示,弹窗保持打开
|
|
|
+ handleCountdownEnd() {
|
|
|
+ this.stopCountdown();
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.$message.warning('学习时长已到,已停止计时');
|
|
|
+ },
|
|
|
+ cancel() {
|
|
|
+ this.stopLearnTimer();
|
|
|
+ this.stopCountdown();
|
|
|
+ this.visible = false;
|
|
|
+ this.showOverlay = true;
|
|
|
+ this.currentFileIds = [];
|
|
|
+ this.currentTaskId = null;
|
|
|
+ this.currentMaterialId = null;
|
|
|
+ this.totalSeconds = 0;
|
|
|
+ this.remainingSeconds = 0;
|
|
|
+ this.totalMinutesValue = 0;
|
|
|
+ this.totalRequiredSeconds = 0;
|
|
|
+ this.completedNotified = false;
|
|
|
+ this.totalCompletedSeconds = 0;
|
|
|
+ this.currentCompletedSeconds = 0;
|
|
|
+ this.currentDuration = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .preview-container {
|
|
|
+ display: flex;
|
|
|
+ height: calc(90vh - 160px);
|
|
|
+ min-height: 500px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-tree {
|
|
|
+ width: 280px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
+ overflow-y: auto;
|
|
|
+
|
|
|
+ .tree-header {
|
|
|
+ padding: 12px 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-meta {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px 15px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ background-color: #fafafa;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep .el-tree {
|
|
|
+ padding: 10px;
|
|
|
+
|
|
|
+ .el-tree-node__content {
|
|
|
+ height: 36px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .custom-tree-node {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 13px;
|
|
|
+ padding-right: 8px;
|
|
|
+
|
|
|
+ .not-learned {
|
|
|
+ color: #409eff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .node-duration {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .node-meta {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 8px;
|
|
|
+ text-align: right;
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-content {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 0 20px;
|
|
|
+
|
|
|
+ .content-title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ padding: 10px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-meta {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 30px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+
|
|
|
+ .iframe-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ background-color: #eef5f9;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-files {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-iframe {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .iframe-placeholder {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+
|
|
|
+ .start-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-color: rgba(238, 245, 249, 0.95);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|