CreateCanvas.vue 26 KB

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