=> {
await delay(300);
const index = mockData.storyboards.findIndex((s) => s.id === id);
if (index !== -1) {
mockData.storyboards.splice(index, 1);
}
},
};
```
### 6.4 API/Mock 自动切换
```typescript
// src/services/api/index.ts
import { storyboardApi } from "./storyboards";
import { mockStoryboardApi } from "../mock/mockClient";
const useMock = import.meta.env.VITE_USE_MOCK === "true";
export const api = {
storyboards: useMock ? mockStoryboardApi : storyboardApi,
// ... 其他模块
};
```
---
## 7. 样式方案
### 7.1 Tailwind CSS v4 配置
**重要变化**:Tailwind v4 不再使用 `tailwind.config.js`,改用 CSS 中的 `@theme` 指令配置。
#### 7.1.1 Vite 插件配置
```typescript
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
```
#### 7.1.2 主题配置(在 CSS 中)
```css
/* src/styles/globals.css */
@import "tailwindcss";
@theme {
/* 文字体系 */
--color-text-primary: var(--text-primary);
--color-text-regular: var(--text-regular);
--color-text-secondary: var(--text-secondary);
--color-text-tertiary: var(--text-tertiary);
--color-text-placeholder: var(--text-placeholder);
/* 边框体系 */
--color-border-dark: var(--border-dark);
--color-border-base: var(--border-base);
--color-border-light: var(--border-light);
--color-border-lighter: var(--border-lighter);
/* 背景体系 */
--color-bg-page: var(--bg-page);
--color-bg-overlay: var(--bg-overlay);
--color-bg-element: var(--bg-element);
--color-fill-default: var(--fill-default);
--color-fill-darker: var(--fill-darker);
--color-fill-lighter: var(--fill-lighter);
/* 品牌色 */
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-hover: var(--primary-hover);
--color-primary-active: var(--primary-active);
/* 功能色 */
--color-success: var(--success);
--color-warning: var(--warning);
--color-error: var(--error);
--color-info: var(--info);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
/* 轨道颜色 */
--color-track-storyboard: var(--track-storyboard);
--color-track-resource: var(--track-resource);
--color-track-video: var(--track-video);
--color-track-sound: var(--track-sound);
--color-track-subtitle: var(--track-subtitle);
--color-track-voice: var(--track-voice);
/* 圆角 */
--radius-sm: 0.125rem;
--radius: 0.5rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
/* 动画 */
--animate-fade-in: fadeIn 0.3s ease-in-out;
--animate-slide-in-up: slideInUp 0.4s ease-out;
--animate-scale-in: scaleIn 0.2s ease-out;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
}
```
### 7.2 Pro Studio 主题(深色/亮色)
```css
/* 深色模式 (默认) */
:root {
color-scheme: dark;
/* 文字体系 */
--text-primary: #fafafc;
--text-regular: #e4e4e7;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-placeholder: #71717a;
/* 边框体系 */
--border-dark: #52525b;
--border-base: #3f3f46;
--border-light: #27272a;
--border-lighter: #18181b;
/* 背景体系 */
--bg-page: #09090b;
--bg-overlay: #18181b;
--bg-element: #18181b;
--fill-default: #27272a;
--fill-darker: #121214;
--fill-lighter: #3f3f46;
/* 品牌色 */
--primary: #3b82f6;
--primary-foreground: #ffffff;
--primary-hover: #2563eb;
--primary-active: #1d4ed8;
/* 功能色 */
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
--destructive: #7f1d1d;
--destructive-foreground: #fef2f2;
/* 轨道颜色 */
--track-storyboard: #60a5fa;
--track-resource: #a78bfa;
--track-video: #34d399;
--track-sound: #fbbf24;
--track-subtitle: #f87171;
--track-voice: #22d3ee;
}
/* 亮色模式 */
.light {
color-scheme: light;
--text-primary: #09090b;
--text-regular: #18181b;
--text-secondary: #71717a;
--text-placeholder: #a1a1aa;
--border-dark: #a1a1aa;
--border-base: #e4e4e7;
--border-light: #f4f4f5;
--border-lighter: #fafafa;
--bg-page: #f4f4f5;
--bg-overlay: #ffffff;
--bg-element: #ffffff;
--fill-default: #f4f4f5;
--fill-darker: #fafafa;
--fill-lighter: #e4e4e7;
--primary: #2563eb;
--primary-foreground: #ffffff;
--primary-hover: #1d4ed8;
--primary-active: #1e40af;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
}
```
}
@layer base {
- {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
}
/_ 自定义滚动条 _/
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted)) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted));
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground));
}
}
````
### 7.3 CSS 工具类规范
```typescript
// src/lib/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
````
使用示例:
```tsx
import { cn } from "@/lib/utils";
function Button({ className, variant, ...props }) {
return (
);
}
```
---
## 8. 国际化(i18n)
### 8.1 技术方案
使用 `react-i18next` 实现多语言支持。
```bash
npm install react-i18next i18next i18next-browser-languagedetector i18next-http-backend
```
### 8.2 i18n 配置
```typescript
// src/lib/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
// 支持的语言
supportedLngs: ["zh-CN", "en-US", "zh-TW"],
// 默认语言
fallbackLng: "zh-CN",
// 默认命名空间
defaultNS: "common",
// 命名空间列表
ns: ["common", "editor", "settings", "errors"],
// 语言检测配置
detection: {
order: ["localStorage", "navigator"],
caches: ["localStorage"],
lookupLocalStorage: "i18nextLng",
},
// 后端配置
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
// React 配置
react: {
useSuspense: true,
},
// 插值配置
interpolation: {
escapeValue: false,
},
});
export default i18n;
```
### 8.3 语言文件结构
```
public/
└── locales/
├── zh-CN/ # 简体中文(默认)
│ ├── common.json
│ ├── editor.json
│ ├── settings.json
│ └── errors.json
├── en-US/ # 英文
│ ├── common.json
│ ├── editor.json
│ ├── settings.json
│ └── errors.json
└── zh-TW/ # 繁体中文
├── common.json
├── editor.json
├── settings.json
└── errors.json
```
### 8.4 语言文件示例
```json
// public/locales/zh-CN/common.json
{
"app": {
"name": "Jointo",
"tagline": "AI 驱动的视频制作平台"
},
"nav": {
"home": "首页",
"projects": "项目",
"settings": "设置",
"help": "帮助"
},
"actions": {
"save": "保存",
"cancel": "取消",
"delete": "删除",
"edit": "编辑",
"create": "创建",
"export": "导出",
"import": "导入"
},
"messages": {
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"confirmDelete": "确定要删除吗?"
}
}
```
```json
// public/locales/en-US/common.json
{
"app": {
"name": "Jointo",
"tagline": "AI-Powered Video Production Platform"
},
"nav": {
"home": "Home",
"projects": "Projects",
"settings": "Settings",
"help": "Help"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"export": "Export",
"import": "Import"
},
"messages": {
"saveSuccess": "Saved successfully",
"saveFailed": "Failed to save",
"confirmDelete": "Are you sure you want to delete?"
}
}
```
### 8.5 使用方式
```tsx
// 在组件中使用
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation();
return (
{t('app.name')}
);
}
// 使用多个命名空间
function EditorComponent() {
const { t } = useTranslation(['editor', 'common']);
return (
{t('editor:title')}
);
}
// 带参数的翻译
// JSON: "welcome": "欢迎, {{name}}!"
{t('welcome', { name: 'John' })}
// 复数处理
// JSON: "items": "{{count}} 个项目", "items_plural": "{{count}} 个项目"
{t('items', { count: 5 })}
```
### 8.6 语言切换
```typescript
// src/stores/settingsStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import i18n from "@/lib/i18n";
type Language = "zh-CN" | "en-US" | "zh-TW";
interface SettingsState {
language: Language;
setLanguage: (lang: Language) => void;
}
export const useSettingsStore = create()(
persist(
(set) => ({
language: "zh-CN",
setLanguage: (lang) => {
i18n.changeLanguage(lang);
set({ language: lang });
},
}),
{
name: "settings-store",
},
),
);
```
---
## 9. 主题系统
### 9.1 技术方案
使用 CSS 变量 + class 切换实现主题系统,支持深色、亮色和跟随系统。
### 9.2 主题 Store
```typescript
// src/stores/themeStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
type Theme = "dark" | "light" | "system";
type ResolvedTheme = "dark" | "light";
interface ThemeState {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
}
// 获取系统主题偏好
const getSystemTheme = (): ResolvedTheme => {
if (typeof window === "undefined") return "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
// 应用主题到 DOM
const applyTheme = (theme: ResolvedTheme) => {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
};
export const useThemeStore = create()(
persist(
(set, get) => ({
theme: "dark",
resolvedTheme: "dark",
setTheme: (theme) => {
const resolvedTheme = theme === "system" ? getSystemTheme() : theme;
applyTheme(resolvedTheme);
set({ theme, resolvedTheme });
},
}),
{
name: "theme-store",
onRehydrateStorage: () => (state) => {
if (state) {
const resolvedTheme =
state.theme === "system" ? getSystemTheme() : state.theme;
applyTheme(resolvedTheme);
state.resolvedTheme = resolvedTheme;
}
},
},
),
);
// 监听系统主题变化
if (typeof window !== "undefined") {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
const { theme, setTheme } = useThemeStore.getState();
if (theme === "system") {
setTheme("system"); // 重新计算 resolvedTheme
}
});
}
```
### 9.3 Theme Provider
```tsx
// src/app/providers/ThemeProvider.tsx
import { useEffect } from "react";
import { useThemeStore } from "@/stores/themeStore";
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: "dark" | "light" | "system";
}
export function ThemeProvider({
children,
defaultTheme = "dark",
}: ThemeProviderProps) {
const { theme, setTheme } = useThemeStore();
// 初始化主题
useEffect(() => {
// 防止首次加载时的闪烁
const savedTheme = localStorage.getItem("theme-store");
if (!savedTheme) {
setTheme(defaultTheme);
}
}, []);
// 监听系统主题变化
useEffect(() => {
if (theme !== "system") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => setTheme("system");
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme, setTheme]);
return <>{children}>;
}
```
### 9.4 防止主题闪烁(FOUC)
在 `index.html` 的 `` 中添加内联脚本:
```html
```
### 9.5 主题切换组件
```tsx
// src/components/common/ThemeToggle.tsx
import { Moon, Sun, Monitor } from "lucide-react";
import { useThemeStore } from "@/stores/themeStore";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslation } from "react-i18next";
export function ThemeToggle() {
const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useThemeStore();
return (
setTheme("light")}>
{t("settings.theme.light")}
setTheme("dark")}>
{t("settings.theme.dark")}
setTheme("system")}>
{t("settings.theme.system")}
);
}
```
### 9.6 Tailwind 深色模式配置
确保 `tailwind.config.js` 中配置了 class 策略:
```javascript
// tailwind.config.js
module.exports = {
darkMode: ["class"], // 使用 class 策略
// ...
};
```
---
## 10. 性能优化
### 8.1 代码分割
```typescript
// 路由级代码分割
const EditorPage = lazy(() => import('@/pages/EditorPage'));
// 组件级代码分割
const HeavyComponent = lazy(() => import('@components/features/HeavyComponent'));
// 使用时包裹 Suspense
}>
```
### 8.2 虚拟列表
使用 `@tanstack/react-virtual` 处理长列表:
```typescript
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // 预估每项高度
overscan: 5, // 预渲染数量
});
return (
{virtualizer.getVirtualItems().map((virtualItem) => (
))}
);
}
```
### 8.3 组件优化
```typescript
// 使用 memo 避免不必要渲染
const StoryboardItem = memo(function StoryboardItem({
storyboard,
isSelected,
onSelect
}: StoryboardItemProps) {
return (
onSelect(storyboard.id)}
>
{/* ... */}
);
});
// 使用 useMemo 缓存计算结果
const sortedStoryboards = useMemo(
() => storyboards.sort((a, b) => a.order - b.order),
[storyboards]
);
// 使用 useCallback 缓存回调
const handleSelect = useCallback((id: string) => {
selectStoryboard(id);
}, [selectStoryboard]);
```
### 8.4 图片优化
```typescript
// 图片懒加载组件
function LazyImage({ src, alt, className }: LazyImageProps) {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && imgRef.current) {
imgRef.current.src = src;
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [src]);
return (
{!loaded &&
}
![{alt}]()
setLoaded(true)}
/>
);
}
```
### 8.5 Debounce 和 Throttle
```typescript
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// 使用示例
function SearchInput() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
useEffect(() => {
if (debouncedSearch) {
// 执行搜索
}
}, [debouncedSearch]);
return setSearch(e.target.value)} />;
}
```
---
## 11. 测试策略
> **注意**:测试工具配置尚未在项目中实现,以下内容作为未来规划参考。
### 11.1 测试金字塔(规划)
```
┌──────────┐
│ E2E │ 少量关键流程
│ Tests │
┌┴──────────┴┐
│Integration │ 主要业务流程
│ Tests │
┌┴────────────┴┐
│ Unit Tests │ 工具函数、Hooks、组件
└──────────────┘
```
### 9.2 单元测试(Vitest)
```typescript
// src/utils/format.test.ts
import { describe, it, expect } from "vitest";
import { formatDuration, formatFileSize } from "./format";
describe("formatDuration", () => {
it("formats seconds to mm:ss", () => {
expect(formatDuration(65)).toBe("01:05");
expect(formatDuration(3661)).toBe("01:01:01");
});
it("handles zero", () => {
expect(formatDuration(0)).toBe("00:00");
});
});
describe("formatFileSize", () => {
it("formats bytes correctly", () => {
expect(formatFileSize(1024)).toBe("1 KB");
expect(formatFileSize(1048576)).toBe("1 MB");
});
});
```
### 9.3 组件测试
```typescript
// src/components/features/storyboard/StoryboardItem.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { StoryboardItem } from './StoryboardItem';
const mockStoryboard = {
id: '1',
title: 'Test Storyboard',
description: 'Test description',
thumbnailUrl: '/test.jpg',
duration: 5,
order: 0,
};
describe('StoryboardItem', () => {
it('renders storyboard info', () => {
render(
{}}
/>
);
expect(screen.getByText('Test Storyboard')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('calls onSelect when clicked', () => {
const onSelect = vi.fn();
render(
);
fireEvent.click(screen.getByRole('button'));
expect(onSelect).toHaveBeenCalledWith('1');
});
it('applies selected styles', () => {
render(
{}}
/>
);
expect(screen.getByRole('button')).toHaveClass('border-primary');
});
});
```
### 9.4 Hook 测试
```typescript
// src/hooks/useDebounce.test.ts
import { describe, it, expect, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useDebounce } from "./useDebounce";
describe("useDebounce", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("returns initial value immediately", () => {
const { result } = renderHook(() => useDebounce("hello", 500));
expect(result.current).toBe("hello");
});
it("debounces value changes", () => {
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 500),
{ initialProps: { value: "hello" } },
);
rerender({ value: "world" });
expect(result.current).toBe("hello");
act(() => {
vi.advanceTimersByTime(500);
});
expect(result.current).toBe("world");
});
});
```
### 9.5 E2E 测试(Playwright)
```typescript
// e2e/editor.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Editor Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/editor/project-1");
});
test("displays storyboard list", async ({ page }) => {
await expect(page.getByTestId("storyboard-panel")).toBeVisible();
await expect(page.getByTestId("storyboard-item")).toHaveCount(5);
});
test("can select a storyboard", async ({ page }) => {
const firstItem = page.getByTestId("storyboard-item").first();
await firstItem.click();
await expect(firstItem).toHaveClass(/selected/);
});
test("plays video on play button click", async ({ page }) => {
await page.getByRole("button", { name: "播放" }).click();
await expect(page.getByRole("button", { name: "暂停" })).toBeVisible();
});
test("timeline zoom works", async ({ page }) => {
const zoomIn = page.getByRole("button", { name: "放大" });
await zoomIn.click();
// 验证缩放效果
});
});
```
---
## 12. 部署方案
### 10.1 构建产物
```bash
# 生产构建
npm run build
# 产物目录结构
dist/
├── index.html
├── assets/
│ ├── index-[hash].js
│ ├── index-[hash].css
│ └── images/
└── favicon.ico
```
### 10.2 部署配置
#### Nginx 配置
```nginx
server {
listen 80;
server_name www.jointo.ai;
root /var/www/jointo/dist;
index index.html;
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
# 静态资源缓存
location /assets {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA 路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
#### Docker 配置
```dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### 10.3 CI/CD Pipeline
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
env:
VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}
- name: Deploy to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
source: "dist/*"
target: "/var/www/jointo/"
```
---
## 13. 开发规范
### 11.1 文件命名规范
| 类型 | 命名规范 | 示例 |
| --------- | ------------- | ------------------------- |
| 组件文件 | PascalCase | `StoryboardItem.tsx` |
| Hook 文件 | camelCase | `useStoryboards.ts` |
| 工具文件 | camelCase | `formatDuration.ts` |
| 类型文件 | camelCase | `storyboard.ts` |
| 常量文件 | camelCase | `routes.ts` |
| 样式文件 | kebab-case | `globals.css` |
| 测试文件 | 原文件名.test | `StoryboardItem.test.tsx` |
### 11.2 组件编写规范
```typescript
// 组件文件结构
import { useState, useCallback, memo } from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import type { Storyboard } from '@types';
// Props 类型定义
interface StoryboardItemProps {
storyboard: Storyboard;
isSelected: boolean;
onSelect: (id: string) => void;
className?: string;
}
// 组件实现
export const StoryboardItem = memo(function StoryboardItem({
storyboard,
isSelected,
onSelect,
className,
}: StoryboardItemProps) {
// Hooks
const [isHovered, setIsHovered] = useState(false);
// 事件处理
const handleClick = useCallback(() => {
onSelect(storyboard.id);
}, [storyboard.id, onSelect]);
// 渲染
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{storyboard.title}
{storyboard.description}
);
});
```
### 11.3 导入顺序规范
```typescript
// 1. React 相关
import { useState, useEffect, useCallback } from "react";
// 2. 第三方库
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
// 3. 内部别名导入 - 组件
import { Button } from "@components/ui/button";
import { LoadingSpinner } from "@components/common";
// 4. 内部别名导入 - 其他
import { useStoryboards } from "@hooks/api/useStoryboards";
import { useEditorStore } from "@stores/editorStore";
import { cn } from "@/lib/utils";
// 5. 类型导入
import type { Storyboard } from "@types";
// 6. 相对路径导入
import { StoryboardItem } from "./StoryboardItem";
import "./StoryboardPanel.css";
```
### 11.4 Git Commit 规范
```
():