CreateCanvas.vue 38 KB

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