CreateCanvas.vue 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  1. <template>
  2. <main
  3. ref="canvas"
  4. class="canvas"
  5. :style="[
  6. {
  7. backgroundImage: data.background_image_url ? `url(${data.background_image_url})` : '',
  8. backgroundSize: data.background_image_url
  9. ? `${data.background_position.width}% ${data.background_position.height}%`
  10. : '',
  11. backgroundPosition: data.background_image_url
  12. ? `${data.background_position.left}% ${data.background_position.top}%`
  13. : '',
  14. },
  15. ]"
  16. >
  17. <template v-if="isEdit">
  18. <div v-for="item in lineList" :key="item[0]" class="group-line" :style="computedGroupLine(item)"></div>
  19. <span class="drag-line" data-row="-1"></span>
  20. <!-- 行 -->
  21. <template v-for="(row, i) in data.row_list">
  22. <div :key="i" class="row" :style="computedRowStyle(i)">
  23. <el-checkbox
  24. v-if="row?.row_id"
  25. v-model="rowCheckList[row.row_id]"
  26. :class="['row-checkbox', `${row.row_id}`]"
  27. />
  28. <!-- 列 -->
  29. <template v-for="(col, j) in row.col_list">
  30. <span
  31. v-if="j === 0"
  32. :key="`start-${i}-${j}`"
  33. class="drag-vertical-line col-start"
  34. :data-row="i"
  35. :data-col="j"
  36. ></span>
  37. <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
  38. <!-- 网格 -->
  39. <template v-for="(grid, k) in col.grid_list">
  40. <span
  41. v-if="k === 0"
  42. :key="`start-${i}-${j}-${k}`"
  43. class="drag-line grid-line drag-row"
  44. :style="{ gridArea: 'grid-top' }"
  45. :data-row="i"
  46. :data-col="j"
  47. :data-grid="k"
  48. data-type="row"
  49. ></span>
  50. <span
  51. v-if="grid.row > 1 && grid.row !== col.grid_list[k - 1].row"
  52. :key="`middle-${i}-${j}-${k}`"
  53. :style="{ gridArea: `middle-${grid.grid_area}` }"
  54. :data-row="i"
  55. :data-col="j"
  56. :data-grid="k"
  57. data-type="col-middle"
  58. class="drag-line grid-line"
  59. ></span>
  60. <span
  61. :key="`left-${i}-${j}-${k}`"
  62. :style="{ gridArea: `left-${grid.grid_area}` }"
  63. :data-row="i"
  64. :data-col="j"
  65. :data-grid="k"
  66. data-type="col-left"
  67. class="drag-vertical-line grid-line grid-line-left"
  68. ></span>
  69. <component
  70. :is="componentList[grid.type]"
  71. :id="grid.id"
  72. ref="component"
  73. :key="`grid-${grid.id}`"
  74. :class="[grid.id]"
  75. :border-color="computedBorderColor(row.row_id)"
  76. :style="computedGridStyle(grid, row.row_id)"
  77. :delete-component="deleteComponent(i, j, k)"
  78. :component-move="componentMove(i, j, k)"
  79. @showSetting="showSetting"
  80. @changeData="changeData"
  81. />
  82. <span
  83. :key="`right-${i}-${j}-${k}`"
  84. :style="{ gridArea: `right-${grid.grid_area}` }"
  85. :data-row="i"
  86. :data-col="j"
  87. :data-grid="k + 1"
  88. data-type="col-right"
  89. class="drag-vertical-line grid-line grid-line-right"
  90. ></span>
  91. <span
  92. v-if="k === col.grid_list.length - 1"
  93. :key="`end-${i}-${j}-${k}`"
  94. class="drag-line grid-line drag-row"
  95. :style="{ gridArea: `grid-bottom` }"
  96. :data-row="i"
  97. :data-col="j"
  98. :data-grid="k + 1"
  99. data-type="row"
  100. ></span>
  101. </template>
  102. </div>
  103. <span :key="`end-${i}-${j}`" class="drag-vertical-line col-end" :data-row="i" :data-col="j + 1"></span>
  104. </template>
  105. </div>
  106. <span v-if="i < data.row_list.length - 1" :key="`row-${i}`" class="drag-line" :data-row="i"></span>
  107. </template>
  108. <span class="drag-line" :data-row="data.row_list.length - 1"></span>
  109. </template>
  110. <PreviewEdit v-else :courseware-id="courseware_id" :row-list="data.row_list" @computedMoveData="computedMoveData" />
  111. </main>
  112. </template>
  113. <script>
  114. import { getRandomNumber } from '@/utils/index';
  115. import { componentList } from '../../data/bookType';
  116. import { ContentSaveCoursewareContent, ContentGetCoursewareContent } from '@/api/book';
  117. import _ from 'lodash';
  118. import PreviewEdit from './PreviewEdit.vue';
  119. export default {
  120. name: 'CreateCanvas',
  121. components: {
  122. PreviewEdit,
  123. },
  124. inject: ['getCurSettingId'],
  125. props: {
  126. isEdit: {
  127. type: Boolean,
  128. required: true,
  129. },
  130. },
  131. data() {
  132. const { project_id } = this.$route.query;
  133. return {
  134. courseware_id: this.$route.params.courseware_id,
  135. project_id,
  136. data: {
  137. background_image_url: '',
  138. background_position: {
  139. width: 100,
  140. height: 100,
  141. top: 0,
  142. left: 0,
  143. },
  144. // 组件列表
  145. row_list: [],
  146. },
  147. rowCheckList: {}, // 行复选框列表
  148. content_group_row_list: [], // 行分组id列表
  149. gridBorderColorList: ['#3fc7cc', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#d863ff', '#724fff'], // 网格边框颜色列表
  150. curType: 'divider',
  151. componentList,
  152. curRow: -2,
  153. curCol: -1,
  154. curGrid: -1,
  155. gridInsertType: '', // 网格插入类型
  156. enterCanvas: false, // 是否进入画布
  157. // 拖拽状态
  158. drag: {
  159. clientX: 0,
  160. clientY: 0,
  161. dragging: false,
  162. },
  163. };
  164. },
  165. computed: {
  166. lineList() {
  167. let arr = [];
  168. this.content_group_row_list.forEach(({ row_id, is_pre_same_group }, i) => {
  169. if (is_pre_same_group) {
  170. arr.push([this.content_group_row_list[i - 1].row_id, row_id]);
  171. }
  172. });
  173. return arr;
  174. },
  175. },
  176. watch: {
  177. drag: {
  178. handler(val) {
  179. if (val.dragging) {
  180. const dragging = document.querySelector('.canvas-dragging');
  181. dragging.style.left = `${val.clientX}px`;
  182. dragging.style.top = `${val.clientY}px`;
  183. }
  184. },
  185. deep: true,
  186. },
  187. enterCanvas: {
  188. handler(val) {
  189. if (val) return;
  190. if (!this.isEdit) return;
  191. const dragLineList = document.querySelectorAll('.drag-line');
  192. dragLineList.forEach((item) => {
  193. item.style.opacity = 0;
  194. });
  195. this.curRow = -2;
  196. const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line');
  197. dragVerticalLineList.forEach((item) => {
  198. item.style.opacity = 0;
  199. });
  200. this.curCol = -1;
  201. this.curGrid = -1;
  202. this.gridInsertType = '';
  203. },
  204. },
  205. 'data.row_list': {
  206. handler(val) {
  207. // row_list 更改时判断是否有当前设置 id 的组件
  208. const curSettingId = this.getCurSettingId();
  209. const find = val.find((row) => {
  210. return row.col_list.find((col) => {
  211. return col.grid_list.find((grid) => grid.id === curSettingId);
  212. });
  213. });
  214. if (!find) {
  215. this.$emit('showSettingEmpty');
  216. }
  217. this.rowCheckList = Object.fromEntries(val.filter((row) => row?.row_id).map((row) => [row.row_id, false]));
  218. // 增加新添的行
  219. val.forEach(({ row_id }, i) => {
  220. let isHas = this.content_group_row_list.some((group) => group.row_id === row_id);
  221. if (!isHas) {
  222. this.content_group_row_list.splice(i, 0, { row_id, is_pre_same_group: false });
  223. }
  224. });
  225. // 过滤掉已经不存在的行
  226. this.content_group_row_list = this.content_group_row_list.filter((group) => {
  227. return val.find((row) => row.row_id === group.row_id);
  228. });
  229. },
  230. immediate: true,
  231. },
  232. rowCheckList: {
  233. handler(val) {
  234. // 如果同时有两个选中,将选中的挑选出来,并将它们的状态变为 false
  235. let selectedRowID = [];
  236. Object.keys(val).forEach((key) => {
  237. if (val[key]) {
  238. selectedRowID.push(key);
  239. }
  240. });
  241. if (selectedRowID.length > 1) {
  242. selectedRowID.forEach((id) => {
  243. this.rowCheckList[id] = false;
  244. });
  245. } else {
  246. return false;
  247. }
  248. let selectIndex = selectedRowID
  249. .map((id) => this.content_group_row_list.findIndex(({ row_id }) => row_id === id))
  250. .sort((a, b) => a - b);
  251. // 只处理选中两行的情况
  252. if (selectIndex[1] - selectIndex[0] === 1) {
  253. // 相邻两行,切换分组状态
  254. this.content_group_row_list[selectIndex[1]].is_pre_same_group =
  255. !this.content_group_row_list[selectIndex[1]].is_pre_same_group;
  256. } else {
  257. // 非相邻,判断中间行是否都已分组
  258. const allGrouped = this.content_group_row_list
  259. .slice(selectIndex[0] + 1, selectIndex[1] + 1)
  260. .every((group) => group.is_pre_same_group);
  261. for (let i = selectIndex[0] + 1; i <= selectIndex[1]; i++) {
  262. this.content_group_row_list[i].is_pre_same_group = !allGrouped;
  263. }
  264. }
  265. },
  266. deep: true,
  267. },
  268. },
  269. created() {
  270. ContentGetCoursewareContent({ id: this.courseware_id }).then(({ content, content_group_row_list }) => {
  271. if (content) {
  272. this.data = JSON.parse(content);
  273. }
  274. if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
  275. this.$watch(
  276. 'data',
  277. () => {
  278. this.changeData();
  279. },
  280. {
  281. deep: true,
  282. },
  283. );
  284. });
  285. },
  286. mounted() {
  287. document.addEventListener('mousemove', this.dragMove);
  288. document.addEventListener('mouseup', this.dragEnd);
  289. },
  290. beforeDestroy() {
  291. document.removeEventListener('mousemove', this.dragMove);
  292. document.removeEventListener('mouseup', this.dragEnd);
  293. },
  294. methods: {
  295. changeData() {
  296. this.$emit('changeData');
  297. },
  298. /**
  299. * 保存课件内容
  300. * @param {string} type 类型
  301. */
  302. saveCoursewareContent(type) {
  303. let component_id_list = this.data.row_list.flatMap((row) =>
  304. row.col_list.flatMap((col) => col.grid_list.map((grid) => grid.id)),
  305. );
  306. this.$refs?.component.forEach((item) => {
  307. item.saveCoursewareComponentContent();
  308. });
  309. const loading = this.$loading({
  310. lock: true,
  311. text: '保存中...',
  312. spinner: 'el-icon-loading',
  313. background: 'rgba(0, 0, 0, 0.7)',
  314. });
  315. let groupIdList = _.cloneDeep(this.content_group_row_list);
  316. let groupList = [];
  317. // 通过判断 is_pre_same_group 将组合并
  318. for (let i = 0; i < groupIdList.length; i++) {
  319. if (groupIdList[i].is_pre_same_group) {
  320. groupList[groupList.length - 1].row_id_list.push(groupIdList[i].row_id);
  321. } else {
  322. groupList.push({
  323. name: '',
  324. row_id_list: [groupIdList[i].row_id],
  325. component_id_list: [],
  326. });
  327. }
  328. }
  329. // 通过合并后的分组,获取对应的组件 id 和分组名称
  330. groupList.forEach(({ row_id_list, component_id_list }, i) => {
  331. row_id_list.forEach((row_id, j) => {
  332. let row = this.data.row_list.find((row) => {
  333. return row.row_id === row_id;
  334. });
  335. // 当前行所有组件id列表
  336. let gridIdList = row.col_list.map((col) => col.grid_list.map((grid) => grid.id)).flat();
  337. component_id_list.push(...gridIdList);
  338. // 查找每组第一行中第一个包含 describe、label 或 stem 的组件
  339. if (j === 0) {
  340. let findKey = '';
  341. let findType = '';
  342. row.col_list.some((col) => {
  343. const findItem = col.grid_list.find(({ type }) => {
  344. return ['describe', 'label', 'stem'].includes(type);
  345. });
  346. if (findItem) {
  347. findKey = findItem.id;
  348. findType = findItem.type;
  349. return true;
  350. }
  351. });
  352. let groupName = `组${i + 1}`;
  353. // 如果有标签类组件,获取对应名称
  354. if (findKey) {
  355. let item = this.findChildComponentByKey(`grid-${findKey}`);
  356. if (['describe', 'stem'].includes(findType)) {
  357. groupName = item.data.content.replace(/<[^>]+>/g, ''); // 去掉html标签
  358. } else if (findType === 'label') {
  359. groupName = item.data.dynamicTags.map((tag) => tag.text).join(', ');
  360. }
  361. }
  362. groupList[i].name = groupName;
  363. }
  364. });
  365. });
  366. ContentSaveCoursewareContent({
  367. id: this.courseware_id,
  368. category: 'NEW',
  369. content: JSON.stringify(this.data),
  370. component_id_list,
  371. content_group_component_list: groupList,
  372. content_group_row_list: this.content_group_row_list,
  373. }).then(() => {
  374. this.$message.success('保存成功');
  375. loading.close();
  376. if (type === 'quit') {
  377. this.$emit('back');
  378. }
  379. if (type === 'edit') {
  380. this.$emit('changeEditStatus');
  381. }
  382. });
  383. },
  384. setBackgroundImage(url, position) {
  385. this.data.background_image_url = url;
  386. this.data.background_position = position;
  387. this.$message.success('设置背景图成功');
  388. },
  389. /**
  390. * 显示设置
  391. * @param {object} setting
  392. * @param {string} type
  393. * @param {string} id
  394. * @param {object} params
  395. */
  396. showSetting(setting, type, id, params = {}) {
  397. this.$emit('showSetting', setting, type, id, params);
  398. },
  399. /**
  400. * 计算组件移动
  401. * @param {number} i 行
  402. * @param {number} j 列
  403. * @param {number} k 格子
  404. */
  405. componentMove(i, j, k) {
  406. return ({ type, offsetX, offsetY, id }) => {
  407. this.computedMoveData({ i, j, k, type, offsetX, offsetY, id });
  408. };
  409. },
  410. /**
  411. * 计算移动数据
  412. * @param {number} i 行
  413. * @param {number} j 列
  414. * @param {number} k 格子
  415. * @param {string} type 移动类型
  416. * @param {number} offsetX x 轴偏移量
  417. * @param {number} offsetY y 轴偏移量
  418. * @param {string} id 组件 id
  419. */
  420. computedMoveData({ i, j, k, type, offsetX, offsetY, id, min_width, min_height, row_width }) {
  421. const row = this.data.row_list[i];
  422. const col = row.col_list[j];
  423. const grid = col.grid_list[k];
  424. // 上下移动
  425. if (['top', 'bottom'].includes(type)) {
  426. this.handleVerticalMove(col, grid, offsetY, id, min_height);
  427. return;
  428. }
  429. // 一行中有多个格子
  430. let gridList = col.grid_list.filter((item) => item.row === grid.row);
  431. if (gridList.length > 1) {
  432. let find = gridList.findIndex((item) => item.id === id); // 当前 id 所在格子索引
  433. // 移动类型为 left 且不是第一个格子
  434. if (type === 'left' && find > 0) {
  435. this.handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
  436. }
  437. // 移动类型为 right 且不是最后一个格子
  438. if (type === 'right' && find < gridList.length - 1) {
  439. this.handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
  440. }
  441. return;
  442. }
  443. // 移动类型为 left 且不是第一个格子
  444. if (type === 'left' && j > 0) {
  445. this.handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width);
  446. }
  447. // 移动类型为 right 且不是最后一个格子
  448. if (type === 'right' && j < row.col_list.length - 1) {
  449. this.handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width);
  450. }
  451. this.$forceUpdate();
  452. },
  453. /**
  454. * 处理垂直移动
  455. * @param {number} col 列数据
  456. * @param {object} grid 格子数据
  457. * @param {number} offsetY y 轴偏移量
  458. * @param {string} id 组件 id
  459. * @param {number} min_height 最小高度
  460. */
  461. handleVerticalMove(col, grid, offsetY, id, min_height) {
  462. // 高度为 auto 时
  463. if (grid.height === 'auto') {
  464. const gridHeight = document.querySelector(`.${id}`).offsetHeight;
  465. const height = gridHeight + offsetY;
  466. if (height < min_height) {
  467. return;
  468. }
  469. grid.height = `${gridHeight + offsetY}px`;
  470. col.grid_template_rows = `0 ${col.grid_list.map(({ height }) => height).join(' 6px ')} 0`;
  471. return;
  472. }
  473. // 高度为数字时
  474. const height = Number(grid.height.replace('px', ''));
  475. const _h = height + offsetY;
  476. if (_h < min_height) {
  477. return;
  478. }
  479. if (_h >= 50) {
  480. grid.height = `${_h}px`;
  481. col.grid_template_rows = `0 ${col.grid_list.map(({ height }) => height).join(' 6px ')} 0`;
  482. }
  483. },
  484. /**
  485. * 处理左移且不是第一个格子
  486. * @param {object} row 行数据
  487. * @param {number} j 第几列
  488. * @param {number} offsetX x 轴偏移量
  489. * @param {number} min_width 最小宽度
  490. * @param {number} row_width 行宽度
  491. */
  492. handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width) {
  493. const prevGrid = row.width_list[j - 1];
  494. const prevWidth = Number(prevGrid.replace('fr', ''));
  495. const width = Number(row.width_list[j].replace('fr', ''));
  496. const max = prevWidth + width - 10;
  497. if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
  498. return;
  499. }
  500. // 计算拖动的距离与总宽度的比例
  501. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  502. const _w = width - ratio;
  503. if ((_w / 100) * row_width < min_width) {
  504. return;
  505. }
  506. row.width_list[j - 1] = `${prevWidth + ratio}fr`;
  507. row.width_list[j] = `${width - ratio}fr`;
  508. },
  509. /**
  510. * 处理一行中有多个格子的左移且不是第一个格子
  511. * @param {object} gridList 格子列表
  512. * @param {object} grid 格子数据
  513. * @param {object} col 列数据
  514. * @param {number} find 当前 id 所在格子索引
  515. * @param {number} k 格子的索引
  516. * @param {number} offsetX x 轴偏移量
  517. * @param {number} min_width 最小宽度
  518. */
  519. handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
  520. const prevGrid = gridList[find - 1];
  521. const prevWidth = Number(prevGrid.width.replace('fr', ''));
  522. const width = Number(grid.width.replace('fr', ''));
  523. const max = prevWidth + width - 10;
  524. if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
  525. return;
  526. }
  527. // 计算拖动的距离与总宽度的比例
  528. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  529. const _w = width - ratio;
  530. if ((_w / 100) * row_width < min_width) {
  531. return;
  532. }
  533. col.grid_list[k - 1].width = `${prevWidth + ratio}fr`;
  534. grid.width = `${_w}fr`;
  535. },
  536. /**
  537. * 处理右移且不是最后一个格子
  538. * @param {object} row 行数据
  539. * @param {number} j 第几列
  540. * @param {number} offsetX x 轴偏移量
  541. * @param {number} min_width 最小宽度
  542. * @param {number} row_width 行宽度
  543. */
  544. handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width) {
  545. let nextGrid = row.width_list[j + 1];
  546. const nextWidth = Number(nextGrid.replace('fr', ''));
  547. const width = Number(row.width_list[j].replace('fr', ''));
  548. const max = nextWidth + width - 10;
  549. if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
  550. return;
  551. }
  552. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  553. const _w = width + ratio;
  554. if ((_w / 100) * row_width < min_width) {
  555. return;
  556. }
  557. row.width_list[j + 1] = `${nextWidth - ratio}fr`;
  558. row.width_list[j] = `${width + ratio}fr`;
  559. },
  560. /**
  561. * 处理一行中有多个格子的右移且不是最后一个格子
  562. * @param {object} gridList 格子列表
  563. * @param {object} grid 格子数据
  564. * @param {object} col 列数据
  565. * @param {number} find 当前 id 所在格子索引
  566. * @param {number} k 格子的索引
  567. * @param {number} offsetX x 轴偏移量
  568. * @param {number} min_width 最小宽度
  569. */
  570. handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
  571. let nextGrid = gridList[find + 1];
  572. const nextWidth = Number(nextGrid.width.replace('fr', ''));
  573. const width = Number(grid.width.replace('fr', ''));
  574. const max = nextWidth + width - 10;
  575. if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
  576. return;
  577. }
  578. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  579. const _w = width + ratio;
  580. if ((_w / 100) * row_width < min_width) {
  581. return;
  582. }
  583. col.grid_list[k + 1].width = `${nextWidth - ratio}fr`;
  584. grid.width = `${width + ratio}fr`;
  585. },
  586. /**
  587. * 重新计算格子宽度
  588. * @param {number} i 行
  589. * @param {number} j 列
  590. */
  591. recalculateGridWidth(i, j) {
  592. let col = this.data.row_list[i].col_list[j];
  593. let grid_template_columns = '0';
  594. col.grid_list.forEach(({ width }, i) => {
  595. const w = `${width}`;
  596. if (i === col.grid_list.length - 1) {
  597. grid_template_columns += ` ${w} 0`;
  598. } else {
  599. grid_template_columns += ` ${w} `;
  600. }
  601. });
  602. col.grid_template_columns = grid_template_columns;
  603. },
  604. /**
  605. * 删除组件
  606. * @param {number} i 行
  607. * @param {number} j 列
  608. * @param {number} k 格子
  609. */
  610. deleteComponent(i, j, k) {
  611. return () => {
  612. const gridList = this.data.row_list[i].col_list[j].grid_list;
  613. let delRow = gridList[k].row; // 删除的 grid 的 row
  614. let delW = gridList[k].width; // 删除的 grid 的 width
  615. gridList.splice(k, 1);
  616. // 如果删除后没有 grid 了则删除列
  617. const colList = this.data.row_list[i].col_list[j];
  618. if (colList.grid_list.length === 0) {
  619. this.data.row_list[i].col_list.splice(j, 1);
  620. let width_list = this.data.row_list[i].width_list;
  621. const delW = width_list[j];
  622. width_list.splice(j, 1);
  623. this.data.row_list[i].width_list = width_list.map((item) => {
  624. return `${Number(item.replace('fr', '')) + Number(delW.replace('fr', '') / width_list.length)}fr`;
  625. });
  626. }
  627. // 如果删除后没有列了则删除行
  628. if (this.data.row_list[i].col_list.length === 0) {
  629. this.data.row_list.splice(i, 1);
  630. }
  631. // 如果删除后还有 grid 则重新计算 grid 的 row 和 width
  632. if (gridList?.length > 0) {
  633. let delNum = gridList.filter(({ row }) => row === delRow).length;
  634. let diff = Number(delW.replace('fr', '')) / delNum;
  635. if (delNum === 0) {
  636. // 删除 grid 后面的 row 都减 1
  637. gridList.forEach((item) => {
  638. if (item.row > delRow) {
  639. item.row -= 1;
  640. }
  641. });
  642. } else {
  643. gridList.forEach((item) => {
  644. if (item.row === delRow) {
  645. item.width = `${Number(item.width.replace('fr', '')) + diff}fr`;
  646. }
  647. });
  648. }
  649. }
  650. };
  651. },
  652. computedColStyle(col) {
  653. let grid = col.grid_list;
  654. let maxCol = 0; // 最大列数
  655. let rowList = new Map();
  656. grid.forEach(({ row }) => {
  657. rowList.set(row, (rowList.get(row) || 0) + 1);
  658. });
  659. let curMaxRow = 0; // 当前列数最多的 row 的值
  660. rowList.forEach((value, key) => {
  661. if (value > maxCol) {
  662. maxCol = value;
  663. curMaxRow = key;
  664. }
  665. });
  666. // 计算 grid_template_areas
  667. let gridStr = '';
  668. let gridArr = [];
  669. let gridRowArr = []; // 存储不同 row 中间的 line
  670. grid.forEach(({ grid_area, row }, i) => {
  671. // 如果 gridArr[row - 1] 不存在则创建
  672. if (!gridArr[row - 1]) {
  673. gridArr[row - 1] = [];
  674. }
  675. if (row > 1 && grid[i - 1].row !== row) {
  676. gridRowArr.push({
  677. row: row - 1,
  678. str: `middle-${grid_area} `.repeat(maxCol * 3),
  679. });
  680. }
  681. // 如果当前 row 是最大列数的 row
  682. if (curMaxRow === row) {
  683. gridArr[row - 1].push(`left-${grid_area} ${grid_area} right-${grid_area}`);
  684. } else {
  685. let filter = grid.filter((item) => item.row === row);
  686. let find = filter.findIndex((item) => item.grid_area === grid_area);
  687. let needNum = (maxCol - filter.length) * 3; // 需要的数量
  688. let str = '';
  689. if (filter.length === 1) {
  690. str = ` ${grid_area} `.repeat(needNum + 1);
  691. } else {
  692. let arr = this.splitInteger(needNum, filter.length);
  693. str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
  694. }
  695. gridArr[row - 1].push(`left-${grid_area} ${str} right-${grid_area}`);
  696. }
  697. });
  698. gridArr.forEach((item, i) => {
  699. let find = gridRowArr.find((row) => row.row === i);
  700. if (find) {
  701. gridStr += `'${find.str}' `;
  702. }
  703. gridStr += `'${item.join(' ')}' `;
  704. });
  705. // 计算 grid_template_columns
  706. let gridTemCols = '';
  707. let max = { row: 0, num: 0 };
  708. grid.forEach(({ row }) => {
  709. // 计算出 row 的哪个值最多
  710. let len = grid.filter((item) => item.row === row).length;
  711. if (max.num < len) {
  712. max.num = len;
  713. max.row = row;
  714. }
  715. });
  716. grid.forEach((item) => {
  717. if (item.row === max.row) {
  718. gridTemCols += `${item.width} 4px 4px `;
  719. }
  720. });
  721. // 计算 grid_template_rows
  722. let gridTemplateRows = '';
  723. // 将 grid 按照 row 分组
  724. let gridMap = new Map();
  725. grid.forEach((item) => {
  726. if (!gridMap.has(item.row)) {
  727. gridMap.set(item.row, []);
  728. }
  729. gridMap.get(item.row).push(item.height);
  730. });
  731. gridMap.forEach((value, i) => {
  732. if (i > 1) {
  733. gridTemplateRows += '4px ';
  734. }
  735. if (value.length === 1) {
  736. gridTemplateRows += `${value[0]} `;
  737. } else {
  738. let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
  739. gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
  740. }
  741. });
  742. return {
  743. width: col.width,
  744. gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${gridStr} '${'grid-bottom '.repeat(maxCol * 3)}'`,
  745. gridTemplateColumns: `0 ${gridTemCols.slice(0, gridTemCols.length - 8)} 0`,
  746. gridTemplateRows: `0 ${gridTemplateRows} 0`,
  747. };
  748. },
  749. /**
  750. * 分割整数为多个 3 的倍数
  751. * @param {number} num
  752. * @param {number} parts
  753. */
  754. splitInteger(num, parts) {
  755. let base = Math.floor(num / parts / 3) * 3;
  756. let arr = Array(parts).fill(base);
  757. let remainder = num - base * parts;
  758. for (let i = 0; remainder > 0; i = (i + 1) % parts) {
  759. arr[i] += 3;
  760. remainder -= 3;
  761. }
  762. return arr;
  763. },
  764. /**
  765. * 拖拽开始
  766. * 用点击模拟拖拽
  767. * @param {MouseEvent} event
  768. * @param {string} type
  769. */
  770. dragStart(event, type) {
  771. // 获取鼠标位置
  772. const { clientX, clientY } = event;
  773. document.body.style.userSelect = 'none'; // 禁止选中文本
  774. this.drag.dragging = true;
  775. this.curType = type;
  776. // 在鼠标位置创建一个拖拽元素
  777. const dragging = document.createElement('div');
  778. dragging.className = 'canvas-dragging';
  779. this.drag.clientX = clientX;
  780. this.drag.clientY = clientY;
  781. document.body.appendChild(dragging);
  782. },
  783. /**
  784. * 鼠标移动
  785. */
  786. dragMove(event) {
  787. if (!this.drag.dragging) return;
  788. const { clientX, clientY } = event;
  789. this.drag.clientX = clientX;
  790. this.drag.clientY = clientY;
  791. if (!this.isEdit) return; // 非编辑状态不允许显隐线
  792. let { isInsideCanvas } = this.getMarginDifferences();
  793. this.enterCanvas = isInsideCanvas;
  794. if (!isInsideCanvas) return;
  795. this.showRecentLine(clientX, clientY);
  796. },
  797. /**
  798. * 显示最近的线
  799. * @param {number} clientX
  800. * @param {number} clientY
  801. */
  802. showRecentLine(clientX, clientY) {
  803. const dragLineList = document.querySelectorAll('.drag-line'); // 获取所有的横线
  804. const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line'); // 获取所有的竖线
  805. let minDistance = Infinity;
  806. let minIndex = -1;
  807. const list = [...dragLineList, ...dragVerticalLineList];
  808. list.forEach((item, index) => {
  809. const rect = item.getBoundingClientRect();
  810. const distance = Math.sqrt(
  811. Math.pow(clientX - rect.left - rect.width / 2, 2) + Math.pow(clientY - rect.top - rect.height / 2, 2),
  812. );
  813. if (distance < minDistance) {
  814. minDistance = distance;
  815. minIndex = index;
  816. }
  817. });
  818. list.forEach((item, index) => {
  819. if (index === minIndex) {
  820. this.curRow = Number(item.getAttribute('data-row'));
  821. this.curCol = Number(item.getAttribute('data-col') || -1);
  822. this.curGrid = Number(item.getAttribute('data-grid') || -1);
  823. this.gridInsertType = item.getAttribute('data-type') || '';
  824. item.style.opacity = 1;
  825. } else {
  826. item.style.opacity = 0;
  827. }
  828. });
  829. },
  830. /**
  831. * 鼠标松开
  832. */
  833. dragEnd() {
  834. document.body.style.userSelect = 'auto';
  835. const dragging = document.querySelector('.canvas-dragging');
  836. if (dragging) {
  837. document.body.removeChild(dragging);
  838. this.drag.dragging = false;
  839. }
  840. if (!this.isEdit) return;
  841. if (this.enterCanvas) {
  842. if (this.curRow >= -1 && this.curCol <= -1) {
  843. this.calculateRowInsertedObject();
  844. }
  845. if (this.curRow >= -1 && this.curCol > -1 && this.curGrid <= -1) {
  846. this.calculateColObject();
  847. }
  848. if (this.curRow >= -1 && this.curCol > -1 && this.curGrid > -1) {
  849. this.calculateGridObject();
  850. }
  851. }
  852. this.enterCanvas = false;
  853. },
  854. computedRowStyle(i) {
  855. let row = this.data.row_list[i];
  856. let col = row.col_list;
  857. if (col.length <= 1) {
  858. return {
  859. gridTemplateColumns: '0 100fr 0',
  860. };
  861. }
  862. let str = row.width_list
  863. .map((item) => {
  864. return `${item}`;
  865. })
  866. .join(' 6px ');
  867. let gridTemplateColumns = `0 ${str} 0`;
  868. return {
  869. gridAutoFlow: 'column',
  870. gridTemplateColumns,
  871. gridTemplateRows: 'auto',
  872. };
  873. },
  874. /**
  875. * 计算网格插入的对象
  876. */
  877. calculateGridObject() {
  878. const id = `ID-${getRandomNumber(12, true)}`;
  879. const letter = `L${getRandomNumber(6, true)}`;
  880. let row = this.data.row_list[this.curRow];
  881. let col = row.col_list[this.curCol];
  882. let grid = col.grid_list;
  883. let type = this.gridInsertType;
  884. if (['row', 'col-middle'].includes(type)) {
  885. let rowNum = this.curGrid === 0 ? 1 : grid[this.curGrid - 1].row + 1;
  886. grid.splice(this.curGrid, 0, {
  887. id,
  888. grid_area: letter,
  889. width: '100fr',
  890. height: 'auto',
  891. row: rowNum,
  892. type: this.curType,
  893. });
  894. // 在新加入的 grid 后面的 row 都加 1
  895. grid.forEach((item, i) => {
  896. if (i > this.curGrid) {
  897. item.row += 1;
  898. }
  899. });
  900. }
  901. if (['col-left', 'col-right'].includes(type)) {
  902. let rowNum = grid[type === 'col-left' ? this.curGrid : this.curGrid - 1].row;
  903. grid.splice(this.curGrid, 0, {
  904. id,
  905. grid_area: letter,
  906. width: '100fr',
  907. height: 'auto',
  908. row: rowNum,
  909. type: this.curType,
  910. });
  911. let allRowNum = grid.filter(({ row }) => row === rowNum).length;
  912. let w = 0;
  913. grid.forEach((item, i) => {
  914. if (item.row === rowNum && i !== this.curGrid) {
  915. let width = Number(item.width.replace('fr', ''));
  916. let diff = width / allRowNum;
  917. item.width = `${width - diff}fr`;
  918. w += diff;
  919. }
  920. });
  921. grid[this.curGrid].width = `${w}fr`;
  922. }
  923. },
  924. /**
  925. * 计算列插入的对象
  926. */
  927. calculateColObject() {
  928. const id = `ID-${getRandomNumber(12, true)}`;
  929. const letter = `L${getRandomNumber(6, true)}`;
  930. let row = this.data.row_list[this.curRow];
  931. let col = row.col_list;
  932. let w = 0;
  933. row.width_list.forEach((item, i) => {
  934. let itemW = Number(item.replace('fr', ''));
  935. let rowW = itemW / (row.width_list.length + 1);
  936. w += rowW;
  937. row.width_list[i] = `${itemW - rowW}fr`;
  938. });
  939. row.width_list.splice(this.curCol, 0, `${w}fr`);
  940. col.splice(this.curCol, 0, {
  941. width: '100fr',
  942. height: 'auto',
  943. grid_list: [
  944. {
  945. id,
  946. grid_area: letter,
  947. row: 1,
  948. width: '100fr',
  949. height: 'auto',
  950. type: this.curType,
  951. },
  952. ],
  953. });
  954. },
  955. /**
  956. * 计算行插入的对象
  957. */
  958. calculateRowInsertedObject() {
  959. const id = `ID-${getRandomNumber(12, true)}`;
  960. const letter = `L${getRandomNumber(6, true)}`;
  961. const row_id = `R${getRandomNumber(6, true)}`;
  962. this.data.row_list.splice(this.curRow + 1, 0, {
  963. width_list: ['100fr'],
  964. row_id,
  965. col_list: [
  966. {
  967. width: '100fr',
  968. height: 'auto',
  969. grid_list: [
  970. {
  971. id,
  972. grid_area: letter,
  973. width: '100fr',
  974. height: 'auto',
  975. row: 1,
  976. type: this.curType,
  977. },
  978. ],
  979. },
  980. ],
  981. });
  982. },
  983. /**
  984. * 获取拖拽元素和画布的边距差值
  985. * @returns {object} { leftMarginDifference, topMarginDifference, isInsideCanvas }
  986. * leftMarginDifference: 拖拽元素和画布左边距差值
  987. * topMarginDifference: 拖拽元素和画布上边距差值
  988. * isInsideCanvas: 是否在画布内
  989. */
  990. getMarginDifferences() {
  991. const rect1 = document.querySelector('.canvas-dragging').getBoundingClientRect();
  992. const rect2 = this.$refs.canvas.getBoundingClientRect();
  993. const leftMarginDifference = rect1.left - rect2.left + 128;
  994. const topMarginDifference = rect1.top - rect2.top + 72;
  995. let isInsideCanvas =
  996. leftMarginDifference > 0 &&
  997. leftMarginDifference < rect2.width &&
  998. topMarginDifference > 0 &&
  999. topMarginDifference < rect2.height;
  1000. return { leftMarginDifference, topMarginDifference, isInsideCanvas };
  1001. },
  1002. // 获取子组件
  1003. findChildComponentByKey(key) {
  1004. return this.$refs.component.find((child) => child.$vnode.key === key);
  1005. },
  1006. /**
  1007. * 计算分组线样式
  1008. * @param {Array} rowIdList 行 ID 列表
  1009. * @returns {Object} 样式对象
  1010. */
  1011. computedGroupLine(rowIdList) {
  1012. // 获取画布顶部位置
  1013. const canvas = this.$refs.canvas;
  1014. const firstCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[0]}`);
  1015. const secCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[1]}`);
  1016. // DOM 未渲染,返回默认样式或空对象
  1017. if (!canvas || !firstCheckbox || !secCheckbox) {
  1018. return {};
  1019. }
  1020. const canvasTop = canvas.getBoundingClientRect().top;
  1021. const firstRowTop = firstCheckbox.getBoundingClientRect().top;
  1022. const secRowTop = secCheckbox.getBoundingClientRect().top;
  1023. return {
  1024. top: `${firstRowTop - canvasTop + 22}px`,
  1025. height: `${secRowTop - firstRowTop - 24}px`,
  1026. };
  1027. },
  1028. computedBorderColor(rowId) {
  1029. const groupList = [];
  1030. this.content_group_row_list.forEach(({ row_id, is_pre_same_group }) => {
  1031. if (is_pre_same_group && groupList.length) {
  1032. groupList[groupList.length - 1].push(row_id);
  1033. } else {
  1034. groupList.push([row_id]);
  1035. }
  1036. });
  1037. const index = groupList.findIndex((g) => g.includes(rowId));
  1038. return this.gridBorderColorList[index % this.gridBorderColorList.length];
  1039. },
  1040. /**
  1041. * 计算网格样式
  1042. * @param {Object} grid 网格对象
  1043. * @returns {Object} 样式对象
  1044. */
  1045. computedGridStyle(grid) {
  1046. let marginTop = grid.row === 1 ? '0' : '6px';
  1047. return {
  1048. gridArea: grid.grid_area,
  1049. height: grid.height,
  1050. marginTop,
  1051. };
  1052. },
  1053. },
  1054. };
  1055. </script>
  1056. <style lang="scss" scoped>
  1057. .canvas {
  1058. position: relative;
  1059. display: flex;
  1060. flex-direction: column;
  1061. row-gap: 8px;
  1062. width: $courseware-width;
  1063. min-height: calc(100% - 6px);
  1064. padding: 24px;
  1065. margin: 0 auto;
  1066. background-color: #fff;
  1067. background-repeat: no-repeat;
  1068. border-radius: 4px;
  1069. .group-line {
  1070. position: absolute;
  1071. left: 11px;
  1072. width: 1px;
  1073. background-color: #165dff;
  1074. }
  1075. .row {
  1076. display: grid;
  1077. row-gap: 16px;
  1078. .row-checkbox {
  1079. position: absolute;
  1080. left: 4px;
  1081. }
  1082. > .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
  1083. left: 6px;
  1084. }
  1085. .drag-vertical-line.col-start {
  1086. left: -12px;
  1087. }
  1088. .drag-vertical-line.col-end {
  1089. right: -8px;
  1090. }
  1091. .col {
  1092. display: grid;
  1093. .drag-vertical-line:first-child {
  1094. left: -8px;
  1095. }
  1096. .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
  1097. left: 6px;
  1098. }
  1099. .drag-vertical-line:last-child {
  1100. right: -4px;
  1101. }
  1102. }
  1103. }
  1104. .drag-line {
  1105. z-index: 2;
  1106. width: calc(100% - 16px);
  1107. height: 4px;
  1108. margin: 0 8px;
  1109. background-color: #379fff;
  1110. border-radius: 4px;
  1111. opacity: 0;
  1112. &.drag-row {
  1113. background-color: $right-color;
  1114. }
  1115. &.grid-line:not(:first-child, :last-child) {
  1116. position: relative;
  1117. top: 6px;
  1118. }
  1119. &.grid-line {
  1120. background-color: #f43;
  1121. }
  1122. }
  1123. .drag-vertical-line {
  1124. position: relative;
  1125. z-index: 2;
  1126. width: 4px;
  1127. height: 100%;
  1128. background-color: #379fff;
  1129. border-radius: 4px;
  1130. opacity: 0;
  1131. &.grid-line {
  1132. background-color: #f43;
  1133. }
  1134. }
  1135. }
  1136. </style>
  1137. <style lang="scss">
  1138. .canvas-dragging {
  1139. position: fixed;
  1140. z-index: 999;
  1141. width: 320px;
  1142. height: 180px;
  1143. background-color: #eaf5ff;
  1144. border: 1px solid #b5dbff;
  1145. border-radius: 4px;
  1146. opacity: 0.5;
  1147. transform: translate(-40%, -40%);
  1148. }
  1149. </style>