hookehuyr

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

实现课程详情页咨询弹窗功能,包含以下特性:
- 底部弹出层设计,仅显示关闭按钮
- 支持富文本内容展示和点击复制
- 电话咨询功能,点击直接拨打
- 自动压缩富文本中的图片
......@@ -31,6 +31,14 @@ https://oa-dev.onwall.cn/f/mlaj
- 位置:`/src/views/courses/CourseDetailPage.vue`,在 `onMounted` 插入,`onUnmounted` 清理。
- 函数:`build_og_image_url(src)``set_og_meta(payload)``remove_og_meta()`
- 课程详情页咨询弹窗(Mock)
- 入口:详情页顶部快捷操作中的“咨询”按钮。
- 展示:底部弹出层,仅底部关闭按钮;内容支持富文本展示。
- 电话:显示咨询电话;点击即可直接拨打(`tel:`)。
- 咨询信息:富文本区域点击即可复制到剪切板;复制成功后提示。
- 图片压缩:富文本中若包含 `cdn.ipadbiz.cn` 图片,使用 `?imageMogr2/thumbnail/200x/strip/quality/70` 参数。
- 位置:`/src/views/courses/CourseDetailPage.vue`,“咨询弹窗”模板与交互逻辑(`open_consult_dialog``close_consult_dialog``call_phone``copy_consult_info`)。
- 401拦截策略优化(公开页面不再跳登录)
- 行为:接口返回 `code=401` 时,不再对公开页面(如课程详情 `/courses/:id`)进行登录重定向;仅当当前路由确实需要登录权限时才跳转至登录页。
- 原理:响应拦截器调用路由守卫 `checkAuth` 判断当前路由是否为受限页面,受限则清理登录信息并附带 `redirect` 重定向至登录页;公开页面保持当前页,由业务自行处理401。
......
......@@ -209,12 +209,21 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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"
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"
/>
</svg>
咨询
分享
</button> -->
<!-- <button class="flex flex-col items-center text-gray-500 text-xs">
<button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
@click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
:fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" />
</svg>
收藏
</button>
<button class="flex flex-col items-center text-gray-500 text-xs" @click="open_consult_dialog">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
......@@ -226,19 +235,10 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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"
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"
/>
</svg>
分享
</button> -->
<button class="flex flex-col items-center text-gray-500 text-xs transition-transform duration-300"
@click="toggleFavorite" :class="{ 'animate-favorite': isFavorite }">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform duration-300"
:fill="isFavorite ? 'red' : 'none'" viewBox="0 0 24 24" :stroke="isFavorite ? 'red' : 'currentColor'">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" />
</svg>
收藏
咨询
</button>
</div>
<div class="flex items-center">
......@@ -327,6 +327,46 @@
</template>
</div>
</van-popup>
<!-- 咨询弹窗:底部只有关闭按钮,内容支持富文本 -->
<van-popup
v-model:show="show_consult_dialog"
round
position="bottom"
:style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }"
>
<div class="ConsultPopup p-4">
<!-- 标题与关闭图标 -->
<div class="flex justify-between items-center mb-3">
<h3 class="font-medium">咨询信息</h3>
<van-icon name="cross" @click="close_consult_dialog" />
</div>
<!-- 电话信息:点击直接拨打 -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<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">
<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" />
</svg>
<span class="text-gray-700">联系电话</span>
</div>
<a class="text-green-600 font-medium" :href="`tel:${consult_phone}`" @click.prevent="call_phone">{{ consult_phone }}</a>
</div>
</div>
<!-- 富文本咨询信息:点击复制到剪切板 -->
<div class="bg-white border border-gray-100 rounded-lg p-3">
<div class="text-gray-700 text-sm leading-6" v-html="consult_html" @click="copy_consult_info"></div>
<div class="text-xs text-gray-400 mt-2">提示:点击上方任意文字即可复制咨询内容</div>
</div>
<!-- 底部关闭按钮(唯一操作) -->
<div class="mt-4">
<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>
</div>
</div>
</van-popup>
<van-back-top right="5vw" bottom="25vh" offset="600" />
</AppLayout>
</template>
......@@ -494,6 +534,94 @@ const selectedCheckIn = ref(null)
const isCheckingIn = ref(false)
const checkInSuccess = ref(false)
// 咨询弹窗相关状态
/**
* 展示咨询弹窗的显隐状态
* @type {import('vue').Ref<boolean>}
*/
const show_consult_dialog = ref(false)
/**
* 咨询联系电话(Mock 数据)
* @type {import('vue').Ref<string>}
*/
const consult_phone = ref('400-888-8888')
/**
* 咨询富文本内容(Mock 数据)
* 说明:示例中包含来自 cdn.ipadbiz.cn 的图片,带有压缩参数
* @type {import('vue').Ref<string>}
*/
const consult_html = ref(
'<p><strong>课程咨询说明:</strong>如需了解课程安排、报名流程、发票开具等信息,请联系课程顾问。</p>' +
'<p>可通过电话或复制下方咨询信息进行沟通。</p>' +
'<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>'
)
/**
* 打开咨询弹窗
* @returns {void}
*/
const open_consult_dialog = () => {
show_consult_dialog.value = true
}
/**
* 关闭咨询弹窗
* @returns {void}
*/
const close_consult_dialog = () => {
show_consult_dialog.value = false
}
/**
* 直接拨打咨询电话
* @returns {void}
*/
const call_phone = () => {
const phone = consult_phone.value || ''
if (phone) {
window.location.href = `tel:${phone}`
}
}
/**
* 将富文本内容转换为纯文本
* @param {string} html 原始富文本 HTML 字符串
* @returns {string} 纯文本内容
*/
const strip_html = (html) => {
const text = (html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
return text
}
/**
* 复制咨询富文本信息为纯文本
* @returns {Promise<void>}
*/
const copy_consult_info = async () => {
const text = strip_html(consult_html.value)
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
} else {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.top = '-1000px'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
showToast('咨询信息已复制')
} catch (err) {
console.error('复制失败: ', err)
showToast('复制失败,请稍后重试')
}
}
const { addToCart, proceedToCheckout } = useCart()
......@@ -887,9 +1015,16 @@ setTimeout(() => {
</script>
<style lang="less">
.animate-favorite {
animation: favorite-animation 0.5s ease;
.ConsultPopup {
// 咨询弹窗样式容器(使用 less 层级嵌套)
h3 {
// 标题样式
font-weight: 500;
}
}
.animate-favorite {
animation: favorite-animation 0.5s ease;
}
@keyframes favorite-animation {
0% {
......