hookehuyr

feat: 添加欢迎页开发计划和通用七牛云上传工具

新增欢迎页功能的详细开发计划文档,包括头脑风暴、架构设计和实现步骤
创建通用七牛云上传工具脚本,支持单文件上传和批量上传
添加视频背景组件和欢迎页路由配置的基础框架
实现首次访问检测逻辑,使用localStorage记录用户访问状态
1 +# 欢迎页功能 - 头脑风暴与设计探索
2 +
3 +## 项目背景
4 +
5 +为 mlaj 平台设计一个欢迎页,作为用户首次进入时的引导页面,展示核心功能入口。
6 +
7 +**核心需求:**
8 +- 视频背景循环播放(星空宇宙主题)
9 +- 悬浮的功能入口图标 + 文字介绍
10 +- 持续循环的缩放位移动效
11 +- 首次进入检测,非首次直接跳转主页
12 +
13 +---
14 +
15 +## 需求探索过程
16 +
17 +### 问题 1: 欢迎页的主要目标?
18 +
19 +**选项:**
20 +- 品牌展示 - 营造高端/科技感的品牌印象
21 +- 功能引导 - 清晰展示核心功能入口
22 +- 新手引导 - 教育用户如何使用平台
23 +- 混合型 - 品牌展示 + 功能引导 ✅
24 +
25 +**决策:** 选择**混合型**,既要有视觉冲击力又要提供实用功能入口。
26 +
27 +---
28 +
29 +### 问题 2: 视频背景的实现方式?
30 +
31 +**选项:**
32 +- 原生 `<video>` 标签
33 +- 简化版组件(参考 StarryBackground)✅
34 +- 页面内直接实现
35 +
36 +**决策:** 选择**简化版组件**,理由:
37 +- 组件化便于复用和维护
38 +- 移除 Canvas 星星特效,保留视频播放逻辑
39 +- 支持 Props 配置,灵活性高
40 +
41 +---
42 +
43 +### 问题 3: 如何判断用户是否首次进入?
44 +
45 +**选项:**
46 +- localStorage 标志 ✅
47 +- 后端接口判断
48 +- URL 参数控制
49 +
50 +**决策:** 选择 **localStorage 标志**,理由:
51 +- 简单高效,无需额外请求
52 +- 符合前端存储特性
53 +- 开发调试方便(可手动清除)
54 +
55 +**权衡:**
56 +- 缺点: 清除缓存后会再次显示
57 +- 解决方案: 文档说明这是预期行为,未来可升级为后端接口
58 +
59 +---
60 +
61 +### 问题 4: 图标和文字的动效风格?
62 +
63 +**选项:**
64 +- 轻量动效(3秒入场 + 悬停交互)
65 +- 持续循环(呼吸/缩放动画)✅
66 +- 一次性入场
67 +
68 +**决策:** 选择**持续循环**,理由:
69 +- 符合"星空宇宙"的神秘感主题
70 +- 视觉冲击力更强,吸引用户注意力
71 +- 不同入口设置不同延迟,形成错落感
72 +
73 +---
74 +
75 +## 架构设计探索
76 +
77 +### 整体架构方案
78 +
79 +**方案 A: 单页面组件**
80 +```
81 +WelcomePage.vue
82 +├── 直接写 video 标签
83 +└── 内联功能入口
84 +```
85 +- 优点: 简单直接
86 +- 缺点: 代码耦合,难以复用
87 +
88 +**方案 B: 组件化设计 ✅**
89 +```
90 +WelcomePage.vue
91 +├── VideoBackground.vue (通用组件)
92 +└── WelcomeContent.vue
93 + └── WelcomeEntryItem.vue
94 +```
95 +- 优点: 组件复用、职责清晰、易维护
96 +- 缺点: 文件稍多
97 +
98 +**决策:** 方案 B,组件化设计
99 +
100 +---
101 +
102 +### 页面层级结构
103 +
104 +```
105 +WelcomePage (视图容器)
106 +├── VideoBackground (简化版背景组件)
107 +│ └── <video> 循环播放,覆盖全屏
108 +└── WelcomeContent (内容悬浮层)
109 + ├── [待定] Logo/标题区 (等设计稿确认)
110 + ├── 中部: 功能入口网格 (2-3列)
111 + └── [待定] CTA 按钮区 (等设计稿确认)
112 +```
113 +
114 +**待确认部分:** 顶部和底部元素需等设计稿确定后再规划
115 +
116 +---
117 +
118 +### 首次访问控制逻辑
119 +
120 +**路由守卫方案:**
121 +```javascript
122 +// src/router/guards.js
123 +const HAS_VISITED_WELCOME = 'has_visited_welcome'
124 +
125 +router.beforeEach((to, from, next) => {
126 + // 欢迎页功能开关
127 + if (import.meta.env.VITE_WELCOME_PAGE_ENABLED !== 'true') {
128 + return next()
129 + }
130 +
131 + // 首次访问检测
132 + if (to.path !== '/welcome' &&
133 + !localStorage.getItem(HAS_VISITED_WELCOME)) {
134 + localStorage.setItem(HAS_VISITED_WELCOME, 'true')
135 + return next({
136 + path: '/welcome',
137 + query: { redirect: to.fullPath }
138 + })
139 + }
140 +
141 + next()
142 +})
143 +```
144 +
145 +**调试功能:**
146 +- URL 参数 `?reset_welcome=true` 重置标志位
147 +- 控制台 `window.resetWelcomeFlag()` 快捷方法
148 +
149 +---
150 +
151 +## 核心组件设计
152 +
153 +### VideoBackground 组件
154 +
155 +**位置:** `src/components/effects/VideoBackground.vue`
156 +
157 +**核心特性:**
158 +- 简化版设计,移除 Canvas 特效
159 +- 原生 `<video>` 标签实现
160 +- 移动端和 PC 端自适应
161 +- 可配置视频源、播放速度、覆盖模式
162 +
163 +**Props 设计:**
164 +```javascript
165 +{
166 + videoSrc: { type: String, required: true },
167 + poster: { type: String },
168 + autoplay: { type: Boolean, default: true },
169 + loop: { type: Boolean, default: true },
170 + muted: { type: Boolean, default: true },
171 + objectFit: { type: String, default: 'cover' }
172 +}
173 +```
174 +
175 +**关键实现点:**
176 +- 使用 `object-fit: cover` 确保全屏覆盖不变形
177 +- 添加 `playsinline` 支持 iOS 内联播放
178 +- 监听 `canplay` 事件移除 loading 状态
179 +- `z-index: -1` 确保在内容层级下方
180 +
181 +---
182 +
183 +### WelcomeContent 组件
184 +
185 +**位置:** `src/components/welcome/WelcomeContent.vue`
186 +
187 +**核心布局:** 功能入口网格(2-3列)
188 +
189 +**入口项动效设计(持续循环):**
190 +- CSS `@keyframes` 实现呼吸缩放效果
191 +- 动画参数: `scale(1.0) → scale(1.08) → scale(1.0)`
192 +- 周期: 2-3秒
193 +- 不同入口项设置不同延迟(0s, 0.5s, 1s...)形成错落感
194 +- Hover 时加速或高亮
195 +
196 +**CSS 动画示例:**
197 +```less
198 +@keyframes breathe {
199 + 0%, 100% {
200 + transform: scale(1);
201 + opacity: 0.9;
202 + }
203 + 50% {
204 + transform: scale(1.08);
205 + opacity: 1;
206 + }
207 +}
208 +
209 +.entry-item {
210 + animation: breathe 2.5s ease-in-out infinite;
211 + animation-delay: calc(var(--index) * 0.3s);
212 +
213 + &:hover {
214 + animation-duration: 1s; // 加速
215 + transform: scale(1.1);
216 + }
217 +}
218 +```
219 +
220 +---
221 +
222 +## 七牛云上传工具设计
223 +
224 +### 现有方案分析
225 +
226 +**从 deploy.sh 提取的关键逻辑:**
227 +- 使用 `qshell qupload` 命令批量上传
228 +- 依赖配置文件 `~/.qshell/stdj_upload.conf`
229 +- 需要预先安装 qshell 工具
230 +
231 +**发现的问题:**
232 +- 用户提到"需要挂代理才能访问"
233 +- 可能是七牛云 API 域名在某些网络环境下被限制
234 +- 需要设置 `HTTP_PROXY`/`HTTPS_PROXY` 环境变量
235 +
236 +---
237 +
238 +### 通用化设计探索
239 +
240 +**原始方案问题:**
241 +- 硬编码了欢迎页特定路径(`/docs/plan/26.1.28-欢迎页开发计划/`
242 +- 配置文件使用项目外的 `~/.qshell/stdj_upload.conf`
243 +- 无法作为通用工具复用
244 +
245 +**改进方案:**
246 +1. **项目内配置**
247 + - 配置文件位置: `scripts/qiniu/configs/`
248 + - 账户信息: `scripts/qiniu/account.json`(不入库)
249 + - 配置模板: `scripts/qiniu/templates/*.template`
250 +
251 +2. **脚本参数化**
252 + - 支持单文件上传: `./script <local_file> <remote_path>`
253 + - 支持批量上传: `./script <config_file>`
254 + - 环境变量控制代理: `USE_PROXY=true PROXY_HOST=127.0.0.1:7890`
255 +
256 +3. **统一账户管理**
257 + - 首次使用时初始化账户
258 + - 账户信息本地加密存储
259 + - 支持多项目共享同一账户
260 +
261 +---
262 +
263 +### 使用场景设计
264 +
265 +**场景 1: 上传欢迎页视频**
266 +```bash
267 +./scripts/upload-to-qiniu.sh video/bg.mp4 mlaj/video/welcome-bg.mp4
268 +```
269 +
270 +**场景 2: 批量上传图片**
271 +```bash
272 +# 复制模板并修改配置
273 +cp scripts/qiniu/templates/image-upload.conf.template \
274 + scripts/qiniu/configs/welcome-images.conf
275 +
276 +# 执行上传
277 +./scripts/upload-to-qiniu.sh scripts/qiniu/configs/welcome-images.conf
278 +```
279 +
280 +**场景 3: 使用代理上传**
281 +```bash
282 +USE_PROXY=true PROXY_HOST="127.0.0.1:7890" \
283 + ./scripts/upload-to-qiniu.sh local.mp4 mlaj/video/remote.mp4
284 +```
285 +
286 +---
287 +
288 +## 风险识别与应对
289 +
290 +### 技术风险
291 +
292 +**1. 视频播放兼容性**
293 +- iOS Safari 可能禁止自动播放
294 +- 部分浏览器不支持 `playsinline`
295 +
296 +**应对:**
297 +```vue
298 +<video
299 + autoplay
300 + loop
301 + muted
302 + playsinline
303 + webkit-playsinline
304 + x5-video-player-type="h5"
305 + x5-video-player-fullscreen="true"
306 +>
307 +```
308 +- 视频加载失败时使用静态背景图降级
309 +
310 +---
311 +
312 +**2. 首次访问标志位问题**
313 +- 清除 localStorage 后会再次显示
314 +- 不同浏览器无法共享标志位
315 +
316 +**应对:**
317 +- 文档说明这是预期行为
318 +- 提供调试工具方便开发
319 +- 未来可升级为后端接口
320 +
321 +---
322 +
323 +**3. 视频加载性能**
324 +- 文件过大导致加载缓慢
325 +- 弱网环境体验差
326 +
327 +**应对:**
328 +- 限制文件大小 < 10MB
329 +- 提供封面图在视频加载前显示
330 +- 添加加载进度提示
331 +- 考虑视频分段加载或 M3U8
332 +
333 +---
334 +
335 +**4. 七牛云代理问题**
336 +- 需要挂代理才能访问
337 +
338 +**应对:**
339 +- 脚本支持 `USE_PROXY` 环境变量
340 +- 提供诊断脚本测试连通性
341 +- 文档说明代理配置方法
342 +
343 +---
344 +
345 +### 业务风险
346 +
347 +**1. 功能入口待确认**
348 +- 设计稿未确定具体入口
349 +
350 +**应对:**
351 +- 先实现框架和核心逻辑
352 +- 入口配置化,后续易于调整
353 +- 使用 Mock 数据开发
354 +
355 +---
356 +
357 +**2. 动效性能影响**
358 +- 持续动画可能消耗 CPU/电量
359 +
360 +**应对:**
361 +- 使用 CSS 动画而非 JS(性能更好)
362 +- 提供环境变量控制动画开关
363 +- 低端设备检测后降级为静态效果
364 +
365 +---
366 +
367 +## 待确认事项
368 +
369 +根据需求文档,以下事项需要确认:
370 +
371 +1.**背景视频文件** - `video/?.mp4` 文件名未知
372 +2.**页面效果图** - `img/` 文件夹为空
373 +3.**功能入口列表** - 具体跳转地址未知
374 +4.**页面布局细节** - 顶部/底部是否需要元素(Logo、标语、按钮等)
375 +
376 +**建议:** 先完成技术框架和上传工具,等设计稿确认后再填充内容。
377 +
378 +---
379 +
380 +## 关键决策总结
381 +
382 +| 决策点 | 选择 | 理由 |
383 +|--------|------|------|
384 +| 页面目标 | 混合型 | 品牌展示 + 功能引导 |
385 +| 视频实现 | 简化版组件 | 组件化,便于复用 |
386 +| 首次检测 | localStorage | 简单高效 |
387 +| 动效风格 | 持续循环 | 符合主题,视觉冲击力强 |
388 +| 架构设计 | 组件化 | 职责清晰,易维护 |
389 +| 上传工具 | 通用化 | 支持多种场景复用 |
390 +
391 +---
392 +
393 +## 下一步行动
394 +
395 +1. ✅ 完成头脑风暴和设计探索
396 +2. ⏳ 编写详细实现计划(plan.md)
397 +3. ⏳ 等待设计稿确认
398 +4. ⏳ 开始实施开发(按优先级分阶段)
399 +
400 +---
401 +
402 +*文档创建时间: 2026-01-28*
403 +*最后更新: 2026-01-28*
1 +#!/usr/bin/env bash
2 +#
3 +# 部署脚本:构建项目 -> 上传七牛 -> 打包并上传到服务器
4 +# 说明:在 macOS 环境下执行,依赖 npm、qshell、ssh/scp
5 +
6 +set -euo pipefail
7 +
8 +# 全局变量(按需修改)
9 +repo_root="$(cd "$(dirname "$0")/.." && pwd)"
10 +server_host="zhsy@oa.jcedu.org"
11 +server_port="12101"
12 +remote_dir="/home/www/f"
13 +local_tar="dist.tar.gz"
14 +local_package_dir="stdj"
15 +qshell_dir="$HOME/.qshell"
16 +qshell_conf="stdj_upload.conf"
17 +build_out_dir="dist"
18 +
19 +# 打印信息日志
20 +# 说明:输出带前缀的日志,便于观察执行过程
21 +log_info() {
22 + echo "[deploy] $1"
23 +}
24 +
25 +# 执行命令并显示日志
26 +# 参数:cmd 命令字符串
27 +# 说明:包装命令执行,便于统一错误处理与日志输出
28 +run_cmd() {
29 + local cmd="$1"
30 + log_info "执行:$cmd"
31 + eval "$cmd"
32 +}
33 +
34 +# 检测构建输出目录
35 +# 说明:按优先级读取 .env.production -> .env -> .env.development 中的 VITE_OUTDIR,缺省为 dist
36 +detect_out_dir() {
37 + # POSIX 兼容实现:按顺序检查 .env.production -> .env -> .env.development
38 + # 若未设置,回退为 dist
39 + local outdir="dist"
40 + for env_file in "$repo_root/.env.production" "$repo_root/.env" "$repo_root/.env.development"; do
41 + if [ -f "$env_file" ]; then
42 + # 提取键值:去注释、去空格、去引号
43 + local value
44 + value=$(grep -E '^\s*VITE_OUTDIR\s*=' "$env_file" | head -n1 | cut -d '=' -f2- | sed 's/#.*$//' | tr -d '[:space:]' | sed -e 's/^"//;s/"$//' -e "s/^'//;s/'$//" || true)
45 + if [ -n "${value:-}" ]; then
46 + outdir="$value"
47 + break
48 + fi
49 + fi
50 + done
51 + echo "$outdir"
52 +}
53 +
54 +# 构建项目
55 +# 说明:使用 npm 脚本进行构建,产物位于 dist/
56 +build_project() {
57 + log_info "开始构建项目"
58 + cd "$repo_root"
59 + if command -v pnpm >/dev/null 2>&1; then
60 + run_cmd "pnpm run build"
61 + elif command -v npm >/dev/null 2>&1; then
62 + run_cmd "npm run build"
63 + elif command -v yarn >/dev/null 2>&1; then
64 + run_cmd "yarn build"
65 + else
66 + echo "错误:未检测到 pnpm/npm/yarn,请先安装其中之一"
67 + exit 1
68 + fi
69 + build_out_dir="$(detect_out_dir)"
70 + if [ ! -d "$repo_root/${build_out_dir:-}" ]; then
71 + echo "错误:未找到构建输出目录 '${build_out_dir:-}/',请检查 .env/.env.production 的 VITE_OUTDIR 或构建是否成功"
72 + exit 1
73 + fi
74 + log_info "构建完成:${build_out_dir:-}/"
75 +}
76 +
77 +# 准备打包目录
78 +# 说明:将 dist 内容复制到 stdj 目录,用于后续打包
79 +prepare_package_dir() {
80 + log_info "准备打包目录:$local_package_dir"
81 + cd "$repo_root"
82 + # 保留 stdj 目录,仅增量同步构建产物
83 + run_cmd "mkdir -p '$local_package_dir'"
84 + if [ "${build_out_dir:-}" = "$local_package_dir" ]; then
85 + # 输出目录与包目录一致,直接使用,无需复制
86 + log_info "输出目录与包目录一致:${build_out_dir:-},跳过复制"
87 + return 0
88 + fi
89 + if command -v rsync >/dev/null 2>&1; then
90 + # 使用 rsync 增量复制,保留 stdj 目录和其他文件(不删除已有文件)
91 + run_cmd "rsync -a '${build_out_dir:-}/' '$local_package_dir/'"
92 + else
93 + # 回退到 cp -R,同样保留 stdj 目录
94 + run_cmd "cp -R '${build_out_dir:-}'/. '$local_package_dir'/"
95 + fi
96 +}
97 +
98 +# 上传静态资源到七牛云
99 +# 说明:进入 ~/.qshell 并执行 qupload,同步 upload.conf 中配置的资源
100 +upload_qiniu() {
101 + log_info "检查 qshell 安装与配置"
102 + if ! command -v qshell >/dev/null 2>&1; then
103 + echo "错误:未检测到 qshell,请先安装 https://developer.qiniu.com/kodo/tools/1302/qshell"
104 + exit 1
105 + fi
106 + if [ ! -d "$qshell_dir" ]; then
107 + echo "错误:未找到目录 $qshell_dir"
108 + exit 1
109 + fi
110 + if [ ! -f "$qshell_dir/$qshell_conf" ]; then
111 + echo "错误:未找到七牛配置文件 $qshell_dir/$qshell_conf"
112 + exit 1
113 + fi
114 + log_info "开始上传七牛云资源(qupload)"
115 + cd "$qshell_dir"
116 + # 说明:部分文件已存在会导致 qshell 返回非零退出码,此处不终止流程
117 + log_info "执行:qshell qupload '$qshell_conf'"
118 + if ! qshell qupload "$qshell_conf"; then
119 + log_info "qshell qupload 退出码非零(可能因文件已存在),继续后续部署"
120 + fi
121 + log_info "七牛云资源上传完成"
122 +}
123 +
124 +# 打包本地目录并上传到服务器
125 +# 说明:打包 stdj -> 通过 scp 上传到指定目录 -> 远端解压并删除压缩包
126 +upload_server() {
127 + log_info "开始打包:${local_tar:-}"
128 + cd "$repo_root"
129 + run_cmd "tar -czvpf '${local_tar:-}' '${local_package_dir:-}'"
130 +
131 + log_info "上传到服务器:${server_host:-}:${remote_dir:-}(端口 ${server_port:-})"
132 + run_cmd "scp -P '${server_port:-}' '${local_tar:-}' '${server_host:-}':'${remote_dir:-}'"
133 +
134 + log_info "服务器解压:${remote_dir:-}/${local_tar:-}"
135 + # 说明:构造远端命令,避免变量名意外字符导致的未绑定错误
136 + local remote_cmd
137 + remote_cmd="cd \"${remote_dir:-}\" && tar -xzvf \"${local_tar:-}\" && rm -rf \"${local_tar:-}\""
138 + run_cmd "ssh -p '${server_port:-}' '${server_host:-}' \"$remote_cmd\""
139 +
140 + log_info "删除本地压缩包"
141 + run_cmd "rm -f '${local_tar:-}'"
142 +}
143 +
144 +# 主流程
145 +# 说明:串行执行:构建 -> 七牛上传 -> 服务器打包上传
146 +main() {
147 + build_project
148 + prepare_package_dir
149 + upload_qiniu
150 + upload_server
151 + log_info "部署完成"
152 +}
153 +
154 +main "$@"
1 +# 欢迎页功能 - 详细实现计划
2 +
3 +## 项目概述
4 +
5 +为 mlaj 平台开发一个欢迎页,作为用户首次进入时的引导页面,展示核心功能入口。
6 +
7 +**核心功能:**
8 +- 视频背景循环播放(星空宇宙主题)
9 +- 悬浮的功能入口图标 + 文字介绍
10 +- 持续循环的缩放位移动效
11 +- 首次进入检测,非首次直接跳转主页
12 +- 通用七牛云上传工具
13 +
14 +---
15 +
16 +## 目录结构
17 +
18 +```
19 +mlaj/
20 +├── src/
21 +│ ├── components/
22 +│ │ ├── effects/
23 +│ │ │ └── VideoBackground.vue # 视频背景组件
24 +│ │ └── welcome/
25 +│ │ ├── WelcomeContent.vue # 内容容器组件
26 +│ │ └── WelcomeEntryItem.vue # 功能入口项组件
27 +│ ├── config/
28 +│ │ └── welcomeEntries.js # 功能入口配置
29 +│ ├── router/
30 +│ │ ├── routes.js # 新增欢迎页路由
31 +│ │ └── guards.js # 修改路由守卫
32 +│ └── views/
33 +│ └── welcome/
34 +│ └── WelcomePage.vue # 欢迎页主视图
35 +├── scripts/
36 +│ ├── upload-to-qiniu.sh # 七牛云上传工具
37 +│ └── qiniu/
38 +│ ├── account.json # 账户信息(不入库)
39 +│ ├── templates/ # 配置模板
40 +│ │ ├── video-upload.conf.template
41 +│ │ └── image-upload.conf.template
42 +│ └── configs/ # 实际配置(不入库)
43 +│ └── welcome-video.conf
44 +└── docs/
45 + └── plan/
46 + └── 26.1.28-欢迎页开发计划/
47 + ├── plan.md # 本文档
48 + └── brainstorm.md # 头脑风暴记录
49 +```
50 +
51 +---
52 +
53 +## 开发步骤
54 +
55 +### 第 0 阶段: 准备工作 (优先级: 🔴 高)
56 +
57 +**目标:** 准备开发环境和资源
58 +
59 +#### 步骤 1: 创建配置文件模板
60 +
61 +**创建 `scripts/qiniu/templates/video-upload.conf.template`:**
62 +```json
63 +{
64 + "src_dir": "./assets/video",
65 + "bucket": "mlaj",
66 + "key_prefix": "mlaj/video/",
67 + "ignore_dir": false,
68 + "overwrite": true,
69 + "check_exists": true,
70 + "check_hash": true,
71 + "rescan_local": true,
72 + "skip_file_prefixes": ".",
73 + "skip_suffixes": ".DS_Store",
74 + "up_host": "https://upload.qiniup.com",
75 + "file_type": 0
76 +}
77 +```
78 +
79 +**创建 `scripts/qiniu/templates/image-upload.conf.template`:**
80 +```json
81 +{
82 + "src_dir": "./assets/images",
83 + "bucket": "mlaj",
84 + "key_prefix": "mlaj/images/",
85 + "ignore_dir": false,
86 + "overwrite": true,
87 + "check_exists": true,
88 + "check_hash": true,
89 + "rescan_local": true,
90 + "skip_file_prefixes": ".",
91 + "skip_suffixes": ".DS_Store",
92 + "up_host": "https://upload.qiniup.com",
93 + "file_type": 0
94 +}
95 +```
96 +
97 +#### 步骤 2: 更新 .gitignore
98 +
99 +```gitignore
100 +# 七牛账户信息(敏感)
101 +scripts/qiniu/account.json
102 +
103 +# 实际配置文件(可能包含路径信息)
104 +scripts/qiniu/configs/
105 +```
106 +
107 +#### 步骤 3: 准备视频资源
108 +
109 +-`docs/plan/26.1.28-欢迎页开发计划/video/` 添加背景视频
110 +- 建议规格:
111 + - 分辨率: 1920x1080 (1080p)
112 + - 编码格式: H.264
113 + - 时长: 10-20秒循环视频
114 + - 文件大小: < 10MB
115 +
116 +#### 步骤 4: 上传资源到七牛云
117 +
118 +```bash
119 +# 初始化七牛账户
120 +chmod +x scripts/upload-to-qiniu.sh
121 +./scripts/upload-to-qiniu.sh init
122 +
123 +# 上传视频(如果有代理)
124 +USE_PROXY=true PROXY_HOST="127.0.0.1:7890" \
125 + ./scripts/upload-to-qiniu.sh video/background.mp4 mlaj/video/welcome-background.mp4
126 +```
127 +
128 +#### 步骤 5: 更新环境变量
129 +
130 +**`.env.development`:**
131 +```bash
132 +# 欢迎页功能开关
133 +VITE_WELCOME_PAGE_ENABLED=true
134 +VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-background.mp4
135 +VITE_WELCOME_VIDEO_POSTER=https://cdn.ipadbiz.cn/mlaj/images/welcome-poster.jpg
136 +```
137 +
138 +**`.env.production`:**
139 +```bash
140 +VITE_WELCOME_PAGE_ENABLED=true
141 +VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-background.mp4
142 +VITE_WELCOME_VIDEO_POSTER=https://cdn.ipadbiz.cn/mlaj/images/welcome-poster.jpg
143 +```
144 +
145 +---
146 +
147 +### 第 1 阶段: 通用上传工具开发 (优先级: 🔴 高)
148 +
149 +**目标:** 实现可复用的七牛云上传工具
150 +
151 +#### 步骤 1: 创建上传脚本
152 +
153 +**文件:** `scripts/upload-to-qiniu.sh`
154 +
155 +```bash
156 +#!/usr/bin/env bash
157 +# 通用七牛云上传工具 - mlaj 项目
158 +# 用法: ./scripts/upload-to-qiniu.sh <local_file> <remote_path>
159 +
160 +set -euo pipefail
161 +
162 +# 项目根目录
163 +repo_root="$(cd "$(dirname "$0")/.." && pwd)"
164 +
165 +# 七牛配置
166 +QINIU_BUCKET="${QINIU_BUCKET:-mlaj}"
167 +QINIU_CONFIG_DIR="$repo_root/scripts/qiniu"
168 +QINIU_ACCOUNT_CONF="account.json"
169 +
170 +# 代理设置(可选)
171 +USE_PROXY=${USE_PROXY:-false}
172 +PROXY_HOST=${PROXY_HOST:-"127.0.0.1:7890"}
173 +
174 +log_info() {
175 + echo "[qiniu-upload] $1"
176 +}
177 +
178 +# 初始化 qshell 账户
179 +init_account() {
180 + if ! command -v qshell >/dev/null 2>&1; then
181 + echo "错误: 未检测到 qshell,请先安装 https://developer.qiniu.com/kodo/tools/1302/qshell"
182 + exit 1
183 + fi
184 +
185 + if [ ! -f "$QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF" ]; then
186 + log_info "首次使用,请输入七牛云账号信息:"
187 + read -p "Access Key: " AK
188 + read -p "Secret Key: " SK
189 +
190 + mkdir -p "$QINIU_CONFIG_DIR"
191 + qshell account "$AK" "$SK" > "$QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF"
192 + log_info "账户信息已保存到 $QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF"
193 + fi
194 +}
195 +
196 +# 单文件上传
197 +upload_single_file() {
198 + local local_file="$1"
199 + local remote_path="$2"
200 +
201 + if [ ! -f "$local_file" ]; then
202 + echo "错误: 文件不存在 $local_file"
203 + exit 1
204 + fi
205 +
206 + # 获取文件目录和文件名
207 + local file_dir=$(cd "$(dirname "$local_file")" && pwd)
208 + local file_name=$(basename "$local_file")
209 +
210 + # 创建临时配置
211 + local temp_conf="$QINIU_CONFIG_DIR/temp_upload_$(date +%s).conf"
212 + cat > "$temp_conf" << EOF
213 +{
214 + "src_dir": "$file_dir",
215 + "bucket": "$QINIU_BUCKET",
216 + "key_prefix": "$(dirname "$remote_path")/",
217 + "ignore_dir": false,
218 + "overwrite": true,
219 + "check_exists": true,
220 + "check_hash": true,
221 + "rescan_local": false,
222 + "skip_file_prefixes": ".",
223 + "skip_suffixes": ".DS_Store",
224 + "up_host": "https://upload.qiniup.com",
225 + "file_type": 0,
226 + "file_list": [
227 + "$file_name"
228 + ]
229 +}
230 +EOF
231 +
232 + execute_upload "$temp_conf"
233 +
234 + # 清理临时配置
235 + rm -f "$temp_conf"
236 +
237 + log_info "✅ 上传成功: https://cdn.ipadbiz.cn/$remote_path"
238 +}
239 +
240 +# 批量上传(使用配置文件)
241 +upload_batch() {
242 + local config_file="$1"
243 +
244 + if [ ! -f "$config_file" ]; then
245 + echo "错误: 配置文件不存在 $config_file"
246 + exit 1
247 + fi
248 +
249 + execute_upload "$config_file"
250 + log_info "✅ 批量上传完成"
251 +}
252 +
253 +# 执行上传(统一处理代理)
254 +execute_upload() {
255 + local config_file="$1"
256 +
257 + if [ "$USE_PROXY" = "true" ]; then
258 + export HTTP_PROXY="http://$PROXY_HOST"
259 + export HTTPS_PROXY="http://$PROXY_HOST"
260 + log_info "使用代理: $PROXY_HOST"
261 + fi
262 +
263 + qshell qupload "$config_file"
264 +}
265 +
266 +# 显示帮助信息
267 +show_help() {
268 + cat << EOF
269 +通用七牛云上传工具
270 +
271 +用法:
272 + $0 <local_file> <remote_path> 单文件上传
273 + $0 <config_file> 批量上传(指定配置文件)
274 + $0 init 初始化七牛账户
275 + $0 help 显示此帮助信息
276 +
277 +参数说明:
278 + local_file 本地文件路径(相对或绝对路径)
279 + remote_path 远程路径,如: mlaj/video/bg.mp4
280 + config_file 配置文件路径
281 +
282 +环境变量:
283 + QINIU_BUCKET 七牛空间名(默认: mlaj)
284 + USE_PROXY=true 启用代理
285 + PROXY_HOST=127.0.0.1:7890 代理地址
286 +
287 +示例:
288 + # 单文件上传
289 + $0 ./assets/video/bg.mp4 mlaj/video/welcome-bg.mp4
290 +
291 + # 批量上传
292 + $0 scripts/qiniu/configs/welcome-video.conf
293 +
294 + # 使用代理上传
295 + USE_PROXY=true $0 ./local/file.mp4 mlaj/video/file.mp4
296 +
297 +配置文件格式:
298 + {
299 + "src_dir": "./assets/video",
300 + "bucket": "mlaj",
301 + "key_prefix": "mlaj/video/",
302 + "overwrite": true,
303 + "check_exists": true
304 + }
305 +EOF
306 +}
307 +
308 +# 主逻辑
309 +main() {
310 + init_account
311 +
312 + case "${1:-}" in
313 + init)
314 + log_info "账户初始化完成"
315 + ;;
316 + help|--help|-h)
317 + show_help
318 + ;;
319 + "")
320 + show_help
321 + exit 1
322 + ;;
323 + *)
324 + if [ $# -eq 1 ]; then
325 + # 单个参数,视为配置文件
326 + upload_batch "$1"
327 + elif [ $# -eq 2 ]; then
328 + # 两个参数,单文件上传
329 + upload_single_file "$1" "$2"
330 + else
331 + echo "错误: 参数数量不正确"
332 + show_help
333 + exit 1
334 + fi
335 + ;;
336 + esac
337 +}
338 +
339 +main "$@"
340 +```
341 +
342 +#### 步骤 2: 添加 npm scripts
343 +
344 +**`package.json`:**
345 +```json
346 +{
347 + "scripts": {
348 + "upload:qiniu": "bash scripts/upload-to-qiniu.sh",
349 + "qiniu:init": "bash scripts/upload-to-qiniu.sh init"
350 + }
351 +}
352 +```
353 +
354 +#### 步骤 3: 测试上传工具
355 +
356 +```bash
357 +# 赋予执行权限
358 +chmod +x scripts/upload-to-qiniu.sh
359 +
360 +# 初始化账户
361 +pnpm run qiniu:init
362 +
363 +# 测试单文件上传
364 +pnpm run upload:qiniu ./test/file.mp4 mlaj/video/test.mp4
365 +
366 +# 测试代理上传(如果需要)
367 +USE_PROXY=true pnpm run upload:qiniu ./test/file.mp4 mlaj/video/test.mp4
368 +```
369 +
370 +---
371 +
372 +### 第 2 阶段: VideoBackground 组件 (优先级: 🔴 高)
373 +
374 +**目标:** 实现视频背景组件
375 +
376 +#### 步骤 1: 创建组件文件
377 +
378 +**文件:** `src/components/effects/VideoBackground.vue`
379 +
380 +```vue
381 +<template>
382 + <div class="video-background">
383 + <!-- Loading 状态 -->
384 + <div v-if="isLoading" class="video-loading">
385 + <van-loading size="24px">加载中...</van-loading>
386 + </div>
387 +
388 + <!-- 视频元素 -->
389 + <video
390 + ref="videoRef"
391 + class="video-element"
392 + :src="videoSrc"
393 + :poster="poster"
394 + :autoplay="autoplay"
395 + :loop="loop"
396 + :muted="muted"
397 + :webkit-playsinline="true"
398 + :playsinline="true"
399 + x5-video-player-type="h5"
400 + x5-video-player-fullscreen="true"
401 + @canplay="onCanPlay"
402 + @error="onError"
403 + ></video>
404 +
405 + <!-- 降级背景图 -->
406 + <div
407 + v-if="showFallback"
408 + class="video-fallback"
409 + :style="{ backgroundImage: `url(${poster || videoSrc})` }"
410 + ></div>
411 + </div>
412 +</template>
413 +
414 +<script setup>
415 +import { ref, onMounted, onUnmounted } from 'vue'
416 +
417 +const props = defineProps({
418 + /** 视频源 URL */
419 + videoSrc: {
420 + type: String,
421 + required: true
422 + },
423 + /** 封面图 URL */
424 + poster: {
425 + type: String,
426 + default: ''
427 + },
428 + /** 是否自动播放 */
429 + autoplay: {
430 + type: Boolean,
431 + default: true
432 + },
433 + /** 是否循环播放 */
434 + loop: {
435 + type: Boolean,
436 + default: true
437 + },
438 + /** 是否静音 */
439 + muted: {
440 + type: Boolean,
441 + default: true
442 + },
443 + /** 视频填充模式 */
444 + objectFit: {
445 + type: String,
446 + default: 'cover' // cover, contain, fill
447 + }
448 +})
449 +
450 +const videoRef = ref(null)
451 +const isLoading = ref(true)
452 +const showFallback = ref(false)
453 +
454 +// 视频可以播放时
455 +const onCanPlay = () => {
456 + isLoading.value = false
457 +
458 + // 尝试自动播放
459 + if (props.autoplay && videoRef.value) {
460 + videoRef.value.play().catch(err => {
461 + console.warn('[VideoBackground] 自动播放失败:', err)
462 + // iOS Safari 可能需要用户交互才能播放
463 + showFallback.value = true
464 + })
465 + }
466 +}
467 +
468 +// 视频加载错误
469 +const onError = (e) => {
470 + console.error('[VideoBackground] 视频加载失败:', e)
471 + isLoading.value = false
472 + showFallback.value = true
473 +}
474 +
475 +// 手动播放(用于处理需要用户交互的情况)
476 +const play = () => {
477 + if (videoRef.value) {
478 + videoRef.value.play().catch(err => {
479 + console.warn('[VideoBackground] 播放失败:', err)
480 + })
481 + }
482 +}
483 +
484 +// 暂停播放
485 +const pause = () => {
486 + if (videoRef.value) {
487 + videoRef.value.pause()
488 + }
489 +}
490 +
491 +onMounted(() => {
492 + // 预加载视频
493 + if (videoRef.value) {
494 + videoRef.value.load()
495 + }
496 +})
497 +
498 +onUnmounted(() => {
499 + // 清理资源
500 + if (videoRef.value) {
501 + videoRef.value.pause()
502 + videoRef.value.src = ''
503 + }
504 +})
505 +
506 +defineExpose({
507 + play,
508 + pause
509 +})
510 +</script>
511 +
512 +<style scoped>
513 +.video-background {
514 + position: fixed;
515 + top: 0;
516 + left: 0;
517 + width: 100vw;
518 + height: 100vh;
519 + z-index: -1;
520 + overflow: hidden;
521 +}
522 +
523 +.video-element {
524 + width: 100%;
525 + height: 100%;
526 + object-fit: v-bind(objectFit);
527 + background-color: #000;
528 +}
529 +
530 +.video-loading {
531 + position: absolute;
532 + top: 50%;
533 + left: 50%;
534 + transform: translate(-50%, -50%);
535 + z-index: 1;
536 +}
537 +
538 +.video-fallback {
539 + position: absolute;
540 + top: 0;
541 + left: 0;
542 + width: 100%;
543 + height: 100%;
544 + background-size: cover;
545 + background-position: center;
546 + background-repeat: no-repeat;
547 + z-index: -1;
548 +}
549 +</style>
550 +```
551 +
552 +#### 步骤 2: 组件使用示例
553 +
554 +```vue
555 +<template>
556 + <VideoBackground
557 + :video-src="videoUrl"
558 + :poster="posterUrl"
559 + autoplay
560 + loop
561 + muted
562 + />
563 +</template>
564 +
565 +<script setup>
566 +import VideoBackground from '@/components/effects/VideoBackground.vue'
567 +
568 +const videoUrl = 'https://cdn.ipadbiz.cn/mlaj/video/welcome-background.mp4'
569 +const posterUrl = 'https://cdn.ipadbiz.cn/mlaj/images/welcome-poster.jpg'
570 +</script>
571 +```
572 +
573 +---
574 +
575 +### 第 3 阶段: 路由与首次访问逻辑 (优先级: 🔴 高)
576 +
577 +**目标:** 实现路由守卫和首次访问检测
578 +
579 +#### 步骤 1: 创建欢迎页视图
580 +
581 +**文件:** `src/views/welcome/WelcomePage.vue`
582 +
583 +```vue
584 +<template>
585 + <div class="welcome-page">
586 + <!-- 视频背景 -->
587 + <VideoBackground
588 + v-if="videoUrl"
589 + :video-src="videoUrl"
590 + :poster="posterUrl"
591 + />
592 +
593 + <!-- 内容区域 -->
594 + <WelcomeContent class="welcome-content" />
595 + </div>
596 +</template>
597 +
598 +<script setup>
599 +import { computed } from 'vue'
600 +import { useRoute } from 'vue-router'
601 +import VideoBackground from '@/components/effects/VideoBackground.vue'
602 +import WelcomeContent from '@/components/welcome/WelcomeContent.vue'
603 +
604 +const route = useRoute()
605 +
606 +const videoUrl = computed(() => {
607 + return import.meta.env.VITE_WELCOME_VIDEO_URL || ''
608 +})
609 +
610 +const posterUrl = computed(() => {
611 + return import.meta.env.VITE_WELCOME_VIDEO_POSTER || ''
612 +})
613 +</script>
614 +
615 +<style scoped>
616 +.welcome-page {
617 + position: relative;
618 + width: 100vw;
619 + min-height: 100vh;
620 + overflow: hidden;
621 +}
622 +
623 +.welcome-content {
624 + position: relative;
625 + z-index: 1;
626 +}
627 +</style>
628 +```
629 +
630 +#### 步骤 2: 添加路由配置
631 +
632 +**文件:** `src/router/routes.js`
633 +
634 +```javascript
635 +// 在路由数组中添加
636 +{
637 + path: '/welcome',
638 + name: 'Welcome',
639 + component: () => import('@/views/welcome/WelcomePage.vue'),
640 + meta: {
641 + requiresAuth: false,
642 + hideInMenu: true
643 + }
644 +}
645 +```
646 +
647 +#### 步骤 3: 实现首次访问检测
648 +
649 +**文件:** `src/router/guards.js`
650 +
651 +```javascript
652 +// 首次访问标志
653 +const HAS_VISITED_WELCOME = 'has_visited_welcome'
654 +const WELCOME_VISITED_AT = 'welcome_visited_at'
655 +
656 +/**
657 + * 检查用户是否已访问过欢迎页
658 + */
659 +export function hasVisitedWelcome() {
660 + return localStorage.getItem(HAS_VISITED_WELCOME) === 'true'
661 +}
662 +
663 +/**
664 + * 标记用户已访问欢迎页
665 + */
666 +export function markWelcomeVisited() {
667 + localStorage.setItem(HAS_VISITED_WELCOME, 'true')
668 + localStorage.setItem(WELCOME_VISITED_AT, Date.now().toString())
669 +}
670 +
671 +/**
672 + * 重置欢迎页标志(用于调试)
673 + */
674 +export function resetWelcomeFlag() {
675 + localStorage.removeItem(HAS_VISITED_WELCOME)
676 + localStorage.removeItem(WELCOME_VISITED_AT)
677 +}
678 +
679 +// 在路由守卫中添加首次访问检测
680 +router.beforeEach((to, from, next) => {
681 + // 欢迎页功能开关
682 + if (import.meta.env.VITE_WELCOME_PAGE_ENABLED !== 'true') {
683 + return next()
684 + }
685 +
686 + // 重置欢迎页标志(URL 参数)
687 + if (to.query.reset_welcome === 'true') {
688 + resetWelcomeFlag()
689 + // 移除 URL 参数
690 + const query = { ...to.query }
691 + delete query.reset_welcome
692 + return next({ path: to.path, query })
693 + }
694 +
695 + // 首次访问检测
696 + if (to.path !== '/welcome' && !hasVisitedWelcome()) {
697 + markWelcomeVisited()
698 + return next({
699 + path: '/welcome',
700 + query: { redirect: to.fullPath }
701 + })
702 + }
703 +
704 + next()
705 +})
706 +```
707 +
708 +#### 步骤 4: 添加调试工具
709 +
710 +**文件:** `src/main.js` (或 `App.vue`)
711 +
712 +```javascript
713 +// 开发环境添加调试工具
714 +if (import.meta.env.DEV) {
715 + window.resetWelcomeFlag = () => {
716 + localStorage.removeItem('has_visited_welcome')
717 + localStorage.removeItem('welcome_visited_at')
718 + console.log('✅ 欢迎页标志已重置,请刷新页面查看欢迎页')
719 + }
720 +
721 + window.showWelcome = () => {
722 + window.location.href = '/welcome'
723 + }
724 +
725 + console.log('🔧 开发工具:')
726 + console.log(' - window.resetWelcomeFlag() 重置欢迎页标志')
727 + console.log(' - window.showWelcome() 跳转到欢迎页')
728 +}
729 +```
730 +
731 +---
732 +
733 +### 第 4 阶段: WelcomeContent 组件 (优先级: 🟡 中)
734 +
735 +**目标:** 实现内容悬浮层
736 +
737 +#### 步骤 1: 创建功能入口配置
738 +
739 +**文件:** `src/config/welcomeEntries.js`
740 +
741 +```javascript
742 +/**
743 + * 欢迎页功能入口配置
744 + * 待设计稿确认后更新具体内容
745 + */
746 +export const welcomeEntries = [
747 + {
748 + id: 'courses',
749 + title: '课程中心',
750 + subtitle: '探索精选课程',
751 + icon: '📚',
752 + route: '/courses',
753 + color: '#4CAF50',
754 + priority: 1
755 + },
756 + {
757 + id: 'checkin',
758 + title: '每日打卡',
759 + subtitle: '记录学习点滴',
760 + icon: '✅',
761 + route: '/checkin',
762 + color: '#2196F3',
763 + priority: 2
764 + }
765 + // ... 更多入口(等设计稿确认)
766 +]
767 +
768 +/**
769 + * 根据优先级排序
770 + */
771 +export function getSortedEntries() {
772 + return [...welcomeEntries].sort((a, b) => a.priority - b.priority)
773 +}
774 +```
775 +
776 +#### 步骤 2: 创建功能入口项组件
777 +
778 +**文件:** `src/components/welcome/WelcomeEntryItem.vue`
779 +
780 +```vue
781 +<template>
782 + <div
783 + class="entry-item"
784 + :style="{ '--index': index }"
785 + @click="handleClick"
786 + >
787 + <!-- 图标 -->
788 + <div class="entry-icon" :style="{ color: entry.color }">
789 + {{ entry.icon }}
790 + </div>
791 +
792 + <!-- 标题 -->
793 + <div class="entry-title">
794 + {{ entry.title }}
795 + </div>
796 +
797 + <!-- 副标题(可选) -->
798 + <div v-if="entry.subtitle" class="entry-subtitle">
799 + {{ entry.subtitle }}
800 + </div>
801 + </div>
802 +</template>
803 +
804 +<script setup>
805 +import { useRouter } from 'vue-router'
806 +
807 +const props = defineProps({
808 + entry: {
809 + type: Object,
810 + required: true
811 + },
812 + index: {
813 + type: Number,
814 + required: true
815 + }
816 +})
817 +
818 +const router = useRouter()
819 +
820 +const handleClick = () => {
821 + const redirect = new URLSearchParams(window.location.search).get('redirect')
822 +
823 + if (props.entry.route) {
824 + router.push(props.entry.route)
825 + } else if (redirect) {
826 + router.push(redirect)
827 + } else {
828 + router.push('/')
829 + }
830 +}
831 +</script>
832 +
833 +<style scoped lang="less">
834 +.entry-item {
835 + display: flex;
836 + flex-direction: column;
837 + align-items: center;
838 + justify-content: center;
839 + padding: 1.5rem;
840 + cursor: pointer;
841 + user-select: none;
842 + transition: all 0.3s ease;
843 +
844 + // 呼吸动画
845 + animation: breathe 2.5s ease-in-out infinite;
846 + animation-delay: calc(var(--index) * 0.3s);
847 +
848 + &:hover {
849 + animation-duration: 1s;
850 + transform: scale(1.1);
851 +
852 + .entry-icon {
853 + transform: scale(1.2);
854 + }
855 + }
856 +
857 + &:active {
858 + transform: scale(0.95);
859 + }
860 +}
861 +
862 +@keyframes breathe {
863 + 0%, 100% {
864 + transform: scale(1);
865 + opacity: 0.9;
866 + }
867 + 50% {
868 + transform: scale(1.08);
869 + opacity: 1;
870 + }
871 +}
872 +
873 +.entry-icon {
874 + font-size: 3rem;
875 + margin-bottom: 0.75rem;
876 + transition: transform 0.3s ease;
877 +}
878 +
879 +.entry-title {
880 + font-size: 1.125rem;
881 + font-weight: 600;
882 + color: #fff;
883 + text-align: center;
884 + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
885 + margin-bottom: 0.25rem;
886 +}
887 +
888 +.entry-subtitle {
889 + font-size: 0.875rem;
890 + color: rgba(255, 255, 255, 0.8);
891 + text-align: center;
892 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
893 +}
894 +</style>
895 +```
896 +
897 +#### 步骤 3: 创建内容容器组件
898 +
899 +**文件:** `src/components/welcome/WelcomeContent.vue`
900 +
901 +```vue
902 +<template>
903 + <div class="welcome-content">
904 + <!-- [待定] Logo/标题区 -->
905 +
906 + <!-- 功能入口网格 -->
907 + <div class="entries-grid">
908 + <WelcomeEntryItem
909 + v-for="(entry, index) in sortedEntries"
910 + :key="entry.id"
911 + :entry="entry"
912 + :index="index"
913 + />
914 + </div>
915 +
916 + <!-- [待定] CTA 按钮区 -->
917 + </div>
918 +</template>
919 +
920 +<script setup>
921 +import { computed } from 'vue'
922 +import { getSortedEntries } from '@/config/welcomeEntries'
923 +import WelcomeEntryItem from './WelcomeEntryItem.vue'
924 +
925 +const sortedEntries = computed(() => getSortedEntries())
926 +</script>
927 +
928 +<style scoped lang="less">
929 +.welcome-content {
930 + position: relative;
931 + width: 100%;
932 + min-height: 100vh;
933 + display: flex;
934 + flex-direction: column;
935 + align-items: center;
936 + justify-content: center;
937 + padding: 2rem;
938 +}
939 +
940 +.entries-grid {
941 + display: grid;
942 + grid-template-columns: repeat(2, 1fr);
943 + gap: 1.5rem;
944 + width: 100%;
945 + max-width: 600px;
946 +
947 + @media (min-width: 768px) {
948 + grid-template-columns: repeat(3, 1fr);
949 + gap: 2rem;
950 + max-width: 900px;
951 + }
952 +}
953 +</style>
954 +```
955 +
956 +---
957 +
958 +### 第 5 阶段: 测试与优化 (优先级: 🟡 中)
959 +
960 +#### 功能测试清单
961 +
962 +- [ ] 首次访问正确跳转到欢迎页
963 +- [ ] 再次访问直接进入主页
964 +- [ ] `?reset_welcome=true` 正确重置标志
965 +- [ ] 点击功能入口正确跳转
966 +- [ ] 视频背景正常循环播放
967 +- [ ] 视频加载失败时显示降级方案
968 +- [ ] 清除 localStorage 后重新访问
969 +
970 +#### 兼容性测试
971 +
972 +- [ ] iOS Safari (video autoplay, playsinline)
973 +- [ ] Android Chrome
974 +- [ ] 微信内置浏览器
975 +- [ ] PC 端浏览器 (Chrome, Firefox, Safari, Edge)
976 +
977 +#### 性能测试
978 +
979 +- [ ] 视频首次加载时间 < 3s
980 +- [ ] 动画帧率 > 50fps
981 +- [ ] 内存占用正常
982 +- [ ] CPU 占用正常
983 +
984 +#### 边界测试
985 +
986 +- [ ] 视频加载失败时降级为静态图
987 +- [ ] 网络慢速时的表现
988 +- [ ] 清除缓存后的行为
989 +- [ ] URL 参数异常处理
990 +
991 +---
992 +
993 +### 第 6 阶段: 文档与部署 (优先级: 🟢 低)
994 +
995 +#### 步骤 1: 更新项目文档
996 +
997 +**`CLAUDE.md` 添加说明:**
998 +```markdown
999 +### 欢迎页
1000 +
1001 +- **功能**: 用户首次进入时的引导页面,展示核心功能入口
1002 +- **路由**: `/welcome`
1003 +- **开关**: `VITE_WELCOME_PAGE_ENABLED=true`
1004 +- **首次检测**: 使用 localStorage 标志 `has_visited_welcome`
1005 +- **调试方法**:
1006 + - URL 参数 `?reset_welcome=true` 重置标志
1007 + - 控制台 `window.resetWelcomeFlag()` 重置标志
1008 + - 控制台 `window.showWelcome()` 跳转欢迎页
1009 +```
1010 +
1011 +#### 步骤 2: 创建使用说明
1012 +
1013 +**文件:** `docs/welcome-page-guide.md`
1014 +
1015 +```markdown
1016 +# 欢迎页使用指南
1017 +
1018 +## 功能说明
1019 +
1020 +欢迎页是用户首次进入平台时的引导页面,展示核心功能入口。
1021 +
1022 +## 开发配置
1023 +
1024 +### 环境变量
1025 +
1026 +```bash
1027 +# 启用欢迎页
1028 +VITE_WELCOME_PAGE_ENABLED=true
1029 +
1030 +# 视频资源
1031 +VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-background.mp4
1032 +VITE_WELCOME_VIDEO_POSTER=https://cdn.ipadbiz.cn/mlaj/images/welcome-poster.jpg
1033 +```
1034 +
1035 +### 功能入口配置
1036 +
1037 +编辑 `src/config/welcomeEntries.js`:
1038 +
1039 +```javascript
1040 +export const welcomeEntries = [
1041 + {
1042 + id: 'courses',
1043 + title: '课程中心',
1044 + subtitle: '探索精选课程',
1045 + icon: '📚',
1046 + route: '/courses',
1047 + color: '#4CAF50',
1048 + priority: 1
1049 + }
1050 + // ... 更多入口
1051 +]
1052 +```
1053 +
1054 +## 调试方法
1055 +
1056 +### 重置欢迎页标志
1057 +
1058 +**方法 1: URL 参数**
1059 +```
1060 +http://localhost:5173/?reset_welcome=true
1061 +```
1062 +
1063 +**方法 2: 控制台**
1064 +```javascript
1065 +window.resetWelcomeFlag()
1066 +location.reload()
1067 +```
1068 +
1069 +### 直接访问欢迎页
1070 +```javascript
1071 +window.showWelcome()
1072 +```
1073 +
1074 +## 上传新的背景视频
1075 +
1076 +### 使用上传工具
1077 +
1078 +```bash
1079 +# 单文件上传
1080 +pnpm run upload:qiniu ./local/video.mp4 mlaj/video/welcome-background.mp4
1081 +
1082 +# 使用代理上传
1083 +USE_PROXY=true pnpm run upload:qiniu ./local/video.mp4 mlaj/video/welcome-background.mp4
1084 +```
1085 +
1086 +### 视频规格建议
1087 +
1088 +- 分辨率: 1920x1080 (1080p)
1089 +- 编码格式: H.264
1090 +- 时长: 10-20秒循环
1091 +- 文件大小: < 10MB
1092 +```
1093 +
1094 +#### 步骤 3: 部署到开发环境
1095 +
1096 +```bash
1097 +# 构建
1098 +pnpm build
1099 +
1100 +# 部署到开发服务器
1101 +pnpm dev_upload
1102 +```
1103 +
1104 +#### 步骤 4: 测试生产环境
1105 +
1106 +- 验证视频 CDN 加载速度
1107 +- 验证首次访问逻辑
1108 +- 验证兼容性
1109 +- 收集用户反馈
1110 +
1111 +---
1112 +
1113 +## 技术要点
1114 +
1115 +### 1. 视频播放兼容性
1116 +
1117 +```vue
1118 +<video
1119 + autoplay
1120 + loop
1121 + muted
1122 + playsinline
1123 + webkit-playsinline
1124 + x5-video-player-type="h5"
1125 + x5-video-player-fullscreen="true"
1126 +>
1127 +```
1128 +
1129 +**关键属性说明:**
1130 +- `muted`: 静音播放(移动端自动播放必需)
1131 +- `playsinline`: iOS 内联播放
1132 +- `webkit-playsinline`: iOS Safari 兼容
1133 +- `x5-video-player-type`: 腾讯 X5 内核(微信/QQ浏览器)
1134 +
1135 +### 2. 动画性能优化
1136 +
1137 +```less
1138 +// 使用 CSS 动画而非 JS 动画
1139 +animation: breathe 2.5s ease-in-out infinite;
1140 +
1141 +// 使用 transform 而非 width/height
1142 +transform: scale(1.08);
1143 +
1144 +// 使用 GPU 加速
1145 +will-change: transform, opacity;
1146 +```
1147 +
1148 +### 3. 降级方案
1149 +
1150 +```vue
1151 +<!-- 视频加载失败时显示静态背景图 -->
1152 +<div
1153 + v-if="showFallback"
1154 + class="video-fallback"
1155 + :style="{ backgroundImage: `url(${poster})` }"
1156 +></div>
1157 +```
1158 +
1159 +---
1160 +
1161 +## 风险与注意事项
1162 +
1163 +### 技术风险
1164 +
1165 +1. **视频播放兼容性**
1166 + - iOS Safari 可能禁止自动播放
1167 + - 解决方案: 添加 `muted` 属性,提供降级方案
1168 +
1169 +2. **首次访问标志位**
1170 + - 清除 localStorage 后会再次显示
1171 + - 解决方案: 文档说明,未来升级为后端接口
1172 +
1173 +3. **视频加载性能**
1174 + - 文件过大导致加载缓慢
1175 + - 解决方案: 限制文件大小 < 10MB,提供封面图
1176 +
1177 +4. **七牛云代理问题**
1178 + - 需要挂代理才能访问
1179 + - 解决方案: 支持 `USE_PROXY` 环境变量
1180 +
1181 +### 业务风险
1182 +
1183 +1. **功能入口待确认**
1184 + - 先实现框架,入口配置化
1185 +
1186 +2. **动效性能影响**
1187 + - 使用 CSS 动画,提供环境变量控制
1188 +
1189 +---
1190 +
1191 +## 优先级总结
1192 +
1193 +### 第 1 批 (核心功能) 🔴
1194 +- ✅ 第 0 阶段: 准备工作
1195 +- ✅ 第 1 阶段: 通用上传工具
1196 +- ✅ 第 2 阶段: VideoBackground 组件
1197 +- ✅ 第 3 阶段: 路由与首次访问逻辑
1198 +
1199 +### 第 2 批 (功能完善) 🟡
1200 +- ⏳ 第 4 阶段: WelcomeContent 组件
1201 +- ⏳ 第 5 阶段: 测试与优化
1202 +
1203 +### 第 3 批 (收尾工作) 🟢
1204 +- ⏳ 第 6 阶段: 文档与部署
1205 +
1206 +---
1207 +
1208 +## 待确认事项
1209 +
1210 +1.**背景视频文件** - 需要提供视频资源
1211 +2.**页面效果图** - 需要设计稿确认布局
1212 +3.**功能入口列表** - 需要确认具体入口和跳转地址
1213 +4.**页面布局细节** - 顶部/底部是否需要元素
1214 +
1215 +**建议:** 先完成技术框架和上传工具,等设计稿确认后再填充内容。
1216 +
1217 +---
1218 +
1219 +*文档创建时间: 2026-01-28*
1220 +*最后更新: 2026-01-28*