hookehuyr

feat(welcome): 完成欢迎页功能入口组件与配置

- 新增 WelcomeContent 组件:实现轨道式布局,3个功能入口环绕排列
- 新增 WelcomeEntryItem 组件:统一的功能入口卡片样式
- 新增 welcomeEntries 配置:课程中心、活动中心(外链)、个人中心
- 添加环境变量:VITE_WELCOME_PAGE_ENABLED 和 VITE_WELCOME_VIDEO_URL
- 支持外部链接跳转(活动中心)和内部路由跳转
- 添加浮动、脉冲等动画效果,提升视觉体验

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -30,3 +30,7 @@ VITE_PROXY_TARGET = http://oa-dev.onwall.cn/
# PC端地址
VITE_MOBILE_URL = http://localhost:5173/
# 欢迎页配置
VITE_WELCOME_PAGE_ENABLED=true
VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4
......
......@@ -17,3 +17,7 @@ VITE_PROXY_TARGET = http://oa.onwall.cn
# PC端地址
# VITE_MOBILE_URL = http://oa.onwall.cn/f/guanzong/web/
# VITE_MOBILE_URL = http://guanzong.onwall.cn/f/guanzong/web/
# 欢迎页配置
VITE_WELCOME_PAGE_ENABLED=true
VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4
......
......@@ -98,8 +98,10 @@ declare module 'vue' {
VanTag: typeof import('vant/es')['Tag']
VanTimePicker: typeof import('vant/es')['TimePicker']
VanUploader: typeof import('vant/es')['Uploader']
VideoBackground: typeof import('./components/media/VideoBackground.vue')['default']
VideoBackground: typeof import('./components/effects/VideoBackground.vue')['default']
VideoPlayer: typeof import('./components/media/VideoPlayer.vue')['default']
WechatPayment: typeof import('./components/payment/WechatPayment.vue')['default']
WelcomeContent: typeof import('./components/welcome/WelcomeContent.vue')['default']
WelcomeEntryItem: typeof import('./components/welcome/WelcomeEntryItem.vue')['default']
}
}
......
<template>
<div class="welcome-content">
<!-- 顶部欢迎文案 -->
<div class="welcome-header">
<h1 class="welcome-title">欢迎来到美乐爱觉</h1>
<p class="welcome-subtitle">开启您的学习之旅</p>
</div>
<!-- 功能入口区域 - 轨道布局 -->
<div class="entry-orbit">
<div class="orbit-center">
<!-- 中心装饰元素(可选) -->
<div class="orbit-decoration"></div>
</div>
<!-- 3个功能入口 - 轨道布局 -->
<div class="orbit-entries">
<WelcomeEntryItem
v-for="entry in entries"
:key="entry.id"
:entry="entry"
class="orbit-entry"
:class="`orbit-entry-${entry.priority}`"
@click="handleEntryClick"
/>
</div>
</div>
<!-- 底部开始按钮 -->
<div class="welcome-actions">
<van-button
type="primary"
size="large"
round
block
@click="handleStart"
>
开始体验
</van-button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { welcomeEntries } from '@/config/welcomeEntries'
import WelcomeEntryItem from './WelcomeEntryItem.vue'
const router = useRouter()
const entries = ref(welcomeEntries)
const handleEntryClick = (entry) => {
if (entry.isExternal) {
// 外部链接:获取用户ID并拼接
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
const userId = currentUser.id || currentUser.user_id || ''
const url = entry.externalUrl + userId
window.open(url, '_blank')
} else {
// 内部路由跳转
router.push(entry.route)
}
}
const handleStart = () => {
// 跳转到首页
router.push('/')
}
</script>
<style lang="less" scoped>
.welcome-content {
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 6rem 2rem 3rem;
z-index: 1;
}
.welcome-header {
text-align: center;
margin-top: 4rem;
.welcome-title {
font-size: 2rem;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
margin-bottom: 0.75rem;
animation: fadeInDown 1s ease;
}
.welcome-subtitle {
font-size: 1rem;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
animation: fadeInDown 1s ease 0.2s both;
}
}
.entry-orbit {
position: relative;
width: 100%;
max-width: 20rem;
aspect-ratio: 1;
margin: 2rem 0;
.orbit-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8rem;
height: 8rem;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
backdrop-filter: blur(10px);
animation: pulse 3s ease-in-out infinite;
.orbit-decoration {
width: 100%;
height: 100%;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.1);
animation: rotate 20s linear infinite;
}
}
.orbit-entries {
position: relative;
width: 100%;
height: 100%;
.orbit-entry {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: float 3s ease-in-out infinite;
&.orbit-entry-1 {
// 课程中心 - 顶部
top: 0;
left: 50%;
animation-delay: 0s;
}
&.orbit-entry-2 {
// 活动中心 - 右下
top: 75%;
left: 85%;
animation-delay: 0.5s;
}
&.orbit-entry-3 {
// 个人中心 - 左下
top: 75%;
left: 15%;
animation-delay: 1s;
}
}
}
}
.welcome-actions {
width: 100%;
max-width: 20rem;
margin-bottom: 2rem;
animation: fadeInUp 1s ease 0.4s both;
:deep(.van-button) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
height: 3rem;
font-size: 1.1rem;
font-weight: 500;
}
}
// 动画定义
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.5;
transform: translate(-50%, -50%) scale(1);
}
50% {
opacity: 0.8;
transform: translate(-50%, -50%) scale(1.05);
}
}
@keyframes float {
0%, 100% {
transform: translate(-50%, -50%) translateY(0);
}
50% {
transform: translate(-50%, -50%) translateY(-10px);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<template>
<div
class="entry-item"
:class="{ 'is-external': entry.isExternal }"
@click="handleClick"
>
<div class="entry-icon-wrapper">
<img
:src="entry.icon"
:alt="entry.title"
class="entry-icon"
/>
</div>
<div class="entry-title">{{ entry.title }}</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
entry: {
type: Object,
required: true,
validator: (value) => {
return value.id && value.title && value.icon && value.route
}
}
})
const emit = defineEmits(['click'])
const handleClick = () => {
emit('click', props.entry)
}
</script>
<style lang="less" scoped>
.entry-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
&.is-external {
// 外部链接的特殊样式(如果需要)
}
.entry-icon-wrapper {
position: relative;
width: 6rem;
height: 6rem;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-bottom: 0.75rem;
.entry-icon {
width: 3rem;
height: 3rem;
object-fit: contain;
filter: brightness(0) invert(1); // 图标反白显示
}
}
.entry-title {
font-size: 1rem;
font-weight: 500;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
white-space: nowrap;
}
}
</style>
/*
* @Date: 2025-01-28 12:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-01-28 12:00:00
* @FilePath: /mlaj/src/config/welcomeEntries.js
* @Description: 欢迎页功能入口配置
*/
/**
* 欢迎页功能入口列表
* 用于展示在欢迎页的3个主要功能入口
*/
export const welcomeEntries = [
{
id: 'courses',
title: '课程中心',
icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
route: '/courses',
priority: 1
},
{
id: 'activity',
title: '活动中心',
icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
route: '/activity',
priority: 2,
isExternal: true,
externalUrl: 'https://wxm.behalo.cc/pages/activity/activity?token=&user_id='
},
{
id: 'profile',
title: '个人中心',
icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
route: '/profile',
priority: 3
}
]