CreateCanvas.vue 44 KB

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