courseDetail.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. <template>
  2. <view>
  3. <view v-if="!loginShow" class="container" :style="{ height : `${loginShow ? '100vh' : 'auto' }`,
  4. overflow: `${loginShow ? 'hidden' : 'auto' }` }">
  5. <view class="container-poster" style="width: 100%;padding: 0 20rpx;">
  6. <image show-menu-by-longpress :src="courseDetail.poster?courseDetail.poster:''" mode="widthFix"
  7. style="width: 100%;"></image>
  8. </view>
  9. <view class="course-tab-list">
  10. <view class="course-tab-item" v-for="(data, index) in items" :key="index" @click="onClickItem(index)"
  11. :class="currentTab === index ? 'tab-active' : ''">
  12. {{ data }}
  13. </view>
  14. </view>
  15. <view class="content" v-if="currentTab === 0">
  16. <view class="content-text">
  17. <view class="text-title">{{courseDetail.courseName}}</view>
  18. <view class="text-title">课程概述</view>
  19. <view class="text-content">{{courseDetail.summary}}</view>
  20. <view class="text-title">课程时间</view>
  21. <view class="text-content">{{courseDetail.courseDate}}</view>
  22. <view class="text-title">培训地点</view>
  23. <view class="text-content">{{courseDetail.loc}}</view>
  24. <view class="text-tip" v-if="!isMember">个人会员或单位会员免费,点击现在入会></view>
  25. </view>
  26. </view>
  27. <view class="content" v-if="currentTab === 1" style="overflow: hidden;">
  28. <view class=""
  29. style="margin-bottom: 140rpx;padding-bottom: 150rpx;height: 100%;overflow: scroll;padding-left: 20rpx;padding-right: 20rpx;">
  30. <view v-for="(comment, index) in sortedCommentList" :key="index" class="comment-list-item">
  31. <view class="comment-list-left">
  32. <image :src="comment.icon" class="comment-list-avator"></image>
  33. </view>
  34. <view class="comment-list-right">
  35. <view style="margin-bottom: 15rpx;">
  36. <text class="comment-list-username">{{ comment.username }}</text>
  37. <text class="comment-list-moment">{{ formatTime(comment.commentTime) }}</text>
  38. </view>
  39. <view>{{ comment.content }}</view>
  40. </view>
  41. </view>
  42. </view>
  43. <view class="section-bottom " style="background-color: #f2f2f2;">
  44. <view class="comment-input-box">
  45. <u-input :custom-style="inputStyle" class="comment-input" v-model="comment" :border="false"
  46. placeholder="写留言" height="60" adjust-position />
  47. <u-button class="comment-button" :hair-line="false" :custom-style="customStyle"
  48. @click="toSend">发送</u-button>
  49. </view>
  50. </view>
  51. </view>
  52. </view>
  53. <u-popup v-model="loginShow" :mask="false" :closeable='false' mode="bottom" :mask-close-able='false'
  54. safe-area-inset-bottom>
  55. <view style="height: 70vh;padding: 40rpx;position: relative;background:none;">
  56. <view style="text-align: center;margin: 70rpx 0;">
  57. <u-button size="medium" type="error" @click="toLogin">登录查看</u-button>
  58. </view>
  59. </view>
  60. </u-popup>
  61. <canvas canvas-id="shareCanvas"
  62. :style="{ position: 'absolute', top: '-10000px', width: '750px', height: '600px' }"></canvas>
  63. </view>
  64. </template>
  65. <script setup>
  66. import {
  67. loadCourseDetail,
  68. loadCommentList,
  69. sendComment
  70. } from "@/api/edu.js"
  71. import {
  72. getToken
  73. } from '@/utils/auth.js'
  74. import {
  75. useAuthStore
  76. } from '@/store/authStore'
  77. const authStore = useAuthStore();
  78. // const isMember = ref(false)
  79. import {
  80. ref,
  81. computed
  82. } from 'vue'
  83. import {
  84. onLoad,
  85. onShow,
  86. onShareAppMessage,
  87. onShareTimeline
  88. } from '@dcloudio/uni-app'
  89. const courseDetail = ref({});
  90. const courseId = ref(null);
  91. const courseName = ref(null);
  92. const items = ref(['课程简介', '观看评论']);
  93. const currentTab = ref(0);
  94. const comment = ref("");
  95. // 评论发送按钮样式
  96. const customStyle = ref({
  97. backgroundColor: '#e6e6e6',
  98. color: '#333333',
  99. fontWeight: 'bold',
  100. height: '60rpx',
  101. marginLeft: '20rpx',
  102. border: 'none',
  103. fontSize: '26rpx'
  104. })
  105. // 评论输入框样式
  106. const inputStyle = ref({
  107. backgroundColor: '#e6e6e6',
  108. color: '#333333',
  109. borderRadius: '5px',
  110. padding: '0 20rpx',
  111. fontSize: '26rpx'
  112. })
  113. // 评论列表
  114. const commentList = ref([])
  115. // 点击tabs,切换
  116. function onClickItem(e) {
  117. if (currentTab.value != e) {
  118. currentTab.value = e;
  119. if (e === 2) {
  120. getComment(courseId.value)
  121. }
  122. }
  123. }
  124. // 初始化
  125. function init(id) {
  126. loadCourseDetail(id).then(res => {
  127. if (res?.data) {
  128. courseDetail.value = res.data;
  129. showBuy.value = showBuyAction()
  130. // console.log(courseDetail, "课程详情")
  131. }
  132. })
  133. }
  134. function getComment(id) {
  135. loadCommentList(id).then(res => {
  136. if (res?.data) {
  137. commentList.value = res.data;
  138. }
  139. })
  140. }
  141. // 购买课程
  142. function toBuy() {
  143. uni.navigateTo({
  144. url: "/pages/goOnEdu/course/courseDetail/courseOrder?id=" + courseId.value
  145. })
  146. // console.log("购买该课程", courseDetail.value.id)
  147. }
  148. // 定义 Canvas 画布尺寸(按 5:4 比例)
  149. const canvasWidth = ref(750); // 推荐 2倍图尺寸
  150. const canvasHeight = ref(600);
  151. const shareImageUrl = ref(''); // 存储处理后的图片路径
  152. // 获取图片信息
  153. const getImageInfo = (imgUrl) => {
  154. return new Promise((resolve) => {
  155. uni.getImageInfo({
  156. src: imgUrl,
  157. success: (res) => resolve(res),
  158. fail: () => resolve(null)
  159. });
  160. });
  161. };
  162. // 动态裁剪并填充背景
  163. const processShareImage = async (imgUrl) => {
  164. try {
  165. const imageInfo = await getImageInfo(imgUrl);
  166. if (!imageInfo) return imgUrl;
  167. const ctx = uni.createCanvasContext('shareCanvas', this);
  168. const targetRatio = 5 / 4;
  169. const originalRatio = imageInfo.width / imageInfo.height;
  170. // 1. 绘制背景填充
  171. ctx.setFillStyle('#FFFFFF'); // 自定义背景色
  172. ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
  173. // 2. 计算绘制区域
  174. if (originalRatio < targetRatio) {
  175. // 比例不足时:居中缩放 + 左右填充
  176. const scale = canvasHeight.value / imageInfo.height;
  177. const drawWidth = imageInfo.width * scale;
  178. ctx.drawImage(
  179. imageInfo.path,
  180. (canvasWidth.value - drawWidth) / 2, // 水平居中
  181. 0,
  182. drawWidth,
  183. canvasHeight.value
  184. );
  185. } else {
  186. // 比例足够时:裁剪中间区域
  187. const cropWidth = imageInfo.height * targetRatio;
  188. ctx.drawImage(
  189. imageInfo.path,
  190. (imageInfo.width - cropWidth) / 2, // 水平居中裁剪
  191. 0,
  192. cropWidth,
  193. imageInfo.height,
  194. 0,
  195. 0,
  196. canvasWidth.value,
  197. canvasHeight.value
  198. );
  199. }
  200. // 3. 导出临时图片
  201. ctx.draw(false, () => {
  202. setTimeout(async () => { // 解决 Canvas 渲染延迟
  203. const res = await uni.canvasToTempFilePath({
  204. canvasId: 'shareCanvas',
  205. fileType: 'jpg',
  206. quality: 0.9
  207. });
  208. shareImageUrl.value = res.tempFilePath;
  209. }, 300);
  210. });
  211. } catch (err) {
  212. return imgUrl; // 降级处理
  213. }
  214. };
  215. function toSend() {
  216. sendComment({
  217. courseId: courseId.value,
  218. content: comment.value,
  219. commentTime: formatDate(new Date())
  220. }).then(res => {
  221. getComment(courseId.value)
  222. comment.value = ""
  223. })
  224. }
  225. function formatDate(date) {
  226. const pad = (num) => num.toString().padStart(2, '0');
  227. const year = date.getFullYear();
  228. const month = pad(date.getMonth() + 1); // 月份从0开始,需加1
  229. const day = pad(date.getDate());
  230. const hours = pad(date.getHours());
  231. const minutes = pad(date.getMinutes());
  232. const seconds = pad(date.getSeconds());
  233. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  234. }
  235. function showBuyAction() {
  236. if (courseDetail.value.viewMode === '2' &&
  237. !isMember.value &&
  238. !courseDetail.value.hasBuy &&
  239. currentTab.value === 0) {
  240. // console.log(1)
  241. return true
  242. }
  243. // 付费,不管是不是会员,并且没买的
  244. if (courseDetail.value.viewMode === '3' &&
  245. !courseDetail.value.hasBuy &&
  246. currentTab.value === 0) {
  247. // console.log(2)
  248. return true
  249. }
  250. // console.log(3)
  251. return false
  252. }
  253. const showBuy = ref(false)
  254. const isMember = computed(() => {
  255. return authStore.userInfo.isMember == '0' ? false : true
  256. })
  257. const isLogin = computed(() => {
  258. if (getToken() && authStore.isAuthenticated) {
  259. return true
  260. } else {
  261. return false
  262. }
  263. })
  264. const loginShow = ref(true)
  265. function toLogin() {
  266. // loginShow.value = false
  267. let url = {
  268. url: '/pages/goOnEdu/course/courseDetail/courseDetail',
  269. id: courseId.value,
  270. title: courseName.value
  271. }
  272. uni.setStorageSync("redirect", url)
  273. uni.navigateTo({
  274. url: `/pages/login/login`
  275. })
  276. }
  277. // 初始化页面
  278. onLoad((option) => {
  279. const {
  280. id,
  281. title
  282. } = option;
  283. courseId.value = id
  284. courseName.value = title
  285. uni.setNavigationBarTitle({
  286. title: title
  287. });
  288. if (isLogin.value) {
  289. loginShow.value = false
  290. } else {
  291. loginShow.value = true
  292. }
  293. // loginShow.value = true
  294. })
  295. onShow(() => {
  296. if (isLogin.value) {
  297. init(courseId.value)
  298. getComment(courseId.value)
  299. }
  300. })
  301. function formatDateS(dateStr) {
  302. return dateStr.replace(" ", "T");
  303. }
  304. function formatTime(timeString) {
  305. const commentDate = new Date(formatDateS(timeString));
  306. const now = new Date();
  307. const diff = now - commentDate;
  308. const minutes = Math.floor(diff / 60000);
  309. const hours = Math.floor(diff / 3600000);
  310. const days = Math.floor(diff / 86400000);
  311. if (minutes < 1) { // 修改这里以处理0分钟
  312. return "刚刚";
  313. } else if (minutes < 60) {
  314. return `${minutes}分钟前`;
  315. } else if (hours < 24) {
  316. return `${hours}小时前`;
  317. } else {
  318. return commentDate.toISOString().split('T')[0];
  319. }
  320. }
  321. const sortedCommentList = computed(() => {
  322. return commentList.value.sort((a, b) =>
  323. new Date(formatDateS(b.commentTime)) - new Date(formatDateS(a.commentTime))
  324. );
  325. });
  326. onShareAppMessage(async (res) => {
  327. const processedImage = await processShareImage(courseDetail.value.cover);
  328. // console.log(processedImage)
  329. return {
  330. title: courseName.value,
  331. path: `/pages/goOnEdu/course/courseDetail/courseDetail?id=${courseId.value}&title=${courseName.value}`,
  332. imageUrl: processedImage
  333. };
  334. })
  335. onShareTimeline(async () => {
  336. return {
  337. title: courseName.value,
  338. query: `id=${courseId.value}&title=${courseName.value}`,
  339. imageUrl: courseDetail.value.cover
  340. };
  341. })
  342. </script>
  343. <style lang="scss">
  344. .u-drawer-bottom {
  345. background-color: transparent !important;
  346. }
  347. </style>
  348. <style lang="scss" scoped>
  349. .container {
  350. // height: 100vh;
  351. width: 100vw;
  352. background-color: #fff;
  353. // padding: 0 20rpx;
  354. }
  355. .course-tab-list {
  356. display: flex;
  357. background-color: #f2f2f2;
  358. flex: 0 0 auto;
  359. margin: 0 20rpx;
  360. .course-tab-item {
  361. width: 100%;
  362. height: 80rpx;
  363. line-height: 80rpx;
  364. text-align: center;
  365. }
  366. .tab-active {
  367. border-bottom: 1px solid #0069f6;
  368. }
  369. }
  370. // .container-poster{
  371. // height: calc(100vh - 500rpx - env(safe-area-inset-bottom, 0));
  372. // }
  373. .content {
  374. overflow: scroll;
  375. // height: calc(100vh - 500rpx - env(safe-area-inset-bottom, 0));
  376. height: 700rpx;
  377. position: relative;
  378. .content-text {
  379. padding: 0 20rpx env(safe-area-inset-bottom, 0);
  380. font-size: 38rpx;
  381. .text-title {
  382. font-weight: bold;
  383. margin-bottom: 15rpx;
  384. }
  385. .text-content {
  386. font-size: 32rpx;
  387. margin-bottom: 20rpx;
  388. }
  389. .text-tip {
  390. color: red;
  391. // margin-bottom: 20rpx;
  392. margin-bottom: env(safe-area-inset-bottom, 0);
  393. }
  394. }
  395. }
  396. .section-bottom {
  397. height: 90rpx;
  398. color: #fff;
  399. font-size: 34rpx;
  400. text-align: center;
  401. line-height: 80rpx;
  402. background-color: #fe0000;
  403. width: 100%;
  404. position: absolute;
  405. bottom: 0;
  406. box-sizing: content-box;
  407. padding-bottom: env(safe-area-inset-bottom, 0);
  408. }
  409. .comment-input-box {
  410. width: 100%;
  411. height: 100%;
  412. display: flex;
  413. box-sizing: border-box;
  414. padding: 0 20rpx;
  415. align-items: center;
  416. .comment-input {
  417. flex: 1;
  418. }
  419. .comment-button {
  420. flex: 0 0 auto;
  421. }
  422. }
  423. .comment-list-item {
  424. display: flex;
  425. padding: 20rpx 0;
  426. font-size: 28rpx;
  427. .comment-list-left {
  428. flex: 0 0 auto;
  429. padding-right: 20rpx;
  430. padding-left: 10rpx;
  431. .comment-list-avator {
  432. width: 100rpx;
  433. height: 100rpx;
  434. border-radius: 50%;
  435. }
  436. }
  437. .comment-list-right {
  438. flex: 1;
  439. .comment-list-username {
  440. padding-right: 25rpx;
  441. font-size: 32rpx;
  442. font-weight: bold;
  443. }
  444. }
  445. }
  446. </style>