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