plan.md 30.6 KB

欢迎页功能 - 详细实现计划

项目概述

为 mlaj 平台开发一个欢迎页,作为用户首次进入时的引导页面,展示核心功能入口。

核心功能:

  • 视频背景循环播放(星空宇宙主题)
  • 悬浮的功能入口图标 + 文字介绍
  • 持续循环的缩放位移动效
  • 首次进入检测,非首次直接跳转主页
  • 通用七牛云上传工具

目录结构

mlaj/
├── src/
│   ├── components/
│   │   ├── effects/
│   │   │   └── VideoBackground.vue          # 视频背景组件
│   │   └── welcome/
│   │       ├── WelcomeContent.vue           # 内容容器组件
│   │       └── WelcomeEntryItem.vue         # 功能入口项组件
│   ├── config/
│   │   └── welcomeEntries.js                # 功能入口配置
│   ├── router/
│   │   ├── routes.js                        # 新增欢迎页路由
│   │   └── guards.js                        # 修改路由守卫
│   └── views/
│       └── welcome/
│           └── WelcomePage.vue              # 欢迎页主视图
├── scripts/
│   ├── upload-to-qiniu.sh                   # 七牛云上传工具
│   └── qiniu/
│       ├── account.json                     # 账户信息(不入库)
│       ├── templates/                       # 配置模板
│       │   ├── video-upload.conf.template
│       │   └── image-upload.conf.template
│       └── configs/                         # 实际配置(不入库)
│           └── welcome-video.conf
└── docs/
    └── plan/
        └── 26.1.28-欢迎页开发计划/
            ├── plan.md                      # 本文档
            └── brainstorm.md                # 头脑风暴记录

开发步骤

第 0 阶段: 准备工作 (优先级: 🔴 高)

目标: 准备开发环境和资源

步骤 1: 创建配置文件模板

创建 scripts/qiniu/templates/video-upload.conf.template:

{
  "src_dir": "./assets/video",
  "bucket": "ipadbiz",
  "key_prefix": "mlaj/video/",
  "ignore_dir": false,
  "overwrite": true,
  "check_exists": true,
  "check_hash": true,
  "rescan_local": true,
  "skip_file_prefixes": ".",
  "skip_suffixes": ".DS_Store",
  "up_host": "https://upload.qiniup.com",
  "file_type": 0
}

创建 scripts/qiniu/templates/image-upload.conf.template:

{
  "src_dir": "./assets/images",
  "bucket": "ipadbiz",
  "key_prefix": "mlaj/images/",
  "ignore_dir": false,
  "overwrite": true,
  "check_exists": true,
  "check_hash": true,
  "rescan_local": true,
  "skip_file_prefixes": ".",
  "skip_suffixes": ".DS_Store",
  "up_host": "https://upload.qiniup.com",
  "file_type": 0
}

步骤 2: 更新 .gitignore

# 七牛账户信息(敏感)
scripts/qiniu/account.json

# 实际配置文件(可能包含路径信息)
scripts/qiniu/configs/

步骤 3: 准备视频资源

视频文件: docs/plan/26.1.28-欢迎页开发计划/video/welcome-bg.mp4 ✅ 已添加

建议规格:

  • 分辨率: 1920x1080 (1080p)
  • 编码格式: H.264
  • 时长: 10-20秒循环视频
  • 文件大小: < 10MB

步骤 4: 上传资源到七牛云

# 初始化七牛账户
chmod +x scripts/upload-to-qiniu.sh
./scripts/upload-to-qiniu.sh init

# 上传欢迎页背景视频
./scripts/upload-to-qiniu.sh video/welcome-bg.mp4 mlaj/video/welcome-bg.mp4

上传成功后的 URL: ✅ 已上传

视频: https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4
封面: https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/0.001

文件信息:

  • 大小: 17.57MB (18420585 字节)
  • Hash: lpipKorSMZBEVa-eCevwvcqkB8ZH
  • 上传时间: 1.23s

步骤 5: 更新环境变量

.env.development:

# 欢迎页功能开关
VITE_WELCOME_PAGE_ENABLED=true

