|
|
@@ -0,0 +1,418 @@
|
|
|
+<template>
|
|
|
+ <div ref="courseware" class="courseware" :style="computedCoursewareStyle">
|
|
|
+ <!-- 行 -->
|
|
|
+ <template v-for="(row, i) in data.row_list">
|
|
|
+ <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row">
|
|
|
+ <el-checkbox
|
|
|
+ v-if="
|
|
|
+ isShowGroup &&
|
|
|
+ groupRowList.find(({ row_id, is_pre_same_group }) => row.row_id === row_id && !is_pre_same_group)
|
|
|
+ "
|
|
|
+ v-model="rowCheckList[row.row_id]"
|
|
|
+ :class="['row-checkbox', `${row.row_id}`]"
|
|
|
+ />
|
|
|
+ <!-- 列 -->
|
|
|
+ <template v-for="(col, j) in row.col_list">
|
|
|
+ <div :key="j" :class="['col', `col-${i}-${j}`]">
|
|
|
+ <!-- 网格 -->
|
|
|
+ <template v-for="(grid, k) in col.grid_list">
|
|
|
+ <component
|
|
|
+ :is="previewComponentList[grid.type]"
|
|
|
+ :id="grid.id"
|
|
|
+ :key="k"
|
|
|
+ ref="preview"
|
|
|
+ :background="computedColBackground(grid.id)"
|
|
|
+ class="grid"
|
|
|
+ :content="computedColContent(grid.id)"
|
|
|
+ :data-row="i"
|
|
|
+ :data-col="j"
|
|
|
+ :data-grid="k"
|
|
|
+ :data-view-order="computedGridViewOrder(grid.id)"
|
|
|
+ :class="[grid.id]"
|
|
|
+ :is-mobile="isMobile"
|
|
|
+ :data-id="grid.id"
|
|
|
+ :style="{
|
|
|
+ gridArea: grid.grid_area,
|
|
|
+ }"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { previewComponentList } from '@/views/book/courseware/data/bookType';
|
|
|
+import { buildCoursewareStyle } from '@/views/book/courseware/preview/common/utils/coursewareStyle';
|
|
|
+import { ensureExtendedFonts } from '@/common/data';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'MobileCoursewarePreview',
|
|
|
+ provide() {
|
|
|
+ return {
|
|
|
+ getDragStatus: () => false,
|
|
|
+ bookInfo: this.bookInfo,
|
|
|
+ getCoursewareId: () => this.courseware_id,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ data: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ coursewareId: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ background: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ componentList: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ canRemark: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ showRemark: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ componentRemarkObj: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ groupRowList: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ isShowGroup: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ groupShowAll: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ // 外部传入的行选中状态(分组部分显示),用于初始化 rowCheckList
|
|
|
+ rowCheckedList: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ project: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ isMobile: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ previewComponentList,
|
|
|
+ courseware_id: this.coursewareId,
|
|
|
+ bookInfo: {
|
|
|
+ theme_color: '',
|
|
|
+ },
|
|
|
+ showMenu: false,
|
|
|
+ divPosition: {
|
|
|
+ left: 0,
|
|
|
+ top: 0,
|
|
|
+ }, // courseware盒子原始距离页面顶部和左边的距离
|
|
|
+ menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
|
|
|
+ componentId: '', // 添加批注的组件id
|
|
|
+ rowCheckList: {},
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 计算课件背景样式
|
|
|
+ computedCoursewareStyle() {
|
|
|
+ return buildCoursewareStyle(this.background, 'courseware');
|
|
|
+ },
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ coursewareId: {
|
|
|
+ handler(val) {
|
|
|
+ this.courseware_id = val;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ groupRowList: {
|
|
|
+ handler(val) {
|
|
|
+ if (!val) return;
|
|
|
+ this.rowCheckList = val
|
|
|
+ .filter(({ is_pre_same_group }) => !is_pre_same_group)
|
|
|
+ .reduce((acc, row) => {
|
|
|
+ acc[row.row_id] = false;
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+ },
|
|
|
+ },
|
|
|
+ rowCheckedList: {
|
|
|
+ handler(val) {
|
|
|
+ this.rowCheckList = val;
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ const element = this.$refs.courseware;
|
|
|
+ const rect = element.getBoundingClientRect();
|
|
|
+ this.divPosition = {
|
|
|
+ left: rect.left,
|
|
|
+ top: rect.top,
|
|
|
+ };
|
|
|
+ ensureExtendedFonts();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 计算组件内容
|
|
|
+ * @param {string} id 组件id
|
|
|
+ * @returns {string} 组件内容
|
|
|
+ */
|
|
|
+ computedColContent(id) {
|
|
|
+ if (!id) return '';
|
|
|
+ return this.componentList.find((item) => item.component_id === id)?.content || '';
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 计算组件背景
|
|
|
+ * @param {string} id 组件id
|
|
|
+ * @returns {object} 组件背景样式
|
|
|
+ */
|
|
|
+ computedColBackground(id) {
|
|
|
+ if (!id) return {};
|
|
|
+ const background = this.componentList.find((item) => item.component_id === id)?.background;
|
|
|
+ return background ? JSON.parse(background) : {};
|
|
|
+ },
|
|
|
+ getMultipleColStyle(i) {
|
|
|
+ let row = this.data.row_list[i];
|
|
|
+ let col = row.col_list;
|
|
|
+ if (col.length <= 1) {
|
|
|
+ return {
|
|
|
+ gridTemplateColumns: '100fr',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ let gridTemplateColumns = row.width_list.join(' ');
|
|
|
+
|
|
|
+ return {
|
|
|
+ gridAutoFlow: 'column',
|
|
|
+ gridTemplateColumns,
|
|
|
+ gridTemplateRows: 'auto',
|
|
|
+ };
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 计算行的可见性
|
|
|
+ * @params {string} rowId 行的ID
|
|
|
+ * @returns {boolean} 行是否可见
|
|
|
+ */
|
|
|
+ computedRowVisibility(rowId) {
|
|
|
+ if (this.groupShowAll) return true;
|
|
|
+ let { row_id, is_pre_same_group } = this.groupRowList.find(({ row_id }) => row_id === rowId);
|
|
|
+ if (is_pre_same_group) {
|
|
|
+ const index = this.groupRowList.findIndex(({ row_id }) => row_id === rowId);
|
|
|
+ if (index === -1) return false;
|
|
|
+ for (let i = index - 1; i >= 0; i--) {
|
|
|
+ if (!this.groupRowList[i].is_pre_same_group) {
|
|
|
+ return this.rowCheckList[this.groupRowList[i].row_id];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return this.rowCheckList[row_id];
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 分割整数为多个 1的倍数
|
|
|
+ * @param {number} num
|
|
|
+ * @param {number} parts
|
|
|
+ */
|
|
|
+ splitInteger(num, parts) {
|
|
|
+ let base = Math.floor(num / parts);
|
|
|
+ let arr = Array(parts).fill(base);
|
|
|
+ let remainder = num - base * parts;
|
|
|
+ for (let i = 0; remainder > 0; i = (i + 1) % parts) {
|
|
|
+ arr[i] += 1;
|
|
|
+ remainder -= 1;
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ },
|
|
|
+ computedColStyle(col) {
|
|
|
+ const grid = col.grid_list || [];
|
|
|
+ if (!grid.length) {
|
|
|
+ return {
|
|
|
+ width: col.width,
|
|
|
+ gridTemplateAreas: '',
|
|
|
+ gridTemplateColumns: '',
|
|
|
+ gridTemplateRows: '',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 一次遍历完成按行分组,并记录最大列数所在行
|
|
|
+ let maxCol = 0;
|
|
|
+ let curMaxRow = 0;
|
|
|
+ const rowMap = new Map();
|
|
|
+ grid.forEach((item) => {
|
|
|
+ if (!rowMap.has(item.row)) {
|
|
|
+ rowMap.set(item.row, []);
|
|
|
+ }
|
|
|
+ const rowItems = rowMap.get(item.row);
|
|
|
+ rowItems.push(item);
|
|
|
+ if (rowItems.length > maxCol) {
|
|
|
+ maxCol = rowItems.length;
|
|
|
+ curMaxRow = item.row;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算 grid_template_areas
|
|
|
+ let gridTemplateAreas = '';
|
|
|
+ const sortedRows = Array.from(rowMap.keys()).sort((a, b) => a - b);
|
|
|
+ sortedRows.forEach((row) => {
|
|
|
+ const rowItems = rowMap.get(row);
|
|
|
+ const rowAreas = [];
|
|
|
+ if (row === curMaxRow) {
|
|
|
+ rowItems.forEach(({ grid_area }) => {
|
|
|
+ rowAreas.push(`${grid_area}`);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const needNum = maxCol - rowItems.length;
|
|
|
+ const splitList = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
|
|
|
+ rowItems.forEach(({ grid_area }, index) => {
|
|
|
+ if (rowItems.length === 1) {
|
|
|
+ rowAreas.push(` ${grid_area} `.repeat(needNum + 1));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const repeatNum = splitList[index] + 1;
|
|
|
+ rowAreas.push(splitList[index] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(repeatNum));
|
|
|
+ });
|
|
|
+ }
|
|
|
+ gridTemplateAreas += `'${rowAreas.join(' ')}' `;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算 grid_template_columns
|
|
|
+ const maxRowItems = rowMap.get(curMaxRow) || [];
|
|
|
+ const gridTemplateColumns = maxRowItems.map(({ width }) => width).join(' ');
|
|
|
+
|
|
|
+ // 移动端高度统一使用 auto
|
|
|
+ const gridTemplateRows = `${sortedRows.map(() => 'auto').join(' ')} `;
|
|
|
+
|
|
|
+ return {
|
|
|
+ width: col.width,
|
|
|
+ gridTemplateAreas,
|
|
|
+ gridTemplateColumns,
|
|
|
+ gridTemplateRows,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 查找子组件
|
|
|
+ * @param {string} id 组件的唯一标识符
|
|
|
+ * @returns {Promise<HTMLElement|null>} 返回找到的子组件或 null
|
|
|
+ */
|
|
|
+ async findChildComponentByKey(id) {
|
|
|
+ await this.$nextTick();
|
|
|
+ return this.$refs.preview.find((child) => child.dataset.id === id);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 模拟回答
|
|
|
+ * @param {boolean} isJudgingRightWrong 是否判断对错
|
|
|
+ * @param {boolean} isShowRightAnswer 是否显示正确答案
|
|
|
+ * @param {boolean} disabled 是否禁用
|
|
|
+ */
|
|
|
+ simulateAnswer(isJudgingRightWrong, isShowRightAnswer, disabled = true) {
|
|
|
+ this.$refs.preview.forEach((item) => {
|
|
|
+ item.showAnswer(isJudgingRightWrong, isShowRightAnswer, null, disabled);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 获取网格 id,是第几行第几列第几个网格,通过 data.row_list 计算
|
|
|
+ * @param {string} id 网格 id
|
|
|
+ * @returns {number} 网格的显示顺序
|
|
|
+ */
|
|
|
+ computedGridViewOrder(id) {
|
|
|
+ let rowIndex = -1;
|
|
|
+ let colIndex = -1;
|
|
|
+ let gridIndex = -1;
|
|
|
+
|
|
|
+ this.data.row_list.forEach((row, i) => {
|
|
|
+ row.col_list.forEach((col, j) => {
|
|
|
+ col.grid_list.forEach((grid, k) => {
|
|
|
+ if (grid.id === id) {
|
|
|
+ rowIndex = i;
|
|
|
+ colIndex = j;
|
|
|
+ gridIndex = k;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ let order = 0;
|
|
|
+ // 计算前几行的网格数量
|
|
|
+ if (rowIndex > 0) {
|
|
|
+ for (let i = 0; i < rowIndex; i++) {
|
|
|
+ this.data.row_list[i].col_list.forEach((col) => {
|
|
|
+ order += col.grid_list.length;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 计算当前行前几列的网格数量
|
|
|
+ if (colIndex > 0) {
|
|
|
+ for (let j = 0; j < colIndex; j++) {
|
|
|
+ order += this.data.row_list[rowIndex].col_list[j].grid_list.length;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 加上当前列前面的网格数量
|
|
|
+ order += gridIndex + 1;
|
|
|
+ return order;
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.courseware {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 6px;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 500px;
|
|
|
+ padding: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #fff;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ border-bottom-right-radius: 12px;
|
|
|
+ border-bottom-left-radius: 12px;
|
|
|
+
|
|
|
+ .row {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .col {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .grid {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .main {
|
|
|
+ .option-list {
|
|
|
+ flex-direction: column !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .row-checkbox {
|
|
|
+ position: absolute;
|
|
|
+ left: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|