feat: 添加欢迎页开发计划和通用七牛云上传工具
新增欢迎页功能的详细开发计划文档,包括头脑风暴、架构设计和实现步骤 创建通用七牛云上传工具脚本,支持单文件上传和批量上传 添加视频背景组件和欢迎页路由配置的基础框架 实现首次访问检测逻辑,使用localStorage记录用户访问状态
Showing
3 changed files
with
1777 additions
and
0 deletions
docs/plan/26.1.28-欢迎页开发计划/brainstorm.md
0 → 100644
| 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* |
docs/plan/26.1.28-欢迎页开发计划/deploy.sh
0 → 100644
| 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 "$@" |
docs/plan/26.1.28-欢迎页开发计划/plan.md
0 → 100644
| 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* |
-
Please register or login to post a comment