# 视频资源 URL
# 封面图会自动通过七牛云处理参数生成: videoUrl + ?vframe/jpg/offset/0.001
VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4

.env.production:

VITE_WELCOME_PAGE_ENABLED=true
VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4

说明:

  • 只需配置视频 URL,封面图会自动从视频中提取第一帧生成
  • 七牛云处理参数: ?vframe/jpg/offset/0.001 表示从视频第 0.001 秒截取一帧作为 JPG 图片
  • 降级方案: 视频加载失败时自动使用该封面图作为静态背景

第 1 阶段: 通用上传工具开发 (优先级: 🔴 高)

目标: 实现可复用的七牛云上传工具

步骤 1: 创建上传脚本

文件: scripts/upload-to-qiniu.sh

#!/usr/bin/env bash
# 通用七牛云上传工具 - mlaj 项目
# 用法: ./scripts/upload-to-qiniu.sh <local_file> <remote_path>

set -euo pipefail

# 项目根目录
repo_root="$(cd "$(dirname "$0")/.." && pwd)"

# 七牛配置
QINIU_BUCKET="${QINIU_BUCKET:-ipadbiz}"
QINIU_CONFIG_DIR="$repo_root/scripts/qiniu"
QINIU_ACCOUNT_CONF="account.json"

# 代理设置(可选)
USE_PROXY=${USE_PROXY:-false}
PROXY_HOST=${PROXY_HOST:-"127.0.0.1:7890"}

log_info() {
    echo "[qiniu-upload] $1"
}

# 初始化 qshell 账户
init_account() {
  if ! command -v qshell >/dev/null 2>&1; then
    echo "错误: 未检测到 qshell,请先安装 https://developer.qiniu.com/kodo/tools/1302/qshell"
    exit 1
  fi

  if [ ! -f "$QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF" ]; then
    log_info "首次使用,请输入七牛云账号信息:"
    read -p "Access Key: " AK
    read -p "Secret Key: " SK

    mkdir -p "$QINIU_CONFIG_DIR"
    qshell account "$AK" "$SK" > "$QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF"
    log_info "账户信息已保存到 $QINIU_CONFIG_DIR/$QINIU_ACCOUNT_CONF"
  fi
}

# 单文件上传
upload_single_file() {
  local local_file="$1"
  local remote_path="$2"

  if [ ! -f "$local_file" ]; then
    echo "错误: 文件不存在 $local_file"
    exit 1
  fi

  # 获取文件目录和文件名
  local file_dir=$(cd "$(dirname "$local_file")" && pwd)
  local file_name=$(basename "$local_file")

  # 创建临时配置
  local temp_conf="$QINIU_CONFIG_DIR/temp_upload_$(date +%s).conf"
  cat > "$temp_conf" << EOF
{
  "src_dir": "$file_dir",
  "bucket": "$QINIU_BUCKET",
  "key_prefix": "$(dirname "$remote_path")/",
  "ignore_dir": false,
  "overwrite": true,
  "check_exists": true,
  "check_hash": true,
  "rescan_local": false,
  "skip_file_prefixes": ".",
  "skip_suffixes": ".DS_Store",
  "up_host": "https://upload.qiniup.com",
  "file_type": 0,
  "file_list": [
    "$file_name"
  ]
}
EOF

  execute_upload "$temp_conf"

  # 清理临时配置
  rm -f "$temp_conf"

  log_info "✅ 上传成功: https://cdn.ipadbiz.cn/$remote_path"
}

# 批量上传(使用配置文件)
upload_batch() {
  local config_file="$1"

  if [ ! -f "$config_file" ]; then
    echo "错误: 配置文件不存在 $config_file"
    exit 1
  fi

  execute_upload "$config_file"
  log_info "✅ 批量上传完成"
}

# 执行上传(统一处理代理)
execute_upload() {
  local config_file="$1"

  if [ "$USE_PROXY" = "true" ]; then
    export HTTP_PROXY="http://$PROXY_HOST"
    export HTTPS_PROXY="http://$PROXY_HOST"
    log_info "使用代理: $PROXY_HOST"
  fi

  qshell qupload "$config_file"
}

