CreateCanvas.vue 65 KB

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