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/ ...@@ -30,3 +30,7 @@ VITE_PROXY_TARGET = http://oa-dev.onwall.cn/
30 30
31 # PC端地址 31 # PC端地址
32 VITE_MOBILE_URL = http://localhost:5173/ 32 VITE_MOBILE_URL = http://localhost:5173/
33 +
34 +# 欢迎页配置
35 +VITE_WELCOME_PAGE_ENABLED=true
36 +VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4
......
...@@ -17,3 +17,7 @@ VITE_PROXY_TARGET = http://oa.onwall.cn ...@@ -17,3 +17,7 @@ VITE_PROXY_TARGET = http://oa.onwall.cn
17 # PC端地址 17 # PC端地址
18 # VITE_MOBILE_URL = http://oa.onwall.cn/f/guanzong/web/ 18 # VITE_MOBILE_URL = http://oa.onwall.cn/f/guanzong/web/
19 # VITE_MOBILE_URL = http://guanzong.onwall.cn/f/guanzong/web/ 19 # VITE_MOBILE_URL = http://guanzong.onwall.cn/f/guanzong/web/
20 +
21 +# 欢迎页配置
22 +VITE_WELCOME_PAGE_ENABLED=true
23 +VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4
......
...@@ -98,8 +98,10 @@ declare module 'vue' { ...@@ -98,8 +98,10 @@ declare module 'vue' {
98 VanTag: typeof import('vant/es')['Tag'] 98 VanTag: typeof import('vant/es')['Tag']
99 VanTimePicker: typeof import('vant/es')['TimePicker'] 99 VanTimePicker: typeof import('vant/es')['TimePicker']
100 VanUploader: typeof import('vant/es')['Uploader'] 100 VanUploader: typeof import('vant/es')['Uploader']
101 - VideoBackground: typeof import('./components/media/VideoBackground.vue')['default'] 101 + VideoBackground: typeof import('./components/effects/VideoBackground.vue')['default']
102 VideoPlayer: typeof import('./components/media/VideoPlayer.vue')['default'] 102 VideoPlayer: typeof import('./components/media/VideoPlayer.vue')['default']
103 WechatPayment: typeof import('./components/payment/WechatPayment.vue')['default'] 103 WechatPayment: typeof import('./components/payment/WechatPayment.vue')['default']
104 + WelcomeContent: typeof import('./components/welcome/WelcomeContent.vue')['default']
105 + WelcomeEntryItem: typeof import('./components/welcome/WelcomeEntryItem.vue')['default']
104 } 106 }
105 } 107 }
......
1 +<template>
2 + <div class="welcome-content">
3 + <!-- 顶部欢迎文案 -->
4 + <div class="welcome-header">
5 + <h1 class="welcome-title">欢迎来到美乐爱觉</h1>
6 + <p class="welcome-subtitle">开启您的学习之旅</p>
7 + </div>
8 +
9 + <!-- 功能入口区域 - 轨道布局 -->
10 + <div class="entry-orbit">
11 + <div class="orbit-center">
12 + <!-- 中心装饰元素(可选) -->
13 + <div class="orbit-decoration"></div>
14 + </div>
15 +
16 + <!-- 3个功能入口 - 轨道布局 -->
17 + <div class="orbit-entries">
18 + <WelcomeEntryItem
19 + v-for="entry in entries"
20 + :key="entry.id"
21 + :entry="entry"
22 + class="orbit-entry"
23 + :class="`orbit-entry-${entry.priority}`"
24 + @click="handleEntryClick"
25 + />
26 + </div>
27 + </div>
28 +
29 + <!-- 底部开始按钮 -->
30 + <div class="welcome-actions">
31 + <van-button
32 + type="primary"
33 + size="large"
34 + round
35 + block
36 + @click="handleStart"
37 + >
38 + 开始体验
39 + </van-button>
40 + </div>
41 + </div>
42 +</template>
43 +
44 +<script setup>
45 +import { ref } from 'vue'
46 +import { useRouter } from 'vue-router'
47 +import { welcomeEntries } from '@/config/welcomeEntries'
48 +import WelcomeEntryItem from './WelcomeEntryItem.vue'
49 +
50 +const router = useRouter()
51 +const entries = ref(welcomeEntries)
52 +
53 +const handleEntryClick = (entry) => {
54 + if (entry.isExternal) {
55 + // 外部链接:获取用户ID并拼接
56 + const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
57 + const userId = currentUser.id || currentUser.user_id || ''
58 + const url = entry.externalUrl + userId
59 + window.open(url, '_blank')
60 + } else {
61 + // 内部路由跳转
62 + router.push(entry.route)
63 + }
64 +}
65 +
66 +const handleStart = () => {
67 + // 跳转到首页
68 + router.push('/')
69 +}
70 +</script>
71 +
72 +<style lang="less" scoped>
73 +.welcome-content {
74 + position: relative;
75 + width: 100%;
76 + min-height: 100vh;
77 + display: flex;
78 + flex-direction: column;
79 + align-items: center;
80 + justify-content: space-between;
81 + padding: 6rem 2rem 3rem;
82 + z-index: 1;
83 +}
84 +
85 +.welcome-header {
86 + text-align: center;
87 + margin-top: 4rem;
88 +
89 + .welcome-title {
90 + font-size: 2rem;
91 + font-weight: bold;
92 + color: #ffffff;
93 + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
94 + margin-bottom: 0.75rem;
95 + animation: fadeInDown 1s ease;
96 + }
97 +
98 + .welcome-subtitle {
99 + font-size: 1rem;
100 + color: rgba(255, 255, 255, 0.9);
101 + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
102 + animation: fadeInDown 1s ease 0.2s both;
103 + }
104 +}
105 +
106 +.entry-orbit {
107 + position: relative;
108 + width: 100%;
109 + max-width: 20rem;
110 + aspect-ratio: 1;
111 + margin: 2rem 0;
112 +
113 + .orbit-center {
114 + position: absolute;
115 + top: 50%;
116 + left: 50%;
117 + transform: translate(-50%, -50%);
118 + width: 8rem;
119 + height: 8rem;
120 + border-radius: 50%;
121 + background: radial-gradient(circle, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
122 + backdrop-filter: blur(10px);
123 + animation: pulse 3s ease-in-out infinite;
124 +
125 + .orbit-decoration {
126 + width: 100%;
127 + height: 100%;
128 + border-radius: 50%;
129 + border: 1px solid rgba(255, 255, 255, 0.1);
130 + animation: rotate 20s linear infinite;
131 + }
132 + }
133 +
134 + .orbit-entries {
135 + position: relative;
136 + width: 100%;
137 + height: 100%;
138 +
139 + .orbit-entry {
140 + position: absolute;
141 + top: 50%;
142 + left: 50%;
143 + transform: translate(-50%, -50%);
144 + animation: float 3s ease-in-out infinite;
145 +
146 + &.orbit-entry-1 {
147 + // 课程中心 - 顶部
148 + top: 0;
149 + left: 50%;
150 + animation-delay: 0s;
151 + }
152 +
153 + &.orbit-entry-2 {
154 + // 活动中心 - 右下
155 + top: 75%;
156 + left: 85%;
157 + animation-delay: 0.5s;
158 + }
159 +
160 + &.orbit-entry-3 {
161 + // 个人中心 - 左下
162 + top: 75%;
163 + left: 15%;
164 + animation-delay: 1s;
165 + }
166 + }
167 + }
168 +}
169 +
170 +.welcome-actions {
171 + width: 100%;
172 + max-width: 20rem;
173 + margin-bottom: 2rem;
174 + animation: fadeInUp 1s ease 0.4s both;
175 +
176 + :deep(.van-button) {
177 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
178 + border: none;
179 + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
180 + height: 3rem;
181 + font-size: 1.1rem;
182 + font-weight: 500;
183 + }
184 +}
185 +
186 +// 动画定义
187 +@keyframes fadeInDown {
188 + from {
189 + opacity: 0;
190 + transform: translateY(-20px);
191 + }
192 + to {
193 + opacity: 1;
194 + transform: translateY(0);
195 + }
196 +}
197 +
198 +@keyframes fadeInUp {
199 + from {
200 + opacity: 0;
201 + transform: translateY(20px);
202 + }
203 + to {
204 + opacity: 1;
205 + transform: translateY(0);
206 + }
207 +}
208 +
209 +@keyframes pulse {
210 + 0%, 100% {
211 + opacity: 0.5;
212 + transform: translate(-50%, -50%) scale(1);
213 + }
214 + 50% {
215 + opacity: 0.8;
216 + transform: translate(-50%, -50%) scale(1.05);
217 + }
218 +}
219 +
220 +@keyframes float {
221 + 0%, 100% {
222 + transform: translate(-50%, -50%) translateY(0);
223 + }
224 + 50% {
225 + transform: translate(-50%, -50%) translateY(-10px);
226 + }
227 +}
228 +
229 +@keyframes rotate {
230 + from {
231 + transform: rotate(0deg);
232 + }
233 + to {
234 + transform: rotate(360deg);
235 + }
236 +}
237 +</style>
1 +<template>
2 + <div
3 + class="entry-item"
4 + :class="{ 'is-external': entry.isExternal }"
5 + @click="handleClick"
6 + >
7 + <div class="entry-icon-wrapper">
8 + <img
9 + :src="entry.icon"
10 + :alt="entry.title"
11 + class="entry-icon"
12 + />
13 + </div>
14 + <div class="entry-title">{{ entry.title }}</div>
15 + </div>
16 +</template>
17 +
18 +<script setup>
19 +import { defineProps, defineEmits } from 'vue'
20 +
21 +const props = defineProps({
22 + entry: {
23 + type: Object,
24 + required: true,
25 + validator: (value) => {
26 + return value.id && value.title && value.icon && value.route
27 + }
28 + }
29 +})
30 +
31 +const emit = defineEmits(['click'])
32 +
33 +const handleClick = () => {
34 + emit('click', props.entry)
35 +}
36 +</script>
37 +
38 +<style lang="less" scoped>
39 +.entry-item {
40 + display: flex;
41 + flex-direction: column;
42 + align-items: center;
43 + justify-content: center;
44 + cursor: pointer;
45 + transition: all 0.3s ease;
46 +
47 + &:active {
48 + transform: scale(0.95);
49 + }
50 +
51 + &.is-external {
52 + // 外部链接的特殊样式(如果需要)
53 + }
54 +
55 + .entry-icon-wrapper {
56 + position: relative;
57 + width: 6rem;
58 + height: 6rem;
59 + border-radius: 50%;
60 + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.05));
61 + backdrop-filter: blur(10px);
62 + display: flex;
63 + align-items: center;
64 + justify-content: center;
65 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
66 + margin-bottom: 0.75rem;
67 +
68 + .entry-icon {
69 + width: 3rem;
70 + height: 3rem;
71 + object-fit: contain;
72 + filter: brightness(0) invert(1); // 图标反白显示
73 + }
74 + }
75 +
76 + .entry-title {
77 + font-size: 1rem;
78 + font-weight: 500;
79 + color: #ffffff;
80 + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
81 + white-space: nowrap;
82 + }
83 +}
84 +</style>
1 +/*
2 + * @Date: 2025-01-28 12:00:00
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-01-28 12:00:00
5 + * @FilePath: /mlaj/src/config/welcomeEntries.js
6 + * @Description: 欢迎页功能入口配置
7 + */
8 +
9 +/**
10 + * 欢迎页功能入口列表
11 + * 用于展示在欢迎页的3个主要功能入口
12 + */
13 +export const welcomeEntries = [
14 + {
15 + id: 'courses',
16 + title: '课程中心',
17 + icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
18 + route: '/courses',
19 + priority: 1
20 + },
21 + {
22 + id: 'activity',
23 + title: '活动中心',
24 + icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
25 + route: '/activity',
26 + priority: 2,
27 + isExternal: true,
28 + externalUrl: 'https://wxm.behalo.cc/pages/activity/activity?token=&user_id='
29 + },
30 + {
31 + id: 'profile',
32 + title: '个人中心',
33 + icon: 'https://cdn.ipadbiz.cn/mlaj/images/welcome_btn_1.png',
34 + route: '/profile',
35 + priority: 3
36 + }
37 +]