|
|
@@ -0,0 +1,480 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ ref="courserware"
|
|
|
+ class="courserware"
|
|
|
+ :style="[
|
|
|
+ {
|
|
|
+ backgroundImage: background.background_image_url ? `url(${background.background_image_url})` : '',
|
|
|
+ backgroundSize: background.background_image_url
|
|
|
+ ? `${background.background_position.width}% ${background.background_position.height}%`
|
|
|
+ : '',
|
|
|
+ backgroundPosition: background.background_image_url
|
|
|
+ ? `${background.background_position.left}% ${background.background_position.top}%`
|
|
|
+ : '',
|
|
|
+ },
|
|
|
+ ]"
|
|
|
+ >
|
|
|
+ <template v-for="(row, i) in data.row_list">
|
|
|
+ <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row" :style="getMultipleColStyle(i)">
|
|
|
+ <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}`]" :style="computedColStyle(col)">
|
|
|
+ <!-- 网格 -->
|
|
|
+ <template v-for="(grid, k) in col.grid_list">
|
|
|
+ <component
|
|
|
+ :is="previewComponentList[grid.type]"
|
|
|
+ :key="k"
|
|
|
+ ref="preview"
|
|
|
+ :content="computedColContent(grid.id)"
|
|
|
+ :data-row="i"
|
|
|
+ :data-col="j"
|
|
|
+ :data-grid="k"
|
|
|
+ :data-view-order="computedGridViewOrder(grid.id)"
|
|
|
+ :class="[grid.id]"
|
|
|
+ :data-id="grid.id"
|
|
|
+ :style="{
|
|
|
+ gridArea: grid.grid_area,
|
|
|
+ height: grid.height,
|
|
|
+ }"
|
|
|
+ @contextmenu.native.prevent="handleContextMenu($event, grid.id)"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="showMenu && componentId === grid.id"
|
|
|
+ :key="'menu' + grid.id + k"
|
|
|
+ class="custom-context-menu"
|
|
|
+ :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }"
|
|
|
+ @click="handleMenuItemClick"
|
|
|
+ >
|
|
|
+ 添加批注
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="showRemark && Object.keys(componentRemarkObj).length !== 0 && componentRemarkObj[grid.id]"
|
|
|
+ :key="'show' + grid.id + k"
|
|
|
+ >
|
|
|
+ <el-popover
|
|
|
+ v-for="(items, indexs) in componentRemarkObj[grid.id]"
|
|
|
+ :key="indexs"
|
|
|
+ placement="bottom"
|
|
|
+ width="200"
|
|
|
+ trigger="click"
|
|
|
+ >
|
|
|
+ <div v-html="items.content"></div>
|
|
|
+ <template #reference>
|
|
|
+ <SvgIcon
|
|
|
+ slot="reference"
|
|
|
+ icon-class="icon-info"
|
|
|
+ size="24"
|
|
|
+ class="remark-info"
|
|
|
+ :style="{ left: items.position_x - 12 + 'px', top: items.position_y - 12 + 'px' }"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-popover>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { previewComponentList } from '@/views/book/courseware/data/bookType';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'MobileCoursewarePreview',
|
|
|
+ provide() {
|
|
|
+ return {
|
|
|
+ getDragStatus: () => false,
|
|
|
+ bookInfo: this.bookInfo,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ data: {
|
|
|
+ type: Object,
|
|
|
+ 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,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ previewComponentList,
|
|
|
+ bookInfo: {
|
|
|
+ theme_color: '',
|
|
|
+ },
|
|
|
+ showMenu: false,
|
|
|
+ divPosition: {
|
|
|
+ left: 0,
|
|
|
+ top: 0,
|
|
|
+ }, // courserware盒子原始距离页面顶部和左边的距离
|
|
|
+ menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
|
|
|
+ componentId: '', // 添加批注的组件id
|
|
|
+ rowCheckList: {},
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ 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;
|
|
|
+ }, {});
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ const element = this.$refs.courserware;
|
|
|
+ const rect = element.getBoundingClientRect();
|
|
|
+ this.divPosition = {
|
|
|
+ left: rect.left,
|
|
|
+ top: rect.top,
|
|
|
+ };
|
|
|
+ window.addEventListener('mousedown', this.handleMouseDown);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('mousedown', this.handleMouseDown);
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 计算组件内容
|
|
|
+ * @param {string} id 组件id
|
|
|
+ * @returns {string} 组件内容
|
|
|
+ */
|
|
|
+ computedColContent(id) {
|
|
|
+ if (!id) return '';
|
|
|
+ return this.componentList.find((item) => item.component_id === id)?.content || '';
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+
|
|
|
+ let maxCol = 0; // 最大列数
|
|
|
+ let rowList = new Map();
|
|
|
+ grid.forEach(({ row }) => {
|
|
|
+ rowList.set(row, (rowList.get(row) || 0) + 1);
|
|
|
+ });
|
|
|
+ let curMaxRow = 0; // 当前数量最大 row 的值
|
|
|
+ rowList.forEach((value, key) => {
|
|
|
+ if (value > maxCol) {
|
|
|
+ maxCol = value;
|
|
|
+ curMaxRow = key;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 计算 grid_template_areas
|
|
|
+ let gridTemplateAreas = '';
|
|
|
+ let gridArr = [];
|
|
|
+ grid.forEach(({ grid_area, row }) => {
|
|
|
+ if (!gridArr[row - 1]) {
|
|
|
+ gridArr[row - 1] = [];
|
|
|
+ }
|
|
|
+ if (curMaxRow === row) {
|
|
|
+ gridArr[row - 1].push(`${grid_area}`);
|
|
|
+ } else {
|
|
|
+ let filter = grid.filter((item) => item.row === row);
|
|
|
+ let find = filter.findIndex((item) => item.grid_area === grid_area);
|
|
|
+ let needNum = maxCol - filter.length; // 需要的数量
|
|
|
+
|
|
|
+ let str = '';
|
|
|
+ if (filter.length === 1) {
|
|
|
+ str = ` ${grid_area} `.repeat(needNum + 1);
|
|
|
+ } else {
|
|
|
+ let arr = this.splitInteger(needNum, filter.length);
|
|
|
+ str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
|
|
|
+ }
|
|
|
+ gridArr[row - 1].push(`${str}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ gridArr.forEach((item) => {
|
|
|
+ gridTemplateAreas += `'${item.join(' ')}' `;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算 grid_template_columns
|
|
|
+ let gridTemplateColumns = '';
|
|
|
+ let max = { row: 0, num: 0 };
|
|
|
+ grid.forEach(({ row }) => {
|
|
|
+ // 计算出 row 的哪个值最多
|
|
|
+ let len = grid.filter((item) => item.row === row).length;
|
|
|
+ if (max.num < len) {
|
|
|
+ max.num = len;
|
|
|
+ max.row = row;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ grid.forEach((item) => {
|
|
|
+ if (item.row === max.row) {
|
|
|
+ gridTemplateColumns += `${item.width} `;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算 grid_template_rows
|
|
|
+ let gridTemplateRows = '';
|
|
|
+ // 将 grid 按照 row 分组
|
|
|
+ let gridMap = new Map();
|
|
|
+ grid.forEach((item) => {
|
|
|
+ if (!gridMap.has(item.row)) {
|
|
|
+ gridMap.set(item.row, []);
|
|
|
+ }
|
|
|
+ gridMap.get(item.row).push(item.height);
|
|
|
+ });
|
|
|
+ gridMap.forEach((value) => {
|
|
|
+ if (value.length === 1) {
|
|
|
+ gridTemplateRows += `${value[0]} `;
|
|
|
+ } else {
|
|
|
+ let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
|
|
|
+ gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ width: col.width,
|
|
|
+ gridTemplateAreas,
|
|
|
+ gridTemplateColumns,
|
|
|
+ gridTemplateRows,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ handleContextMenu(event, id) {
|
|
|
+ console.log(this.canRemark);
|
|
|
+
|
|
|
+ if (this.canRemark) {
|
|
|
+ event.preventDefault(); // 阻止默认的上下文菜单显示
|
|
|
+ this.menuPosition = {
|
|
|
+ x: event.clientX - this.divPosition.left,
|
|
|
+ y: event.clientY - this.divPosition.top,
|
|
|
+ }; // 设置菜单位置
|
|
|
+ this.componentId = id;
|
|
|
+ this.$emit('computeScroll');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleResult(top, left, select_node) {
|
|
|
+ this.menuPosition = {
|
|
|
+ x: this.menuPosition.x + left,
|
|
|
+ y: this.menuPosition.y + top,
|
|
|
+ select_node,
|
|
|
+ }; // 设置菜单位置
|
|
|
+ this.showMenu = true; // 显示菜单
|
|
|
+ },
|
|
|
+ handleMenuItemClick() {
|
|
|
+ this.showMenu = false; // 隐藏菜单
|
|
|
+ this.$emit(
|
|
|
+ 'addRemark',
|
|
|
+ this.menuPosition.select_node,
|
|
|
+ this.menuPosition.x,
|
|
|
+ this.menuPosition.y,
|
|
|
+ this.componentId,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ handleMouseDown(event) {
|
|
|
+ if (event.button === 0 && event.target.className !== 'custom-context-menu') {
|
|
|
+ // 0 表示左键
|
|
|
+ this.showMenu = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 查找子组件
|
|
|
+ * @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);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ computedGridViewOrder(id) {
|
|
|
+ // 获取网格 id,是第几行第几列第几个网格,通过 data.row_list 计算
|
|
|
+ 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">
|
|
|
+.courserware {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 6px;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 500px;
|
|
|
+ padding: 2%;
|
|
|
+ background-color: #fff;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ border-bottom-right-radius: 12px;
|
|
|
+ border-bottom-left-radius: 12px;
|
|
|
+
|
|
|
+ .row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr !important;
|
|
|
+ grid-auto-flow: unset !important;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .col {
|
|
|
+ display: grid;
|
|
|
+ gap: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .main {
|
|
|
+ .option-list {
|
|
|
+ flex-direction: column !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .row-checkbox {
|
|
|
+ position: absolute;
|
|
|
+ left: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .custom-context-menu,
|
|
|
+ .remark-info {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 999;
|
|
|
+ display: flex;
|
|
|
+ gap: 3px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .custom-context-menu {
|
|
|
+ padding-left: 30px;
|
|
|
+ background: url('../../../../assets/icon-publish.png') left center no-repeat;
|
|
|
+ background-size: 24px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|