hookehuyr

feat(课程详情页): 添加咨询弹窗功能

实现课程详情页咨询弹窗功能,包含以下特性:
- 底部弹出层设计,仅显示关闭按钮
- 支持富文本内容展示和点击复制
- 电话咨询功能,点击直接拨打
- 自动压缩富文本中的图片
...@@ -31,6 +31,14 @@ https://oa-dev.onwall.cn/f/mlaj ...@@ -31,6 +31,14 @@ https://oa-dev.onwall.cn/f/mlaj
31 - 位置:`/src/views/courses/CourseDetailPage.vue`,在 `onMounted` 插入,`onUnmounted` 清理。 31 - 位置:`/src/views/courses/CourseDetailPage.vue`,在 `onMounted` 插入,`onUnmounted` 清理。
32 - 函数:`build_og_image_url(src)``set_og_meta(payload)``remove_og_meta()` 32 - 函数:`build_og_image_url(src)``set_og_meta(payload)``remove_og_meta()`
33 33
34 + - 课程详情页咨询弹窗(Mock)
35 + - 入口:详情页顶部快捷操作中的“咨询”按钮。
36 + - 展示:底部弹出层,仅底部关闭按钮;内容支持富文本展示。
37 + - 电话:显示咨询电话;点击即可直接拨打(`tel:`)。
38 + - 咨询信息:富文本区域点击即可复制到剪切板;复制成功后提示。
39 + - 图片压缩:富文本中若包含 `cdn.ipadbiz.cn` 图片,使用 `?imageMogr2/thumbnail/200x/strip/quality/70` 参数。
40 + - 位置:`/src/views/courses/CourseDetailPage.vue`,“咨询弹窗”模板与交互逻辑(`open_consult_dialog``close_consult_dialog``call_phone``copy_consult_info`)。
41 +
34 - 401拦截策略优化(公开页面不再跳登录) 42 - 401拦截策略优化(公开页面不再跳登录)
35 - 行为:接口返回 `code=401` 时,不再对公开页面(如课程详情 `/courses/:id`)进行登录重定向;仅当当前路由确实需要登录权限时才跳转至登录页。 43 - 行为:接口返回 `code=401` 时,不再对公开页面(如课程详情 `/courses/:id`)进行登录重定向;仅当当前路由确实需要登录权限时才跳转至登录页。
36 - 原理:响应拦截器调用路由守卫 `checkAuth` 判断当前路由是否为受限页面,受限则清理登录信息并附带 `redirect` 重定向至登录页;公开页面保持当前页,由业务自行处理401。 44 - 原理:响应拦截器调用路由守卫 `checkAuth` 判断当前路由是否为受限页面,受限则清理登录信息并附带 `redirect` 重定向至登录页;公开页面保持当前页,由业务自行处理401。
......
...@@ -209,12 +209,21 @@ ...@@ -209,12 +209,21 @@
209 stroke-linecap="round" 209 stroke-linecap="round"
210 stroke-linejoin="round" 210 stroke-linejoin="round"
211 stroke-width="2" 211 stroke-width="2"
212 - d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" 212 + d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
213 /> 213 />
214 </svg> 214 </svg>
215 - 咨询 215 + 分享
216 </button> --> 216 </button> -->
217 - <!-- <button class="flex flex-col items-center text-gray-500 text-xs"> 217 + <button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
218 + @click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
219 + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
220 + :fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
221 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
222 + d="M4.318 6.318 a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682 a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318 a4.5 4.5 0 00-6.364 0z" />
223 + </svg>
224 + 收藏
225 + </button>
226 + <button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog">
218 <svg 227 <svg
219 xmlns="http://www.w3.org/2000/svg" 228 xmlns="http://www.w3.org/2000/svg"
220 class="h-6 w-6" 229 class="h-6 w-6"
...@@ -226,19 +235,10 @@ ...@@ -226,19 +235,10 @@
226 stroke-linecap="round" 235 stroke-linecap="round"
227 stroke-linejoin="round" 236 stroke-linejoin="round"
228 stroke-width="2" 237 stroke-width="2"
229 - d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" 238 + d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
230 /> 239 />
231 </svg> 240 </svg>
232 - 分享 241 + 咨询
233 - </button> -->
234 - <button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
235 - @click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
236 - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
237 - :fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
238 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
239 - d="M4.318 6.318 a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682 a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318 a4.5 4.5 0 00-6.364 0z" />
240 - </svg>
241 - 收藏
242 </button> 242 </button>
243 </div> 243 </div>
244 <div class="flex items-center"> 244 <div class="flex items-center">
...@@ -327,6 +327,46 @@ ...@@ -327,6 +327,46 @@
327 </template> 327 </template>
328 </div> 328 </div>
329 </van-popup> 329 </van-popup>
330 +
331 + <!-- 咨询弹窗:底部只有关闭按钮,内容支持富文本 -->
332 + <van-popup
333 + v-model:show="show_consult_dialog"
334 + round
335 + position="bottom"
336 + :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }"
337 + >
338 + <div class="ConsultPopup p-4">
339 + <!-- 标题与关闭图标 -->
340 + <div class="flex justify-between items-center mb-3">
341 + <h3 class="font-medium">咨询信息</h3>
342 + <van-icon name="cross" @click="close_consult_dialog" />
343 + </div>
344 +
345 + <!-- 电话信息:点击直接拨打 -->
346 + <div class="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
347 + <div class="flex items-center justify-between">
348 + <div class="flex items-center">
349 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor">
350 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h2.28a2 2 0 011.789 1.106l1.152 2.305a2 2 0 01-.42 2.317L9.384 10.09a16.001 16.001 0 006.526 6.526l1.356-1.102a2 2 0 012.317-.42l2.305 1.152A2 2 0 0121 18.72V21a2 2 0 01-2 2h-1a18 18 0 01-17-17V5z" />
351 + </svg>
352 + <span class="text-gray-700">联系电话</span>
353 + </div>
354 + <a class="text-green-600 font-medium" :href="`tel:${consult_phone}`" @click.prevent="call_phone">{{ consult_phone }}</a>
355 + </div>
356 + </div>
357 +
358 + <!-- 富文本咨询信息:点击复制到剪切板 -->
359 + <div class="bg-white border border-gray-100 rounded-lg p-3">
360 + <div class="text-gray-700 text-sm leading-6" v-html="consult_html" @click="copy_consult_info"></div>
361 + <div class="text-xs text-gray-400 mt-2">提示:点击上方任意文字即可复制咨询内容</div>
362 + </div>
363 +
364 + <!-- 底部关闭按钮(唯一操作) -->
365 + <div class="mt-4">
366 + <button class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg" @click="close_consult_dialog">关闭</button>
367 + </div>
368 + </div>
369 + </van-popup>
330 <van-back-top right="5vw" bottom="25vh" offset="600" /> 370 <van-back-top right="5vw" bottom="25vh" offset="600" />
331 </AppLayout> 371 </AppLayout>
332 </template> 372 </template>
...@@ -494,6 +534,94 @@ const selectedCheckIn = ref(null) ...@@ -494,6 +534,94 @@ const selectedCheckIn = ref(null)
494 const isCheckingIn = ref(false) 534 const isCheckingIn = ref(false)
495 const checkInSuccess = ref(false) 535 const checkInSuccess = ref(false)
496 536
537 +// 咨询弹窗相关状态
538 +/**
539 + * 展示咨询弹窗的显隐状态
540 + * @type {import('vue').Ref<boolean>}
541 + */
542 +const show_consult_dialog = ref(false)
543 +
544 +/**
545 + * 咨询联系电话(Mock 数据)
546 + * @type {import('vue').Ref<string>}
547 + */
548 +const consult_phone = ref('400-888-8888')
549 +
550 +/**
551 + * 咨询富文本内容(Mock 数据)
552 + * 说明:示例中包含来自 cdn.ipadbiz.cn 的图片,带有压缩参数
553 + * @type {import('vue').Ref<string>}
554 + */
555 +const consult_html = ref(
556 + '<p><strong>课程咨询说明:</strong>如需了解课程安排、报名流程、发票开具等信息,请联系课程顾问。</p>' +
557 + '<p>可通过电话或复制下方咨询信息进行沟通。</p>' +
558 + '<p><img src="https://cdn.ipadbiz.cn/images/consult_demo.png?imageMogr2/thumbnail/200x/strip/quality/70" alt="咨询示例" style="max-width:100%;border-radius:8px;"/></p>'
559 +)
560 +
561 +/**
562 + * 打开咨询弹窗
563 + * @returns {void}
564 + */
565 +const open_consult_dialog = () => {
566 + show_consult_dialog.value = true
567 +}
568 +
569 +/**
570 + * 关闭咨询弹窗
571 + * @returns {void}
572 + */
573 +const close_consult_dialog = () => {
574 + show_consult_dialog.value = false
575 +}
576 +
577 +/**
578 + * 直接拨打咨询电话
579 + * @returns {void}
580 + */
581 +const call_phone = () => {
582 + const phone = consult_phone.value || ''
583 + if (phone) {
584 + window.location.href = `tel:${phone}`
585 + }
586 +}
587 +
588 +/**
589 + * 将富文本内容转换为纯文本
590 + * @param {string} html 原始富文本 HTML 字符串
591 + * @returns {string} 纯文本内容
592 + */
593 +const strip_html = (html) => {
594 + const text = (html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
595 + return text
596 +}
597 +
598 +/**
599 + * 复制咨询富文本信息为纯文本
600 + * @returns {Promise<void>}
601 + */
602 +const copy_consult_info = async () => {
603 + const text = strip_html(consult_html.value)
604 + try {
605 + if (navigator.clipboard && navigator.clipboard.writeText) {
606 + await navigator.clipboard.writeText(text)
607 + } else {
608 + const textarea = document.createElement('textarea')
609 + textarea.value = text
610 + textarea.style.position = 'fixed'
611 + textarea.style.top = '-1000px'
612 + document.body.appendChild(textarea)
613 + textarea.focus()
614 + textarea.select()
615 + document.execCommand('copy')
616 + document.body.removeChild(textarea)
617 + }
618 + showToast('咨询信息已复制')
619 + } catch (err) {
620 + console.error('复制失败: ', err)
621 + showToast('复制失败,请稍后重试')
622 + }
623 +}
624 +
497 const { addToCart, proceedToCheckout } = useCart() 625 const { addToCart, proceedToCheckout } = useCart()
498 626
499 627
...@@ -887,9 +1015,16 @@ setTimeout(() => { ...@@ -887,9 +1015,16 @@ setTimeout(() => {
887 </script> 1015 </script>
888 1016
889 <style lang="less"> 1017 <style lang="less">
890 -.animate-favorite { 1018 +.ConsultPopup {
891 - animation: favorite-animation 0.5s ease; 1019 + // 咨询弹窗样式容器(使用 less 层级嵌套)
1020 + h3 {
1021 + // 标题样式
1022 + font-weight: 500;
1023 + }
892 } 1024 }
1025 + .animate-favorite {
1026 + animation: favorite-animation 0.5s ease;
1027 + }
893 1028
894 @keyframes favorite-animation { 1029 @keyframes favorite-animation {
895 0% { 1030 0% {
......