index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <template>
  2. <view class="ezy-xuexi-page3">
  3. <view class="icon-title-navBar-box">
  4. <text class="nav-bar-title">学习</text>
  5. </view>
  6. <!-- 吸顶单元标题 -->
  7. <view
  8. v-if="currentStickyTitle"
  9. class="item-dy-box item-fixed"
  10. >
  11. <view class="dy-left-box">L{{ currentStickyDengjiId }}</view>
  12. <view class="dy-right-box">
  13. <view class="right-content">
  14. <view class="dy-name">{{ currentStickyTitle }}</view>
  15. <view>{{ currentStickyIntro }}</view>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- 滚动区域 -->
  20. <scroll-view
  21. v-if="existData"
  22. class="ezy-page-body xuexi-page-body"
  23. scroll-y
  24. :scroll-top="scrollTop"
  25. @scroll="handleScroll"
  26. >
  27. <view class="xxjl-card-box-padding">
  28. <view class="xxjl-card-box">
  29. <!-- 显示内容 -->
  30. <view class="card-body-box">
  31. <img :src="banbenInfo.cover" />
  32. <view class="body-right">
  33. <view class="right-name">{{ banbenInfo.chanpinName }}</view>
  34. <view>等级:{{banbenInfo.dengjiName}}</view>
  35. <view>版本:{{banbenInfo.name}}</view>
  36. <view>单元:{{banbenInfo.curDanyuanName}}</view>
  37. <view>课程:{{banbenInfo.curKechengName}}</view>
  38. </view>
  39. </view>
  40. <view class="card-progress-box">
  41. <view class="xx-progress-box">
  42. <view>学习进度</view>
  43. <progress :percent="curProcess" class="xx-progress" stroke-width="20"
  44. backgroundColor="#3c7dfd" activeColor="#ffd11c" />
  45. </view>
  46. <ezyActiveVue class="ezy-btn-active jxxx-btn" @aclick="handlePlay(banbenInfo,'jixu')"></ezyActiveVue>
  47. </view>
  48. </view>
  49. </view>
  50. <view class="xx-item-list">
  51. <!-- <view class="xx-item-title">— 以下为当前等级课程目录 —</view> -->
  52. <view v-for="(danyuanItem, index) in danyuanList" :key="danyuanItem.danyuanId">
  53. <!-- 单元标题(带唯一ID,用于位置查询) -->
  54. <view class="xx-item-title" :id="`title-${danyuanItem.danyuanId}`">
  55. — {{ danyuanItem.danyuanName }} {{ danyuanItem.danyuanIntro }} —
  56. </view>
  57. <!-- 节列表 -->
  58. <ezyActiveVue
  59. class="ezy-list-item-active xx-item-box"
  60. v-for="jieItem in danyuanItem.jieList"
  61. :key="jieItem.jieId"
  62. @aclick="handlePlay(jieItem,'play')"
  63. >
  64. <view
  65. class="xx-item-status"
  66. :class="jieItem.wanchengFlag == 1 ? 'completed-status' : 'uncompleted-status'"
  67. ></view>
  68. <img :src="jieItem.cover" />
  69. <view class="xx-text-box">
  70. <view>{{ jieItem.jieName }}</view>
  71. <view>{{ jieItem.jieIntro }}</view>
  72. </view>
  73. <view class="xx-item-btn"></view>
  74. </ezyActiveVue>
  75. </view>
  76. <view class="xx-item-title">本级别最后一单元啦~</view>
  77. <view class="xx-more-btn" @click="moreBtn"></view>
  78. </view>
  79. </scroll-view>
  80. <!-- 回到顶部 -->
  81. <view v-if="currentStickyTitle" class="go-top-btn" @click="goTopBtn"></view>
  82. <!-- 无数据占位 -->
  83. <view v-if="!existData" class="ezy-page-body xuexi-page-body"></view>
  84. <!-- 弹窗组件 -->
  85. <danyuanInfoVue ref="dyRef" v-if="isShow" @close="isShow = false"></danyuanInfoVue>
  86. <!-- 底部 tabBar -->
  87. <custom-tab-bar :show="true" :current-index="currentTabIndex" />
  88. </view>
  89. </template>
  90. <script>
  91. import ezyActiveVue from "@/components/ezyActive/ezyActive.vue";
  92. import CustomTabBar from '@/components/custom-tabbar/index.vue';
  93. import cacheManager from "@/utils/cacheManager.js";
  94. import {
  95. shuxueChanpinBanbenInfo,
  96. shuxueSave
  97. } from "@/api/chanpinneirong.js"
  98. import {
  99. onLoad,
  100. onShow,
  101. onHide,
  102. onUnload
  103. } from "@dcloudio/uni-app"
  104. import danyuanInfoVue from '@/pages/xinshuxue/components/danyuanInfo.vue';
  105. import {
  106. toast
  107. } from '../../utils/common';
  108. import {updateXuexiProcess} from "./useNeirongShuxue"
  109. export default {
  110. data() {
  111. return {
  112. canExitApp: false,
  113. danyuanList: [],
  114. banbenInfo: {},
  115. banbenId: '',
  116. danyuanId: '',
  117. chanpinId: '',
  118. dengjiId: '',
  119. curProcess: '',
  120. currentTabIndex: 1,
  121. existData: true,
  122. isShow: false,
  123. stickyHeight: 0, // 吸顶栏高度(px)
  124. currentStickyTitle: '', // 初始为空,不显示吸顶
  125. currentStickyIntro: '',
  126. currentStickyDengjiId: '',
  127. titlePositions: [],
  128. scrollTop: 0,
  129. }
  130. },
  131. components: {
  132. CustomTabBar,
  133. danyuanInfoVue,
  134. ezyActiveVue
  135. },
  136. onLoad(options) {
  137. uni.hideTabBar()
  138. },
  139. onShow() {
  140. this.currentTabIndex = 1
  141. const cacheData = cacheManager.get('xuexi-shuxue');
  142. if (cacheData) {
  143. console.log('使用缓存数据');
  144. this.updateFromCache();
  145. } else {
  146. console.log('重新请求数据');
  147. const cacheDataAuth = cacheManager.get('auth');
  148. this.initFromOptions(cacheDataAuth);
  149. }
  150. },
  151. onHide() {
  152. console.log('学习页面隐藏')
  153. },
  154. onUnload() {
  155. // 页面卸载无需特殊处理
  156. },
  157. methods: {
  158. goTopBtn() {
  159. this.scrollTop = -1; // 先设一个无效值(确保变化)
  160. this.$nextTick(() => {
  161. this.scrollTop = 0; // 再滚到顶部
  162. });
  163. },
  164. moreBtn() {
  165. uni.switchTab({
  166. url: '/pages/chanpinXuanze/index'
  167. })
  168. },
  169. initFromOptions(options) {
  170. console.log('options', options);
  171. this.chanpinId = options.chanpinId;
  172. this.danyuanId = options.danyuanId;
  173. this.banbenId = options.banbenId;
  174. this.dengjiId = options.dengjiId;
  175. this.loadDataFromApi();
  176. },
  177. // 从缓存更新数据方法
  178. updateFromCache() {
  179. updateXuexiProcess()
  180. const cacheData = cacheManager.get('xuexi-shuxue');
  181. console.log('cacheData 从缓存更新数据方法', cacheData);
  182. if (cacheData) {
  183. this.banbenInfo = { ...cacheData };
  184. this.curProcess = cacheData.curProcess;
  185. this.danyuanList = [...(cacheData.danyuanList || [])];
  186. this.$nextTick(() => {
  187. this.updateTitlePositions();
  188. });
  189. }
  190. },
  191. loadDataFromApi() {
  192. this.banbenInfo = {}
  193. this.curProcess = ''
  194. this.danyuanList = []
  195. const req = {
  196. banbenId: this.banbenId
  197. }
  198. shuxueChanpinBanbenInfo(req).then(res => {
  199. if (res.code === 0) {
  200. this.banbenInfo = res.data;
  201. this.curProcess =res.data.curProcess * 100;
  202. this.danyuanList = res.data.danyuanList || [];
  203. if (!this.danyuanList.length) {
  204. this.existData = false
  205. }
  206. // 保存到缓存(新增了参数保存)
  207. const cacheData = {
  208. ...res.data,
  209. banbenId: this.banbenId,
  210. chanpinId: this.chanpinId,
  211. danyuanId: this.danyuanId,
  212. dengjiId: this.dengjiId
  213. };
  214. cacheManager.set('xuexi-shuxue', cacheData);
  215. // 更新全局auth信息
  216. cacheManager.updateObject('auth', {
  217. chanpinId: res.data.chanpinId,
  218. banbenId: this.banbenId,
  219. danyuanId: res.data.curDanyuanId,
  220. dengjiId: res.data.dengjiId
  221. });
  222. this.danyuanId = res.data.curDanyuanId
  223. this.dengjiId = res.data.dengjiId
  224. this.chanpinId = res.data.chanpinId
  225. // 数据加载完成后初始化观察器
  226. this.$nextTick(() => {
  227. this.updateTitlePositions();
  228. });
  229. }
  230. }).catch(res => {
  231. cacheManager.remove("xuexi-shuxue");
  232. toast("加载失败,请重试");
  233. });
  234. },
  235. updateStickyHeight() {
  236. const query = uni.createSelectorQuery().in(this);
  237. query.select('.item-fixed').boundingClientRect(res => {
  238. if (res) {
  239. this.stickyHeight = res.height; // 单位 px
  240. } else {
  241. this.stickyHeight = 100; // 默认 fallback(约 200rpx = 100px)
  242. }
  243. }).exec();
  244. },
  245. // 获取所有标题在 scroll-view 中的绝对位置
  246. updateTitlePositions() {
  247. this.titlePositions = [];
  248. this.danyuanTitleRefs = [];
  249. if (!this.danyuanList.length) return;
  250. const query = uni.createSelectorQuery().in(this);
  251. this.danyuanList.forEach(item => {
  252. query.select(`#title-${item.danyuanId}`).boundingClientRect();
  253. this.danyuanTitleRefs.push(item);
  254. });
  255. query.exec((rects) => {
  256. rects.forEach((rect, index) => {
  257. if (rect) {
  258. // 在 scroll-view 初始未滚动时,rect.top 就是内容中的绝对 top
  259. this.titlePositions.push({
  260. danyuanId: this.danyuanTitleRefs[index].danyuanId,
  261. name: this.danyuanTitleRefs[index].danyuanName,
  262. intro: this.danyuanTitleRefs[index].danyuanIntro,
  263. top: rect.top
  264. });
  265. }
  266. });
  267. this.titlePositions.sort((a, b) => a.top - b.top);
  268. // 👇 新增:更新吸顶栏高度
  269. this.updateStickyHeight();
  270. });
  271. },
  272. handleScroll(e) {
  273. const scrollTop = e.detail.scrollTop;
  274. const firstTitle = this.titlePositions[0];
  275. if (!firstTitle) {
  276. this.currentStickyTitle = '';
  277. return;
  278. }
  279. // 👇 关键修改:提前触发吸顶
  280. const triggerOffset = scrollTop + this.stickyHeightPx;
  281. // 如果还没滚到第一个标题的顶部(考虑吸顶高度),则隐藏
  282. if (triggerOffset < firstTitle.top) {
  283. this.currentStickyTitle = '';
  284. return;
  285. }
  286. // 找出最后一个 top <= triggerOffset 的标题(即刚进入吸顶区的单元)
  287. let matched = null;
  288. for (let i = this.titlePositions.length - 1; i >= 0; i--) {
  289. if (this.titlePositions[i].top <= triggerOffset) {
  290. matched = this.titlePositions[i];
  291. break;
  292. }
  293. }
  294. if (matched) {
  295. this.currentStickyTitle = matched.name;
  296. this.currentStickyIntro = matched.intro;
  297. this.currentStickyDengjiId = this.banbenInfo.dengjiId || 1;
  298. } else {
  299. this.currentStickyTitle = '';
  300. }
  301. },
  302. getJieAndDanyuan(data, jieId) {
  303. for (let danyuan of data.danyuanList) {
  304. for (let jie of danyuan.jieList) {
  305. if (jie.jieId == jieId) {
  306. return { danyuan, jie }
  307. }
  308. }
  309. }
  310. return null;
  311. },
  312. async saveAndNavigate(jieId, type, da, code) {
  313. if (code == 'jixu') {
  314. if (!this.banbenId || !this.danyuanId) {
  315. toast("banbenId或者danyuanId 丢失")
  316. return false
  317. }
  318. }
  319. let req = {
  320. "banbenId": this.banbenId,
  321. "danyuanId": da.danyuanId,
  322. "jieId": jieId
  323. }
  324. console.log('req',req);
  325. try {
  326. const res = await shuxueSave(req);
  327. if (res.code == 0 && res.data) {
  328. let curJieAndDanyuan = this.getJieAndDanyuan(this.banbenInfo, jieId);
  329. if (!curJieAndDanyuan) {
  330. toast("未找到课程信息");
  331. return false;
  332. }
  333. const cacheData = cacheManager.get('xuexi-shuxue') || {};
  334. cacheData.curDanyuanName = curJieAndDanyuan.danyuan.danyuanName;
  335. cacheData.curKechengName = curJieAndDanyuan.jie.jieIntro;
  336. cacheData.curJieId = jieId;
  337. cacheData.type = curJieAndDanyuan.jie.type;
  338. cacheManager.set('xuexi-shuxue', cacheData);
  339. if (type == 1) {
  340. uni.navigateTo({ url: `/pages/xinshuxue/lookShipin?jieId=${jieId}` })
  341. } else {
  342. uni.navigateTo({ url: `/pages/xinshuxue/unitTest?jieId=${jieId}` })
  343. }
  344. } else {
  345. toast("保存位置出错");
  346. return false;
  347. }
  348. } catch (error) {
  349. toast("保存失败");
  350. return false;
  351. }
  352. },
  353. handlePlay(da, code) {
  354. let jieId = code === 'jixu' ? da.curJieId : da.jieId;
  355. if (!jieId) {
  356. toast("无课程ID");
  357. return;
  358. }
  359. this.saveAndNavigate(jieId, da.type, da, code);
  360. },
  361. handleClickDanyuan(item) {
  362. if (!item.danyuanId) {
  363. toast("danyuanId丢失")
  364. return false
  365. }
  366. this.isShow = true;
  367. setTimeout(() => {
  368. // 更新为点击的动态单元Id [临时]
  369. this.$refs.dyRef.handleShow(item.danyuanId)
  370. }, 100)
  371. },
  372. handleBack() {
  373. uni.navigateTo({
  374. url: `/pages/chanpinXuanze/banben?dengjiId=` + this.dengjiId
  375. })
  376. },
  377. },
  378. // 计算吸顶栏下方的偏移(确保内容不被遮挡)
  379. computed: {
  380. stickyHeightPx() {
  381. // 175rpx ≈ 87.5px,取整 90px 足够安全
  382. return 90;
  383. },
  384. topOffset() {
  385. // 只有当吸顶栏显示时,才增加上边距避免内容被遮挡
  386. return this.currentStickyTitle ? '80rpx' : '0rpx';
  387. }
  388. }
  389. }
  390. </script>