CreateCanvas.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529
  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="row.row_id" 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-${row.row_id}-${col.col_id}`"
  33. class="drag-vertical-line col-start"
  34. :data-row="i"
  35. :data-col="j"
  36. ></span>
  37. <div :key="col.col_id" :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-${grid.id}`"
  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-${grid.id}`"
  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-${grid.id}`"
  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. :data-row="i"
  76. :data-col="j"
  77. :data-grid="k"
  78. :data-view-order="computedGridViewOrder(grid.id)"
  79. :border-color="computedBorderColor(row.row_id)"
  80. :style="computedGridStyle(grid, row.row_id)"
  81. :component-move="componentMove(i, j, k)"
  82. @deleteComponent="deleteComponent"
  83. @showSetting="showSetting"
  84. @copyComponent="copyComponent"
  85. @changeData="changeData"
  86. />
  87. <span
  88. :key="`right-${grid.id}`"
  89. :style="{ gridArea: `right-${grid.grid_area}` }"
  90. :data-row="i"
  91. :data-col="j"
  92. :data-grid="k + 1"
  93. data-type="col-right"
  94. class="drag-vertical-line grid-line grid-line-right"
  95. ></span>
  96. <span
  97. v-if="k === col.grid_list.length - 1"
  98. :key="`end-${grid.id}`"
  99. class="drag-line grid-line drag-row"
  100. :style="{ gridArea: `grid-bottom` }"
  101. :data-row="i"
  102. :data-col="j"
  103. :data-grid="k + 1"
  104. data-type="row"
  105. ></span>
  106. </template>
  107. </div>
  108. <span
  109. :key="`end-${row.row_id}-${col.col_id}`"
  110. class="drag-vertical-line col-end"
  111. :data-row="i"
  112. :data-col="j + 1"
  113. ></span>
  114. </template>
  115. </div>
  116. <span v-if="i < data.row_list.length - 1" :key="`row-${row.row_id}`" class="drag-line" :data-row="i"></span>
  117. </template>
  118. <span class="drag-line" :data-row="data.row_list.length - 1"></span>
  119. </template>
  120. <PreviewEdit
  121. v-else
  122. ref="previewEdit"
  123. :courseware-id="courseware_id"
  124. :row-list="data.row_list"
  125. @computedMoveData="computedMoveData"
  126. />
  127. <FullTextSettings
  128. :book-id="project_id"
  129. :visible.sync="visibleFullTextSettings"
  130. :settings="data.unified_attrib"
  131. @fullTextSettings="fullTextSettings"
  132. />
  133. </main>
  134. </template>
  135. <script>
  136. import { getRandomNumber } from '@/utils/index';
  137. import { componentList } from '../../data/bookType';
  138. import { ContentSaveCoursewareContent, ContentGetCoursewareContent, GetBookUnifiedAttrib } from '@/api/book';
  139. import _ from 'lodash';
  140. import { unified_attrib } from '@/common/data';
  141. import PreviewEdit from './PreviewEdit.vue';
  142. import FullTextSettings from '../components/FullTextSettings.vue';
  143. export default {
  144. name: 'CreateCanvas',
  145. components: {
  146. PreviewEdit,
  147. FullTextSettings,
  148. },
  149. inject: ['getCurSettingId'],
  150. provide() {
  151. return {
  152. getBookUnifiedAttr: () => this.book_unified_attrib,
  153. };
  154. },
  155. props: {
  156. isEdit: {
  157. type: Boolean,
  158. required: true,
  159. },
  160. },
  161. data() {
  162. const { project_id } = this.$route.query;
  163. return {
  164. courseware_id: this.$route.params.courseware_id,
  165. project_id,
  166. data: {
  167. background_image_url: '',
  168. background_position: {
  169. width: 100,
  170. height: 100,
  171. top: 0,
  172. left: 0,
  173. },
  174. // 组件列表
  175. row_list: [],
  176. // 全篇设置
  177. unified_attrib,
  178. },
  179. rowCheckList: {}, // 行复选框列表
  180. content_group_row_list: [], // 行分组id列表
  181. gridBorderColorList: ['#3fc7cc', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#d863ff', '#724fff'], // 网格边框颜色列表
  182. curType: 'divider',
  183. componentList,
  184. curRow: -2,
  185. curCol: -1,
  186. curGrid: -1,
  187. gridInsertType: '', // 网格插入类型
  188. enterCanvas: false, // 是否进入画布
  189. // 拖拽状态
  190. drag: {
  191. clientX: 0,
  192. clientY: 0,
  193. dragging: false,
  194. },
  195. visibleFullTextSettings: false,
  196. book_unified_attrib: unified_attrib,
  197. };
  198. },
  199. computed: {
  200. lineList() {
  201. let arr = [];
  202. this.content_group_row_list.forEach(({ row_id, is_pre_same_group }, i) => {
  203. if (is_pre_same_group) {
  204. arr.push([this.content_group_row_list[i - 1].row_id, row_id]);
  205. }
  206. });
  207. return arr;
  208. },
  209. },
  210. watch: {
  211. drag: {
  212. handler(val) {
  213. if (val.dragging) {
  214. const dragging = document.querySelector('.canvas-dragging');
  215. dragging.style.left = `${val.clientX}px`;
  216. dragging.style.top = `${val.clientY}px`;
  217. }
  218. },
  219. deep: true,
  220. },
  221. enterCanvas: {
  222. handler(val) {
  223. if (val) return;
  224. if (!this.isEdit) return;
  225. const dragLineList = document.querySelectorAll('.drag-line');
  226. dragLineList.forEach((item) => {
  227. item.style.opacity = 0;
  228. });
  229. this.curRow = -2;
  230. const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line');
  231. dragVerticalLineList.forEach((item) => {
  232. item.style.opacity = 0;
  233. });
  234. this.curCol = -1;
  235. this.curGrid = -1;
  236. this.gridInsertType = '';
  237. },
  238. },
  239. 'data.row_list': {
  240. handler(val) {
  241. // row_list 更改时判断是否有当前设置 id 的组件
  242. const curSettingId = this.getCurSettingId();
  243. const find = val.find((row) => {
  244. return row.col_list.find((col) => {
  245. return col.grid_list.find((grid) => grid.id === curSettingId);
  246. });
  247. });
  248. if (!find) {
  249. this.$emit('showSettingEmpty');
  250. }
  251. this.rowCheckList = Object.fromEntries(val.filter((row) => row?.row_id).map((row) => [row.row_id, false]));
  252. // 增加新添的行
  253. val.forEach(({ row_id }, i) => {
  254. let isHas = this.content_group_row_list.some((group) => group.row_id === row_id);
  255. if (!isHas) {
  256. this.content_group_row_list.splice(i, 0, { row_id, is_pre_same_group: false });
  257. [i - 1, i + 1].forEach((start) => {
  258. let step = start < i ? -1 : 1;
  259. for (let j = start; j >= 0 && j < this.content_group_row_list.length; j += step) {
  260. if (this.content_group_row_list[j].is_pre_same_group) {
  261. this.content_group_row_list[j].is_pre_same_group = false;
  262. } else {
  263. break;
  264. }
  265. }
  266. });
  267. }
  268. });
  269. // 过滤掉已经不存在的行,并将第一行的 is_pre_same_group 设置为 false
  270. this.content_group_row_list = this.content_group_row_list.filter((group) =>
  271. val.find((row) => row.row_id === group.row_id),
  272. );
  273. if (this.content_group_row_list[0]) {
  274. this.content_group_row_list[0].is_pre_same_group = false;
  275. }
  276. },
  277. immediate: true,
  278. },
  279. rowCheckList: {
  280. handler(val) {
  281. // 如果同时有两个选中,将选中的挑选出来,并将它们的状态变为 false
  282. let selectedRowID = [];
  283. Object.keys(val).forEach((key) => {
  284. if (val[key]) {
  285. selectedRowID.push(key);
  286. }
  287. });
  288. if (selectedRowID.length > 1) {
  289. selectedRowID.forEach((id) => {
  290. this.rowCheckList[id] = false;
  291. });
  292. } else {
  293. return false;
  294. }
  295. let selectIndex = selectedRowID
  296. .map((id) => this.content_group_row_list.findIndex(({ row_id }) => row_id === id))
  297. .sort((a, b) => a - b);
  298. // 只处理选中两行的情况
  299. if (selectIndex[1] - selectIndex[0] === 1) {
  300. // 相邻两行,切换分组状态
  301. this.content_group_row_list[selectIndex[1]].is_pre_same_group =
  302. !this.content_group_row_list[selectIndex[1]].is_pre_same_group;
  303. } else {
  304. // 非相邻,判断中间行是否都已分组
  305. const allGrouped = this.content_group_row_list
  306. .slice(selectIndex[0] + 1, selectIndex[1] + 1)
  307. .every((group) => group.is_pre_same_group);
  308. for (let i = selectIndex[0] + 1; i <= selectIndex[1]; i++) {
  309. this.content_group_row_list[i].is_pre_same_group = !allGrouped;
  310. }
  311. }
  312. },
  313. deep: true,
  314. },
  315. },
  316. created() {
  317. const loading = this.$loading({
  318. lock: true,
  319. text: '组件加载中...',
  320. spinner: 'el-icon-loading',
  321. });
  322. ContentGetCoursewareContent({ id: this.courseware_id })
  323. .then(({ content, content_group_row_list }) => {
  324. if (content) {
  325. let parsedContent = JSON.parse(content);
  326. if (
  327. parsedContent &&
  328. Array.isArray(parsedContent.row_list) &&
  329. parsedContent.row_list.length > 0 &&
  330. !parsedContent.row_list[0]?.row_id
  331. ) {
  332. this.data = this.normalizeRowColIds(parsedContent);
  333. } else {
  334. this.data = parsedContent;
  335. }
  336. loading.close();
  337. }
  338. if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
  339. this.$watch(
  340. 'data',
  341. () => {
  342. this.changeData();
  343. },
  344. {
  345. deep: true,
  346. },
  347. );
  348. })
  349. .finally(() => {
  350. loading.close();
  351. });
  352. this.getBookUnifiedAttr();
  353. },
  354. mounted() {
  355. document.addEventListener('mousemove', this.dragMove);
  356. document.addEventListener('mouseup', this.dragEnd);
  357. },
  358. beforeDestroy() {
  359. document.removeEventListener('mousemove', this.dragMove);
  360. document.removeEventListener('mouseup', this.dragEnd);
  361. },
  362. methods: {
  363. changeData() {
  364. this.$emit('changeData');
  365. },
  366. showFullTextSettings() {
  367. this.visibleFullTextSettings = true;
  368. },
  369. fullTextSettings(data) {
  370. this.$refs.component.forEach((item) => {
  371. item.updateProperty('view_pinyin', data.view_pinyin);
  372. item.updateProperty('pinyin_position', data.pinyin_position);
  373. item.updateRichTextProperty('fontFamily', data.font);
  374. item.updateRichTextProperty('fontSize', data.font_size);
  375. item.updateRichTextProperty('lineHeight', data.line_height);
  376. item.updateRichTextProperty('color', data.text_color);
  377. item.updateRichTextProperty('align', data.align);
  378. item.setUnifiedAttr(data);
  379. });
  380. this.data.unified_attrib = data;
  381. },
  382. getBookUnifiedAttr() {
  383. GetBookUnifiedAttrib({ book_id: this.project_id }).then(({ content }) => {
  384. if (content) {
  385. this.book_unified_attrib = JSON.parse(content);
  386. }
  387. });
  388. },
  389. /**
  390. * 保存课件内容
  391. * @param {string} type 类型
  392. */
  393. async saveCoursewareContent(type) {
  394. let isAllLoader = false;
  395. if (this.$refs?.component === undefined || this.$refs?.component.length === 0) {
  396. isAllLoader = true;
  397. } else if (this.isEdit) {
  398. isAllLoader = this.$refs?.component?.every((item) => item.property.isGetContent);
  399. } else {
  400. isAllLoader = this.$refs?.previewEdit?.$refs?.preview?.every((item) => item.loader);
  401. }
  402. if (!isAllLoader) {
  403. this.$message.warning('有组件内容未加载完成,请稍后再试');
  404. return false;
  405. }
  406. const loading = this.$loading({
  407. lock: true,
  408. text: '保存中...',
  409. spinner: 'el-icon-loading',
  410. background: 'rgba(0, 0, 0, 0.7)',
  411. });
  412. try {
  413. // 先等待所有子组件内容保存完成
  414. if (this.isEdit) {
  415. const comps = Array.isArray(this.$refs?.component)
  416. ? this.$refs.component
  417. : [this.$refs?.component].filter(Boolean);
  418. await Promise.all(comps.map((item) => item.saveCoursewareComponentContent()));
  419. }
  420. // 再收集并保存页面结构
  421. let component_id_list = this.data.row_list.flatMap((row) =>
  422. row.col_list.flatMap((col) => col.grid_list.map((grid) => grid.id)),
  423. );
  424. let groupIdList = _.cloneDeep(this.content_group_row_list);
  425. let groupList = [];
  426. // 通过判断 is_pre_same_group 将组合并
  427. for (let i = 0; i < groupIdList.length; i++) {
  428. if (groupIdList[i].is_pre_same_group) {
  429. groupList[groupList.length - 1].row_id_list.push(groupIdList[i].row_id);
  430. } else {
  431. groupList.push({
  432. name: '',
  433. row_id_list: [groupIdList[i].row_id],
  434. component_id_list: [],
  435. });
  436. }
  437. }
  438. // 通过合并后的分组,获取对应的组件 id 和分组名称
  439. groupList.forEach(({ row_id_list, component_id_list }, i) => {
  440. row_id_list.forEach((row_id, j) => {
  441. let row = this.data.row_list.find((row) => {
  442. return row.row_id === row_id;
  443. });
  444. // 当前行所有组件id列表
  445. let gridIdList = row.col_list.map((col) => col.grid_list.map((grid) => grid.id)).flat();
  446. component_id_list.push(...gridIdList);
  447. // 查找每组第一行中第一个包含 describe、label 或 stem 的组件
  448. if (j === 0) {
  449. let findKey = '';
  450. let findType = '';
  451. row.col_list.some((col) => {
  452. const findItem = col.grid_list.find(({ type }) => {
  453. return ['describe', 'label', 'stem'].includes(type);
  454. });
  455. if (findItem) {
  456. findKey = findItem.id;
  457. findType = findItem.type;
  458. return true;
  459. }
  460. });
  461. let groupName = `组${i + 1}`;
  462. // 如果有标签类组件,获取对应名称
  463. if (findKey) {
  464. let item = this.isEdit
  465. ? this.findChildComponentByKey(`grid-${findKey}`)
  466. : this.$refs.previewEdit.findChildComponentByKey(`preview-${findKey}`);
  467. if (['describe', 'stem'].includes(findType)) {
  468. groupName = item.data.content.replace(/<[^>]+>/g, '');
  469. } else if (findType === 'label') {
  470. groupName = item.data.dynamicTags.map((tag) => tag.text).join(', ');
  471. }
  472. }
  473. groupList[i].name = groupName;
  474. }
  475. });
  476. });
  477. await ContentSaveCoursewareContent({
  478. id: this.courseware_id,
  479. category: 'NEW',
  480. content: JSON.stringify(this.data),
  481. component_id_list,
  482. content_group_component_list: groupList,
  483. content_group_row_list: this.content_group_row_list,
  484. });
  485. this.$message.success('保存成功');
  486. if (type === 'quit') {
  487. this.$emit('back');
  488. }
  489. if (type === 'edit') {
  490. this.$emit('changeEditStatus');
  491. }
  492. } finally {
  493. loading.close();
  494. }
  495. },
  496. setBackgroundImage(url, position) {
  497. this.data.background_image_url = url;
  498. this.data.background_position = position;
  499. this.$message.success('设置背景图成功');
  500. },
  501. /**
  502. * 显示设置
  503. * @param {object} setting
  504. * @param {string} type
  505. * @param {string} id
  506. * @param {object} params
  507. */
  508. showSetting(setting, type, id, params = {}) {
  509. this.$emit('showSetting', setting, type, id, params);
  510. },
  511. /**
  512. * 计算组件移动
  513. * @param {number} i 行
  514. * @param {number} j 列
  515. * @param {number} k 格子
  516. */
  517. componentMove(i, j, k) {
  518. return ({ type, offsetX, offsetY, id }) => {
  519. this.computedMoveData({ i, j, k, type, offsetX, offsetY, id });
  520. };
  521. },
  522. /**
  523. * 计算移动数据
  524. * @param {number} i 行
  525. * @param {number} j 列
  526. * @param {number} k 格子
  527. * @param {string} type 移动类型
  528. * @param {number} offsetX x 轴偏移量
  529. * @param {number} offsetY y 轴偏移量
  530. * @param {string} id 组件 id
  531. */
  532. computedMoveData({ i, j, k, type, offsetX, offsetY, id, min_width, min_height, row_width }) {
  533. const row = this.data.row_list[i];
  534. const col = row.col_list[j];
  535. const grid = col.grid_list[k];
  536. // 上下移动
  537. if (['top', 'bottom'].includes(type)) {
  538. this.handleVerticalMove(col, grid, offsetY, id, min_height);
  539. return;
  540. }
  541. // 一行中有多个格子
  542. let gridList = col.grid_list.filter((item) => item.row === grid.row);
  543. if (gridList.length > 1) {
  544. let find = gridList.findIndex((item) => item.id === id); // 当前 id 所在格子索引
  545. // 移动类型为 left 且不是第一个格子
  546. if (type === 'left' && find > 0) {
  547. this.handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
  548. }
  549. // 移动类型为 right 且不是最后一个格子
  550. if (type === 'right' && find < gridList.length - 1) {
  551. this.handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
  552. }
  553. return;
  554. }
  555. // 移动类型为 left 且不是第一个格子
  556. if (type === 'left' && j > 0) {
  557. this.handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width);
  558. }
  559. // 移动类型为 right 且不是最后一个格子
  560. if (type === 'right' && j < row.col_list.length - 1) {
  561. this.handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width);
  562. }
  563. this.$forceUpdate();
  564. },
  565. /**
  566. * 处理垂直移动
  567. * @param {number} col 列数据
  568. * @param {object} grid 格子数据
  569. * @param {number} offsetY y 轴偏移量
  570. * @param {string} id 组件 id
  571. * @param {number} min_height 最小高度
  572. */
  573. handleVerticalMove(col, grid, offsetY, id, min_height = 0) {
  574. let height = 0;
  575. const _h = this.isEdit ? grid?.edit_height : grid.height;
  576. // 高度为 auto 时
  577. if (_h === 'auto' || _h === undefined) {
  578. const gridHeight = document.querySelector(`.${id}`).offsetHeight;
  579. height = gridHeight + offsetY;
  580. } else {
  581. // 高度为数字时
  582. const h = this.isEdit ? grid?.edit_height : grid.height;
  583. const gridHeight = Number(h?.replace('px', ''));
  584. height = gridHeight + offsetY;
  585. }
  586. // 当高度小于最小高度时,设置为最小高度
  587. height = Math.max(height, min_height, 50);
  588. if (this.isEdit) {
  589. grid.edit_height = `${height}px`;
  590. } else {
  591. grid.height = `${height}px`;
  592. }
  593. },
  594. /**
  595. * 处理左移且不是第一个格子
  596. * @param {object} row 行数据
  597. * @param {number} j 第几列
  598. * @param {number} offsetX x 轴偏移量
  599. * @param {number} min_width 最小宽度
  600. * @param {number} row_width 行宽度
  601. */
  602. handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width) {
  603. const prevGrid = row.width_list[j - 1];
  604. const prevWidth = Number(prevGrid.replace('fr', ''));
  605. const width = Number(row.width_list[j].replace('fr', ''));
  606. const max = prevWidth + width - 10;
  607. if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
  608. return;
  609. }
  610. // 计算拖动的距离与总宽度的比例
  611. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  612. const _w = width - ratio;
  613. if ((_w / 100) * row_width < min_width) {
  614. return;
  615. }
  616. row.width_list[j - 1] = `${prevWidth + ratio}fr`;
  617. row.width_list[j] = `${width - ratio}fr`;
  618. },
  619. /**
  620. * 处理一行中有多个格子的左移且不是第一个格子
  621. * @param {object} gridList 格子列表
  622. * @param {object} grid 格子数据
  623. * @param {object} col 列数据
  624. * @param {number} find 当前 id 所在格子索引
  625. * @param {number} k 格子的索引
  626. * @param {number} offsetX x 轴偏移量
  627. * @param {number} min_width 最小宽度
  628. */
  629. handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
  630. const prevGrid = gridList[find - 1];
  631. const prevWidth = Number(prevGrid.width.replace('fr', ''));
  632. const width = Number(grid.width.replace('fr', ''));
  633. const max = prevWidth + width - 10;
  634. if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
  635. return;
  636. }
  637. // 计算拖动的距离与总宽度的比例
  638. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  639. const _w = width - ratio;
  640. if ((_w / 100) * row_width < min_width) {
  641. return;
  642. }
  643. col.grid_list[k - 1].width = `${prevWidth + ratio}fr`;
  644. grid.width = `${_w}fr`;
  645. },
  646. /**
  647. * 处理右移且不是最后一个格子
  648. * @param {object} row 行数据
  649. * @param {number} j 第几列
  650. * @param {number} offsetX x 轴偏移量
  651. * @param {number} min_width 最小宽度
  652. * @param {number} row_width 行宽度
  653. */
  654. handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width) {
  655. let nextGrid = row.width_list[j + 1];
  656. const nextWidth = Number(nextGrid.replace('fr', ''));
  657. const width = Number(row.width_list[j].replace('fr', ''));
  658. const max = nextWidth + width - 10;
  659. if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
  660. return;
  661. }
  662. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  663. const _w = width + ratio;
  664. if ((_w / 100) * row_width < min_width) {
  665. return;
  666. }
  667. row.width_list[j + 1] = `${nextWidth - ratio}fr`;
  668. row.width_list[j] = `${width + ratio}fr`;
  669. },
  670. /**
  671. * 处理一行中有多个格子的右移且不是最后一个格子
  672. * @param {object} gridList 格子列表
  673. * @param {object} grid 格子数据
  674. * @param {object} col 列数据
  675. * @param {number} find 当前 id 所在格子索引
  676. * @param {number} k 格子的索引
  677. * @param {number} offsetX x 轴偏移量
  678. * @param {number} min_width 最小宽度
  679. */
  680. handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
  681. let nextGrid = gridList[find + 1];
  682. const nextWidth = Number(nextGrid.width.replace('fr', ''));
  683. const width = Number(grid.width.replace('fr', ''));
  684. const max = nextWidth + width - 10;
  685. if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
  686. return;
  687. }
  688. const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
  689. const _w = width + ratio;
  690. if ((_w / 100) * row_width < min_width) {
  691. return;
  692. }
  693. col.grid_list[k + 1].width = `${nextWidth - ratio}fr`;
  694. grid.width = `${width + ratio}fr`;
  695. },
  696. /**
  697. * 重新计算格子宽度
  698. * @param {number} i 行
  699. * @param {number} j 列
  700. */
  701. recalculateGridWidth(i, j) {
  702. let col = this.data.row_list[i].col_list[j];
  703. let grid_template_columns = '0';
  704. col.grid_list.forEach(({ width }, i) => {
  705. const w = `${width}`;
  706. if (i === col.grid_list.length - 1) {
  707. grid_template_columns += ` ${w} 0`;
  708. } else {
  709. grid_template_columns += ` ${w} `;
  710. }
  711. });
  712. col.grid_template_columns = grid_template_columns;
  713. },
  714. /**
  715. * 删除组件
  716. * @param {String} id 组件 id
  717. */
  718. deleteComponent(id) {
  719. const attrs = this.findChildComponentByKey(`grid-${id}`)?.$attrs;
  720. if (!attrs) return;
  721. const i = Number(attrs['data-row']);
  722. const j = Number(attrs['data-col']);
  723. const k = Number(attrs['data-grid']);
  724. const gridList = this.data.row_list[i].col_list[j].grid_list;
  725. let delRow = gridList[k].row; // 删除的 grid 的 row
  726. let delW = gridList[k].width; // 删除的 grid 的 width
  727. gridList.splice(k, 1);
  728. // 如果删除后没有 grid 了则删除列
  729. const colList = this.data.row_list[i].col_list[j];
  730. if (colList.grid_list.length === 0) {
  731. this.data.row_list[i].col_list.splice(j, 1);
  732. let width_list = this.data.row_list[i].width_list;
  733. const delW = width_list[j];
  734. width_list.splice(j, 1);
  735. this.data.row_list[i].width_list = width_list.map((item) => {
  736. return `${Number(item.replace('fr', '')) + Number(delW.replace('fr', '') / width_list.length)}fr`;
  737. });
  738. }
  739. // 如果删除后没有列了则删除行
  740. if (this.data.row_list[i].col_list.length === 0) {
  741. this.data.row_list.splice(i, 1);
  742. }
  743. // 如果删除后还有 grid 则重新计算 grid 的 row 和 width
  744. if (gridList?.length > 0) {
  745. let delNum = gridList.filter(({ row }) => row === delRow).length;
  746. let diff = Number(delW.replace('fr', '')) / delNum;
  747. if (delNum === 0) {
  748. // 删除 grid 后面的 row 都减 1
  749. gridList.forEach((item) => {
  750. if (item.row > delRow) {
  751. item.row -= 1;
  752. }
  753. });
  754. } else {
  755. gridList.forEach((item) => {
  756. if (item.row === delRow) {
  757. item.width = `${Number(item.width.replace('fr', '')) + diff}fr`;
  758. }
  759. });
  760. }
  761. }
  762. },
  763. computedColStyle(col) {
  764. let grid = col.grid_list;
  765. let maxCol = 0; // 最大列数
  766. let rowList = new Map();
  767. grid.forEach(({ row }) => {
  768. rowList.set(row, (rowList.get(row) || 0) + 1);
  769. });
  770. let curMaxRow = 0; // 当前列数最多的 row 的值
  771. rowList.forEach((value, key) => {
  772. if (value > maxCol) {
  773. maxCol = value;
  774. curMaxRow = key;
  775. }
  776. });
  777. // 计算 grid_template_areas
  778. let gridStr = '';
  779. let gridArr = [];
  780. let gridRowArr = []; // 存储不同 row 中间的 line
  781. grid.forEach(({ grid_area, row }, i) => {
  782. // 如果 gridArr[row - 1] 不存在则创建
  783. if (!gridArr[row - 1]) {
  784. gridArr[row - 1] = [];
  785. }
  786. if (row > 1 && grid[i - 1].row !== row) {
  787. gridRowArr.push({
  788. row: row - 1,
  789. str: `middle-${grid_area} `.repeat(maxCol * 3),
  790. });
  791. }
  792. // 如果当前 row 是最大列数的 row
  793. if (curMaxRow === row) {
  794. gridArr[row - 1].push(`left-${grid_area} ${grid_area} right-${grid_area}`);
  795. } else {
  796. let filter = grid.filter((item) => item.row === row);
  797. let find = filter.findIndex((item) => item.grid_area === grid_area);
  798. let needNum = (maxCol - filter.length) * 3; // 需要的数量
  799. let str = '';
  800. if (filter.length === 1) {
  801. str = ` ${grid_area} `.repeat(needNum + 1);
  802. } else {
  803. let arr = this.splitInteger(needNum, filter.length);
  804. str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
  805. }
  806. gridArr[row - 1].push(`left-${grid_area} ${str} right-${grid_area}`);
  807. }
  808. });
  809. gridArr.forEach((item, i) => {
  810. let find = gridRowArr.find((row) => row.row === i);
  811. if (find) {
  812. gridStr += `'${find.str}' `;
  813. }
  814. gridStr += `'${item.join(' ')}' `;
  815. });
  816. // 计算 grid_template_columns
  817. let gridTemCols = '';
  818. let max = { row: 0, num: 0 };
  819. grid.forEach(({ row }) => {
  820. // 计算出 row 的哪个值最多
  821. let len = grid.filter((item) => item.row === row).length;
  822. if (max.num < len) {
  823. max.num = len;
  824. max.row = row;
  825. }
  826. });
  827. grid.forEach((item) => {
  828. if (item.row === max.row) {
  829. gridTemCols += `${item.width} 4px 4px `;
  830. }
  831. });
  832. // 计算 grid_template_rows
  833. let gridTemplateRows = '';
  834. // 将 grid 按照 row 分组
  835. let gridMap = new Map();
  836. grid.forEach((item) => {
  837. if (!gridMap.has(item.row)) {
  838. gridMap.set(item.row, []);
  839. }
  840. gridMap.get(item.row).push(item?.edit_height || item.height);
  841. });
  842. gridMap.forEach((value, i) => {
  843. if (i > 1) {
  844. gridTemplateRows += '4px ';
  845. }
  846. if (value.length === 1) {
  847. gridTemplateRows += `${value[0]} `;
  848. } else {
  849. let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
  850. gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
  851. }
  852. });
  853. return {
  854. width: col.width,
  855. gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${gridStr} '${'grid-bottom '.repeat(maxCol * 3)}'`,
  856. gridTemplateColumns: `0 ${gridTemCols.slice(0, gridTemCols.length - 8)} 0`,
  857. gridTemplateRows: `0 ${gridTemplateRows} 0`,
  858. };
  859. },
  860. /**
  861. * 分割整数为多个 3 的倍数
  862. * @param {number} num
  863. * @param {number} parts
  864. */
  865. splitInteger(num, parts) {
  866. let base = Math.floor(num / parts / 3) * 3;
  867. let arr = Array(parts).fill(base);
  868. let remainder = num - base * parts;
  869. for (let i = 0; remainder > 0; i = (i + 1) % parts) {
  870. arr[i] += 3;
  871. remainder -= 3;
  872. }
  873. return arr;
  874. },
  875. /**
  876. * 拖拽开始
  877. * 用点击模拟拖拽
  878. * @param {MouseEvent} event
  879. * @param {string} type
  880. */
  881. dragStart(event, type) {
  882. // 获取鼠标位置
  883. const { clientX, clientY } = event;
  884. document.body.style.userSelect = 'none'; // 禁止选中文本
  885. this.drag.dragging = true;
  886. this.curType = type;
  887. // 在鼠标位置创建一个拖拽元素
  888. const dragging = document.createElement('div');
  889. dragging.className = 'canvas-dragging';
  890. this.drag.clientX = clientX;
  891. this.drag.clientY = clientY;
  892. document.body.appendChild(dragging);
  893. },
  894. /**
  895. * 鼠标移动
  896. */
  897. dragMove(event) {
  898. if (!this.drag.dragging) return;
  899. const { clientX, clientY } = event;
  900. this.drag.clientX = clientX;
  901. this.drag.clientY = clientY;
  902. if (!this.isEdit) return; // 非编辑状态不允许显隐线
  903. let { isInsideCanvas } = this.getMarginDifferences();
  904. this.enterCanvas = isInsideCanvas;
  905. if (!isInsideCanvas) return;
  906. this.showRecentLine(clientX, clientY);
  907. },
  908. /**
  909. * 显示最近的线
  910. * @param {number} clientX
  911. * @param {number} clientY
  912. */
  913. showRecentLine(clientX, clientY) {
  914. const dragLineList = document.querySelectorAll('.drag-line'); // 获取所有的横线
  915. const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line'); // 获取所有的竖线
  916. let minDistance = Infinity;
  917. let minIndex = -1;
  918. const list = [...dragLineList, ...dragVerticalLineList];
  919. list.forEach((item, index) => {
  920. const rect = item.getBoundingClientRect();
  921. const distance = Math.sqrt(
  922. Math.pow(clientX - rect.left - rect.width / 2, 2) + Math.pow(clientY - rect.top - rect.height / 2, 2),
  923. );
  924. if (distance < minDistance) {
  925. minDistance = distance;
  926. minIndex = index;
  927. }
  928. });
  929. list.forEach((item, index) => {
  930. if (index === minIndex) {
  931. this.curRow = Number(item.getAttribute('data-row'));
  932. this.curCol = Number(item.getAttribute('data-col') || -1);
  933. this.curGrid = Number(item.getAttribute('data-grid') || -1);
  934. this.gridInsertType = item.getAttribute('data-type') || '';
  935. item.style.opacity = 1;
  936. } else {
  937. item.style.opacity = 0;
  938. }
  939. });
  940. },
  941. /**
  942. * 鼠标松开
  943. */
  944. dragEnd() {
  945. document.body.style.userSelect = 'auto';
  946. const dragging = document.querySelector('.canvas-dragging');
  947. if (dragging) {
  948. document.body.removeChild(dragging);
  949. this.drag.dragging = false;
  950. }
  951. if (!this.isEdit) return;
  952. if (this.enterCanvas) {
  953. if (this.curRow >= -1 && this.curCol <= -1) {
  954. this.calculateRowInsertedObject();
  955. }
  956. if (this.curRow >= -1 && this.curCol > -1 && this.curGrid <= -1) {
  957. this.calculateColObject();
  958. }
  959. if (this.curRow >= -1 && this.curCol > -1 && this.curGrid > -1) {
  960. this.calculateGridObject();
  961. }
  962. }
  963. this.enterCanvas = false;
  964. },
  965. computedRowStyle(i) {
  966. let row = this.data.row_list[i];
  967. let col = row.col_list;
  968. if (col.length <= 1) {
  969. return {
  970. gridTemplateColumns: '0 100fr 0',
  971. };
  972. }
  973. let str = row.width_list
  974. .map((item) => {
  975. return `${item}`;
  976. })
  977. .join(' 6px ');
  978. let gridTemplateColumns = `0 ${str} 0`;
  979. return {
  980. gridAutoFlow: 'column',
  981. gridTemplateColumns,
  982. gridTemplateRows: 'auto',
  983. };
  984. },
  985. /**
  986. * 计算网格插入的对象
  987. */
  988. calculateGridObject() {
  989. const id = `ID-${getRandomNumber(12, true)}`;
  990. const letter = `L${getRandomNumber(6, true)}`;
  991. let row = this.data.row_list[this.curRow];
  992. let col = row.col_list[this.curCol];
  993. let grid = col.grid_list;
  994. let type = this.gridInsertType;
  995. if (['row', 'col-middle'].includes(type)) {
  996. let rowNum = this.curGrid === 0 ? 1 : grid[this.curGrid - 1].row + 1;
  997. grid.splice(this.curGrid, 0, {
  998. id,
  999. grid_area: letter,
  1000. width: '100fr',
  1001. height: 'auto',
  1002. edit_height: 'auto',
  1003. row: rowNum,
  1004. type: this.curType,
  1005. });
  1006. // 在新加入的 grid 后面的 row 都加 1
  1007. grid.forEach((item, i) => {
  1008. if (i > this.curGrid) {
  1009. item.row += 1;
  1010. }
  1011. });
  1012. }
  1013. if (['col-left', 'col-right'].includes(type)) {
  1014. let rowNum = grid[type === 'col-left' ? this.curGrid : this.curGrid - 1].row;
  1015. grid.splice(this.curGrid, 0, {
  1016. id,
  1017. grid_area: letter,
  1018. width: '100fr',
  1019. height: 'auto',
  1020. edit_height: 'auto',
  1021. row: rowNum,
  1022. type: this.curType,
  1023. });
  1024. let allRowNum = grid.filter(({ row }) => row === rowNum).length;
  1025. let w = 0;
  1026. grid.forEach((item, i) => {
  1027. if (item.row === rowNum && i !== this.curGrid) {
  1028. let width = Number(item.width.replace('fr', ''));
  1029. let diff = width / allRowNum;
  1030. item.width = `${width - diff}fr`;
  1031. w += diff;
  1032. }
  1033. });
  1034. grid[this.curGrid].width = `${w}fr`;
  1035. }
  1036. },
  1037. /**
  1038. * 计算列插入的对象
  1039. */
  1040. calculateColObject() {
  1041. const id = `ID-${getRandomNumber(12, true)}`;
  1042. const letter = `L${getRandomNumber(6, true)}`;
  1043. const col_id = `C${getRandomNumber(8, true)}`;
  1044. let row = this.data.row_list[this.curRow];
  1045. let col = row.col_list;
  1046. let w = 0;
  1047. row.width_list.forEach((item, i) => {
  1048. let itemW = Number(item.replace('fr', ''));
  1049. let rowW = itemW / (row.width_list.length + 1);
  1050. w += rowW;
  1051. row.width_list[i] = `${itemW - rowW}fr`;
  1052. });
  1053. row.width_list.splice(this.curCol, 0, `${w}fr`);
  1054. col.splice(this.curCol, 0, {
  1055. col_id,
  1056. width: '100fr',
  1057. height: 'auto',
  1058. grid_list: [
  1059. {
  1060. id,
  1061. grid_area: letter,
  1062. row: 1,
  1063. width: '100fr',
  1064. height: 'auto',
  1065. edit_height: 'auto',
  1066. type: this.curType,
  1067. },
  1068. ],
  1069. });
  1070. },
  1071. /**
  1072. * 计算行插入的对象
  1073. */
  1074. calculateRowInsertedObject() {
  1075. const id = `ID-${getRandomNumber(12, true)}`;
  1076. const letter = `L${getRandomNumber(6, true)}`;
  1077. const row_id = `R${getRandomNumber(6, true)}`;
  1078. const col_id = `C${getRandomNumber(8, true)}`;
  1079. this.data.row_list.splice(this.curRow + 1, 0, {
  1080. width_list: ['100fr'],
  1081. row_id,
  1082. col_list: [
  1083. {
  1084. col_id,
  1085. width: '100fr',
  1086. height: 'auto',
  1087. grid_list: [
  1088. {
  1089. id,
  1090. grid_area: letter,
  1091. width: '100fr',
  1092. height: 'auto',
  1093. edit_height: 'auto',
  1094. row: 1,
  1095. type: this.curType,
  1096. },
  1097. ],
  1098. },
  1099. ],
  1100. });
  1101. },
  1102. /**
  1103. * 获取拖拽元素和画布的边距差值
  1104. * @returns {object} { leftMarginDifference, topMarginDifference, isInsideCanvas }
  1105. * leftMarginDifference: 拖拽元素和画布左边距差值
  1106. * topMarginDifference: 拖拽元素和画布上边距差值
  1107. * isInsideCanvas: 是否在画布内
  1108. */
  1109. getMarginDifferences() {
  1110. const rect1 = document.querySelector('.canvas-dragging').getBoundingClientRect();
  1111. const rect2 = this.$refs.canvas.getBoundingClientRect();
  1112. const leftMarginDifference = rect1.left - rect2.left + 128;
  1113. const topMarginDifference = rect1.top - rect2.top + 72;
  1114. let isInsideCanvas =
  1115. leftMarginDifference > 0 &&
  1116. leftMarginDifference < rect2.width &&
  1117. topMarginDifference > 0 &&
  1118. topMarginDifference < rect2.height;
  1119. return { leftMarginDifference, topMarginDifference, isInsideCanvas };
  1120. },
  1121. // 获取子组件
  1122. findChildComponentByKey(key) {
  1123. return this.$refs.component.find((child) => child.$vnode.key === key);
  1124. },
  1125. /**
  1126. * 计算分组线样式
  1127. * @param {Array} rowIdList 行 ID 列表
  1128. * @returns {Object} 样式对象
  1129. */
  1130. computedGroupLine(rowIdList) {
  1131. // 获取画布顶部位置
  1132. const canvas = this.$refs.canvas;
  1133. const firstCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[0]}`);
  1134. const secCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[1]}`);
  1135. // DOM 未渲染,返回默认样式或空对象
  1136. if (!canvas || !firstCheckbox || !secCheckbox) {
  1137. return {};
  1138. }
  1139. const canvasTop = canvas.getBoundingClientRect().top;
  1140. const firstRowTop = firstCheckbox.getBoundingClientRect().top;
  1141. const secRowTop = secCheckbox.getBoundingClientRect().top;
  1142. return {
  1143. top: `${firstRowTop - canvasTop + 22}px`,
  1144. height: `${secRowTop - firstRowTop - 24}px`,
  1145. };
  1146. },
  1147. computedBorderColor(rowId) {
  1148. const groupList = [];
  1149. this.content_group_row_list.forEach(({ row_id, is_pre_same_group }) => {
  1150. if (is_pre_same_group && groupList.length) {
  1151. groupList[groupList.length - 1].push(row_id);
  1152. } else {
  1153. groupList.push([row_id]);
  1154. }
  1155. });
  1156. const index = groupList.findIndex((g) => g.includes(rowId));
  1157. return this.gridBorderColorList[index % this.gridBorderColorList.length];
  1158. },
  1159. /**
  1160. * 计算网格样式
  1161. * @param {Object} grid 网格对象
  1162. * @returns {Object} 样式对象
  1163. */
  1164. computedGridStyle(grid) {
  1165. let marginTop = grid.row === 1 ? '0' : '6px';
  1166. return {
  1167. gridArea: grid.grid_area,
  1168. height: grid?.edit_height || grid.height,
  1169. marginTop,
  1170. };
  1171. },
  1172. /**
  1173. * 计算网格视图顺序
  1174. * @param {String} id 网格 ID
  1175. * @returns {Number} 顺序值
  1176. */
  1177. computedGridViewOrder(id) {
  1178. // 获取网格 id,是第几行第几列第几个网格,通过 data.row_list 计算
  1179. let rowIndex = -1;
  1180. let colIndex = -1;
  1181. let gridIndex = -1;
  1182. this.data.row_list.forEach((row, i) => {
  1183. row.col_list.forEach((col, j) => {
  1184. col.grid_list.forEach((grid, k) => {
  1185. if (grid.id === id) {
  1186. rowIndex = i;
  1187. colIndex = j;
  1188. gridIndex = k;
  1189. }
  1190. });
  1191. });
  1192. });
  1193. let order = 0;
  1194. // 计算前几行的网格数量
  1195. if (rowIndex > 0) {
  1196. for (let i = 0; i < rowIndex; i++) {
  1197. this.data.row_list[i].col_list.forEach((col) => {
  1198. order += col.grid_list.length;
  1199. });
  1200. }
  1201. }
  1202. // 计算当前行前几列的网格数量
  1203. if (colIndex > 0) {
  1204. for (let j = 0; j < colIndex; j++) {
  1205. order += this.data.row_list[rowIndex].col_list[j].grid_list.length;
  1206. }
  1207. }
  1208. // 加上当前列前面的网格数量
  1209. order += gridIndex + 1;
  1210. return order;
  1211. },
  1212. /**
  1213. * 归一化 row_list 中的 row_id / col_id(为后端旧数据补 id)
  1214. */
  1215. normalizeRowColIds(data) {
  1216. if (!data || !Array.isArray(data.row_list)) return data;
  1217. data.row_list = data.row_list.map((row) => {
  1218. if (!row.row_id) row.row_id = `R${getRandomNumber(6, true)}`;
  1219. if (!Array.isArray(row.col_list)) row.col_list = [];
  1220. row.col_list = row.col_list.map((col) => {
  1221. if (!col.col_id) col.col_id = `C${getRandomNumber(8, true)}`;
  1222. return col;
  1223. });
  1224. return row;
  1225. });
  1226. return data;
  1227. },
  1228. /**
  1229. * 加载模板数据
  1230. * @param {Object} param
  1231. * @param {Object} param.data - 模板数据
  1232. * @param {Array} param.content_group_row_list - 内容分组行列表
  1233. */
  1234. loadTemplateData_CreateCanvas({ data, content_group_row_list }) {
  1235. this.data = data || {
  1236. row_list: [],
  1237. };
  1238. this.content_group_row_list = content_group_row_list || [];
  1239. },
  1240. /**
  1241. * @description 复制组件
  1242. * @param {object} data 组件数据
  1243. */
  1244. copyComponent(data) {
  1245. this.$emit('copyComponent', data);
  1246. },
  1247. /**
  1248. * @description 粘贴组件
  1249. * @param {Object} componentData 组件数据
  1250. * @param {String} position 放入位置类型
  1251. */
  1252. pasteComponent(componentData, position) {
  1253. if (!componentData) return;
  1254. let _data = JSON.parse(JSON.stringify(componentData));
  1255. const curId = this.getCurSettingId();
  1256. const attrs = this.findChildComponentByKey(`grid-${curId}`)?.$attrs;
  1257. if (!attrs) return;
  1258. const i = Number(attrs['data-row']);
  1259. const j = Number(attrs['data-col']);
  1260. let k = Number(attrs['data-grid']);
  1261. let type = _data.type;
  1262. let row = this.data.row_list[i];
  1263. let col = row.col_list[j];
  1264. let grid = col.grid_list;
  1265. const id = `ID-${getRandomNumber(12, true)}`;
  1266. const letter = `L${getRandomNumber(6, true)}`;
  1267. if (['top', 'bottom'].includes(position)) {
  1268. let rowNum = k === 0 ? 1 : grid[k - 1].row + 1;
  1269. if (position === 'bottom') {
  1270. rowNum = grid[k].row + 1;
  1271. }
  1272. k = position === 'top' ? k : k + 1;
  1273. grid.splice(k, 0, {
  1274. id,
  1275. grid_area: letter,
  1276. width: '100fr',
  1277. height: 'auto',
  1278. edit_height: 'auto',
  1279. row: rowNum,
  1280. type,
  1281. });
  1282. grid.forEach((item, i) => {
  1283. if (i > k) {
  1284. item.row += 1;
  1285. }
  1286. });
  1287. }
  1288. if (['left', 'right'].includes(position)) {
  1289. let rowNum = grid[k].row;
  1290. let _k = position === 'left' ? k : k + 1;
  1291. grid.splice(_k, 0, {
  1292. id,
  1293. grid_area: letter,
  1294. width: '100fr',
  1295. height: 'auto',
  1296. edit_height: 'auto',
  1297. row: rowNum,
  1298. type,
  1299. });
  1300. let allRowNum = grid.filter(({ row }) => row === rowNum).length;
  1301. let w = 0;
  1302. grid.forEach((item, i) => {
  1303. if (item.row === rowNum && i !== k) {
  1304. let width = Number(item.width.replace('fr', ''));
  1305. let diff = width / allRowNum;
  1306. item.width = `${width - diff}fr`;
  1307. w += diff;
  1308. }
  1309. });
  1310. grid[_k].width = `${w}fr`;
  1311. }
  1312. this.$nextTick(() => {
  1313. let newComponent = this.findChildComponentByKey(`grid-${id}`); // 获取新添加的组件实例
  1314. newComponent?.setData(_data); // 设置新添加的组件数据
  1315. });
  1316. },
  1317. },
  1318. };
  1319. </script>
  1320. <style lang="scss" scoped>
  1321. .canvas {
  1322. position: relative;
  1323. display: flex;
  1324. flex-direction: column;
  1325. row-gap: 8px;
  1326. width: $courseware-width;
  1327. min-height: calc(100% - 6px);
  1328. padding: 24px;
  1329. margin: 0 auto;
  1330. background-color: #fff;
  1331. background-repeat: no-repeat;
  1332. border-radius: 4px;
  1333. .group-line {
  1334. position: absolute;
  1335. left: 11px;
  1336. width: 1px;
  1337. background-color: #165dff;
  1338. }
  1339. .row {
  1340. display: grid;
  1341. row-gap: 16px;
  1342. .row-checkbox {
  1343. position: absolute;
  1344. left: 4px;
  1345. }
  1346. > .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
  1347. left: 6px;
  1348. }
  1349. .drag-vertical-line.grid-line-right,
  1350. .drag-vertical-line.grid-line-left {
  1351. top: 25%;
  1352. height: 50%;
  1353. }
  1354. .drag-vertical-line.col-start {
  1355. left: -12px;
  1356. }
  1357. .drag-vertical-line.col-end {
  1358. right: -8px;
  1359. }
  1360. .col {
  1361. display: grid;
  1362. .drag-vertical-line:first-child {
  1363. left: -8px;
  1364. }
  1365. .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
  1366. left: 6px;
  1367. }
  1368. .drag-vertical-line:last-child {
  1369. right: -4px;
  1370. }
  1371. }
  1372. }
  1373. .drag-line {
  1374. z-index: 2;
  1375. width: calc(100% - 16px);
  1376. height: 4px;
  1377. margin: 0 8px;
  1378. background-color: #379fff;
  1379. border-radius: 4px;
  1380. opacity: 0;
  1381. &.drag-row {
  1382. width: 50%;
  1383. margin-left: 25%;
  1384. background-color: $right-color;
  1385. }
  1386. &.grid-line:not(:first-child, :last-child) {
  1387. position: relative;
  1388. top: 6px;
  1389. }
  1390. &.grid-line {
  1391. background-color: #f43;
  1392. }
  1393. }
  1394. .drag-vertical-line {
  1395. position: relative;
  1396. z-index: 2;
  1397. width: 4px;
  1398. height: 100%;
  1399. background-color: #f43;
  1400. border-radius: 4px;
  1401. opacity: 0;
  1402. &.grid-line {
  1403. background-color: #f43;
  1404. }
  1405. }
  1406. }
  1407. </style>
  1408. <style lang="scss">
  1409. .canvas-dragging {
  1410. position: fixed;
  1411. z-index: 999;
  1412. width: 320px;
  1413. height: 180px;
  1414. background-color: #eaf5ff;
  1415. border: 1px solid #b5dbff;
  1416. border-radius: 4px;
  1417. opacity: 0.5;
  1418. transform: translate(-40%, -40%);
  1419. }
  1420. </style>