# 显示帮助信息
show_help() {
  cat << EOF
通用七牛云上传工具

用法:
  $0 <local_file> <remote_path>       单文件上传
  $0 <config_file>                    批量上传(指定配置文件)
  $0 init                             初始化七牛账户
  $0 help                             显示此帮助信息

参数说明:
  local_file                          本地文件路径(相对或绝对路径)
  remote_path                         远程路径,如: mlaj/video/bg.mp4
  config_file                         配置文件路径

环境变量:
  QINIU_BUCKET                        七牛空间名(默认: ipadbiz)
  USE_PROXY=true                      启用代理
  PROXY_HOST=127.0.0.1:7890          代理地址

示例:
  # 单文件上传
  $0 ./assets/video/bg.mp4 mlaj/video/welcome-bg.mp4

  # 批量上传
  $0 scripts/qiniu/configs/welcome-video.conf

  # 使用代理上传
  USE_PROXY=true $0 ./local/file.mp4 mlaj/video/file.mp4

配置文件格式:
  {
    "src_dir": "./assets/video",
    "bucket": "ipadbiz",
    "key_prefix": "mlaj/video/",
    "overwrite": true,
    "check_exists": true
  }
EOF
}

# 主逻辑
main() {
  init_account

  case "${1:-}" in
    init)
      log_info "账户初始化完成"
      ;;
    help|--help|-h)
      show_help
      ;;
    "")
      show_help
      exit 1
      ;;
    *)
      if [ $# -eq 1 ]; then
        # 单个参数,视为配置文件
        upload_batch "$1"
      elif [ $# -eq 2 ]; then
        # 两个参数,单文件上传
        upload_single_file "$1" "$2"
      else
        echo "错误: 参数数量不正确"
        show_help
        exit 1
      fi
      ;;
  esac
}

main "$@"

步骤 2: 添加 npm scripts

package.json:

{
  "scripts": {
    "upload:qiniu": "bash scripts/upload-to-qiniu.sh",
    "qiniu:init": "bash scripts/upload-to-qiniu.sh init"
  }
}

步骤 3: 测试上传工具

# 赋予执行权限
chmod +x scripts/upload-to-qiniu.sh

# 初始化账户
pnpm run qiniu:init

# 测试单文件上传
pnpm run upload:qiniu ./test/file.mp4 mlaj/video/test.mp4

# 测试代理上传(如果需要)
USE_PROXY=true pnpm run upload:qiniu ./test/file.mp4 mlaj/video/test.mp4

第 2 阶段: VideoBackground 组件 (优先级: 🔴 高)

目标: 实现视频背景组件

步骤 1: 创建组件文件

文件: src/components/effects/VideoBackground.vue

<template>
  <div class="video-background">
    <!-- Loading 状态 -->
    <div v-if="isLoading" class="video-loading">
      <van-loading size="24px">加载中...</van-loading>
    </div>

    <!-- 视频元素 -->
    <video
      ref="videoRef"
      class="video-element"
      :src="videoSrc"
      :poster="posterUrl"
      :autoplay="autoplay"
      :loop="loop"
      :muted="muted"
      :webkit-playsinline="true"
      :playsinline="true"
      x5-video-player-type="h5"
      x5-video-player-fullscreen="true"
      @canplay="onCanPlay"
      @error="onError"
    ></video>

    <!-- 降级背景图 -->
    <div
      v-if="showFallback"
      class="video-fallback"
      :style="{ backgroundImage: `url(${posterUrl})` }"
    ></div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  /** 视频源 URL */
  videoSrc: {
    type: String,
    required: true
  },
  /** 封面图 URL (可选,不传则自动从视频生成) */
  poster: {
    type: String,
    default: ''
  },
  /** 是否自动播放 */
  autoplay: {
    type: Boolean,
    default: true
  },
  /** 是否循环播放 */
  loop: {
    type: Boolean,
    default: true
  },
  /** 是否静音 */
  muted: {
    type: Boolean,
    default: true
  },
  /** 视频填充模式 */
  objectFit: {
    type: String,
    default: 'cover' // cover, contain, fill
  }
})

