Browse Source

教材编辑分组功能

dusenyao 2 days ago
parent
commit
07755d4922

+ 191 - 34
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -15,10 +15,17 @@
     ]"
     ]"
   >
   >
     <template v-if="isEdit">
     <template v-if="isEdit">
+      <div v-for="item in lineList" :key="item[0]" class="group-line" :style="computedGroupLine(item)"></div>
       <span class="drag-line" data-row="-1"></span>
       <span class="drag-line" data-row="-1"></span>
       <!-- 行 -->
       <!-- 行 -->
       <template v-for="(row, i) in data.row_list">
       <template v-for="(row, i) in data.row_list">
         <div :key="i" class="row" :style="computedRowStyle(i)">
         <div :key="i" class="row" :style="computedRowStyle(i)">
+          <el-checkbox
+            v-if="row?.row_id"
+            v-model="rowCheckList[row.row_id]"
+            :class="['row-checkbox', `${row.row_id}`]"
+          />
+
           <!-- 列 -->
           <!-- 列 -->
           <template v-for="(col, j) in row.col_list">
           <template v-for="(col, j) in row.col_list">
             <span
             <span
@@ -66,7 +73,8 @@
                   ref="component"
                   ref="component"
                   :key="`grid-${grid.id}`"
                   :key="`grid-${grid.id}`"
                   :class="[grid.id]"
                   :class="[grid.id]"
-                  :style="{ gridArea: grid.grid_area, height: grid.height, marginTop: grid.row !== 1 ? '16px' : '0' }"
+                  :border-color="computedBorderColor(row.row_id)"
+                  :style="computedGridStyle(grid, row.row_id)"
                   :delete-component="deleteComponent(i, j, k)"
                   :delete-component="deleteComponent(i, j, k)"
                   :component-move="componentMove(i, j, k)"
                   :component-move="componentMove(i, j, k)"
                   @showSetting="showSetting"
                   @showSetting="showSetting"
@@ -109,6 +117,7 @@
 import { getRandomNumber } from '@/utils/index';
 import { getRandomNumber } from '@/utils/index';
 import { componentList } from '../../data/bookType';
 import { componentList } from '../../data/bookType';
 import { ContentSaveCoursewareContent, ContentGetCoursewareContent } from '@/api/book';
 import { ContentSaveCoursewareContent, ContentGetCoursewareContent } from '@/api/book';
+import _ from 'lodash';
 
 
 import PreviewEdit from './PreviewEdit.vue';
 import PreviewEdit from './PreviewEdit.vue';
 
 
@@ -142,6 +151,9 @@ export default {
         // 组件列表
         // 组件列表
         row_list: [],
         row_list: [],
       },
       },
+      rowCheckList: {}, // 行复选框列表
+      content_group_row_list: [], // 行分组id列表
+      gridBorderColorList: ['#3fc7cc', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#d863ff', '#724fff'], // 网格边框颜色列表
       curType: 'divider',
       curType: 'divider',
       componentList,
       componentList,
       curRow: -2,
       curRow: -2,
@@ -157,6 +169,17 @@ export default {
       },
       },
     };
     };
   },
   },
