CreateCanvas.vue 58 KB

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