const videoRef = ref(null)
const isLoading = ref(true)
const showFallback = ref(false)

/**
 * 自动生成封面图 URL
 * 如果没有传入 poster,使用七牛云视频处理参数从视频中提取第一帧
 * 处理参数: ?vframe/jpg/offset/0.001 表示从视频第 0.001 秒截取一帧作为 JPG
 */
const posterUrl = computed(() => {
  if (props.poster) {
    return props.poster
  }
  // 从视频 URL 自动生成封面图
  // 七牛云视频处理: https://developer.qiniu.com/dora/1316/video-frame-operation
  return `${props.videoSrc}?vframe/jpg/offset/0.001`
})

// 视频可以播放时
const onCanPlay = () => {
  isLoading.value = false

  // 尝试自动播放
  if (props.autoplay && videoRef.value) {
    videoRef.value.play().catch(err => {
      console.warn('[VideoBackground] 自动播放失败:', err)
      // iOS Safari 可能需要用户交互才能播放
      showFallback.value = true
    })
  }
}

// 视频加载错误
const onError = (e) => {
  console.error('[VideoBackground] 视频加载失败:', e)
  isLoading.value = false
  showFallback.value = true
}

// 手动播放(用于处理需要用户交互的情况)
const play = () => {
  if (videoRef.value) {
    videoRef.value.play().catch(err => {
      console.warn('[VideoBackground] 播放失败:', err)
    })
  }
}

// 暂停播放
const pause = () => {
  if (videoRef.value) {
    videoRef.value.pause()
  }
}

onMounted(() => {
  // 预加载视频
  if (videoRef.value) {
    videoRef.value.load()
  }
})

onUnmounted(() => {
  // 清理资源
  if (videoRef.value) {
    videoRef.value.pause()
    videoRef.value.src = ''
  }
})

defineExpose({
  play,
  pause
})
</script>

<style scoped>
.video-background {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  overflow: hidden;
}

.video-element {
  width: 100%;
  height: 100%;
  object-fit: v-bind(objectFit);
  background-color: #000;
}

.video-loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
}

.video-fallback {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: -1;
}
</style>

步骤 2: 组件使用示例

<template>
  <VideoBackground
    :video-src="videoUrl"
    autoplay
    loop
    muted
  />
</template>

<script setup>
import VideoBackground from '@/components/effects/VideoBackground.vue'

const videoUrl = 'https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4'
// poster 会自动生成: videoUrl + ?vframe/jpg/offset/0.001
// 即: https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/0.001
</script>

第 3 阶段: 路由与首次访问逻辑 (优先级: 🔴 高)

目标: 实现路由守卫和首次访问检测

步骤 1: 创建欢迎页视图

文件: src/views/welcome/WelcomePage.vue

<template>
  <div class="welcome-page">
    <!-- 视频背景 -->
    <VideoBackground
      v-if="videoUrl"
      :video-src="videoUrl"
    />

    <!-- 内容区域 -->
    <WelcomeContent class="welcome-content" />
  </div>
</template>

<script setup>
import { computed } from 'vue'
import VideoBackground from '@/components/effects/VideoBackground.vue'
import WelcomeContent from '@/components/welcome/WelcomeContent.vue'

const videoUrl = computed(() => {
  return import.meta.env.VITE_WELCOME_VIDEO_URL || ''
})
// poster 会自动从 videoUrl 生成,无需单独配置
</script>

<style scoped>
.welcome-page {
  position: relative;
  width: 100vw;
  min-height: 100vh;
  overflow: hidden;
}

.welcome-content {
  position: relative;
  z-index: 1;
}
</style>

步骤 2: 添加路由配置

文件: src/router/routes.js

// 在路由数组中添加
{
  path: '/welcome',
  name: 'Welcome',
  component: () => import('@/views/welcome/WelcomePage.vue'),
  meta: {
    requiresAuth: false,
    hideInMenu: true
  }
}

步骤 3: 实现首次访问检测

文件: src/router/guards.js

// 首次访问标志
const HAS_VISITED_WELCOME = 'has_visited_welcome'
const WELCOME_VISITED_AT = 'welcome_visited_at'