+  computed: {
+    lineList() {
+      let arr = [];
+      this.content_group_row_list.forEach(({ row_id, is_pre_same_group }, i) => {
+        if (is_pre_same_group) {
+          arr.push([this.content_group_row_list[i - 1].row_id, row_id]);
+        }
+      });
+      return arr;
+    },
+  },
   watch: {
   watch: {
     drag: {
     drag: {
       handler(val) {
       handler(val) {
@@ -198,15 +221,72 @@ export default {
         if (!find) {
         if (!find) {
           this.$emit('showSettingEmpty');
           this.$emit('showSettingEmpty');
         }
         }
+
+        this.rowCheckList = Object.fromEntries(val.filter((row) => row?.row_id).map((row) => [row.row_id, false]));
+
+        // 增加新添的行
+        val.forEach(({ row_id }, i) => {
+          let isHas = this.content_group_row_list.some((group) => group.row_id === row_id);
+          if (!isHas) {
+            this.content_group_row_list.splice(i, 0, { row_id, is_pre_same_group: false });
+          }
+        });
+
+        // 过滤掉已经不存在的行
+        this.content_group_row_list = this.content_group_row_list.filter((group) => {
+          return val.find((row) => row.row_id === group.row_id);
+        });
       },
       },
       immediate: true,
       immediate: true,
     },
     },
+    rowCheckList: {
+      handler(val) {
+        // 如果同时有两个选中,将选中的挑选出来,并将它们的状态变为 false
+        let selectedRowID = [];
+        Object.keys(val).forEach((key) => {
+          if (val[key]) {
+            selectedRowID.push(key);
+          }
+        });
+
+        if (selectedRowID.length > 1) {
+          selectedRowID.forEach((id) => {
+            this.rowCheckList[id] = false;
+          });
+        } else {
+          return false;
+        }
+
+        let selectIndex = selectedRowID
+          .map((id) => this.content_group_row_list.findIndex(({ row_id }) => row_id === id))
+          .sort((a, b) => a - b);
+
+        // 只处理选中两行的情况
+        if (selectIndex[1] - selectIndex[0] === 1) {
+          // 相邻两行,切换分组状态
+          this.content_group_row_list[selectIndex[1]].is_pre_same_group =
+            !this.content_group_row_list[selectIndex[1]].is_pre_same_group;
+        } else {
+          // 非相邻,判断中间行是否都已分组
+          const allGrouped = this.content_group_row_list
+            .slice(selectIndex[0] + 1, selectIndex[1] + 1)
+            .every((group) => group.is_pre_same_group);
+
+          for (let i = selectIndex[0] + 1; i <= selectIndex[1]; i++) {
+            this.content_group_row_list[i].is_pre_same_group = !allGrouped;
+          }
+        }
+      },
+      deep: true,
+    },
   },
   },
   created() {
   created() {
-    ContentGetCoursewareContent({ id: this.courseware_id }).then(({ content }) => {
+    ContentGetCoursewareContent({ id: this.courseware_id }).then(({ content, content_group_row_list }) => {
       if (content) {
       if (content) {
         this.data = JSON.parse(content);
         this.data = JSON.parse(content);
       }
       }
+      if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
+
       this.$watch(
       this.$watch(
         'data',
         'data',
         () => {
         () => {
@@ -250,40 +330,59 @@ export default {
         background: 'rgba(0, 0, 0, 0.7)',
         background: 'rgba(0, 0, 0, 0.7)',
       });
       });
 
 
-      /**
-       * 分组id列表
-       * 将 data.row_list 中的每一行转换为一个组
-       */
-      const group_component_id = this.data.row_list.flatMap((row, index) => {
-        // 查找每行中第一个包含 describe、label 或 stem 的组件
-        let findKey = '';
-        let findType = '';
-        row.col_list.some((col) => {
-          const findItem = col.grid_list.find(({ type }) => {
-            return ['describe', 'label', 'stem'].includes(type);
+      let groupIdList = _.cloneDeep(this.content_group_row_list);
+      let groupList = [];
+      // 通过判断 is_pre_same_group 将组合并
+      for (let i = 0; i < groupIdList.length; i++) {
+        if (groupIdList[i].is_pre_same_group) {
+          groupList[groupList.length - 1].row_id_list.push(groupIdList[i].row_id);
+        } else {
+          groupList.push({
+            name: '',
+            row_id_list: [groupIdList[i].row_id],
+            component_id_list: [],
           });
           });
-          if (findItem) {
-            findKey = findItem.id;
-            findType = findItem.type;
-            return true;
-          }
-        });
-        let name = `组${index + 1}`;
-
-        // 如果有标签类组件,获取对应名称
-        if (findKey) {
-          let item = this.findChildComponentByKey(`grid-${findKey}`);
-          if (['describe', 'stem'].includes(findType)) {
-            name = item.data.content.replace(/<[^>]+>/g, ''); // 去掉html标签
-          } else if (findType === 'label') {
-            name = item.data.dynamicTags.map((tag) => tag.text).join(', ');
-          }
         }
         }
+      }
 
 
-        return {
-          name,
-          id_list: row.col_list.map((col) => col.grid_list.map((grid) => grid.id)),
-        };
+      // 通过合并后的分组,获取对应的组件 id 和分组名称
+      groupList.forEach(({ row_id_list, component_id_list }, i) => {
+        row_id_list.forEach((row_id, j) => {
+          let row = this.data.row_list.find((row) => {
+            return row.row_id === row_id;
+          });
+          // 当前行所有组件id列表
+          let gridIdList = row.col_list.map((col) => col.grid_list.map((grid) => grid.id)).flat();
+          component_id_list.push(...gridIdList);
+          // 查找每组第一行中第一个包含 describe、label 或 stem 的组件
+          if (j === 0) {
+            let findKey = '';
+            let findType = '';
+            row.col_list.some((col) => {
+              const findItem = col.grid_list.find(({ type }) => {
+                return ['describe', 'label', 'stem'].includes(type);
+              });
+              if (findItem) {
+                findKey = findItem.id;
+                findType = findItem.type;
+                return true;
+              }
+            });
+            let groupName = `组${i + 1}`;
+
+            // 如果有标签类组件,获取对应名称
+            if (findKey) {
+              let item = this.findChildComponentByKey(`grid-${findKey}`);
+              if (['describe', 'stem'].includes(findType)) {
+                groupName = item.data.content.replace(/<[^>]+>/g, ''); // 去掉html标签
+              } else if (findType === 'label') {
+                groupName = item.data.dynamicTags.map((tag) => tag.text).join(', ');
+              }
+            }
+
+            groupList[i].name = groupName;
+          }
+        });
       });
       });
 
 
       ContentSaveCoursewareContent({
       ContentSaveCoursewareContent({
@@ -291,7 +390,8 @@ export default {
         category: 'NEW',
         category: 'NEW',
         content: JSON.stringify(this.data),
         content: JSON.stringify(this.data),
         component_id_list,
         component_id_list,
-        group_component_id,
+        content_group_component_list: groupList,
+        content_group_row_list: this.content_group_row_list,
       }).then(() => {
       }).then(() => {
         this.$message.success('保存成功');
         this.$message.success('保存成功');
         loading.close();
         loading.close();
@@ -923,9 +1023,11 @@ export default {
     calculateRowInsertedObject() {
     calculateRowInsertedObject() {
       const id = `ID-${getRandomNumber(12, true)}`;
       const id = `ID-${getRandomNumber(12, true)}`;
       const letter = `L${getRandomNumber(6, true)}`;
       const letter = `L${getRandomNumber(6, true)}`;
+      const row_id = `R${getRandomNumber(6, true)}`;
 
 
       this.data.row_list.splice(this.curRow + 1, 0, {
       this.data.row_list.splice(this.curRow + 1, 0, {
         width_list: ['100fr'],
         width_list: ['100fr'],
+        row_id,
         col_list: [
         col_list: [
           {
           {
             width: '100fr',
             width: '100fr',
@@ -971,12 +1073,55 @@ export default {
     findChildComponentByKey(key) {
     findChildComponentByKey(key) {
       return this.$refs.component.find((child) => child.$vnode.key === key);
       return this.$refs.component.find((child) => child.$vnode.key === key);
     },
     },
+
+    /**
+     * 计算分组线样式
+     * @param {Array} rowIdList 行 ID 列表
+     * @returns {Object} 样式对象
+     */
+    computedGroupLine(rowIdList) {
+      const canvasTop = this.$refs.canvas.getBoundingClientRect().top; // 获取画布顶部位置
+      const firstRowTop = this.$el.querySelector(`.row-checkbox.${rowIdList[0]}`).getBoundingClientRect().top; // 获取第一个复选框的顶部位置
+      const secRowTop = this.$el.querySelector(`.row-checkbox.${rowIdList[1]}`).getBoundingClientRect().top; // 获取第二个复选框的顶部位置
+
+      return {
+        top: `${firstRowTop - canvasTop + 22}px`,
+        height: `${secRowTop - firstRowTop - 24}px`,
+      };
+    },
+    computedBorderColor(rowId) {
+      const groupList = [];
+      this.content_group_row_list.forEach(({ row_id, is_pre_same_group }) => {
+        if (is_pre_same_group && groupList.length) {
+          groupList[groupList.length - 1].push(row_id);
+        } else {
+          groupList.push([row_id]);
+        }
+      });
+      const index = groupList.findIndex((g) => g.includes(rowId));
+      return this.gridBorderColorList[index % this.gridBorderColorList.length];
+    },
+    /**
+     * 计算网格样式
+     * @param {Object} grid 网格对象
+     * @returns {Object} 样式对象
+     */
+    computedGridStyle(grid) {
+      let marginTop = grid.row === 1 ? '0' : '16px';
+
+      return {
+        gridArea: grid.grid_area,
+        height: grid.height,
+        marginTop,
+      };
+    },
   },
   },
 };
 };
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .canvas {
 .canvas {
+  position: relative;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   row-gap: 6px;
   row-gap: 6px;
@@ -988,10 +1133,22 @@ export default {
   background-repeat: no-repeat;
   background-repeat: no-repeat;
   border-radius: 4px;
   border-radius: 4px;
 
 
+  .group-line {
+    position: absolute;
+    left: 11px;
+    width: 1px;
+    background-color: #165dff;
+  }
+
   .row {
   .row {
     display: grid;
     display: grid;
     row-gap: 16px;
     row-gap: 16px;
 
 
+    .row-checkbox {
+      position: absolute;
+      left: 4px;
+    }
+
     > .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
     > .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
       left: 6px;
       left: 6px;
     }
     }

+ 5 - 8
src/views/book/courseware/create/components/common/ModuleBase.vue

@@ -2,12 +2,12 @@
   <div class="module-wrapper" @click="clickWrapper">
   <div class="module-wrapper" @click="clickWrapper">
     <div
     <div
       class="horizontal-line top"
       class="horizontal-line top"
-      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : bgColor }"
+      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : borderColor.value }"
       @mousedown="dragStart($event, 'ns-resize', 'top')"
       @mousedown="dragStart($event, 'ns-resize', 'top')"
     ></div>
     ></div>
     <div
     <div
       class="vertical-line left"
       class="vertical-line left"
-      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : bgColor }"
+      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : borderColor.value }"
       @mousedown="dragStart($event, 'ew-resize', 'left')"
       @mousedown="dragStart($event, 'ew-resize', 'left')"
     ></div>
     ></div>
     <div class="module" draggable="false">
     <div class="module" draggable="false">
@@ -27,12 +27,12 @@
     </div>
     </div>
     <div
     <div
       class="vertical-line right"
       class="vertical-line right"
-      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : bgColor }"
+      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : borderColor.value }"
       @mousedown="dragStart($event, 'ew-resize', 'right')"
       @mousedown="dragStart($event, 'ew-resize', 'right')"
     ></div>
     ></div>
     <div
     <div
       class="horizontal-line bottom"
       class="horizontal-line bottom"
-      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : bgColor }"
+      :style="{ backgroundColor: id === getCurSettingId() ? activeBgColor : borderColor.value }"
       @mousedown="dragStart($event, 'ns-resize', 'bottom')"
       @mousedown="dragStart($event, 'ns-resize', 'bottom')"
     ></div>
     ></div>
   </div>
   </div>
@@ -43,7 +43,7 @@ import { componentNameList } from '@/views/book/courseware/data/bookType.js';
 
 
 export default {
 export default {
   name: 'ModuleBase',
   name: 'ModuleBase',
-  inject: ['id', 'showSetting', 'getCurSettingId', 'deleteComponent', 'handleComponentMove'],
+  inject: ['id', 'showSetting', 'getCurSettingId', 'deleteComponent', 'handleComponentMove', 'borderColor'],
   props: {
   props: {
     type: {
     type: {
       type: String,
       type: String,
@@ -59,7 +59,6 @@ export default {
         startY: 0,
         startY: 0,
         type: '',
         type: '',
       },
       },
-      bgColor: '#ebebeb',
       activeBgColor: '#82b4ff',
       activeBgColor: '#82b4ff',
     };
     };
   },
   },
@@ -87,7 +86,6 @@ export default {
         type,
         type,
       };
       };
 
 
-      this.bgColor = '#272727';
       document.body.style.cursor = cursor;
       document.body.style.cursor = cursor;
     },
     },
     /**
     /**
@@ -118,7 +116,6 @@ export default {
         type: '',
         type: '',
       };
       };
 
 
-      this.bgColor = '#ebebeb';
       document.body.style.cursor = 'auto';
       document.body.style.cursor = 'auto';
     },
     },
     clickWrapper() {
     clickWrapper() {

+ 12 - 0
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -1,4 +1,5 @@
 // 组件混入
 // 组件混入
+import Vue from 'vue';
 import ModuleBase from './ModuleBase.vue';
 import ModuleBase from './ModuleBase.vue';
 import RichText from '@/components/RichText.vue';
 import RichText from '@/components/RichText.vue';
 
 
@@ -14,6 +15,7 @@ const mixin = {
       property: {
       property: {
         isGetContent: false, // 是否已获取内容
         isGetContent: false, // 是否已获取内容
       },
       },
+      borderColorObj: Vue.observable({ value: this.borderColor }), // 边框颜色
     };
     };
   },
   },
   props: {
   props: {
@@ -29,6 +31,10 @@ const mixin = {
       type: Function,
       type: Function,
       required: true,
       required: true,
     },
     },
+    borderColor: {
+      type: String,
+      required: true,
+    },
   },
   },
   components: {
   components: {
     ModuleBase,
     ModuleBase,
@@ -41,8 +47,14 @@ const mixin = {
       deleteComponent: this.deleteComponent,
       deleteComponent: this.deleteComponent,
       handleComponentMove: this.handleComponentMove,
       handleComponentMove: this.handleComponentMove,
       property: this.property,
       property: this.property,
+      borderColor: this.borderColorObj,
     };
     };
   },
   },
+  watch: {
+    borderColor(newVal) {
+      this.borderColorObj.value = newVal;
+    },
+  },
   inject: ['courseware_id'],
   inject: ['courseware_id'],
   created() {
   created() {
     ContentGetCoursewareComponentContent({ courseware_id: this.courseware_id, component_id: this.id }).then(
     ContentGetCoursewareComponentContent({ courseware_id: this.courseware_id, component_id: this.id }).then(