Parcourir la source

移动端预览初始框架

zq il y a 1 mois
Parent
commit
fb8a3cccc7

+ 1 - 1
src/enterPage.vue

@@ -32,7 +32,7 @@ export default {
           this.data = response;
           setToken(response);
           if (this.isMobile()) {
-            // this.$router.replace({ name: 'MobileBookPreview', query: { book_id: this.data.book_id } });
+            this.$router.replace({ name: 'MobileBookPreview', query: { book_id: this.data.book_id } });
           } else {
             this.$router.replace({ name: 'BookPreview', query: { book_id: this.data.book_id } });
           }

+ 6 - 0
src/router/modules/basic.js

@@ -10,6 +10,12 @@ export const BookPreview = {
   component: () => import('@/web_preview/index.vue'),
 };
 
+export const MobileBookPreview = {
+  path: '/mobile_book_preview',
+  name: 'MobileBookPreview',
+  component: () => import('@/mobile_preview/index.vue'),
+};
+
 export const NotFoundPage = {
   path: '*',
   name: '404',

+ 480 - 0
src/views/book/courseware/preview/MobileCoursewarePreview.vue

@@ -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>