/**
 * 检查用户是否已访问过欢迎页
 */
export function hasVisitedWelcome() {
  return localStorage.getItem(HAS_VISITED_WELCOME) === 'true'
}

/**
 * 标记用户已访问欢迎页
 */
export function markWelcomeVisited() {
  localStorage.setItem(HAS_VISITED_WELCOME, 'true')
  localStorage.setItem(WELCOME_VISITED_AT, Date.now().toString())
}

/**
 * 重置欢迎页标志(用于调试)
 */
export function resetWelcomeFlag() {
  localStorage.removeItem(HAS_VISITED_WELCOME)
  localStorage.removeItem(WELCOME_VISITED_AT)
}

// 在路由守卫中添加首次访问检测
router.beforeEach((to, from, next) => {
  // 欢迎页功能开关
  if (import.meta.env.VITE_WELCOME_PAGE_ENABLED !== 'true') {
    return next()
  }

  // 重置欢迎页标志(URL 参数)
  if (to.query.reset_welcome === 'true') {
    resetWelcomeFlag()
    // 移除 URL 参数
    const query = { ...to.query }
    delete query.reset_welcome
    return next({ path: to.path, query })
  }

  // 首次访问检测
  if (to.path !== '/welcome' && !hasVisitedWelcome()) {
    markWelcomeVisited()
    return next({
      path: '/welcome',
      query: { redirect: to.fullPath }
    })
  }

  next()
})

步骤 4: 添加调试工具

文件: src/main.js (或 App.vue)

// 开发环境添加调试工具
if (import.meta.env.DEV) {
  window.resetWelcomeFlag = () => {
    localStorage.removeItem('has_visited_welcome')
    localStorage.removeItem('welcome_visited_at')
    console.log('✅ 欢迎页标志已重置,请刷新页面查看欢迎页')
  }

  window.showWelcome = () => {
    window.location.href = '/welcome'
  }

  console.log('🔧 开发工具:')
  console.log('  - window.resetWelcomeFlag() 重置欢迎页标志')
  console.log('  - window.showWelcome() 跳转到欢迎页')
}

第 4 阶段: WelcomeContent 组件 (优先级: 🟡 中)

目标: 实现内容悬浮层

步骤 1: 创建功能入口配置

文件: src/config/welcomeEntries.js

/**
 * 欢迎页功能入口配置
 * 采用环绕式布局,3个功能入口像天体行星一样排列
 */
export const welcomeEntries = [
  {
    id: 'courses',
    title: '课程中心',
    subtitle: '探索精选课程',
    icon: '/img/wecome_btn_1.png',  // 使用图片图标
    route: '/courses',
    color: '#4CAF50',
    priority: 1
  },
  {
    id: 'activity',
    title: '活动中心',
    subtitle: '精彩活动不容错过',
    icon: '/img/wecome_btn_1.png',  // 使用图片图标
    route: '/activity',
    color: '#FF9800',
    priority: 2,
    isExternal: true,  // 标记为外链跳转
    externalUrl: 'https://wxm.behalo.cc/pages/activity/activity?token=&user_id='
  },
  {
    id: 'profile',
    title: '个人中心',
    subtitle: '管理您的账户',
    icon: '/img/wecome_btn_1.png',  // 使用图片图标
    route: '/profile',
    color: '#2196F3',
    priority: 3
  }
]

/**
 * 根据优先级排序
 */
export function getSortedEntries() {
  return [...welcomeEntries].sort((a, b) => a.priority - b.priority)
}

功能入口说明:

  1. 课程中心 (/courses)

    • 对应底部Tab第二个入口
    • 浏览和购买课程
    • 查看学习进度
  2. 活动中心 (/activity)

    • 对应底部Tab第三个入口
    • 查看最新活动信息
    • 活动报名和参与
    • ⚠️ 特殊处理: 当前为外链跳转,跳转到 https://wxm.behalo.cc/pages/activity/activity
  3. 个人中心 (/profile)

    • 对应底部Tab第四个入口
    • 个人资料管理
    • 学习记录和设置

