diff --git a/apps/web/src/pages/my-templates/MyTemplates.tsx b/apps/web/src/pages/my-templates/MyTemplates.tsx
index cf60fafea..bacf0e9f4 100644
--- a/apps/web/src/pages/my-templates/MyTemplates.tsx
+++ b/apps/web/src/pages/my-templates/MyTemplates.tsx
@@ -3,126 +3,43 @@
* 卡片视图展示用户已保存的剪辑模板
* 支持搜索、分类筛选、编辑/复制/删除/使用模板生成
*/
-import React, { useState } from "react"
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import {
- Typography,
- Card,
- Input,
- Select,
- Tag,
- Button,
- Space,
- Empty,
- Spin,
- Tooltip,
- message,
- Popconfirm,
- Row,
- Col,
-} from "antd"
+import React from "react"
+import { Typography, Input, Select, Button, Empty, Spin, Row, Col } from "antd"
import {
SearchOutlined,
- EditOutlined,
- CopyOutlined,
- DeleteOutlined,
- VideoCameraOutlined,
AppstoreOutlined,
PlusOutlined,
} from "@ant-design/icons"
import { useNavigate } from "react-router-dom"
-import {
- getEditingTemplates,
- getTemplateCategories,
- deleteEditingTemplate,
- createEditingTemplate,
- MODE_LABELS,
- MODE_COLORS,
- type EditingTemplate,
- type TemplateMode,
-} from "@/api/editing-planner"
+import type { EditingTemplate } from "@/api/editing-planner"
+import { useMyTemplates } from "./hooks/useMyTemplates"
+import { TemplateCard } from "./components/TemplateCard"
import "./MyTemplates.css"
const { Title, Text } = Typography
const MyTemplates: React.FC = () => {
const navigate = useNavigate()
- const queryClient = useQueryClient()
+ const {
+ searchText,
+ setSearchText,
+ filterCategory,
+ setFilterCategory,
+ templates,
+ categories,
+ isLoading,
+ handleCopy,
+ handleDelete,
+ } = useMyTemplates()
- const [searchText, setSearchText] = useState("")
- const [filterCategory, setFilterCategory] = useState("")
-
- /* ── 数据查询 ── */
- const { data: templates = [], isLoading } = useQuery({
- queryKey: ["editing-templates", filterCategory, searchText],
- queryFn: () =>
- getEditingTemplates({
- category: filterCategory || undefined,
- tag: searchText || undefined,
- }),
- })
-
- const { data: categories = [] } = useQuery({
- queryKey: ["template-categories"],
- queryFn: getTemplateCategories,
- })
-
- /* ── Mutations ── */
- const deleteMutation = useMutation({
- mutationFn: deleteEditingTemplate,
- onSuccess: () => {
- message.success("模板已删除")
- queryClient.invalidateQueries({ queryKey: ["editing-templates"] })
- },
- onError: (err: unknown) => {
- if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("删除失败")
- },
- })
-
- const copyMutation = useMutation({
- mutationFn: (tpl: EditingTemplate) =>
- createEditingTemplate({
- name: `${tpl.name}(副本)`,
- mode: tpl.mode,
- category: tpl.category,
- tags: tpl.tags,
- title_config: tpl.title_config,
- subtitle_config: tpl.subtitle_config,
- bgm_config: tpl.bgm_config,
- estimated_duration:
- tpl.estimated_duration ??
- Math.round(
- tpl.segments.reduce((s, seg) => s + (seg.duration_min + seg.duration_max) / 2, 0),
- ),
- segments: tpl.segments.map(({ id: _id, ...rest }) => rest),
- }),
- onSuccess: () => {
- message.success("模板已复制")
- queryClient.invalidateQueries({ queryKey: ["editing-templates"] })
- },
- onError: (err: unknown) => {
- if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("复制失败")
- },
- })
-
- /* ── 操作 ── */
const handleEdit = (tpl: EditingTemplate) => {
navigate(`/editing-planner?template=${tpl.id}`)
}
const handleGenerate = (tpl: EditingTemplate) => {
- // 跳转到智能剪辑页面,统一从智能剪辑出片
navigate(`/generate?templateId=${tpl.id}`)
}
- const handleCopy = (tpl: EditingTemplate) => {
- copyMutation.mutate(tpl)
- }
-
- const handleDelete = (id: string) => {
- deleteMutation.mutate(id)
- }
-
return (
{/* 页面头部 */}
@@ -179,69 +96,13 @@ const MyTemplates: React.FC = () => {
{templates.map((tpl) => (
-
- handleEdit(tpl)} />
- ,
-
- handleCopy(tpl)} />
- ,
-
- handleGenerate(tpl)} />
- ,
- handleDelete(tpl.id)}
- okText="删除"
- cancelText="取消"
- >
-
-
-
- ,
- ]}
- >
-
-
- {tpl.name}
-
-
- {MODE_LABELS[tpl.mode as TemplateMode] || tpl.mode}
-
- 用户自制
-
-
-
-
- {tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
-
- {tpl.category && (
- {tpl.category}
- )}
-
-
- {tpl.tags.length > 0 && (
-
- {tpl.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
-
-
-
- {tpl.title_config.ai_auto_select && AI标题}
- {tpl.subtitle_config.enabled && 字幕}
- {tpl.bgm_config.enabled && BGM}
-
-
-
+
))}
diff --git a/apps/web/src/pages/my-templates/components/TemplateCard.tsx b/apps/web/src/pages/my-templates/components/TemplateCard.tsx
new file mode 100644
index 000000000..2979228c3
--- /dev/null
+++ b/apps/web/src/pages/my-templates/components/TemplateCard.tsx
@@ -0,0 +1,92 @@
+import React from "react"
+import { Card, Tag, Tooltip, Popconfirm, Space, Typography } from "antd"
+import { EditOutlined, CopyOutlined, DeleteOutlined, VideoCameraOutlined } from "@ant-design/icons"
+import {
+ MODE_LABELS,
+ MODE_COLORS,
+ type EditingTemplate,
+ type TemplateMode,
+} from "@/api/editing-planner"
+
+const { Text } = Typography
+
+interface TemplateCardProps {
+ tpl: EditingTemplate
+ onEdit: (tpl: EditingTemplate) => void
+ onCopy: (tpl: EditingTemplate) => void
+ onGenerate: (tpl: EditingTemplate) => void
+ onDelete: (id: string) => void
+}
+
+/**
+ * 单个模板卡片组件
+ */
+export const TemplateCard: React.FC
= ({
+ tpl,
+ onEdit,
+ onCopy,
+ onGenerate,
+ onDelete,
+}) => (
+
+ onEdit(tpl)} />
+ ,
+
+ onCopy(tpl)} />
+ ,
+
+ onGenerate(tpl)} />
+ ,
+ onDelete(tpl.id)}
+ okText="删除"
+ cancelText="取消"
+ >
+
+
+
+ ,
+ ]}
+ >
+
+
+ {tpl.name}
+
+
+ {MODE_LABELS[tpl.mode as TemplateMode] || tpl.mode}
+
+ 用户自制
+
+
+
+
+ {tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
+
+ {tpl.category && {tpl.category}}
+
+
+ {tpl.tags.length > 0 && (
+
+ {tpl.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+ {tpl.title_config.ai_auto_select && AI标题}
+ {tpl.subtitle_config.enabled && 字幕}
+ {tpl.bgm_config.enabled && BGM}
+
+
+
+)
diff --git a/apps/web/src/pages/my-templates/hooks/useMyTemplates.ts b/apps/web/src/pages/my-templates/hooks/useMyTemplates.ts
new file mode 100644
index 000000000..76610297c
--- /dev/null
+++ b/apps/web/src/pages/my-templates/hooks/useMyTemplates.ts
@@ -0,0 +1,100 @@
+import { useState } from "react"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { message } from "antd"
+import {
+ getEditingTemplates,
+ getTemplateCategories,
+ deleteEditingTemplate,
+ createEditingTemplate,
+ type EditingTemplate,
+} from "@/api/editing-planner"
+
+/**
+ * 我的模板数据 Hook
+ * 封装模板列表查询、筛选、删除、复制等数据操作
+ */
+export function useMyTemplates() {
+ const queryClient = useQueryClient()
+ const [searchText, setSearchText] = useState("")
+ const [filterCategory, setFilterCategory] = useState("")
+
+ /* 模板列表 */
+ const { data: templates = [], isLoading } = useQuery({
+ queryKey: ["editing-templates", filterCategory, searchText],
+ queryFn: () =>
+ getEditingTemplates({
+ category: filterCategory || undefined,
+ tag: searchText || undefined,
+ }),
+ })
+
+ /* 分类列表 */
+ const { data: categories = [] } = useQuery({
+ queryKey: ["template-categories"],
+ queryFn: getTemplateCategories,
+ })
+
+ /* 删除 mutation */
+ const deleteMutation = useMutation({
+ mutationFn: deleteEditingTemplate,
+ onSuccess: () => {
+ message.success("模板已删除")
+ queryClient.invalidateQueries({ queryKey: ["editing-templates"] })
+ },
+ onError: (err: unknown) => {
+ if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("删除失败")
+ },
+ })
+
+ /* 复制 mutation */
+ const copyMutation = useMutation({
+ mutationFn: (tpl: EditingTemplate) =>
+ createEditingTemplate({
+ name: `${tpl.name}(副本)`,
+ mode: tpl.mode,
+ category: tpl.category,
+ tags: tpl.tags,
+ title_config: tpl.title_config,
+ subtitle_config: tpl.subtitle_config,
+ bgm_config: tpl.bgm_config,
+ estimated_duration:
+ tpl.estimated_duration ??
+ Math.round(
+ tpl.segments.reduce((s, seg) => s + (seg.duration_min + seg.duration_max) / 2, 0),
+ ),
+ segments: tpl.segments.map(({ id: _id, ...rest }) => rest),
+ }),
+ onSuccess: () => {
+ message.success("模板已复制")
+ queryClient.invalidateQueries({ queryKey: ["editing-templates"] })
+ },
+ onError: (err: unknown) => {
+ if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("复制失败")
+ },
+ })
+
+ const handleCopy = (tpl: EditingTemplate) => {
+ copyMutation.mutate(tpl)
+ }
+
+ const handleDelete = (id: string) => {
+ deleteMutation.mutate(id)
+ }
+
+ return {
+ // 状态
+ searchText,
+ setSearchText,
+ filterCategory,
+ setFilterCategory,
+ // 数据
+ templates,
+ categories,
+ isLoading,
+ // 操作
+ handleCopy,
+ handleDelete,
+ isDeleting: deleteMutation.isPending,
+ isCopying: copyMutation.isPending,
+ }
+}