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 "$@"
This diff is collapsed. Click to expand it.