PreviewEdit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <template>
  2. <div ref="previewRoot" class="preview">
  3. <div v-if="heightPrompt" class="height-prompt"></div>
  4. <template v-for="(row, i) in rowList">
  5. <!-- 行 -->
  6. <div :key="i" :class="['row', `row-${i}`]" :style="getMultipleColStyle(i)">
  7. <!-- 列 -->
  8. <template v-for="(col, j) in row.col_list">
  9. <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
  10. <!-- 网格 -->
  11. <div
  12. v-for="(grid, k) in col.grid_list"
  13. :key="`grid-${i}-${j}-${k}`"
  14. :style="{ gridArea: grid.grid_area, height: grid.height }"
  15. :class="[!noMoveComponent.includes(grid.type) ? 'grid' : 'no-grid']"
  16. >
  17. <template v-for="{ type, cursor, lineClass } in moveLineList">
  18. <span
  19. v-if="!noMoveComponent.includes(grid.type)"
  20. :key="`${type}-${i}-${j}-${k}`"
  21. class="drag-line"
  22. :class="[type, ...lineClass]"
  23. :style="{ gridArea: type }"
  24. :data-type="type"
  25. @mousedown="dragStart($event, { cursor, type, i, j, k, id: grid.id })"
  26. ></span>
  27. </template>
  28. <component
  29. :is="previewComponentList[grid.type]"
  30. :id="grid.id"
  31. ref="preview"
  32. :key="`preview-${grid.id}`"
  33. :courseware-id="coursewareId"
  34. type="edit"
  35. :class="[grid.id]"
  36. :style="{
  37. gridArea: 'preview',
  38. height: grid.height,
  39. overflow: 'auto',
  40. }"
  41. @handleHeightChange="handleHeightChange"
  42. />
  43. </div>
  44. </div>
  45. </template>
  46. </div>
  47. </template>
  48. </div>
  49. </template>
  50. <script>
  51. import { previewComponentList } from '@/views/book/courseware/data/bookType';
  52. export default {
  53. name: 'PreviewEdit',
  54. provide() {
  55. return {
  56. getDragStatus: () => this.drag.dragging,
  57. bookInfo: this.bookInfo,
  58. getPermissionControl: () => this.permissionControl,
  59. };
  60. },
  61. props: {
  62. rowList: {
  63. type: Array,
  64. required: true,
  65. },
  66. coursewareId: {
  67. type: String,
  68. required: true,
  69. },
  70. },
  71. data() {
  72. return {
  73. previewComponentList,
  74. drag: {
  75. dragging: false,
  76. i: -1,
  77. j: -1,
  78. k: -1,
  79. startX: 0,
  80. startY: 0,
  81. type: '',
  82. id: '',
  83. },
  84. moveLineList: [
  85. {
  86. type: 'top',
  87. cursor: 'ns-resize',
  88. lineClass: ['drag-line'],
  89. },
  90. {
  91. type: 'left',
  92. cursor: 'ew-resize',
  93. lineClass: ['drag-vertical-line'],
  94. },
  95. {
  96. type: 'right',
  97. cursor: 'ew-resize',
  98. lineClass: ['drag-vertical-line'],
  99. },
  100. {
  101. type: 'bottom',
  102. cursor: 'ns-resize',
  103. lineClass: ['drag-line'],
  104. },
  105. ],
  106. dragElement: null, // 当前拖拽的组件实例
  107. // 不需要移动的组件
  108. noMoveComponent: ['divider', 'spacing'],
  109. bookInfo: {
  110. theme_color: '',
  111. },
  112. resizeObserver: null, // 用于监听高度变化
  113. heightPrompt: false, // 是否显示高度提示线
  114. permissionControl: {
  115. can_answer: false, // 可作答
  116. can_judge_correct: false, // 可判断对错(客观题)
  117. can_show_answer: false, // 可查看答案
  118. can_correct: false, // 可批改
  119. can_check_correct: false, // 可查看批改
  120. },
  121. };
  122. },
  123. created() {
  124. document.addEventListener('mousemove', this.dragMove);
  125. document.addEventListener('mouseup', this.dragEnd);
  126. },
  127. mounted() {
  128. const element = this.$refs.previewRoot;
  129. // 监听 courserware 高度变化,获取其高度
  130. this.resizeObserver = new ResizeObserver(() => {
  131. const rect = element.getBoundingClientRect();
  132. this.heightPrompt = rect.height > 1620;
  133. });
  134. this.resizeObserver.observe(element);
  135. },
  136. beforeDestroy() {
  137. document.removeEventListener('mousemove', this.dragMove);
  138. document.removeEventListener('mouseup', this.dragEnd);
  139. if (this.resizeObserver) {
  140. this.resizeObserver.disconnect();
  141. }
  142. },
  143. methods: {
  144. handleHeightChange(id, newHeight) {
  145. this.$emit('handleHeightChange', id, newHeight);
  146. },
  147. getMultipleColStyle(i) {
  148. let row = this.rowList[i];
  149. let col = row.col_list;
  150. if (col.length <= 1) {
  151. return {
  152. gridTemplateColumns: '100fr',
  153. };
  154. }
  155. let gridTemplateColumns = row.width_list.join(' ');
  156. return {
  157. gridAutoFlow: 'column',
  158. gridTemplateColumns,
  159. gridTemplateRows: 'auto',
  160. };
  161. },
  162. /**
  163. * 分割整数为多个 1的倍数
  164. * @param {number} num
  165. * @param {number} parts
  166. */
  167. splitInteger(num, parts) {
  168. let base = Math.floor(num / parts);
  169. let arr = Array(parts).fill(base);
  170. let remainder = num - base * parts;
  171. for (let i = 0; remainder > 0; i = (i + 1) % parts) {
  172. arr[i] += 1;
  173. remainder -= 1;
  174. }
  175. return arr;
  176. },
  177. /**
  178. * 计算列样式
  179. * @param {Object} col 列对象
  180. */
  181. computedColStyle(col) {
  182. const grid = col.grid_list;
  183. // 单次分组:后续 areas / columns / rows 统一复用 rowGroups。
  184. const rowGroups = new Map();
  185. grid.forEach((item) => {
  186. if (!rowGroups.has(item.row)) {
  187. rowGroups.set(item.row, []);
  188. }
  189. rowGroups.get(item.row).push(item);
  190. });
  191. let maxCol = 0; // 最大列数
  192. let curMaxRow = 0; // 当前数量最大 row 的值
  193. rowGroups.forEach((items, row) => {
  194. if (items.length > maxCol) {
  195. maxCol = items.length;
  196. curMaxRow = row;
  197. }
  198. });
  199. const sortedRows = Array.from(rowGroups.keys()).sort((a, b) => a - b);
  200. // 计算 grid_template_areas
  201. let gridTemplateAreas = '';
  202. sortedRows.forEach((row) => {
  203. const rowItems = rowGroups.get(row) || [];
  204. const needNum = maxCol - rowItems.length; // 需要补齐的数量
  205. const splitArr = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
  206. const areaList = rowItems.map(({ grid_area }, index) => {
  207. if (curMaxRow === row) {
  208. return `${grid_area}`;
  209. }
  210. if (rowItems.length === 1) {
  211. return ` ${grid_area} `.repeat(needNum + 1);
  212. }
  213. return splitArr[index] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(splitArr[index] + 1);
  214. });
  215. gridTemplateAreas += `'${areaList.join(' ')}' `;
  216. });
  217. // 计算 grid_template_columns
  218. let gridTemplateColumns = '';
  219. const maxRowItems = rowGroups.get(curMaxRow) || [];
  220. maxRowItems.forEach((item) => {
  221. gridTemplateColumns += `${item.width} `;
  222. });
  223. // 计算 grid_template_rows
  224. let gridTemplateRows = '';
  225. sortedRows.forEach((row) => {
  226. const heights = (rowGroups.get(row) || []).map((item) => item.height);
  227. if (heights.length === 1) {
  228. gridTemplateRows += `${heights[0]} `;
  229. } else {
  230. const isAllAuto = heights.every((item) => item === 'auto'); // 是否全是 auto
  231. gridTemplateRows += isAllAuto ? 'auto ' : `max(${heights.join(', ')}) `;
  232. }
  233. });
  234. return {
  235. width: col.width,
  236. gridTemplateAreas,
  237. gridTemplateColumns,
  238. gridTemplateRows,
  239. };
  240. },
  241. /**
  242. * 拖拽开始
  243. * @param {MouseEvent} event
  244. * @param {string} cursor
  245. * @param {string} type
  246. * @param {string} id
  247. * @param {number} i
  248. * @param {number} j
  249. * @param {number} k
  250. */
  251. dragStart(event, { cursor, type, id, i, j, k }) {
  252. const dragElement = this.findChildComponentByKey(`preview-${id}`);
  253. if (!dragElement) return;
  254. this.dragElement = dragElement;
  255. const { clientX, clientY } = event;
  256. this.drag = {
  257. dragging: true,
  258. startX: clientX,
  259. startY: clientY,
  260. type,
  261. id,
  262. i,
  263. j,
  264. k,
  265. };
  266. document.body.style.cursor = cursor;
  267. },
  268. /**
  269. * 拖拽移动
  270. * @param {MouseEvent} event
  271. */
  272. dragMove(event) {
  273. if (!this.drag.dragging) return;
  274. const { clientX, clientY } = event;
  275. const { i, j, k, id, startX, startY, type } = this.drag;
  276. const offsetX = clientX - startX;
  277. const offsetY = clientY - startY;
  278. let { min_height, min_width } = this.dragElement.data;
  279. const ROW_WIDTH = 1000;
  280. this.$emit('computedMoveData', {
  281. i,
  282. j,
  283. k,
  284. offsetX,
  285. offsetY,
  286. type,
  287. id,
  288. min_width,
  289. min_height,
  290. row_width: ROW_WIDTH,
  291. });
  292. this.drag.startX = clientX;
  293. this.drag.startY = clientY;
  294. this.$forceUpdate();
  295. },
  296. /**
  297. * 拖拽结束
  298. */
  299. dragEnd() {
  300. this.drag = {
  301. dragging: false,
  302. startX: 0,
  303. startY: 0,
  304. type: '',
  305. id: '',
  306. i: -1,
  307. j: -1,
  308. k: -1,
  309. };
  310. this.dragElement = null;
  311. document.body.style.cursor = 'auto';
  312. },
  313. // 获取子组件
  314. findChildComponentByKey(key) {
  315. return this.$children.find((child) => child.$vnode.key === key);
  316. },
  317. },
  318. };
  319. </script>
  320. <style lang="scss" scoped>
  321. .preview {
  322. position: relative;
  323. display: flex;
  324. flex-direction: column;
  325. row-gap: $component-spacing;
  326. width: calc($courseware-width + 200px);
  327. height: 100%;
  328. min-height: calc(100vh - 240px);
  329. padding: calc($courseware-top-padding + 15px) 100px calc($courseware-bottom-padding + 15px) 100px;
  330. margin: 0 auto;
  331. overflow: hidden;
  332. background-color: #fff;
  333. background-repeat: no-repeat;
  334. border-bottom-right-radius: 12px;
  335. border-bottom-left-radius: 12px;
  336. .height-prompt {
  337. position: absolute;
  338. top: 1620px;
  339. left: -200px;
  340. width: 1400px;
  341. border-top: 2px dashed #903ff8;
  342. }
  343. .row {
  344. display: grid;
  345. gap: $component-spacing;
  346. .col {
  347. display: grid;
  348. gap: $component-spacing;
  349. align-items: flex-start;
  350. .grid {
  351. display: grid;
  352. grid-template:
  353. 'top top top' 3px
  354. 'left preview right' 1fr
  355. 'bottom bottom bottom' 3px / 3px 1fr 3px;
  356. }
  357. .drag-line {
  358. z-index: 2;
  359. width: 100%;
  360. height: 6px;
  361. cursor: ns-resize;
  362. background: linear-gradient(to bottom, transparent, transparent 40%, #e5e6eb 40%, #e5e6eb 60%, transparent 60%);
  363. &.bottom {
  364. background: linear-gradient(
  365. to bottom,
  366. transparent,
  367. transparent 20%,
  368. #e5e6eb 20%,
  369. #e5e6eb 50%,
  370. transparent 50%
  371. );
  372. }
  373. }
  374. .drag-vertical-line {
  375. z-index: 2;
  376. width: 6px;
  377. height: 100%;
  378. cursor: ew-resize;
  379. background: linear-gradient(to right, transparent, transparent 40%, #e5e6eb 40%, #e5e6eb 60%, transparent 60%);
  380. &.left {
  381. background: linear-gradient(to left, transparent, transparent 60%, #e5e6eb 60%, #e5e6eb);
  382. }
  383. }
  384. }
  385. }
  386. }
  387. </style>