diff --git a/apps/web/src/pages/templates/TemplateLibrary.tsx b/apps/web/src/pages/templates/TemplateLibrary.tsx index 58442e57e..ab8f44f88 100644 --- a/apps/web/src/pages/templates/TemplateLibrary.tsx +++ b/apps/web/src/pages/templates/TemplateLibrary.tsx @@ -1,244 +1,251 @@ /** - * 模板库页面 - * 展示系统模板,支持预览和筛选 + * 模板库页面 — V21 设计系统 + * 页面头部 + 工具栏(搜索+分类胶囊按钮组)+ 4列模板卡片网格 + 空状态 + * 使用 mock 数据,CSS 变量,V21 组件 */ import React, { useState } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - Card, - Button, - Typography, - Space, - Row, - Col, - Tag, - Empty, - Spin, - Input, - Select, - Modal, - Image, - message, -} from "antd"; -import { - PlayCircleOutlined, - StarOutlined, - StarFilled, - SearchOutlined, -} from "@ant-design/icons"; -import { - getTemplates, - toggleFavoriteTemplate, - type TemplateItem, -} from "@/api/templates"; -import PageHead from "@/components/layout/PageHead"; +import { Button } from "@/components/ui"; +import "./templates.css"; -const { Paragraph } = Typography; +/* ============================================================ + * Mock 数据 + * ============================================================ */ +type TemplateCategory = "口播" | "种草" | "产品" | "品牌"; +interface TemplateItem { + id: string; + name: string; + category: TemplateCategory; + usageCount: number; + isFavorite: boolean; + gradient: string; + codeSnippet: string; +} + +const categories: Array<"全部" | TemplateCategory> = [ + "全部", + "口播", + "种草", + "产品", + "品牌", +]; + +const mockTemplates: TemplateItem[] = [ + { + id: "tpl-1", + name: "商品口播模板", + category: "口播", + usageCount: 128, + isFavorite: true, + gradient: "linear-gradient(135deg, #6366f1, #8b5cf6)", + codeSnippet: + "# 商品口播脚本\n## 开场白\n大家好,今天给大家推荐...\n## 产品介绍\n这款产品的特点是...", + }, + { + id: "tpl-2", + name: "种草笔记视频模板", + category: "种草", + usageCount: 96, + isFavorite: false, + gradient: "linear-gradient(135deg, #10b981, #059669)", + codeSnippet: + "# 种草视频\n## 使用场景\n早上出门前的护肤步骤...\n## 产品亮点\n温和不刺激,适合敏感肌...", + }, + { + id: "tpl-3", + name: "新品发布展示模板", + category: "产品", + usageCount: 74, + isFavorite: false, + gradient: "linear-gradient(135deg, #0ea5e9, #0284c7)", + codeSnippet: + "# 新品发布\n## 产品概览\n全新升级,性能提升50%...\n## 核心功能\nAI智能识别,一键优化...", + }, + { + id: "tpl-4", + name: "品牌故事宣传片", + category: "品牌", + usageCount: 52, + isFavorite: true, + gradient: "linear-gradient(135deg, #f59e0b, #d97706)", + codeSnippet: + "# 品牌故事\n## 品牌起源\n2020年,我们从一个想法开始...\n## 品牌理念\n让创作更简单...", + }, + { + id: "tpl-5", + name: "知识分享口播模板", + category: "口播", + usageCount: 215, + isFavorite: false, + gradient: "linear-gradient(135deg, #8b5cf6, #7c3aed)", + codeSnippet: + "# 知识分享\n## 主题引入\n今天我们来聊一个很多人问的问题...\n## 核心内容\n第一点,第二点,第三点...", + }, + { + id: "tpl-6", + name: "好物推荐种草模板", + category: "种草", + usageCount: 183, + isFavorite: true, + gradient: "linear-gradient(135deg, #059669, #047857)", + codeSnippet: + "# 好物推荐\n## 痛点引入\n你是不是也有这样的困扰...\n## 解决方案\n直到我发现了这个神器...", + }, + { + id: "tpl-7", + name: "产品对比评测模板", + category: "产品", + usageCount: 67, + isFavorite: false, + gradient: "linear-gradient(135deg, #0284c7, #0369a1)", + codeSnippet: + "# 产品对比\n## 对比维度\n外观、性能、价格、体验...\n## 总结推荐\n综合来看,A适合...B适合...", + }, + { + id: "tpl-8", + name: "品牌活动预热模板", + category: "品牌", + usageCount: 41, + isFavorite: false, + gradient: "linear-gradient(135deg, #d97706, #b45309)", + codeSnippet: + "# 活动预热\n## 悬念引入\n倒计时3天,惊喜即将揭晓...\n## 活动亮点\n福利一、福利二、福利三...", + }, +]; + +/* ============================================================ + * 组件 + * ============================================================ */ const TemplateLibrary: React.FC = () => { - const queryClient = useQueryClient(); const [searchText, setSearchText] = useState(""); - const [categoryFilter, setCategoryFilter] = useState(""); - const [previewTemplate, setPreviewTemplate] = useState( - null, + const [activeCategory, setActiveCategory] = useState< + "全部" | TemplateCategory + >("全部"); + const [favorites, setFavorites] = useState>( + () => new Set(mockTemplates.filter((t) => t.isFavorite).map((t) => t.id)) ); - // 获取模板列表 - const { data: templates = [], isLoading } = useQuery({ - queryKey: ["templates"], - queryFn: getTemplates, - }); + /** 切换收藏 */ + const toggleFavorite = (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + setFavorites((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; - // 收藏/取消收藏 - const favMutation = useMutation({ - mutationFn: toggleFavoriteTemplate, - onSuccess: (data) => { - message.success(data.is_favorite ? "已收藏" : "已取消收藏"); - queryClient.invalidateQueries({ queryKey: ["templates"] }); - }, - onError: (err: unknown) => { - if (!(err as { __msgShown?: boolean })?.__msgShown) - message.error("操作失败"); - }, - }); - - /** 提取所有分类 */ - const categories = Array.from( - new Set(templates.map((t) => t.category).filter(Boolean)), - ); - - /** 过滤后的模板 */ - const filteredTemplates = templates.filter((t) => { + /** 过滤模板 */ + const filtered = mockTemplates.filter((t) => { const matchSearch = !searchText || t.name.toLowerCase().includes(searchText.toLowerCase()) || - (t.description || "").toLowerCase().includes(searchText.toLowerCase()); - const matchCategory = !categoryFilter || t.category === categoryFilter; + t.codeSnippet.toLowerCase().includes(searchText.toLowerCase()); + const matchCategory = + activeCategory === "全部" || t.category === activeCategory; return matchSearch && matchCategory; }); return ( -
- +
+ {/* ── 页面头部 ──────────────────────────────────────────── */} +
+
+

模板库

+

选择模板快速开始创作,支持自定义修改

+
+ +
- {/* 搜索和筛选 */} - - - } - placeholder="搜索模板..." + {/* ── 工具栏:搜索 + 分类按钮组 ─────────────────────────── */} +
+
+ 🔍 + setSearchText(e.target.value)} - allowClear /> - - -