index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <template>
  2. <view>
  3. <nav-bar title="课程详情"></nav-bar>
  4. <view class="videoBox">
  5. <view>
  6. <view class="video_box" v-if="!startStatus">
  7. <image
  8. :src="$method.splitImgHost(detail.coverUrl)"
  9. style="width: 100%; height: 460rpx"
  10. ></image>
  11. <image
  12. v-if="false"
  13. class="video_play"
  14. src="/static/play.png"
  15. @click="startVideo"
  16. ></image>
  17. </view>
  18. <view v-else class="video_box" style="width: 100%; height: 460rpx">
  19. <polyv-player
  20. id="playerVideo"
  21. playerId="playerVideo"
  22. height="460rpx"
  23. :vid="vid"
  24. :showSettingBtn="true"
  25. :enablePlayGesture="true"
  26. :playbackRate="playbackRate"
  27. :isAllowSeek="isAllowSeek"
  28. :autoplay="autoplay"
  29. :startTime="startTime"
  30. @statechange="onStateChange"
  31. ></polyv-player>
  32. </view>
  33. <view style="padding: 20rpx">
  34. <view style="display: flex">
  35. <view class="yearTag" v-if="detail.year">{{ detail.year }}</view>
  36. <view class="titleTag">{{ detail.goodsName }}</view>
  37. </view>
  38. <view
  39. style="
  40. display: flex;
  41. justify-content: space-between;
  42. margin-top: 13rpx;
  43. "
  44. >
  45. <view class="noteTag"
  46. ><image src="/static/icon/wk_icon1.png" class="wk_icon"></image>
  47. <text class="blackFont"
  48. >{{ courseList.length }} 课程 {{ detail.classHours }}</text
  49. >
  50. 学时</view
  51. >
  52. </view>
  53. </view>
  54. </view>
  55. <u-line color="#D6D6DB" />
  56. <view style="height: 80rpx">
  57. <view
  58. ><u-tabs
  59. :list="list"
  60. :item-width="itemWidth()"
  61. font-size="30"
  62. bar-width="24"
  63. :current="current"
  64. @change="change"
  65. active-color="#007AFF"
  66. ></u-tabs
  67. ></view>
  68. </view>
  69. </view>
  70. <view
  71. style="padding: 20rpx; padding-bottom: 100rpx; position: relative"
  72. v-show="current == 0"
  73. >
  74. <view class="content">
  75. <view
  76. v-html="detail.mobileDetailHtml"
  77. style="width: 100%; overflow: hidden"
  78. ></view>
  79. </view>
  80. </view>
  81. <view
  82. style="padding: 20rpx; padding-bottom: 100rpx; position: relative"
  83. v-show="current == 1"
  84. >
  85. <view>
  86. <view v-for="(item, index) in courseList" :key="index">
  87. <view class="courseItemBox">
  88. <view class="courseItem" @click="openCourse(item)">
  89. <view class="courseName">{{ item.courseName }}</view>
  90. <view>
  91. <image
  92. src="/static/icon/up.png"
  93. class="icon_up"
  94. v-if="item.down"
  95. ></image>
  96. <image
  97. src="/static/icon/down.png"
  98. class="icon_up"
  99. v-if="!item.down"
  100. ></image>
  101. </view>
  102. </view>
  103. <view v-show="!item.down">
  104. <view v-for="(itemM, indexM) in item.menuList" :key="indexM">
  105. <courseModule
  106. :courseId="itemM.courseId"
  107. :needOpen="
  108. isFirstEnter &&
  109. menuIndex[0] === index &&
  110. menuIndex[1] === indexM
  111. ? true
  112. : false
  113. "
  114. v-if="itemM.type == 1"
  115. :menuItem="itemM"
  116. ></courseModule>
  117. <courseChapter
  118. :courseId="itemM.courseId"
  119. :needOpen="
  120. isFirstEnter &&
  121. menuIndex[0] === index &&
  122. menuIndex[1] === indexM
  123. ? true
  124. : false
  125. "
  126. v-if="itemM.type == 2"
  127. :isBuy="false"
  128. :menuItem="itemM"
  129. ></courseChapter>
  130. <courseSection
  131. :courseId="itemM.courseId"
  132. v-if="itemM.type == 3"
  133. :isBuy="false"
  134. :menuItem="itemM"
  135. ></courseSection>
  136. <u-line></u-line>
  137. </view>
  138. </view>
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. <view
  144. style="padding: 20rpx; padding-bottom: 100rpx; position: relative"
  145. v-show="current == 2"
  146. >
  147. <view>
  148. <view v-for="(item, index) in freeMenuList" :key="index">
  149. <view class="courseItemBox">
  150. <view class="courseItem">
  151. <view class="courseName">{{ item.freeExamName }}</view>
  152. </view>
  153. </view>
  154. </view>
  155. </view>
  156. </view>
  157. <view class="bottomBox" v-if="!hideBuyState">
  158. <view>
  159. <text class="priceTag">¥ {{ toFixed(detail.standPrice) }}</text>
  160. <!-- <view class="priceTag">¥ {{toFixed(detail.linePrice)}}</view> -->
  161. <text v-if="detail.linePrice" class="sale"> ¥ </text>
  162. <text v-if="detail.linePrice" class="price_line"
  163. >&nbsp;{{ detail.linePrice }}</text
  164. >
  165. </view>
  166. <view style="display: flex; color: #ffffff; align-items: center">
  167. <view class="btn1" @click="addCart">加购物车</view>
  168. <view class="btn2" @click="buy">立即购买 </view>
  169. </view>
  170. </view>
  171. </view>
  172. </template>
  173. <script>
  174. import courseModule from "@/components/course/courseModule.vue";
  175. import courseChapter from "@/components/course/courseChapter.vue";
  176. import courseSection from "@/components/course/courseSection.vue";
  177. import { mapGetters, mapMutations } from "vuex";
  178. export default {
  179. components: {
  180. courseModule,
  181. courseChapter,
  182. courseSection,
  183. },
  184. data() {
  185. return {
  186. id: 0,
  187. list: [],
  188. menuIndex: [],
  189. current: 0,
  190. detail: {},
  191. courseList: [],
  192. menuList: [],
  193. freeMenuList: [],
  194. startStatus: false,
  195. playbackRate: [1.0],
  196. isAllowSeek: "no",
  197. vid: "",
  198. autoplay: true,
  199. listenConfigList: [],
  200. listenSecond: 0,
  201. isFirstEnter: true, //是否首次进入
  202. timer: null,
  203. businessData: {},
  204. startTime: 0,
  205. };
  206. },
  207. computed: {
  208. ...mapGetters([
  209. "userInfo",
  210. "goodsAuditionConfigIdList",
  211. "playSectionId",
  212. "hideBuyState",
  213. ]),
  214. },
  215. onLoad(option) {
  216. this.id = option.id;
  217. this.getDetail();
  218. this.goodsCourseList();
  219. this.appCommonGoodsCourseModuleFreeExamList();
  220. },
  221. onUnload(option) {
  222. this.$store.commit("setPlaySectionId", { playSectionId: 0 });
  223. //移除所有的事件监听器
  224. uni.$off();
  225. },
  226. mounted() {
  227. let self = this;
  228. uni.$on("getSection", (item) => {
  229. //播放试听
  230. self.listenSecond = 0;
  231. for (var itemChild of self.listenConfigList) {
  232. if (
  233. self.playSectionId == (itemChild.sectionId || itemChild.menuId) &&
  234. item.courseId == itemChild.courseId
  235. ) {
  236. if (itemChild.auditionMinute > 0) {
  237. // self.listenSecond = itemChild.auditionMinute *60 //试听秒数
  238. self.listenSecond = itemChild.auditionMinute; //试听秒数 auditionMinute调整为秒单位
  239. }
  240. }
  241. }
  242. if (self.listenSecond > 0) {
  243. if (self.timer) {
  244. clearInterval(self.timer);
  245. }
  246. if (self.vid) {
  247. //切换视频
  248. var polyvPlayerContext = self.selectComponent("#playerVideo");
  249. polyvPlayerContext.changeVid(item.recordingUrl);
  250. } else {
  251. self.vid = item.recordingUrl;
  252. }
  253. self.startStatus = true;
  254. self.startTime = 0;
  255. } else {
  256. self.$u.toast("试听配置错误");
  257. }
  258. });
  259. this.updateChapterOpen(true);
  260. },
  261. methods: {
  262. ...mapMutations(["updateChapterOpen"]),
  263. itemWidth() {
  264. return 100 / this.list.length + "%";
  265. },
  266. appCommonGoodsCourseModuleFreeExamList() {
  267. this.$api.appCommonGoodsCourseModuleFreeExamList(this.id).then((res) => {
  268. if (res.data.data.length) {
  269. this.freeMenuList = res.data.data;
  270. this.list = [
  271. {
  272. name: "详情",
  273. },
  274. {
  275. name: "大纲",
  276. },
  277. {
  278. name: "赠送",
  279. },
  280. ];
  281. } else {
  282. this.list = [
  283. {
  284. name: "详情",
  285. },
  286. {
  287. name: "大纲",
  288. },
  289. ];
  290. }
  291. console.log(this.list);
  292. });
  293. },
  294. courseBusiness() {
  295. this.$api.courseBusiness(this.detail.businessId).then((res) => {
  296. this.businessData = res.data.data;
  297. });
  298. },
  299. toFixed(number) {
  300. if (number > 0) {
  301. return number.toFixed(2);
  302. } else {
  303. return "0.00";
  304. }
  305. },
  306. onStateChange(newstate, oldstate) {
  307. if (newstate.detail.newstate == "playing") {
  308. //开始播放
  309. if (this.timer) {
  310. clearInterval(this.timer);
  311. }
  312. this.timer = setInterval(this.timeEvent, 1500); //定时器
  313. }
  314. },
  315. closePlay() {
  316. this.$store.commit("setPlaySectionId", { playSectionId: 0 });
  317. this.vid = "";
  318. this.startStatus = false;
  319. },
  320. timeEvent() {
  321. let self = this;
  322. var polyvPlayerContext = this.selectComponent("#playerVideo");
  323. if (polyvPlayerContext != null) {
  324. let PlayCurrentTime = polyvPlayerContext.getCurrentTime();
  325. if (PlayCurrentTime >= this.listenSecond) {
  326. polyvPlayerContext.stop();
  327. polyvPlayerContext.exitFullScreen();
  328. clearInterval(this.timer);
  329. this.timer = null;
  330. uni.showModal({
  331. title: "提示",
  332. content: "试听结束,购买课程可学习全部",
  333. showCancel: false,
  334. success: function (resst) {
  335. self.closePlay();
  336. },
  337. });
  338. }
  339. }
  340. },
  341. openCourse(item) {
  342. item.down = !item.down;
  343. if (!item.down && item.menuList.length == 0) {
  344. this.getMenuList(item);
  345. }
  346. },
  347. addShopCart() {
  348. let self = this;
  349. this.$api.addCart({ goodsId: this.id, choiceStatus: 1 }).then((res) => {
  350. if (res.data.code == 200) {
  351. uni.setStorageSync("updateCart", 1); //提醒刷新购物车
  352. uni.showToast({
  353. title: "添加成功",
  354. });
  355. } else {
  356. this.$u.toast(res.data.msg);
  357. }
  358. });
  359. },
  360. goodsCourseList() {
  361. let self = this;
  362. this.$api.goodsCourseList(this.id).then((res) => {
  363. if (res.data.code == 200) {
  364. for (let i = 0; i < res.data.rows.length; i++) {
  365. let item = res.data.rows[i];
  366. item.down = true;
  367. item.menuList = [];
  368. }
  369. self.courseList = res.data.rows;
  370. this.getFirstCourse();
  371. }
  372. });
  373. },
  374. /**
  375. * 获取第一个有模块或者章的课程
  376. */
  377. async getFirstCourse() {
  378. for (let i = 0; i < this.courseList.length; i++) {
  379. let menuIndexOrFalse = await this.getCourseMenus(this.courseList[i]);
  380. if (menuIndexOrFalse !== false) {
  381. this.menuIndex = [i, menuIndexOrFalse];
  382. this.openCourse(this.courseList[i]);
  383. break;
  384. }
  385. }
  386. },
  387. getCourseMenus(item) {
  388. return new Promise((resolve) => {
  389. this.$api.menuList({ courseId: item.courseId }).then((res) => {
  390. if (res.data.code == 200) {
  391. for (let i = 0; i < res.data.rows.length; i++) {
  392. if (res.data.rows[i].type == 1 || res.data.rows[i].type == 2) {
  393. resolve(i);
  394. break;
  395. }
  396. }
  397. }
  398. });
  399. });
  400. },
  401. getMenuList(item) {
  402. let self = this;
  403. this.$api.menuList({ courseId: item.courseId }).then((res) => {
  404. if (res.data.code == 200) {
  405. for (let i = 0; i < res.data.rows.length; i++) {
  406. let item = res.data.rows[i];
  407. item.down = true;
  408. item.id = item.menuId;
  409. item.name = item.menuName;
  410. if (item.type == 3) {
  411. //判断是否试听
  412. item.tryListen = false;
  413. if (self.goodsAuditionConfigIdList.indexOf(item.id) !== -1) {
  414. item.tryListen = true;
  415. }
  416. }
  417. }
  418. item.menuList = res.data.rows;
  419. }
  420. });
  421. },
  422. getDetail() {
  423. let self = this;
  424. let sectionIdList = [];
  425. this.$api.commonGoodsDetail(this.id).then((res) => {
  426. if (res.data.code == 200) {
  427. if (res.data.data.mobileDetailHtml) {
  428. res.data.data.mobileDetailHtml =
  429. res.data.data.mobileDetailHtml.replace(
  430. /<img/gi,
  431. '<img style="max-width:100%;"'
  432. );
  433. }
  434. self.detail = res.data.data;
  435. this.courseBusiness();
  436. if (self.detail.goodsAuditionConfig) {
  437. self.listenConfigList = JSON.parse(self.detail.goodsAuditionConfig);
  438. for (var itemChild of self.listenConfigList) {
  439. sectionIdList.push(itemChild.sectionId); //存储试听节ID
  440. }
  441. self.$store.commit("setGoodsAuditionConfigIdList", {
  442. goodsAuditionConfigIdList: sectionIdList,
  443. });
  444. }
  445. }
  446. });
  447. },
  448. buy() {
  449. if (this.$method.isGoLogin()) {
  450. return;
  451. }
  452. this.$navTo.togo("/pages2/order/confirm_list?id=" + this.id);
  453. },
  454. addCart() {
  455. if (this.$method.isGoLogin()) {
  456. return;
  457. }
  458. this.addShopCart();
  459. },
  460. open(item) {
  461. item.showChildren = !item.showChildren;
  462. },
  463. change(index) {
  464. this.current = index;
  465. },
  466. },
  467. };
  468. </script>
  469. <style>
  470. page {
  471. background-color: #eaeef1;
  472. }
  473. </style>
  474. <style scope>
  475. .video_t2 {
  476. font-size: 24rpx;
  477. font-family: PingFang SC;
  478. font-weight: 500;
  479. color: #666666;
  480. }
  481. .video_t1 {
  482. height: 80rpx;
  483. color: #333333;
  484. line-height: 80rpx;
  485. font-size: 30rpx;
  486. font-family: PingFang SC;
  487. font-weight: bold;
  488. color: #333333;
  489. overflow: hidden;
  490. text-overflow: ellipsis;
  491. white-space: nowrap;
  492. }
  493. .video_t1_t {
  494. display: flex;
  495. flex-direction: column;
  496. height: 80rpx;
  497. color: #333333;
  498. text-align: center;
  499. align-items: center;
  500. border-left: solid 1px #d6d6db;
  501. }
  502. .video_play {
  503. position: absolute;
  504. width: 95rpx;
  505. height: 95rpx;
  506. top: 0;
  507. left: 0;
  508. right: 0;
  509. bottom: 0;
  510. margin: auto;
  511. }
  512. .video_box {
  513. position: relative;
  514. }
  515. .courseName {
  516. white-space: nowrap;
  517. overflow: hidden;
  518. text-overflow: ellipsis;
  519. }
  520. .videoBox {
  521. background-color: #ffffff;
  522. width: 100%;
  523. /* height: 680rpx; */
  524. z-index: 999;
  525. }
  526. .icon_up {
  527. width: 32rpx;
  528. height: 32rpx;
  529. }
  530. .contentBox {
  531. }
  532. .courseItemBox {
  533. background: #ffffff;
  534. border-radius: 16rpx;
  535. padding: 0 10rpx;
  536. margin-bottom: 20rpx;
  537. }
  538. .courseItem {
  539. height: 80rpx;
  540. color: #333333;
  541. font-size: 32rpx;
  542. line-height: 80rpx;
  543. font-weight: bold;
  544. display: flex;
  545. justify-content: space-between;
  546. }
  547. .content {
  548. background-color: #ffffff;
  549. width: 100%;
  550. }
  551. .btn2 {
  552. width: 200rpx;
  553. height: 64rpx;
  554. background: linear-gradient(0deg, #ffb102, #fd644f);
  555. box-shadow: 0rpx 10rpx 16rpx 4rpx rgba(1, 99, 235, 0.04);
  556. border-radius: 32rpx;
  557. line-height: 64rpx;
  558. text-align: center;
  559. }
  560. .btn1 {
  561. width: 200rpx;
  562. height: 64rpx;
  563. background: linear-gradient(0deg, #015eea, #00c0fa);
  564. border-radius: 32rpx;
  565. line-height: 64rpx;
  566. text-align: center;
  567. margin-right: 20rpx;
  568. }
  569. .bottomBox {
  570. position: fixed;
  571. bottom: 0;
  572. width: 100%;
  573. left: 0;
  574. height: 98rpx;
  575. background-color: #ffffff;
  576. display: flex;
  577. justify-content: space-between;
  578. align-items: center;
  579. padding: 0 30rpx;
  580. }
  581. .blackFont {
  582. color: #333333;
  583. margin: 0 4rpx;
  584. }
  585. .wk_icon {
  586. width: 24rpx;
  587. height: 24rpx;
  588. margin-right: 12rpx;
  589. }
  590. .noteTag {
  591. font-size: 24rpx;
  592. font-family: PingFang SC;
  593. font-weight: 500;
  594. color: #999999;
  595. align-items: center;
  596. }
  597. .priceTag {
  598. font-size: 30rpx;
  599. font-family: PingFang SC;
  600. font-weight: bold;
  601. color: #ff2d55;
  602. }
  603. .sale {
  604. color: #999999;
  605. font-size: 28rpx;
  606. margin-left: 8rpx;
  607. }
  608. .price_line {
  609. color: #999999;
  610. font-size: 28rpx;
  611. text-decoration: line-through;
  612. font-weight: 400;
  613. }
  614. .titleTag {
  615. font-size: 32rpx;
  616. font-weight: bold;
  617. color: #333333;
  618. margin-left: 8rpx;
  619. }
  620. .yearTag {
  621. width: 80rpx;
  622. height: 32rpx;
  623. background: #ebf5ff;
  624. border: 2rpx solid #007aff;
  625. border-radius: 16rpx;
  626. font-size: 24rpx;
  627. color: #007aff;
  628. text-align: center;
  629. line-height: 32rpx;
  630. }
  631. .itemBox {
  632. background: #ffffff;
  633. box-shadow: 0rpx 10rpx 9rpx 1rpx rgba(165, 196, 239, 0.1);
  634. border-radius: 24rpx;
  635. width: 100%;
  636. padding: 20rpx;
  637. margin-bottom: 20rpx;
  638. }
  639. </style>