hookehuyr

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

添加召回老客户选择页路由和视图组件
实现二维码获取API和埋点功能
优化视频背景组件并更新默认资源
/*
* @Date: 2025-12-19 10:43:09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-27 23:10:17
* @LastEditTime: 2025-12-31 11:42:58
* @FilePath: /mlaj/src/api/recall_users.js
* @Description: 引入外部接口, 召回旧用户相关接口
*/
......@@ -16,6 +16,7 @@ const Api = {
USER_GET_SUPPLEMENT: '/srv/?a=desk_calendar&t=get_supplement',
USER_EDIT_SUPPLEMENT: '/srv/?a=desk_calendar&t=edit_supplement',
USER_TRACKING: '/srv/?a=desk_calendar&t=tracking',
USER_GET_QRCODE: '/srv/?a=desk_calendar&t=get_qrcode',
}
/**
......@@ -75,8 +76,16 @@ export const editSupplementAPI = (params) => request(fetch.post(Api.USER_EDIT_SU
/**
* @description: 埋点
* @param: event_type 事件类型。edit_user=完善用户信息, share_poster=转发海报
* @param: event_type 事件类型。qrcode_page=进入二维码页面, qrcode_redirect=台历二维码跳转, login_page=进入时光机登录页, edit_user=完善用户信息, share_poster=转发海报
* @param: campaign_id 活动ID(转发海报时填写)
* @param: stu_uid 学员ID(转发海报时填写)
* @param: qrcode_id 二维码ID(进入二维码页面时填写)
*/
export const trackingAPI = (params) => request(fetch.post(Api.USER_TRACKING, params));
/**
* @description: 获取二维码
* @param: id 二维码ID
* @return: data: { id, title, content_url}
*/
export const getQrcodeAPI = (params) => request(fetch.get(Api.USER_GET_QRCODE, params));
......
<!--
* @Date: 2025-12-26 14:15:46
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-26 14:35:49
* @LastEditTime: 2025-12-31 12:52:38
* @FilePath: /mlaj/src/components/ui/VideoBackground.vue
* @Description: 文件描述
-->
<template>
<div class="video-background fixed top-0 left-0 w-full h-full -z-10 overflow-hidden">
<!-- 优先显示传入的静态图片背景 -->
<div v-if="backgroundImage" class="w-full h-full bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${backgroundImage})` }">
</div>
<template v-else>
<div class="video-background fixed inset-0 -z-10 overflow-hidden">
<video v-show="!use_image_bg" ref="videoPlayer" autoplay muted loop playsinline webkit-playsinline
preload="auto" @error="on_video_error" @stalled="on_video_error" @abort="on_video_error"
@emptied="on_video_error" @loadeddata="on_video_loaded" class="w-full h-full object-cover">
......@@ -21,11 +15,12 @@
</video>
<!-- 图片降级背景(视频不可用时显示) -->
<StarryBackground v-if="use_image_bg" />
</template>
<div v-if="use_image_bg" class="w-full h-full bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${backgroundImage})` }">
</div>
<!-- 遮罩层,确保内容可读性 -->
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-0 bg-black/10"></div>
</div>
</template>
......@@ -38,12 +33,13 @@ import { ref, onMounted } from 'vue'
const props = defineProps({
videoUrl: {
type: String,
default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/jimeng-2025-12-26-3484.mp4'
// default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/%E5%AE%87%E5%AE%99-1.mp4'
default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/u8yvl6rs7q.mp4'
},
backgroundImage: {
type: String,
// TODO: 图片是假的, 如果是真实情况需要重新弄一张正式图片
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'
// default: 'https://cdn.ipadbiz.cn/mlaj/recall/video/%E5%AE%87%E5%AE%99-2.png'
default: 'https://cdn.ipadbiz.cn/mlaj/recall/img/i3a85rdk.png'
}
})
......@@ -52,9 +48,6 @@ const use_image_bg = ref(false)
const is_video_ready = ref(false)
onMounted(() => {
// 如果有传入背景图,则不执行视频逻辑
if (props.backgroundImage) return
const video = videoPlayer.value
if (video) {
// 尝试播放视频
......@@ -87,11 +80,11 @@ onMounted(() => {
}
// 兜底:在一定时间内仍未加载完成则切换到图片背景
// setTimeout(() => {
// if (!is_video_ready.value) {
// enable_image_fallback()
// }
// }, 5000)
setTimeout(() => {
if (!is_video_ready.value) {
enable_image_fallback()
}
}, 5000)
})
/**
......@@ -125,5 +118,10 @@ const enable_image_fallback = () => {
.video-background {
/* 确保在所有内容之下 */
z-index: -1;
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
background: #000;
}
</style>
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-27 22:18:08
* @LastEditTime: 2025-12-30 13:59:24
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
......@@ -145,6 +145,12 @@ export const routes = [
meta: { title: '积分汇总' },
},
{
path: '/recall/choose',
name: 'ChoosePage',
component: () => import('../views/recall/ChoosePage.vue'),
meta: { title: '召回老客户-选择页' },
},
{
path: '/checkout',
name: 'CheckoutPage',
component: () => import('../views/checkout/CheckoutPage.vue'),
......
<!--
* @Date: 2025-12-30 13:58:55
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-31 13:11:28
* @FilePath: /mlaj/src/views/recall/ChoosePage.vue
* @Description: 文件描述
-->
<template>
<div class="choose-page w-full min-h-screen relative overflow-hidden flex flex-col items-center">
<VideoBackground />
<!-- 标题区域 -->
<div class="mt-20 flex flex-col items-center z-10 w-full px-8">
<img :src="titleImg" class="w-full max-w-[300px] mb-4 object-contain" alt="title" />
</div>
<!-- 容器靠页面底部对齐 -->
<div class="flex flex-col items-center h-full w-full px-6 pt-16 pb-8 relative mt-auto">
<div class="text-white text-center space-y-1 tracking-wider text-shadow-md mb-3">
<p class="text-base">{{ viewOther.text }}</p>
</div>
<!-- Bottom Section -->
<div class="mt-auto w-full flex flex-col items-center text-center animate-fade-in-up delay-200">
<van-button block
class="submit-btn !rounded-lg !border-[#FFDD01] !text-[#FFF] !font-bold !text-lg !h-[48px] !max-w-xs !mb-4"
@click="handleViewOther">
立即查看
</van-button>
<van-button block
class="submit-btn !rounded-lg !border-[#FFDD01] !text-[#FFDD01] !font-bold !text-lg !h-[48px] !max-w-xs"
@click="handleViewTimeLine">
我的时光机
</van-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import VideoBackground from '@/components/ui/VideoBackground.vue'
import { getQrcodeAPI, trackingAPI } from '@/api/recall_users'
// 导入图片
const titleImg = 'https://cdn.ipadbiz.cn/mlaj/recall/img/title007@2x.png'
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
const id = $route.query.id || '';
const entry = $route.query.entry || '';
const viewOther = ref({})
const handleViewTimeLine = async () => {
// 进入台历H5登录页的埋点
await trackingAPI({
event_type: 'login_page',
entry
})
// 跳转到台历H5登录页
$router.push({
path:'/recall/login',
query: { entry }
})
}
const handleViewOther = async () => {
// 点击【查看内容】按钮埋点
await trackingAPI({
event_type: 'qrcode_redirect',
qrcode_id: id,
entry
})
if (viewOther.value.url) {
location.href = viewOther.value.url
} else {
console.error('URL is undefined')
}
}
onMounted(async () => {
// 进入这个新页面埋点
await trackingAPI({
event_type: 'qrcode_page',
qrcode_id: id,
entry
})
if (id) {
try {
const response = await getQrcodeAPI({ id })
if (response.code) {
viewOther.value = {
id: response.data.id,
text: response.data.title,
url: response.data.content_url,
}
}
} catch (error) {
console.error('Error fetching QR code:', error)
}
}
})
</script>
<style lang="less" scoped>
.choose-page {
.submit-btn {
background: linear-gradient(180deg, rgba(251, 249, 224, 0.3) 0%, rgba(41, 46, 1, 0.3) 100%) !important;
backdrop-filter: blur(4px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
}
</style>