CoursewarePreview.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <template>
  2. <div
  3. class="courserware"
  4. ref="courserware"
  5. :style="[
  6. {
  7. backgroundImage: background.background_image_url ? `url(${background.background_image_url})` : '',
  8. backgroundSize: background.background_image_url
  9. ? `${background.background_position.width}% ${background.background_position.height}%`
  10. : '',
  11. backgroundPosition: background.background_image_url
  12. ? `${background.background_position.left}% ${background.background_position.top}%`
  13. : '',
  14. },
  15. ]"
  16. >
  17. <template v-for="(row, i) in data.row_list">
  18. <div :key="i" class="row" :style="getMultipleColStyle(i)">
  19. <!-- 列 -->
  20. <template v-for="(col, j) in row.col_list">
  21. <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
  22. <!-- 网格 -->
  23. <template v-for="(grid, k) in col.grid_list">
  24. <div @contextmenu.prevent="handleContextMenu($event, grid.id)" :key="k">
  25. <component
  26. :is="previewComponentList[grid.type]"
  27. ref="preview"
  28. :content="computedColContent(grid.id)"
  29. :class="[grid.id]"
  30. :style="{
  31. gridArea: grid.grid_area,
  32. height: grid.height,
  33. }"
  34. />
  35. </div>
  36. <div
  37. v-if="showMenu && componentId === grid.id"
  38. class="custom-context-menu"
  39. :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }"
  40. @click="handleMenuItemClick"
  41. :key="'menu' + grid.id + k"
  42. >
  43. <SvgIcon icon-class="icon-publish" size="24" />
  44. 添加批注
  45. </div>
  46. <div
  47. :key="'show' + grid.id + k"
  48. v-if="showRemark && Object.keys(componentRemarkObj).length !== 0 && componentRemarkObj[grid.id]"
  49. >
  50. <el-popover
  51. v-for="(items, indexs) in componentRemarkObj[grid.id]"
  52. :key="indexs"
  53. placement="bottom"
  54. width="200"
  55. trigger="click"
  56. >
  57. <div v-html="items.content"></div>
  58. <template #reference>
  59. <SvgIcon
  60. icon-class="icon-info"
  61. size="24"
  62. class="remark-info"
  63. slot="reference"
  64. :style="{ left: items.position_x - 12 + 'px', top: items.position_y - 12 + 'px' }"
  65. />
  66. </template>
  67. </el-popover>
  68. </div>
  69. </template>
  70. </div>
  71. </template>
  72. </div>
  73. </template>
  74. </div>
  75. </template>
  76. <script>
  77. import { previewComponentList } from '@/views/book/courseware/data/bookType';
  78. export default {
  79. name: 'CoursewarePreview',
  80. provide() {
  81. return {
  82. getDragStatus: () => false,
  83. bookInfo: this.bookInfo,
  84. };
  85. },
  86. props: {
  87. data: {
  88. type: Object,
  89. default: () => ({}),
  90. },
  91. background: {
  92. type: Object,
  93. default: () => ({}),
  94. },
  95. componentList: {
  96. type: Array,
  97. required: true,
  98. },
  99. canRemark: {
  100. type: Boolean,
  101. default: false,
  102. },
  103. showRemark: {
  104. type: Boolean,
  105. default: false,
  106. },
  107. componentRemarkObj: {
  108. type: Object,
  109. default: () => ({}),
  110. },
  111. },
  112. data() {
  113. return {
  114. previewComponentList,
  115. bookInfo: {
  116. theme_color: '',
  117. },
  118. showMenu: false,
  119. divPosition: {
  120. left: 0,
  121. top: 0,
  122. }, // courserware盒子原始距离页面顶部和左边的距离
  123. menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
  124. componentId: '', // 添加批注的组件id
  125. };
  126. },
  127. mounted() {
  128. const element = this.$refs.courserware;
  129. const rect = element.getBoundingClientRect();
  130. this.divPosition = {
  131. left: rect.left,
  132. top: rect.top,
  133. };
  134. window.addEventListener('mousedown', this.handleMouseDown);
  135. },
  136. methods: {
  137. /**
  138. * 计算组件内容
  139. * @param {string} id 组件id
  140. * @returns {string} 组件内容
  141. */
  142. computedColContent(id) {
  143. if (!id) return '';
  144. return this.componentList.find((item) => item.component_id === id)?.content || '';
  145. },
  146. getMultipleColStyle(i) {
  147. let row = this.data.row_list[i];
  148. let col = row.col_list;
  149. if (col.length <= 1) {
  150. return {
  151. gridTemplateColumns: '100fr',
  152. };
  153. }
  154. let gridTemplateColumns = row.width_list.join(' ');
  155. return {
  156. gridAutoFlow: 'column',
  157. gridTemplateColumns,
  158. gridTemplateRows: 'auto',
  159. };
  160. },
  161. /**
  162. * 分割整数为多个 1的倍数
  163. * @param {number} num
  164. * @param {number} parts
  165. */
  166. splitInteger(num, parts) {
  167. let base = Math.floor(num / parts);
  168. let arr = Array(parts).fill(base);
  169. let remainder = num - base * parts;
  170. for (let i = 0; remainder > 0; i = (i + 1) % parts) {
  171. arr[i] += 1;
  172. remainder -= 1;
  173. }
  174. return arr;
  175. },
  176. computedColStyle(col) {
  177. const grid = col.grid_list;
  178. let maxCol = 0; // 最大列数
  179. let rowList = new Map();
  180. grid.forEach(({ row }) => {
  181. rowList.set(row, (rowList.get(row) || 0) + 1);
  182. });
  183. let curMaxRow = 0; // 当前数量最大 row 的值
  184. rowList.forEach((value, key) => {
  185. if (value > maxCol) {
  186. maxCol = value;
  187. curMaxRow = key;
  188. }
  189. });
  190. // 计算 grid_template_areas
  191. let gridTemplateAreas = '';
  192. let gridArr = [];
  193. grid.forEach(({ grid_area, row }) => {
  194. if (!gridArr[row - 1]) {
  195. gridArr[row - 1] = [];
  196. }
  197. if (curMaxRow === row) {
  198. gridArr[row - 1].push(`${grid_area}`);
  199. } else {
  200. let filter = grid.filter((item) => item.row === row);
  201. let find = filter.findIndex((item) => item.grid_area === grid_area);
  202. let needNum = maxCol - filter.length; // 需要的数量
  203. let str = '';
  204. if (filter.length === 1) {
  205. str = ` ${grid_area} `.repeat(needNum + 1);
  206. } else {
  207. let arr = this.splitInteger(needNum, filter.length);
  208. str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
  209. }
  210. gridArr[row - 1].push(`${str}`);
  211. }
  212. });
  213. gridArr.forEach((item) => {
  214. gridTemplateAreas += `'${item.join(' ')}' `;
  215. });
  216. // 计算 grid_template_columns
  217. let gridTemplateColumns = '';
  218. let max = { row: 0, num: 0 };
  219. grid.forEach(({ row }) => {
  220. // 计算出 row 的哪个值最多
  221. let len = grid.filter((item) => item.row === row).length;
  222. if (max.num < len) {
  223. max.num = len;
  224. max.row = row;
  225. }
  226. });
  227. grid.forEach((item) => {
  228. if (item.row === max.row) {
  229. gridTemplateColumns += `${item.width} `;
  230. }
  231. });
  232. // 计算 grid_template_rows
  233. let gridTemplateRows = '';
  234. // 将 grid 按照 row 分组
  235. let gridMap = new Map();
  236. grid.forEach((item) => {
  237. if (!gridMap.has(item.row)) {
  238. gridMap.set(item.row, []);
  239. }
  240. gridMap.get(item.row).push(item.height);
  241. });
  242. gridMap.forEach((value) => {
  243. if (value.length === 1) {
  244. gridTemplateRows += `${value[0]} `;
  245. } else {
  246. let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
  247. gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
  248. }
  249. });
  250. return {
  251. width: col.width,
  252. gridTemplateAreas,
  253. gridTemplateColumns,
  254. gridTemplateRows,
  255. };
  256. },
  257. handleContextMenu(event, id) {
  258. if (this.canRemark) {
  259. event.preventDefault(); // 阻止默认的上下文菜单显示
  260. this.menuPosition = {
  261. x: event.clientX - this.divPosition.left,
  262. y: event.clientY - this.divPosition.top,
  263. }; // 设置菜单位置
  264. this.componentId = id;
  265. this.$emit('computeScroll');
  266. }
  267. },
  268. handleResult(top, left, select_node) {
  269. this.menuPosition = {
  270. x: this.menuPosition.x + left,
  271. y: this.menuPosition.y + top,
  272. select_node: select_node,
  273. }; // 设置菜单位置
  274. this.showMenu = true; // 显示菜单
  275. },
  276. handleMenuItemClick() {
  277. this.showMenu = false; // 隐藏菜单
  278. this.$emit(
  279. 'addRemark',
  280. this.menuPosition.select_node,
  281. this.menuPosition.x,
  282. this.menuPosition.y,
  283. this.componentId,
  284. );
  285. },
  286. handleMouseDown(event) {
  287. if (event.button === 0) {
  288. // 0 表示左键
  289. this.showMenu = false;
  290. }
  291. },
  292. },
  293. beforeDestroy() {
  294. window.removeEventListener('mousedown', this.handleMouseDown);
  295. },
  296. };
  297. </script>
  298. <style lang="scss" scoped>
  299. .courserware {
  300. position: relative;
  301. display: flex;
  302. flex-direction: column;
  303. row-gap: 6px;
  304. width: 100%;
  305. height: 100%;
  306. min-height: 500px;
  307. padding: 24px;
  308. background-color: #fff;
  309. background-repeat: no-repeat;
  310. border-bottom-right-radius: 12px;
  311. border-bottom-left-radius: 12px;
  312. .row {
  313. display: grid;
  314. gap: 16px;
  315. .col {
  316. display: grid;
  317. gap: 16px;
  318. overflow: hidden;
  319. }
  320. }
  321. .custom-context-menu,
  322. .remark-info {
  323. position: absolute;
  324. z-index: 999;
  325. display: flex;
  326. gap: 3px;
  327. align-items: center;
  328. font-size: 14px;
  329. cursor: pointer;
  330. }
  331. }
  332. </style>