布局设计:

  • 🌌 环绕式布局: 3个图标像天体行星一样环绕排列
  • 📍 位置: 页面中下位置
  • 🎨 视觉效果: 星空宇宙主题,行星轨道感
  • 🖼️ 图标: 统一使用 /img/wecome_btn_1.png

步骤 2: 创建功能入口项组件

文件: src/components/welcome/WelcomeEntryItem.vue

<template>
  <div
    class="entry-item"
    :style="{ '--index': index }"
    @click="handleClick"
  >
    <!-- 图标 (图片) -->
    <div class="entry-icon">
      <img :src="entry.icon" :alt="entry.title" />
    </div>

    <!-- 标题 -->
    <div class="entry-title">
      {{ entry.title }}
    </div>

    <!-- 副标题 -->
    <div v-if="entry.subtitle" class="entry-subtitle">
      {{ entry.subtitle }}
    </div>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'

const props = defineProps({
  entry: {
    type: Object,
    required: true
  },
  index: {
    type: Number,
    required: true
  }
})

const router = useRouter()

const handleClick = () => {
  const redirect = new URLSearchParams(window.location.search).get('redirect')

  // 特殊处理:活动中心外链跳转
  if (props.entry.isExternal && props.entry.externalUrl) {
    window.open(props.entry.externalUrl, '_blank')
    return
  }

  // 正常路由跳转
  if (props.entry.route) {
    router.push(props.entry.route)
  } else if (redirect) {
    router.push(redirect)
  } else {
    router.push('/')
  }
}
</script>

<style scoped lang="less">
.entry-item {
  position: absolute;  // 绝对定位,形成环绕效果
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  cursor: pointer;
  user-select: none;
  transition: all 0.3s ease;

  // 呼吸动画
  animation: breathe 2.5s ease-in-out infinite;
  animation-delay: calc(var(--index) * 0.5s);

  // 环绕式布局位置计算
  &:nth-child(1) {
    // 左侧入口
    left: 0;
    bottom: 0;
  }

  &:nth-child(2) {
    // 顶部入口
    left: 50%;
    transform: translateX(-50%);
    bottom: 60%;
  }

  &:nth-child(3) {
    // 右侧入口
    right: 0;
    bottom: 0;
  }

  &:hover {
    animation-duration: 1s;

    .entry-icon img {
      transform: scale(1.15);
    }
  }

  &:active {
    transform: scale(0.95);
  }
}

@keyframes breathe {
  0%, 100% {
    opacity: 0.9;
  }
  50% {
    opacity: 1;
  }
}

.entry-icon {
  width: 80px;
  height: 80px;
  margin-bottom: 0.75rem;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
  transition: all 0.3s ease;

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.3s ease;
  }
}

.entry-title {
  font-size: 1.125rem;
  font-weight: 600;
  color: #fff;
  text-align: center;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
  margin-bottom: 0.25rem;
  white-space: nowrap;
}

.entry-subtitle {
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.8);
  text-align: center;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  white-space: nowrap;
}
</style>

步骤 3: 创建内容容器组件

文件: src/components/welcome/WelcomeContent.vue

<template>
  <div class="welcome-content">
    <!-- [待定] Logo/标题区 -->

    <!-- 功能入口网格 -->
    <div class="entries-grid">
      <WelcomeEntryItem
        v-for="(entry, index) in sortedEntries"
        :key="entry.id"
        :entry="entry"
        :index="index"
      />
    </div>

    <!-- [待定] CTA 按钮区 -->
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { getSortedEntries } from '@/config/welcomeEntries'
import WelcomeEntryItem from './WelcomeEntryItem.vue'

const sortedEntries = computed(() => getSortedEntries())
</script>

<style scoped lang="less">
.welcome-content {
  position: relative;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;  // 内容靠下,位于中下位置
  padding: 2rem;
  padding-bottom: 6rem;  // 距离底部有足够空间
}

