CreateCanvas.vue 32 KB

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