MobileCoursewarePreview.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <template>
  2. <div
  3. ref="courseware"
  4. class="courseware"
  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. <!-- 行 -->
  18. <template v-for="(row, i) in data.row_list">
  19. <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row">
  20. <el-checkbox
  21. v-if="
  22. isShowGroup &&
  23. groupRowList.find(({ row_id, is_pre_same_group }) => row.row_id === row_id && !is_pre_same_group)
  24. "
  25. v-model="rowCheckList[row.row_id]"
  26. :class="['row-checkbox', `${row.row_id}`]"
  27. />
  28. <!-- 列 -->
  29. <template v-for="(col, j) in row.col_list">
  30. <div :key="j" :class="['col', `col-${i}-${j}`]">
  31. <!-- 网格 -->
  32. <template v-for="(grid, k) in col.grid_list">
  33. <component
  34. :is="previewComponentList[grid.type]"
  35. :key="k"
  36. ref="preview"
  37. class="grid"
  38. :content="computedColContent(grid.id)"
  39. :data-row="i"
  40. :data-col="j"
  41. :data-grid="k"
  42. :data-view-order="computedGridViewOrder(grid.id)"
  43. :class="[grid.id]"
  44. :is-mobile="true"
  45. :data-id="grid.id"
  46. :style="{
  47. gridArea: grid.grid_area,
  48. }"
  49. />
  50. </template>
  51. </div>
  52. </template>
  53. </div>
  54. </template>
  55. </div>
  56. </template>
  57. <script>
  58. import { previewComponentList } from '@/views/book/courseware/data/bookType';
  59. export default {
  60. name: 'MobileCoursewarePreview',
  61. provide() {
  62. return {
  63. getDragStatus: () => false,
  64. bookInfo: this.bookInfo,
  65. };
  66. },
  67. props: {
  68. data: {
  69. type: Object,
  70. default: () => ({}),
  71. },
  72. background: {
  73. type: Object,
  74. default: () => ({}),
  75. },
  76. componentList: {
  77. type: Array,
  78. required: true,
  79. },
  80. canRemark: {
  81. type: Boolean,
  82. default: false,
  83. },
  84. showRemark: {
  85. type: Boolean,
  86. default: false,
  87. },
  88. componentRemarkObj: {
  89. type: Object,
  90. default: () => ({}),
  91. },
  92. groupRowList: {
  93. type: Array,
  94. default: () => [],
  95. },
  96. isShowGroup: {
  97. type: Boolean,
  98. default: false,
  99. },
  100. groupShowAll: {
  101. type: Boolean,
  102. default: true,
  103. },
  104. },
  105. data() {
  106. return {
  107. previewComponentList,
  108. bookInfo: {
  109. theme_color: '',
  110. },
  111. showMenu: false,
  112. divPosition: {
  113. left: 0,
  114. top: 0,
  115. }, // courseware盒子原始距离页面顶部和左边的距离
  116. menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
  117. componentId: '', // 添加批注的组件id
  118. rowCheckList: {},
  119. };
  120. },
  121. watch: {
  122. groupRowList: {
  123. handler(val) {
  124. if (!val) return;
  125. this.rowCheckList = val
  126. .filter(({ is_pre_same_group }) => !is_pre_same_group)
  127. .reduce((acc, row) => {
  128. acc[row.row_id] = false;
  129. return acc;
  130. }, {});
  131. },
  132. },
  133. },
  134. mounted() {
  135. const element = this.$refs.courseware;
  136. const rect = element.getBoundingClientRect();
  137. this.divPosition = {
  138. left: rect.left,
  139. top: rect.top,
  140. };
  141. },
  142. methods: {
  143. /**
  144. * 计算组件内容
  145. * @param {string} id 组件id
  146. * @returns {string} 组件内容
  147. */
  148. computedColContent(id) {
  149. if (!id) return '';
  150. return this.componentList.find((item) => item.component_id === id)?.content || '';
  151. },
  152. getMultipleColStyle(i) {
  153. let row = this.data.row_list[i];
  154. let col = row.col_list;
  155. if (col.length <= 1) {
  156. return {
  157. gridTemplateColumns: '100fr',
  158. };
  159. }
  160. let gridTemplateColumns = row.width_list.join(' ');
  161. return {
  162. gridAutoFlow: 'column',
  163. gridTemplateColumns,
  164. gridTemplateRows: 'auto',
  165. };
  166. },
  167. /**
  168. * 计算行的可见性
  169. * @params {string} rowId 行的ID
  170. * @returns {boolean} 行是否可见
  171. */
  172. computedRowVisibility(rowId) {
  173. if (this.groupShowAll) return true;
  174. let { row_id, is_pre_same_group } = this.groupRowList.find(({ row_id }) => row_id === rowId);
  175. if (is_pre_same_group) {
  176. const index = this.groupRowList.findIndex(({ row_id }) => row_id === rowId);
  177. if (index === -1) return false;
  178. for (let i = index - 1; i >= 0; i--) {
  179. if (!this.groupRowList[i].is_pre_same_group) {
  180. return this.rowCheckList[this.groupRowList[i].row_id];
  181. }
  182. }
  183. return false;
  184. }
  185. return this.rowCheckList[row_id];
  186. },
  187. /**
  188. * 分割整数为多个 1的倍数
  189. * @param {number} num
  190. * @param {number} parts
  191. */
  192. splitInteger(num, parts) {
  193. let base = Math.floor(num / parts);
  194. let arr = Array(parts).fill(base);
  195. let remainder = num - base * parts;
  196. for (let i = 0; remainder > 0; i = (i + 1) % parts) {
  197. arr[i] += 1;
  198. remainder -= 1;
  199. }
  200. return arr;
  201. },
  202. computedColStyle(col) {
  203. const grid = col.grid_list || [];
  204. if (!grid.length) {
  205. return {
  206. width: col.width,
  207. gridTemplateAreas: '',
  208. gridTemplateColumns: '',
  209. gridTemplateRows: '',
  210. };
  211. }
  212. // 一次遍历完成按行分组,并记录最大列数所在行
  213. let maxCol = 0;
  214. let curMaxRow = 0;
  215. const rowMap = new Map();
  216. grid.forEach((item) => {
  217. if (!rowMap.has(item.row)) {
  218. rowMap.set(item.row, []);
  219. }
  220. const rowItems = rowMap.get(item.row);
  221. rowItems.push(item);
  222. if (rowItems.length > maxCol) {
  223. maxCol = rowItems.length;
  224. curMaxRow = item.row;
  225. }
  226. });
  227. // 计算 grid_template_areas
  228. let gridTemplateAreas = '';
  229. const sortedRows = Array.from(rowMap.keys()).sort((a, b) => a - b);
  230. sortedRows.forEach((row) => {
  231. const rowItems = rowMap.get(row);
  232. const rowAreas = [];
  233. if (row === curMaxRow) {
  234. rowItems.forEach(({ grid_area }) => {
  235. rowAreas.push(`${grid_area}`);
  236. });
  237. } else {
  238. const needNum = maxCol - rowItems.length;
  239. const splitList = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
  240. rowItems.forEach(({ grid_area }, index) => {
  241. if (rowItems.length === 1) {
  242. rowAreas.push(` ${grid_area} `.repeat(needNum + 1));
  243. return;
  244. }
  245. const repeatNum = splitList[index] + 1;
  246. rowAreas.push(splitList[index] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(repeatNum));
  247. });
  248. }
  249. gridTemplateAreas += `'${rowAreas.join(' ')}' `;
  250. });
  251. // 计算 grid_template_columns
  252. const maxRowItems = rowMap.get(curMaxRow) || [];
  253. const gridTemplateColumns = maxRowItems.map(({ width }) => width).join(' ');
  254. // 移动端高度统一使用 auto
  255. const gridTemplateRows = `${sortedRows.map(() => 'auto').join(' ')} `;
  256. return {
  257. width: col.width,
  258. gridTemplateAreas,
  259. gridTemplateColumns,
  260. gridTemplateRows,
  261. };
  262. },
  263. /**
  264. * 查找子组件
  265. * @param {string} id 组件的唯一标识符
  266. * @returns {Promise<HTMLElement|null>} 返回找到的子组件或 null
  267. */
  268. async findChildComponentByKey(id) {
  269. await this.$nextTick();
  270. return this.$refs.preview.find((child) => child.dataset.id === id);
  271. },
  272. /**
  273. * 模拟回答
  274. * @param {boolean} isJudgingRightWrong 是否判断对错
  275. * @param {boolean} isShowRightAnswer 是否显示正确答案
  276. * @param {boolean} disabled 是否禁用
  277. */
  278. simulateAnswer(isJudgingRightWrong, isShowRightAnswer, disabled = true) {
  279. this.$refs.preview.forEach((item) => {
  280. item.showAnswer(isJudgingRightWrong, isShowRightAnswer, null, disabled);
  281. });
  282. },
  283. /**
  284. * 获取网格 id,是第几行第几列第几个网格,通过 data.row_list 计算
  285. * @param {string} id 网格 id
  286. * @returns {number} 网格的显示顺序
  287. */
  288. computedGridViewOrder(id) {
  289. let rowIndex = -1;
  290. let colIndex = -1;
  291. let gridIndex = -1;
  292. this.data.row_list.forEach((row, i) => {
  293. row.col_list.forEach((col, j) => {
  294. col.grid_list.forEach((grid, k) => {
  295. if (grid.id === id) {
  296. rowIndex = i;
  297. colIndex = j;
  298. gridIndex = k;
  299. }
  300. });
  301. });
  302. });
  303. let order = 0;
  304. // 计算前几行的网格数量
  305. if (rowIndex > 0) {
  306. for (let i = 0; i < rowIndex; i++) {
  307. this.data.row_list[i].col_list.forEach((col) => {
  308. order += col.grid_list.length;
  309. });
  310. }
  311. }
  312. // 计算当前行前几列的网格数量
  313. if (colIndex > 0) {
  314. for (let j = 0; j < colIndex; j++) {
  315. order += this.data.row_list[rowIndex].col_list[j].grid_list.length;
  316. }
  317. }
  318. // 加上当前列前面的网格数量
  319. order += gridIndex + 1;
  320. return order;
  321. },
  322. },
  323. };
  324. </script>
  325. <style lang="scss">
  326. .courseware {
  327. position: relative;
  328. display: flex;
  329. flex-direction: column;
  330. row-gap: 6px;
  331. width: 100%;
  332. height: 100%;
  333. min-height: 500px;
  334. padding: 2%;
  335. background-color: #fff;
  336. background-repeat: no-repeat;
  337. border-bottom-right-radius: 12px;
  338. border-bottom-left-radius: 12px;
  339. .row {
  340. display: flex;
  341. flex-direction: column;
  342. gap: 16px;
  343. .col {
  344. display: flex;
  345. flex-direction: column;
  346. gap: 16px;
  347. overflow: hidden;
  348. .grid {
  349. width: 100%;
  350. }
  351. .main {
  352. .option-list {
  353. flex-direction: column !important;
  354. }
  355. }
  356. }
  357. .row-checkbox {
  358. position: absolute;
  359. left: 4px;
  360. }
  361. }
  362. .custom-context-menu,
  363. .remark-info {
  364. position: absolute;
  365. z-index: 999;
  366. display: flex;
  367. gap: 3px;
  368. align-items: center;
  369. font-size: 14px;
  370. cursor: pointer;
  371. }
  372. .custom-context-menu {
  373. padding-left: 30px;
  374. background: url('../../../../assets/icon-publish.png') left center no-repeat;
  375. background-size: 24px;
  376. }
  377. }
  378. </style>