CreateCanvas.vue 51 KB

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