.entries-grid {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: 500px;

  // 环绕式布局:3个图标呈弧形排列
  // 使用绝对定位模拟行星轨道效果
  &::before {
    content: '';
    position: absolute;
    width: 300px;
    height: 300px;
    border: 2px dashed rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    animation: rotate 60s linear infinite;
  }

  @media (min-width: 768px) {
    max-width: 700px;

    &::before {
      width: 450px;
      height: 450px;
    }
  }
}

// 旋转轨道动画
@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

第 5 阶段: 测试与优化 (优先级: 🟡 中)

功能测试清单

  • 首次访问正确跳转到欢迎页
  • 再次访问直接进入主页
  • ?reset_welcome=true 正确重置标志
  • 点击功能入口正确跳转
  • 视频背景正常循环播放
  • 视频加载失败时显示降级方案
  • 清除 localStorage 后重新访问

兼容性测试

  • iOS Safari (video autoplay, playsinline)
  • Android Chrome
  • 微信内置浏览器
  • PC 端浏览器 (Chrome, Firefox, Safari, Edge)

性能测试

  • 视频首次加载时间 < 3s
  • 动画帧率 > 50fps
  • 内存占用正常
  • CPU 占用正常

边界测试

  • 视频加载失败时降级为静态图
  • 网络慢速时的表现
  • 清除缓存后的行为
  • URL 参数异常处理

第 6 阶段: 文档与部署 (优先级: 🟢 低)

步骤 1: 更新项目文档

CLAUDE.md 添加说明:

### 欢迎页

- **功能**: 用户首次进入时的引导页面,展示核心功能入口
- **路由**: `/welcome`
- **开关**: `VITE_WELCOME_PAGE_ENABLED=true`
- **首次检测**: 使用 localStorage 标志 `has_visited_welcome`
- **调试方法**:
  - URL 参数 `?reset_welcome=true` 重置标志
  - 控制台 `window.resetWelcomeFlag()` 重置标志
  - 控制台 `window.showWelcome()` 跳转欢迎页

步骤 2: 创建使用说明

文件: docs/welcome-page-guide.md

# 欢迎页使用指南

## 功能说明

欢迎页是用户首次进入平台时的引导页面,展示核心功能入口。

## 开发配置

### 环境变量

