Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f02ea7207f |
@@ -3,126 +3,43 @@
|
|||||||
* 卡片视图展示用户已保存的剪辑模板
|
* 卡片视图展示用户已保存的剪辑模板
|
||||||
* 支持搜索、分类筛选、编辑/复制/删除/使用模板生成
|
* 支持搜索、分类筛选、编辑/复制/删除/使用模板生成
|
||||||
*/
|
*/
|
||||||
import React, { useState } from "react"
|
import React from "react"
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
import { Typography, Input, Select, Button, Empty, Spin, Row, Col } from "antd"
|
||||||
import {
|
|
||||||
Typography,
|
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Tag,
|
|
||||||
Button,
|
|
||||||
Space,
|
|
||||||
Empty,
|
|
||||||
Spin,
|
|
||||||
Tooltip,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
} from "antd"
|
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EditOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
VideoCameraOutlined,
|
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
} from "@ant-design/icons"
|
} from "@ant-design/icons"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import {
|
import type { EditingTemplate } from "@/api/editing-planner"
|
||||||
getEditingTemplates,
|
import { useMyTemplates } from "./hooks/useMyTemplates"
|
||||||
getTemplateCategories,
|
import { TemplateCard } from "./components/TemplateCard"
|
||||||
deleteEditingTemplate,
|
|
||||||
createEditingTemplate,
|
|
||||||
MODE_LABELS,
|
|
||||||
MODE_COLORS,
|
|
||||||
type EditingTemplate,
|
|
||||||
type TemplateMode,
|
|
||||||
} from "@/api/editing-planner"
|
|
||||||
import "./MyTemplates.css"
|
import "./MyTemplates.css"
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
const MyTemplates: React.FC = () => {
|
const MyTemplates: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
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) => {
|
const handleEdit = (tpl: EditingTemplate) => {
|
||||||
navigate(`/editing-planner?template=${tpl.id}`)
|
navigate(`/editing-planner?template=${tpl.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGenerate = (tpl: EditingTemplate) => {
|
const handleGenerate = (tpl: EditingTemplate) => {
|
||||||
// 跳转到智能剪辑页面,统一从智能剪辑出片
|
|
||||||
navigate(`/generate?templateId=${tpl.id}`)
|
navigate(`/generate?templateId=${tpl.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = (tpl: EditingTemplate) => {
|
|
||||||
copyMutation.mutate(tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
deleteMutation.mutate(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-page">
|
<div className="mt-page">
|
||||||
{/* 页面头部 */}
|
{/* 页面头部 */}
|
||||||
@@ -179,69 +96,13 @@ const MyTemplates: React.FC = () => {
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{templates.map((tpl) => (
|
{templates.map((tpl) => (
|
||||||
<Col key={tpl.id} xs={24} sm={12} md={8} lg={6}>
|
<Col key={tpl.id} xs={24} sm={12} md={8} lg={6}>
|
||||||
<Card
|
<TemplateCard
|
||||||
className="mt-card"
|
tpl={tpl}
|
||||||
hoverable
|
onEdit={handleEdit}
|
||||||
actions={[
|
onCopy={handleCopy}
|
||||||
<Tooltip title="编辑" key="edit">
|
onGenerate={handleGenerate}
|
||||||
<EditOutlined onClick={() => handleEdit(tpl)} />
|
onDelete={handleDelete}
|
||||||
</Tooltip>,
|
/>
|
||||||
<Tooltip title="复制" key="copy">
|
|
||||||
<CopyOutlined onClick={() => handleCopy(tpl)} />
|
|
||||||
</Tooltip>,
|
|
||||||
<Tooltip title="使用模板生成" key="generate">
|
|
||||||
<VideoCameraOutlined onClick={() => handleGenerate(tpl)} />
|
|
||||||
</Tooltip>,
|
|
||||||
<Popconfirm
|
|
||||||
key="delete"
|
|
||||||
title="确定删除此模板?"
|
|
||||||
onConfirm={() => handleDelete(tpl.id)}
|
|
||||||
okText="删除"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Tooltip title="删除">
|
|
||||||
<DeleteOutlined style={{ color: "#ff4d4f" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</Popconfirm>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div className="mt-card-head">
|
|
||||||
<Text strong ellipsis style={{ fontSize: 15 }}>
|
|
||||||
{tpl.name}
|
|
||||||
</Text>
|
|
||||||
<Tag color={MODE_COLORS[tpl.mode as TemplateMode] || "default"}>
|
|
||||||
{MODE_LABELS[tpl.mode as TemplateMode] || tpl.mode}
|
|
||||||
</Tag>
|
|
||||||
<Tag color="green">用户自制</Tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-card-meta">
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
|
|
||||||
</Text>
|
|
||||||
{tpl.category && (
|
|
||||||
<Tag style={{ fontSize: 11, marginTop: 4 }}>{tpl.category}</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tpl.tags.length > 0 && (
|
|
||||||
<div className="mt-card-tags">
|
|
||||||
{tpl.tags.map((tag) => (
|
|
||||||
<Tag key={tag} style={{ fontSize: 11 }}>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-card-config">
|
|
||||||
<Space size={4} wrap>
|
|
||||||
{tpl.title_config.ai_auto_select && <Tag color="cyan">AI标题</Tag>}
|
|
||||||
{tpl.subtitle_config.enabled && <Tag color="geekblue">字幕</Tag>}
|
|
||||||
{tpl.bgm_config.enabled && <Tag color="pink">BGM</Tag>}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -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<TemplateCardProps> = ({
|
||||||
|
tpl,
|
||||||
|
onEdit,
|
||||||
|
onCopy,
|
||||||
|
onGenerate,
|
||||||
|
onDelete,
|
||||||
|
}) => (
|
||||||
|
<Card
|
||||||
|
className="mt-card"
|
||||||
|
hoverable
|
||||||
|
actions={[
|
||||||
|
<Tooltip title="编辑" key="edit">
|
||||||
|
<EditOutlined onClick={() => onEdit(tpl)} />
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title="复制" key="copy">
|
||||||
|
<CopyOutlined onClick={() => onCopy(tpl)} />
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title="使用模板生成" key="generate">
|
||||||
|
<VideoCameraOutlined onClick={() => onGenerate(tpl)} />
|
||||||
|
</Tooltip>,
|
||||||
|
<Popconfirm
|
||||||
|
key="delete"
|
||||||
|
title="确定删除此模板?"
|
||||||
|
onConfirm={() => onDelete(tpl.id)}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<DeleteOutlined style={{ color: "#ff4d4f" }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="mt-card-head">
|
||||||
|
<Text strong ellipsis style={{ fontSize: 15 }}>
|
||||||
|
{tpl.name}
|
||||||
|
</Text>
|
||||||
|
<Tag color={MODE_COLORS[tpl.mode as TemplateMode] || "default"}>
|
||||||
|
{MODE_LABELS[tpl.mode as TemplateMode] || tpl.mode}
|
||||||
|
</Tag>
|
||||||
|
<Tag color="green">用户自制</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-card-meta">
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
|
||||||
|
</Text>
|
||||||
|
{tpl.category && <Tag style={{ fontSize: 11, marginTop: 4 }}>{tpl.category}</Tag>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tpl.tags.length > 0 && (
|
||||||
|
<div className="mt-card-tags">
|
||||||
|
{tpl.tags.map((tag) => (
|
||||||
|
<Tag key={tag} style={{ fontSize: 11 }}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-card-config">
|
||||||
|
<Space size={4} wrap>
|
||||||
|
{tpl.title_config.ai_auto_select && <Tag color="cyan">AI标题</Tag>}
|
||||||
|
{tpl.subtitle_config.enabled && <Tag color="geekblue">字幕</Tag>}
|
||||||
|
{tpl.bgm_config.enabled && <Tag color="pink">BGM</Tag>}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user