hookehuyr

feat(recall): 新增老客户召回选择页及相关功能

添加召回老客户选择页路由和视图组件
实现二维码获取API和埋点功能
优化视频背景组件并更新默认资源
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'),
......
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>