Files
ViGent2/Docs/FRONTEND_DEV.md
Kevin Wong 661a8f357c 更新
2026-01-29 12:16:41 +08:00

6.0 KiB
Raw Blame History

前端开发规范

目录结构

frontend/src/
├── app/                # Next.js App Router 页面
│   ├── page.tsx        # 首页(视频生成)
│   ├── publish/        # 发布页面
│   ├── admin/          # 管理员页面
│   ├── login/          # 登录页面
│   └── register/       # 注册页面
├── lib/                # 公共工具函数
│   ├── axios.ts        # Axios 实例(含 401/403 拦截器)
│   └── auth.ts         # 认证相关函数
└── proxy.ts            # 路由代理(原 middleware

iOS Safari 安全区域兼容

问题

iPhone Safari 浏览器顶部(刘海/灵动岛和底部Home 指示条)有安全区域,默认情况下页面背景不会延伸到这些区域,导致白边。

解决方案(三层配合)

1. Viewport 配置 (layout.tsx)

import type { Viewport } from "next";

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  viewportFit: 'cover',    // 允许内容延伸到安全区域
  themeColor: '#0f172a',   // 顶部状态栏颜色(与背景一致)
};

2. 全局背景统一到 body (layout.tsx)

<html lang="en" style={{ backgroundColor: '#0f172a' }}>
  <body
    style={{
      margin: 0,
      minHeight: '100dvh',  // 使用 dvh 而非 vh
      background: 'linear-gradient(to bottom, #0f172a 0%, #0f172a 5%, #581c87 50%, #0f172a 95%, #0f172a 100%)',
    }}
  >
    {children}
  </body>
</html>

3. CSS 安全区域支持 (globals.css)

html {
  background-color: #0f172a !important;
  min-height: 100%;
}

body {
  margin: 0 !important;
  min-height: 100dvh;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

关键要点

  • 渐变背景放 body不放页面 div - 安全区域在 div 之外
  • 使用 100dvh 而非 100vh - dvh 是动态视口高度,适配移动端
  • themeColor 与背景边缘色一致 - 避免状态栏色差
  • 页面 div 移除独立背景 - 使用透明,继承 body 渐变

移动端响应式规范

Header 按钮布局

// 移动端紧凑,桌面端宽松
<div className="flex items-center gap-1 sm:gap-4">
  <button className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base ...">
    按钮
  </button>
</div>

常用响应式断点

断点 宽度 用途
默认 < 640px 移动端
sm: ≥ 640px 平板/桌面
lg: ≥ 1024px 大屏桌面

API 请求规范

必须使用 api (axios 实例)

所有需要认证的 API 请求必须使用 @/lib/axios 导出的 axios 实例。该实例已配置:

  • 自动携带 credentials: include
  • 遇到 401/403 时自动清除 cookie 并跳转登录页

使用方式:

import api from '@/lib/axios';

// GET 请求
const { data } = await api.get('/api/materials');

// POST 请求
const { data } = await api.post('/api/videos/generate', {
    text: '...',
    voice: '...',
});

// DELETE 请求
await api.delete(`/api/materials/${id}`);

// 带上传进度的文件上传
await api.post('/api/materials', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    onUploadProgress: (e) => {
        if (e.total) {
            const progress = Math.round((e.loaded / e.total) * 100);
            setProgress(progress);
        }
    },
});

SWR 配合使用

import api from '@/lib/axios';

// SWR fetcher 使用 axios
const fetcher = (url: string) => api.get(url).then(res => res.data);

const { data } = useSWR('/api/xxx', fetcher, { refreshInterval: 2000 });

日期格式化规范

禁止使用 toLocaleString()

toLocaleString() 在服务端和客户端可能返回不同格式,导致 Hydration 错误。

错误示例:

// ❌ 会导致 Hydration 错误
new Date(timestamp * 1000).toLocaleString('zh-CN')

正确做法:

// ✅ 使用固定格式
const formatDate = (timestamp: number) => {
    const d = new Date(timestamp * 1000);
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const hour = String(d.getHours()).padStart(2, '0');
    const minute = String(d.getMinutes()).padStart(2, '0');
    return `${year}/${month}/${day} ${hour}:${minute}`;
};

新增页面 Checklist

  1. 导入 import api from '@/lib/axios'
  2. 所有 API 请求使用 api.get/post/delete() 而非原生 fetch
  3. 日期格式化使用固定格式函数,不用 toLocaleString()
  4. 添加 'use client' 指令(如需客户端交互)

声音克隆 (Voice Clone) 功能

API 端点

接口 方法 功能
/api/ref-audios POST 上传参考音频 (multipart/form-data: file + ref_text)
/api/ref-audios GET 列出用户的参考音频
/api/ref-audios/{id} DELETE 删除参考音频 (id 需 encodeURIComponent)

视频生成 API 扩展

// EdgeTTS 模式 (默认)
await api.post('/api/videos/generate', {
    material_path: '...',
    text: '口播文案',
    tts_mode: 'edgetts',
    voice: 'zh-CN-YunxiNeural',
});

// 声音克隆模式
await api.post('/api/videos/generate', {
    material_path: '...',
    text: '口播文案',
    tts_mode: 'voiceclone',
    ref_audio_id: 'user_id/timestamp_name.wav',
    ref_text: '参考音频对应文字',
});

在线录音

使用 MediaRecorder API 录制音频,格式为 audio/webm,上传后后端自动转换为 WAV (16kHz mono)。

// 录音需要用户授权麦克风
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });

UI 结构

配音方式使用 Tab 切换:

  • EdgeTTS 音色 - 预设音色 2x3 网格
  • 声音克隆 - 参考音频列表 + 在线录音 + 参考文字输入