```bash
# 启用欢迎页
VITE_WELCOME_PAGE_ENABLED=true

# 视频资源
VITE_WELCOME_VIDEO_URL=https://cdn.ipadbiz.cn/mlaj/video/welcome-background.mp4
VITE_WELCOME_VIDEO_POSTER=https://cdn.ipadbiz.cn/mlaj/images/welcome-poster.jpg

功能入口配置

编辑 src/config/welcomeEntries.js:

export const welcomeEntries = [
  {
    id: 'courses',
    title: '课程中心',
    subtitle: '探索精选课程',
    icon: '📚',
    route: '/courses',
    color: '#4CAF50',
    priority: 1
  }
  // ... 更多入口
]

调试方法

重置欢迎页标志

方法 1: URL 参数

http://localhost:5173/?reset_welcome=true

方法 2: 控制台

window.resetWelcomeFlag()
location.reload()

直接访问欢迎页

window.showWelcome()

上传新的背景视频

使用上传工具

当前视频文件: docs/plan/26.1.28-欢迎页开发计划/video/welcome-bg.mp4

# 上传欢迎页背景视频
pnpm run upload:qiniu video/welcome-bg.mp4 mlaj/video/welcome-bg.mp4

# 使用代理上传
USE_PROXY=true pnpm run upload:qiniu video/welcome-bg.mp4 mlaj/video/welcome-bg.mp4

视频规格建议

  • 分辨率: 1920x1080 (1080p)
  • 编码格式: H.264
  • 时长: 10-20秒循环
  • 文件大小: < 10MB

步骤 3: 部署到开发环境

# 构建
pnpm build

# 部署到开发服务器
pnpm dev_upload

步骤 4: 测试生产环境

  • 验证视频 CDN 加载速度
  • 验证首次访问逻辑
  • 验证兼容性
  • 收集用户反馈

技术要点

1. 视频播放兼容性

<video
  autoplay
  loop
  muted
  playsinline
  webkit-playsinline
  x5-video-player-type="h5"
  x5-video-player-fullscreen="true"
>

关键属性说明:

  • muted: 静音播放(移动端自动播放必需)
  • playsinline: iOS 内联播放
  • webkit-playsinline: iOS Safari 兼容
  • x5-video-player-type: 腾讯 X5 内核(微信/QQ浏览器)

2. 动画性能优化

// 使用 CSS 动画而非 JS 动画
animation: breathe 2.5s ease-in-out infinite;

// 使用 transform 而非 width/height
transform: scale(1.08);

// 使用 GPU 加速
will-change: transform, opacity;

3. 七牛云视频处理 - 自动生成封面图

封面图自动生成:

// VideoBackground 组件内部实现
const posterUrl = computed(() => {
  if (props.poster) {
    return props.poster
  }
  // 从视频 URL 自动生成封面图
  return `${props.videoSrc}?vframe/jpg/offset/0.001`
})

七牛云视频帧处理参数说明:

高级参数 (可选):

// 指定截取时间点 (第 1 秒)
https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/1

// 指定输出尺寸 (1920x1080)
https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/0.001/w/1920/h/1080

// 指定图片质量 (质量 85)
https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/0.001/quality/85

// 组合使用
https://cdn.ipadbiz.cn/mlaj/video/welcome-bg.mp4?vframe/jpg/offset/0.001/w/1920/h/1080/quality/85

优势:

  • ✅ 只需上传一个视频文件,无需单独准备封面图
  • ✅ 封面图与视频内容一致,不会出现不匹配
  • ✅ 减少资源管理成本
  • ✅ 支持动态调整截取时间和图片质量

4. 降级方案

<!-- 视频加载失败时显示静态背景图 -->
<div
  v-if="showFallback"
  class="video-fallback"
  :style="{ backgroundImage: `url(${posterUrl})` }"
></div>

降级触发条件:

  1. 视频加载失败 (网络错误、格式不支持)
  2. 视频自动播放被阻止 (iOS Safari 政策)
  3. 视频解码失败

降级方案:

  • 使用自动生成的封面图作为静态背景
  • 保证用户始终能看到背景内容

风险与注意事项

技术风险

  1. 视频播放兼容性

    • iOS Safari 可能禁止自动播放
    • 解决方案: 添加 muted 属性,提供降级方案
  2. 首次访问标志位

    • 清除 localStorage 后会再次显示
    • 解决方案: 文档说明,未来升级为后端接口
  3. 视频加载性能

    • 文件过大导致加载缓慢
    • 解决方案:
      • 限制文件大小 < 10MB
      • 使用七牛云自动生成封面图作为 loading 状态
      • 支持封面图质量参数优化加载速度
      • 考虑使用 CDN 加速
  4. 七牛云代理问题

    • 需要挂代理才能访问
    • 解决方案: 支持 USE_PROXY 环境变量

业务风险

  1. 功能入口待确认

    • 先实现框架,入口配置化
  2. 动效性能影响

    • 使用 CSS 动画,提供环境变量控制

优先级总结

第 1 批 (核心功能) 🔴

  • ✅ 第 0 阶段: 准备工作
  • ✅ 第 1 阶段: 通用上传工具
  • ✅ 第 2 阶段: VideoBackground 组件
  • ✅ 第 3 阶段: 路由与首次访问逻辑

第 2 批 (功能完善) 🟡

  • ⏳ 第 4 阶段: WelcomeContent 组件
  • ⏳ 第 5 阶段: 测试与优化

第 3 批 (收尾工作) 🟢

  • ⏳ 第 6 阶段: 文档与部署

待确认事项

  1. 背景视频文件 - video/welcome-bg.mp4 已添加并上传到七牛云
  2. 页面效果图 - 需要设计稿确认布局
  3. 功能入口列表 - 已确定3个入口(课程、活动、我的)
  4. 功能图标 - 已确定使用 /img/wecome_btn_1.png
  5. 页面布局细节 - 顶部/底部是否需要其他元素(Logo、标语等)

建议: 先完成技术框架和上传工具,等设计稿确认后再填充内容。


文档创建时间: 2026-01-28 最后更新: 2026-01-28