Jelajahi Sumber

优化行样式计算

dsy 1 Minggu lalu
induk
melakukan
13974aeac7

+ 47 - 84
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -838,106 +838,69 @@ export default {
       }
     },
     computedColStyle(col) {
-      let grid = col.grid_list;
+      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;
+      // 只分组一次,后续复用,避免在循环中反复 filter/find。
+      const rowMap = new Map();
+      const rowOrder = [];
+      let maxCol = 0;
+      let curMaxRow = 0;
+
+      grid.forEach((item) => {
+        if (!rowMap.has(item.row)) {
+          rowMap.set(item.row, []);
+          rowOrder.push(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 gridStr = '';
-      let gridArr = [];
-      let gridRowArr = []; // 存储不同 row 中间的 line
-      grid.forEach(({ grid_area, row }, i) => {
-        // 如果 gridArr[row - 1] 不存在则创建
-        if (!gridArr[row - 1]) {
-          gridArr[row - 1] = [];
+      const areaLines = [];
+      rowOrder.forEach((row, index) => {
+        const rowItems = rowMap.get(row) || [];
+        if (index > 0 && row > 1) {
+          areaLines.push(`'${`middle-${rowItems[0].grid_area} `.repeat(maxCol * 3)}'`);
         }
 
-        if (row > 1 && grid[i - 1].row !== row) {
-          gridRowArr.push({
-            row: row - 1,
-            str: `middle-${grid_area} `.repeat(maxCol * 3),
+        const rowAreas = [];
+        if (row === curMaxRow) {
+          rowItems.forEach(({ grid_area }) => {
+            rowAreas.push(`left-${grid_area} ${grid_area} right-${grid_area}`);
           });
-        }
-
-        // 如果当前 row 是最大列数的 row
-        if (curMaxRow === row) {
-          gridArr[row - 1].push(`left-${grid_area} ${grid_area} right-${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) * 3; // 需要的数量
-
-          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(`left-${grid_area} ${str} right-${grid_area}`);
-        }
-      });
-      gridArr.forEach((item, i) => {
-        let find = gridRowArr.find((row) => row.row === i);
-        if (find) {
-          gridStr += `'${find.str}' `;
-        }
-        gridStr += `'${item.join(' ')}' `;
-      });
-      // 计算 grid_template_columns
-      let gridTemCols = '';
-      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) {
-          gridTemCols += `${item.width} 4px 4px `;
+          const needNum = (maxCol - rowItems.length) * 3;
+          const spread = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
+          rowItems.forEach(({ grid_area }, i) => {
+            const repeatNum = rowItems.length === 1 ? needNum + 1 : spread[i] + 1;
+            const inner = ` ${grid_area} `.repeat(repeatNum);
+            rowAreas.push(`left-${grid_area} ${inner} right-${grid_area}`);
+          });
         }
+        areaLines.push(`'${rowAreas.join(' ')}'`);
       });
 
+      // 计算 grid_template_columns(使用列数最多的一行作为基准)
+      const maxRowItems = rowMap.get(curMaxRow) || [];
+      const gridTemCols = maxRowItems.map(({ width }) => width).join(' 4px 4px ');
+
       // 计算 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?.edit_height || item.height);
-      });
-      gridMap.forEach((value, i) => {
-        if (i > 1) {
-          gridTemplateRows += '4px ';
-        }
-        if (value.length === 1) {
-          gridTemplateRows += `${value[0]} `;
-        } else {
-          let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
-          gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
-        }
+      const rowHeights = rowOrder.map((row) => {
+        const heights = (rowMap.get(row) || []).map((item) => item?.edit_height || item.height);
+        if (heights.length <= 1) return heights[0] || 'auto';
+        const isAllAuto = heights.every((item) => item === 'auto');
+        return isAllAuto ? 'auto' : `max(${heights.join(', ')})`;
       });
 
       return {
         width: col.width,
-        gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${gridStr} '${'grid-bottom '.repeat(maxCol * 3)}'`,
-        gridTemplateColumns: `0 ${gridTemCols.slice(0, gridTemCols.length - 8)} 0`,
-        gridTemplateRows: `0 ${gridTemplateRows} 0`,
+        gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${areaLines.join(' ')} '${'grid-bottom '.repeat(maxCol * 3)}'`,
+        gridTemplateColumns: `0 ${gridTemCols} 0`,
+        gridTemplateRows: `0 ${rowHeights.join(' 4px ')} 0`,
       };
     },
     /**

+ 61 - 67
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -1,5 +1,5 @@
 <template>
-  <div ref="preview" class="preview">
+  <div ref="previewRoot" class="preview">
     <div v-if="heightPrompt" class="height-prompt"></div>
     <template v-for="(row, i) in rowList">
       <!-- 行 -->
@@ -22,7 +22,7 @@
                   :class="[type, ...lineClass]"
                   :style="{ gridArea: type }"
                   :data-type="type"
-                  @mousedown="dragStart($event, { cursor, type: type, i, j, k, id: grid.id })"
+                  @mousedown="dragStart($event, { cursor, type, i, j, k, id: grid.id })"
                 ></span>
               </template>
               <component
@@ -30,7 +30,6 @@
                 :id="grid.id"
                 ref="preview"
                 :key="`preview-${grid.id}`"
-                @handleHeightChange="handleHeightChange"
                 :courseware-id="coursewareId"
                 type="edit"
                 :class="[grid.id]"
@@ -39,6 +38,7 @@
                   height: grid.height,
                   overflow: 'auto',
                 }"
+                @handleHeightChange="handleHeightChange"
               />
             </div>
           </div>
@@ -105,6 +105,7 @@ export default {
           lineClass: ['drag-line'],
         },
       ],
+      dragElement: null, // 当前拖拽的组件实例
       // 不需要移动的组件
       noMoveComponent: ['divider', 'spacing'],
       bookInfo: {
@@ -126,7 +127,7 @@ export default {
     document.addEventListener('mouseup', this.dragEnd);
   },
   mounted() {
-    const element = this.$refs.preview;
+    const element = this.$refs.previewRoot;
     // 监听 courserware 高度变化,获取其高度
     this.resizeObserver = new ResizeObserver(() => {
       const rect = element.getBoundingClientRect();
@@ -176,82 +177,69 @@ export default {
       }
       return arr;
     },
+    /**
+     * 计算列样式
+     * @param {Object} col 列对象
+     */
     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);
+      // 单次分组:后续 areas / columns / rows 统一复用 rowGroups。
+      const rowGroups = new Map();
+      grid.forEach((item) => {
+        if (!rowGroups.has(item.row)) {
+          rowGroups.set(item.row, []);
+        }
+        rowGroups.get(item.row).push(item);
       });
+
+      let maxCol = 0; // 最大列数
       let curMaxRow = 0; // 当前数量最大 row 的值
-      rowList.forEach((value, key) => {
-        if (value > maxCol) {
-          maxCol = value;
-          curMaxRow = key;
+      rowGroups.forEach((items, row) => {
+        if (items.length > maxCol) {
+          maxCol = items.length;
+          curMaxRow = row;
         }
       });
+
+      const sortedRows = Array.from(rowGroups.keys()).sort((a, b) => a - b);
+
       // 计算 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; // 需要的数量
+      sortedRows.forEach((row) => {
+        const rowItems = rowGroups.get(row) || [];
+        const needNum = maxCol - rowItems.length; // 需要补齐的数量
+        const splitArr = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.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);
+        const areaList = rowItems.map(({ grid_area }, index) => {
+          if (curMaxRow === row) {
+            return `${grid_area}`;
           }
-          gridArr[row - 1].push(`${str}`);
-        }
-      });
-      gridArr.forEach((item) => {
-        gridTemplateAreas += `'${item.join(' ')}' `;
+          if (rowItems.length === 1) {
+            return ` ${grid_area} `.repeat(needNum + 1);
+          }
+          return splitArr[index] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(splitArr[index] + 1);
+        });
+
+        gridTemplateAreas += `'${areaList.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} `;
-        }
+      const maxRowItems = rowGroups.get(curMaxRow) || [];
+      maxRowItems.forEach((item) => {
+        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]} `;
+      sortedRows.forEach((row) => {
+        const heights = (rowGroups.get(row) || []).map((item) => item.height);
+        if (heights.length === 1) {
+          gridTemplateRows += `${heights[0]} `;
         } else {
-          let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
-          gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
+          const isAllAuto = heights.every((item) => item === 'auto'); // 是否全是 auto
+          gridTemplateRows += isAllAuto ? 'auto ' : `max(${heights.join(', ')}) `;
         }
       });
 
@@ -273,6 +261,10 @@ export default {
      * @param {number} k
      */
     dragStart(event, { cursor, type, id, i, j, k }) {
+      const dragElement = this.findChildComponentByKey(`preview-${id}`);
+      if (!dragElement) return;
+      this.dragElement = dragElement;
+
       const { clientX, clientY } = event;
       this.drag = {
         dragging: true,
@@ -285,7 +277,6 @@ export default {
         k,
       };
 
-      this.bgColor = '#272727';
       document.body.style.cursor = cursor;
     },
     /**
@@ -300,11 +291,10 @@ export default {
       const offsetX = clientX - startX;
       const offsetY = clientY - startY;
 
-      let el = this.findChildComponentByKey(`preview-${id}`);
-      let { min_height, min_width } = el.data;
+      let { min_height, min_width } = this.dragElement.data;
+
+      const ROW_WIDTH = 1000;
 
-      // 获取行的宽度
-      const row_width = document.getElementsByClassName(`row-${i}`)[0].getBoundingClientRect().width;
       this.$emit('computedMoveData', {
         i,
         j,
@@ -315,7 +305,7 @@ export default {
         id,
         min_width,
         min_height,
-        row_width,
+        row_width: ROW_WIDTH,
       });
 
       this.drag.startX = clientX;
@@ -332,9 +322,13 @@ export default {
         startX: 0,
         startY: 0,
         type: '',
+        id: '',
+        i: -1,
+        j: -1,
+        k: -1,
       };
 
-      this.bgColor = '#ebebeb';
+      this.dragElement = null;
       document.body.style.cursor = 'auto';
     },
     // 获取子组件

+ 85 - 95
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -372,120 +372,113 @@ export default {
      * @returns {Object} 列的样式对象
      */
     computedColStyle(col) {
-      const grid = col.grid_list;
+      const grid = col.grid_list || [];
+      if (grid.length === 0) {
+        return {
+          width: col.width,
+          gridTemplateAreas: '',
+          gridTemplateColumns: '',
+          gridTemplateRows: '',
+        };
+      }
 
-      let maxCol = 0; // 最大列数
-      let rowList = new Map();
-      grid.forEach(({ row }) => {
-        rowList.set(row, (rowList.get(row) || 0) + 1);
+      // 先按 row 分组,避免后续重复 filter 扫描
+      const rowMap = new Map();
+      grid.forEach((item) => {
+        if (!rowMap.has(item.row)) {
+          rowMap.set(item.row, []);
+        }
+        rowMap.get(item.row).push(item);
       });
-      let curMaxRow = 0; // 当前数量最大 row 的值
-      rowList.forEach((value, key) => {
-        if (value > maxCol) {
-          maxCol = value;
-          curMaxRow = key;
+
+      let maxCol = 0;
+      let curMaxRow = 0;
+      rowMap.forEach((items, row) => {
+        if (items.length > maxCol) {
+          maxCol = items.length;
+          curMaxRow = row;
         }
       });
+
       // 计算 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}`);
+      rowMap.forEach((items, row) => {
+        let rowAreas = [];
+        if (row === curMaxRow) {
+          rowAreas = items.map((item) => item.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);
+          const needNum = maxCol - items.length;
+          if (items.length === 1) {
+            rowAreas = Array(needNum + 1).fill(items[0].grid_area);
           } else {
-            let arr = this.splitInteger(needNum, filter.length);
-            str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
+            const splitArr = this.splitInteger(needNum, items.length);
+            rowAreas = items.flatMap((item, index) => Array(splitArr[index] + 1).fill(item.grid_area));
           }
-          gridArr[row - 1].push(`${str}`);
         }
-      });
-      gridArr.forEach((item) => {
-        gridTemplateAreas += `'${item.join(' ')}' `;
+        gridTemplateAreas += `'${rowAreas.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} `;
-        }
-      });
+      const maxRowItems = rowMap.get(curMaxRow) || [];
+      const gridTemplateColumns = maxRowItems.length ? `${maxRowItems.map((item) => item.width).join(' ')} ` : '';
 
       // 计算 grid_template_rows
-      let gridTemplateRows = '';
-      // 将 grid 按照 row 分组
-      let gridMap = new Map();
-      grid.forEach((item) => {
-        if (!gridMap.has(item.row)) {
-          gridMap.set(item.row, []);
+      const previewById = new Map();
+      (this.$refs.preview || []).forEach((child) => {
+        const id = child?.$el?.dataset?.id;
+        if (id) {
+          previewById.set(id, child);
         }
-        gridMap.get(item.row).push({ height: item.height, id: item.id });
       });
+      const hasOperationById = (id) => {
+        const component = previewById.get(id);
+        return Boolean(component && component.$el.querySelector('.operation'));
+      };
+      const toNumberHeight = (height) => {
+        const num = Number(String(height).replace('px', ''));
+        return Number.isFinite(num) ? num : NaN;
+      };
 
-      gridMap.forEach((value) => {
-        if (value.length === 1) {
-          const component = this.$refs.preview?.find(
-            (child) => child.$el && child.$el.dataset && child.$el.dataset.id === value[0].id,
-          );
+      let gridTemplateRows = '';
+      rowMap.forEach((items) => {
+        if (items.length === 1) {
+          const current = items[0];
+          if (current.height === 'auto') {
+            gridTemplateRows += 'auto ';
+            return;
+          }
+          let baseHeight = toNumberHeight(current.height);
+          if (Number.isNaN(baseHeight)) {
+            gridTemplateRows += `${current.height} `;
+            return;
+          }
+          if (hasOperationById(current.id)) {
+            baseHeight += 48;
+          }
+          gridTemplateRows += `${baseHeight}px `;
+          return;
+        }
 
-          const hasOperation = component && component.$el.querySelector('.operation') !== null; // 判断是否有操作按钮
-          let height = hasOperation ? `${Number(value[0].height.replace('px', '')) + 48}px` : value[0].height;
+        const nonAutoItems = items.filter((item) => item.height !== 'auto');
+        if (nonAutoItems.length === 0) {
+          gridTemplateRows += 'auto ';
+          return;
+        }
 
-          gridTemplateRows += `${height} `;
-        } else {
-          let isAllAuto = value.every((item) => item.height === 'auto'); // 是否全是 auto
-          if (!isAllAuto) {
-            // 判断 value 中height 最大的组件是那个,不包括 auto 的组件
-            let maxHeight = 0;
-            let maxId = '';
-            value.forEach((item) => {
-              if (item.height !== 'auto') {
-                const heightNum = Number(item.height.replace('px', ''));
-                if (heightNum > maxHeight) {
-                  maxHeight = heightNum;
-                  maxId = item.id;
-                }
-              }
-            });
-            // 判断最高组件是否有操作按钮
-            const component = this.$refs.preview?.find(
-              (child) => child.$el && child.$el.dataset && child.$el.dataset.id === maxId,
-            );
-            const hasOperation = component && component.$el.querySelector('.operation') !== null; // 判断是否有操作按钮
-            if (hasOperation) {
-              maxHeight += 48;
-            }
-            // 将 maxId 的 height 设置为 maxHeight
-            value.forEach((item) => {
-              if (item.id === maxId) {
-                item.height = `${maxHeight}px`;
-              }
-            });
+        let maxItem = null;
+        let maxHeight = 0;
+        nonAutoItems.forEach((item) => {
+          const current = toNumberHeight(item.height);
+          if (!Number.isNaN(current) && current > maxHeight) {
+            maxHeight = current;
+            maxItem = item;
           }
-          gridTemplateRows += isAllAuto
-            ? 'auto '
-            : `${Math.max(...value.map((item) => Number(item.height.replace('px', ''))))}px `;
+        });
+
+        if (maxItem && hasOperationById(maxItem.id)) {
+          maxHeight += 48;
         }
+        gridTemplateRows += `${maxHeight}px `;
       });
 
       return {
@@ -937,9 +930,6 @@ export default {
       if (!_offset) _offset = 0;
       const element = document.querySelector(`div[data-id="${dataId}"]`);
       if (element) {
-        const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
-        const offsetPosition = elementPosition - _offset;
-
         element.scrollIntoView({
           behavior: 'smooth', // 滚动行为:'auto' | 'smooth'
           block: 'center', // 垂直对齐:'start' | 'center' | 'end' | 'nearest'

+ 5 - 10
src/views/book/courseware/preview/components/record_input/SoundRecord.vue

@@ -52,14 +52,9 @@
         v-if="microphoneStatus"
         icon-class="luyin-ing"
         size="24"
-        :style="{ color: attrib.topic_color ? attrib.topic_color : '#DCDFE6' }"
-      />
-      <SvgIcon
-        v-else
-        icon-class="luyin-radio-button"
-        size="24"
-        :style="{ color: attrib.topic_color ? attrib.topic_color : '#DCDFE6' }"
+        :style="{ color: attrib?.topic_color ?? '#DCDFE6' }"
       />
+      <SvgIcon v-else icon-class="luyin-radio-button" size="24" :style="{ color: attrib?.topic_color ?? '#DCDFE6' }" />
     </div>
 
     <span
@@ -71,7 +66,7 @@
       ]"
       :style="{
         fontSize: attrib && attrib.font_size ? attrib.font_size : '',
-        color: microphoneStatus && attrib && attrib.topic_color ? attrib.topic_color : '#DCDFE6',
+        color: microphoneStatus && attrib && attrib?.topic_color ? attrib?.topic_color : '#DCDFE6',
       }"
       >{{ isPlaying ? '-' : '' }}{{ handleDateTime(recordtime) }}</span
     >
@@ -92,7 +87,7 @@
       <SvgIcon
         icon-class="luyin-delete"
         size="24"
-        :style="{ color: hasMicro ? (attrib.topic_color ? attrib.topic_color : '#DCDFE6') : 'rgba(0, 0, 0, 0.3)' }"
+        :style="{ color: hasMicro ? (attrib?.topic_color ?? '#DCDFE6') : 'rgba(0, 0, 0, 0.3)' }"
       />
     </a>
   </div>
@@ -102,6 +97,7 @@
 import Recorder from 'js-audio-recorder'; // 录音插件
 
 export default {
+  inject: ['convertText'],
   props: {
     wavData: {
       type: Object,
@@ -145,7 +141,6 @@ export default {
       default: () => {},
     },
   },
-  inject: ['convertText'],
   data() {
     return {
       recorder: new Recorder({