feat(recall): 新增老客户召回选择页及相关功能
添加召回老客户选择页路由和视图组件 实现二维码获取API和埋点功能 优化视频背景组件并更新默认资源
Showing
4 changed files
with
163 additions
and
31 deletions
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-12-19 10:43:09 | 2 | * @Date: 2025-12-19 10:43:09 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-27 23:10:17 | 4 | + * @LastEditTime: 2025-12-31 11:42:58 |
| 5 | * @FilePath: /mlaj/src/api/recall_users.js | 5 | * @FilePath: /mlaj/src/api/recall_users.js |
| 6 | * @Description: 引入外部接口, 召回旧用户相关接口 | 6 | * @Description: 引入外部接口, 召回旧用户相关接口 |
| 7 | */ | 7 | */ |
| ... | @@ -16,6 +16,7 @@ const Api = { | ... | @@ -16,6 +16,7 @@ const Api = { |
| 16 | USER_GET_SUPPLEMENT: '/srv/?a=desk_calendar&t=get_supplement', | 16 | USER_GET_SUPPLEMENT: '/srv/?a=desk_calendar&t=get_supplement', |
| 17 | USER_EDIT_SUPPLEMENT: '/srv/?a=desk_calendar&t=edit_supplement', | 17 | USER_EDIT_SUPPLEMENT: '/srv/?a=desk_calendar&t=edit_supplement', |
| 18 | USER_TRACKING: '/srv/?a=desk_calendar&t=tracking', | 18 | USER_TRACKING: '/srv/?a=desk_calendar&t=tracking', |
| 19 | + USER_GET_QRCODE: '/srv/?a=desk_calendar&t=get_qrcode', | ||
| 19 | } | 20 | } |
| 20 | 21 | ||
| 21 | /** | 22 | /** |
| ... | @@ -75,8 +76,16 @@ export const editSupplementAPI = (params) => request(fetch.post(Api.USER_EDIT_SU | ... | @@ -75,8 +76,16 @@ export const editSupplementAPI = (params) => request(fetch.post(Api.USER_EDIT_SU |
| 75 | 76 | ||
| 76 | /** | 77 | /** |
| 77 | * @description: 埋点 | 78 | * @description: 埋点 |
| 78 | - * @param: event_type 事件类型。edit_user=完善用户信息, share_poster=转发海报 | 79 | + * @param: event_type 事件类型。qrcode_page=进入二维码页面, qrcode_redirect=台历二维码跳转, login_page=进入时光机登录页, edit_user=完善用户信息, share_poster=转发海报 |
| 79 | * @param: campaign_id 活动ID(转发海报时填写) | 80 | * @param: campaign_id 活动ID(转发海报时填写) |
| 80 | * @param: stu_uid 学员ID(转发海报时填写) | 81 | * @param: stu_uid 学员ID(转发海报时填写) |
| 82 | + * @param: qrcode_id 二维码ID(进入二维码页面时填写) | ||
| 81 | */ | 83 | */ |
| 82 | export const trackingAPI = (params) => request(fetch.post(Api.USER_TRACKING, params)); | 84 | export const trackingAPI = (params) => request(fetch.post(Api.USER_TRACKING, params)); |
| 85 | + | ||
| 86 | +/** | ||
| 87 | + * @description: 获取二维码 | ||
| 88 | + * @param: id 二维码ID | ||
| 89 | + * @return: data: { id, title, content_url} | ||
| 90 | + */ | ||
| 91 | +export const getQrcodeAPI = (params) => request(fetch.get(Api.USER_GET_QRCODE, params)); | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-12-26 14:15:46 | 2 | * @Date: 2025-12-26 14:15:46 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-26 14:35:49 | 4 | + * @LastEditTime: 2025-12-31 12:52:38 |
| 5 | * @FilePath: /mlaj/src/components/ui/VideoBackground.vue | 5 | * @FilePath: /mlaj/src/components/ui/VideoBackground.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <div class="video-background fixed top-0 left-0 w-full h-full -z-10 overflow-hidden"> | 9 | + <div class="video-background fixed inset-0 -z-10 overflow-hidden"> |
| 10 | - <!-- 优先显示传入的静态图片背景 --> | 10 | + <video v-show="!use_image_bg" ref="videoPlayer" autoplay muted loop playsinline webkit-playsinline |
| 11 | - <div v-if="backgroundImage" class="w-full h-full bg-cover bg-center bg-no-repeat" | 11 | + preload="auto" @error="on_video_error" @stalled="on_video_error" @abort="on_video_error" |
| 12 | + @emptied="on_video_error" @loadeddata="on_video_loaded" class="w-full h-full object-cover"> | ||
| 13 | + <source :src="videoUrl" type="video/mp4" /> | ||
| 14 | + Your browser does not support the video tag. | ||
| 15 | + </video> | ||
| 16 | + | ||
| 17 | + <!-- 图片降级背景(视频不可用时显示) --> | ||
| 18 | + <div v-if="use_image_bg" class="w-full h-full bg-cover bg-center bg-no-repeat" | ||
| 12 | :style="{ backgroundImage: `url(${backgroundImage})` }"> | 19 | :style="{ backgroundImage: `url(${backgroundImage})` }"> |
| 13 | </div> | 20 | </div> |
| 14 | 21 | ||
| 15 | - <template v-else> | ||
| 16 | - <video v-show="!use_image_bg" ref="videoPlayer" autoplay muted loop playsinline webkit-playsinline | ||
| 17 | - preload="auto" @error="on_video_error" @stalled="on_video_error" @abort="on_video_error" | ||
| 18 | - @emptied="on_video_error" @loadeddata="on_video_loaded" class="w-full h-full object-cover"> | ||
| 19 | - <source :src="videoUrl" type="video/mp4" /> | ||
| 20 | - Your browser does not support the video tag. | ||
| 21 | - </video> | ||
| 22 | - | ||
| 23 | - <!-- 图片降级背景(视频不可用时显示) --> | ||
| 24 | - <StarryBackground v-if="use_image_bg" /> | ||
| 25 | - </template> | ||
| 26 | - | ||
| 27 | <!-- 遮罩层,确保内容可读性 --> | 22 | <!-- 遮罩层,确保内容可读性 --> |
| 28 | - <div class="absolute inset-0 bg-black/30"></div> | 23 | + <div class="absolute inset-0 bg-black/10"></div> |
| 29 | </div> | 24 | </div> |
| 30 | </template> | 25 | </template> |
| 31 | 26 | ||
| ... | @@ -38,12 +33,13 @@ import { ref, onMounted } from 'vue' | ... | @@ -38,12 +33,13 @@ import { ref, onMounted } from 'vue' |
| 38 | const props = defineProps({ | 33 | const props = defineProps({ |
| 39 | videoUrl: { | 34 | videoUrl: { |
| 40 | type: String, | 35 | type: String, |
| 41 | - default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/jimeng-2025-12-26-3484.mp4' | 36 | + // default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/%E5%AE%87%E5%AE%99-1.mp4' |
| 37 | + default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/u8yvl6rs7q.mp4' | ||
| 42 | }, | 38 | }, |
| 43 | backgroundImage: { | 39 | backgroundImage: { |
| 44 | type: String, | 40 | type: String, |
| 45 | - // TODO: 图片是假的, 如果是真实情况需要重新弄一张正式图片 | 41 | + // default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/%E5%AE%87%E5%AE%99-2.png' |
| 46 | - default: 'https://cdn.ipadbiz.cn/stdj/images/%E5%90%AF%E5%8A%A8%E9%A1%B5%E6%B5%B7%E6%8A%A5%E8%83%8C%E6%99%AF@2x.png?imageMogr2/thumbnail/400x/strip/quality/70' | 42 | + default: 'https://cdn.ipadbiz.cn/mlaj/recall/img/i3a85rdk.png' |
| 47 | } | 43 | } |
| 48 | }) | 44 | }) |
| 49 | 45 | ||
| ... | @@ -52,9 +48,6 @@ const use_image_bg = ref(false) | ... | @@ -52,9 +48,6 @@ const use_image_bg = ref(false) |
| 52 | const is_video_ready = ref(false) | 48 | const is_video_ready = ref(false) |
| 53 | 49 | ||
| 54 | onMounted(() => { | 50 | onMounted(() => { |
| 55 | - // 如果有传入背景图,则不执行视频逻辑 | ||
| 56 | - if (props.backgroundImage) return | ||
| 57 | - | ||
| 58 | const video = videoPlayer.value | 51 | const video = videoPlayer.value |
| 59 | if (video) { | 52 | if (video) { |
| 60 | // 尝试播放视频 | 53 | // 尝试播放视频 |
| ... | @@ -87,11 +80,11 @@ onMounted(() => { | ... | @@ -87,11 +80,11 @@ onMounted(() => { |
| 87 | } | 80 | } |
| 88 | 81 | ||
| 89 | // 兜底:在一定时间内仍未加载完成则切换到图片背景 | 82 | // 兜底:在一定时间内仍未加载完成则切换到图片背景 |
| 90 | - // setTimeout(() => { | 83 | + setTimeout(() => { |
| 91 | - // if (!is_video_ready.value) { | 84 | + if (!is_video_ready.value) { |
| 92 | - // enable_image_fallback() | 85 | + enable_image_fallback() |
| 93 | - // } | 86 | + } |
| 94 | - // }, 5000) | 87 | + }, 5000) |
| 95 | }) | 88 | }) |
| 96 | 89 | ||
| 97 | /** | 90 | /** |
| ... | @@ -125,5 +118,10 @@ const enable_image_fallback = () => { | ... | @@ -125,5 +118,10 @@ const enable_image_fallback = () => { |
| 125 | .video-background { | 118 | .video-background { |
| 126 | /* 确保在所有内容之下 */ | 119 | /* 确保在所有内容之下 */ |
| 127 | z-index: -1; | 120 | z-index: -1; |
| 121 | + top: -1px; | ||
| 122 | + right: -1px; | ||
| 123 | + bottom: -1px; | ||
| 124 | + left: -1px; | ||
| 125 | + background: #000; | ||
| 128 | } | 126 | } |
| 129 | </style> | 127 | </style> | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-27 22:18:08 | 4 | + * @LastEditTime: 2025-12-30 13:59:24 |
| 5 | * @FilePath: /mlaj/src/router/routes.js | 5 | * @FilePath: /mlaj/src/router/routes.js |
| 6 | * @Description: 路由地址映射配置 | 6 | * @Description: 路由地址映射配置 |
| 7 | */ | 7 | */ |
| ... | @@ -145,6 +145,12 @@ export const routes = [ | ... | @@ -145,6 +145,12 @@ export const routes = [ |
| 145 | meta: { title: '积分汇总' }, | 145 | meta: { title: '积分汇总' }, |
| 146 | }, | 146 | }, |
| 147 | { | 147 | { |
| 148 | + path: '/recall/choose', | ||
| 149 | + name: 'ChoosePage', | ||
| 150 | + component: () => import('../views/recall/ChoosePage.vue'), | ||
| 151 | + meta: { title: '召回老客户-选择页' }, | ||
| 152 | + }, | ||
| 153 | + { | ||
| 148 | path: '/checkout', | 154 | path: '/checkout', |
| 149 | name: 'CheckoutPage', | 155 | name: 'CheckoutPage', |
| 150 | component: () => import('../views/checkout/CheckoutPage.vue'), | 156 | component: () => import('../views/checkout/CheckoutPage.vue'), | ... | ... |
src/views/recall/ChoosePage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-12-30 13:58:55 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-12-31 13:11:28 | ||
| 5 | + * @FilePath: /mlaj/src/views/recall/ChoosePage.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="choose-page w-full min-h-screen relative overflow-hidden flex flex-col items-center"> | ||
| 10 | + <VideoBackground /> | ||
| 11 | + | ||
| 12 | + <!-- 标题区域 --> | ||
| 13 | + <div class="mt-20 flex flex-col items-center z-10 w-full px-8"> | ||
| 14 | + <img :src="titleImg" class="w-full max-w-[300px] mb-4 object-contain" alt="title" /> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <!-- 容器靠页面底部对齐 --> | ||
| 18 | + <div class="flex flex-col items-center h-full w-full px-6 pt-16 pb-8 relative mt-auto"> | ||
| 19 | + <div class="text-white text-center space-y-1 tracking-wider text-shadow-md mb-3"> | ||
| 20 | + <p class="text-base">{{ viewOther.text }}</p> | ||
| 21 | + </div> | ||
| 22 | + <!-- Bottom Section --> | ||
| 23 | + <div class="mt-auto w-full flex flex-col items-center text-center animate-fade-in-up delay-200"> | ||
| 24 | + <van-button block | ||
| 25 | + class="submit-btn !rounded-lg !border-[#FFDD01] !text-[#FFF] !font-bold !text-lg !h-[48px] !max-w-xs !mb-4" | ||
| 26 | + @click="handleViewOther"> | ||
| 27 | + 立即查看 | ||
| 28 | + </van-button> | ||
| 29 | + <van-button block | ||
| 30 | + class="submit-btn !rounded-lg !border-[#FFDD01] !text-[#FFDD01] !font-bold !text-lg !h-[48px] !max-w-xs" | ||
| 31 | + @click="handleViewTimeLine"> | ||
| 32 | + 我的时光机 | ||
| 33 | + </van-button> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | +</template> | ||
| 38 | + | ||
| 39 | +<script setup> | ||
| 40 | +import { ref, onMounted } from 'vue' | ||
| 41 | +import { useRoute, useRouter } from 'vue-router' | ||
| 42 | +import { useTitle } from '@vueuse/core' | ||
| 43 | +import VideoBackground from '@/components/ui/VideoBackground.vue' | ||
| 44 | + | ||
| 45 | +import { getQrcodeAPI, trackingAPI } from '@/api/recall_users' | ||
| 46 | + | ||
| 47 | +// 导入图片 | ||
| 48 | +const titleImg = 'https://cdn.ipadbiz.cn/mlaj/recall/img/title007@2x.png' | ||
| 49 | + | ||
| 50 | +const $route = useRoute(); | ||
| 51 | +const $router = useRouter(); | ||
| 52 | +useTitle($route.meta.title); | ||
| 53 | + | ||
| 54 | +const id = $route.query.id || ''; | ||
| 55 | +const entry = $route.query.entry || ''; | ||
| 56 | + | ||
| 57 | +const viewOther = ref({}) | ||
| 58 | + | ||
| 59 | +const handleViewTimeLine = async () => { | ||
| 60 | + // 进入台历H5登录页的埋点 | ||
| 61 | + await trackingAPI({ | ||
| 62 | + event_type: 'login_page', | ||
| 63 | + entry | ||
| 64 | + }) | ||
| 65 | + // 跳转到台历H5登录页 | ||
| 66 | + $router.push({ | ||
| 67 | + path:'/recall/login', | ||
| 68 | + query: { entry } | ||
| 69 | + }) | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +const handleViewOther = async () => { | ||
| 73 | + // 点击【查看内容】按钮埋点 | ||
| 74 | + await trackingAPI({ | ||
| 75 | + event_type: 'qrcode_redirect', | ||
| 76 | + qrcode_id: id, | ||
| 77 | + entry | ||
| 78 | + }) | ||
| 79 | + if (viewOther.value.url) { | ||
| 80 | + location.href = viewOther.value.url | ||
| 81 | + } else { | ||
| 82 | + console.error('URL is undefined') | ||
| 83 | + } | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +onMounted(async () => { | ||
| 87 | + // 进入这个新页面埋点 | ||
| 88 | + await trackingAPI({ | ||
| 89 | + event_type: 'qrcode_page', | ||
| 90 | + qrcode_id: id, | ||
| 91 | + entry | ||
| 92 | + }) | ||
| 93 | + | ||
| 94 | + if (id) { | ||
| 95 | + try { | ||
| 96 | + const response = await getQrcodeAPI({ id }) | ||
| 97 | + if (response.code) { | ||
| 98 | + viewOther.value = { | ||
| 99 | + id: response.data.id, | ||
| 100 | + text: response.data.title, | ||
| 101 | + url: response.data.content_url, | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | + } catch (error) { | ||
| 105 | + console.error('Error fetching QR code:', error) | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | +}) | ||
| 109 | +</script> | ||
| 110 | + | ||
| 111 | +<style lang="less" scoped> | ||
| 112 | +.choose-page { | ||
| 113 | + .submit-btn { | ||
| 114 | + background: linear-gradient(180deg, rgba(251, 249, 224, 0.3) 0%, rgba(41, 46, 1, 0.3) 100%) !important; | ||
| 115 | + backdrop-filter: blur(4px); | ||
| 116 | + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); | ||
| 117 | + } | ||
| 118 | +} | ||
| 119 | +</style> |
-
Please register or login to post a comment