MobileCoursewarePreview.vue 11 KB

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