CreateCanvas.vue 62 KB

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