diff --git a/apps/web/src/pages/templates/TemplateLibrary.tsx b/apps/web/src/pages/templates/TemplateLibrary.tsx index 33387ee74..7c58dad15 100644 --- a/apps/web/src/pages/templates/TemplateLibrary.tsx +++ b/apps/web/src/pages/templates/TemplateLibrary.tsx @@ -1,154 +1,661 @@ /** - * 模板库页面 — V21 设计系统 - * 页面头部 + 工具栏(搜索+分类胶囊按钮组)+ 4列模板卡片网格 + 空状态 - * 使用 mock 数据,CSS 变量,V21 组件 + * 模板库页面(升级版)— V21 设计系统 + * 任务 2.13:模板类型分类展示、模板预览功能、创建 EditPlan 入口 + * + * - 按 EditTemplate 类型分组展示 + * - 缩略图 + 预览弹窗 + * - 创建 EditPlan 入口 UI + * - Mock 数据 + 预留 API 对接接口 */ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui"; import "./templates.css"; /* ============================================================ - * Mock 数据 + * 类型定义(对齐后端 EditTemplate / EditPlan / TemplateClipConfig) * ============================================================ */ -type TemplateCategory = "口播" | "种草" | "产品" | "品牌"; -interface TemplateItem { +/** 模板片段配置 */ +interface TemplateClipConfig { id: string; - name: string; - category: TemplateCategory; - usageCount: number; - isFavorite: boolean; - gradient: string; - codeSnippet: string; + order: number; + clipType: string; + description: string; + duration: number; // 秒 } -const categories: Array<"全部" | TemplateCategory> = [ - "全部", - "口播", - "种草", - "产品", - "品牌", +/** 模板类型 */ +type EditTemplateType = + | "口播" + | "种草" + | "产品" + | "品牌" + | "混剪" + | "Vlog"; + +/** 模板数据 */ +interface EditTemplate { + id: string; + name: string; + type: EditTemplateType; + description: string; + usageCount: number; + isFavorite: boolean; + thumbnailGradient: string; + scriptContent: string; + clipConfigs: TemplateClipConfig[]; + recommendedDuration: number; // 秒 + tags: string[]; +} + +/** 创建 EditPlan 参数 */ +interface CreateEditPlanParams { + templateId: string; + planName: string; +} + +/* ============================================================ + * API 占位函数(预留后端对接) + * ============================================================ */ + +/** 获取模板列表 */ +export async function fetchTemplates( + type?: EditTemplateType, +): Promise { + // TODO: 对接后端 GET /api/templates?type=xxx + void type; + return mockTemplates; +} + +/** 获取模板详情 */ +export async function fetchTemplateById( + id: string, +): Promise { + // TODO: 对接后端 GET /api/templates/:id + return mockTemplates.find((t) => t.id === id) ?? null; +} + +/** 创建 EditPlan */ +export async function createEditPlan( + params: CreateEditPlanParams, +): Promise<{ planId: string }> { + // TODO: 对接后端 POST /api/edit-plans + console.log("[Mock] createEditPlan", params); + return { planId: `plan-${Date.now()}` }; +} + +/* ============================================================ + * 模板类型配置 + * ============================================================ */ + +const TEMPLATE_TYPES: Array<{ + type: EditTemplateType | "全部"; + label: string; + icon: string; + color: string; +}> = [ + { type: "全部", label: "全部", icon: "📋", color: "#6366f1" }, + { type: "口播", label: "口播", icon: "🎙️", color: "#6366f1" }, + { type: "种草", label: "种草", icon: "🌱", color: "#10b981" }, + { type: "产品", label: "产品", icon: "📦", color: "#0ea5e9" }, + { type: "品牌", label: "品牌", icon: "🏷️", color: "#f59e0b" }, + { type: "混剪", label: "混剪", icon: "🎬", color: "#8b5cf6" }, + { type: "Vlog", label: "Vlog", icon: "📹", color: "#ec4899" }, ]; -const mockTemplates: TemplateItem[] = [ +/* ============================================================ + * Mock 数据 + * ============================================================ */ + +const mockTemplates: EditTemplate[] = [ { id: "tpl-1", name: "商品口播模板", - category: "口播", + type: "口播", + description: "适用于电商商品介绍的口播视频模板,包含开场、产品介绍、卖点展示、结尾引导等完整结构。", usageCount: 128, isFavorite: true, - gradient: "linear-gradient(135deg, #6366f1, #8b5cf6)", - codeSnippet: - "# 商品口播脚本\n## 开场白\n大家好,今天给大家推荐...\n## 产品介绍\n这款产品的特点是...", + thumbnailGradient: "linear-gradient(135deg, #6366f1, #8b5cf6)", + scriptContent: "# 商品口播脚本\n## 开场白\n大家好,今天给大家推荐...\n## 产品介绍\n这款产品的特点是...\n## 使用演示\n我们来看实际效果...\n## 总结\n赶紧下单吧!", + clipConfigs: [ + { id: "c1", order: 1, clipType: "开场", description: "吸引注意力的开场白", duration: 5 }, + { id: "c2", order: 2, clipType: "产品展示", description: "产品外观和功能展示", duration: 15 }, + { id: "c3", order: 3, clipType: "卖点讲解", description: "核心卖点详细说明", duration: 20 }, + { id: "c4", order: 4, clipType: "结尾", description: "引导下单的结尾", duration: 5 }, + ], + recommendedDuration: 45, + tags: ["电商", "口播", "商品介绍"], }, { id: "tpl-2", name: "种草笔记视频模板", - category: "种草", + type: "种草", + description: "适合美妆、护肤、生活方式等种草类内容,真实体验分享风格。", usageCount: 96, isFavorite: false, - gradient: "linear-gradient(135deg, #10b981, #059669)", - codeSnippet: - "# 种草视频\n## 使用场景\n早上出门前的护肤步骤...\n## 产品亮点\n温和不刺激,适合敏感肌...", + thumbnailGradient: "linear-gradient(135deg, #10b981, #059669)", + scriptContent: "# 种草视频\n## 使用场景\n早上出门前的护肤步骤...\n## 产品亮点\n温和不刺激,适合敏感肌...\n## 使用心得\n用了一个月后的感受...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "场景引入", description: "日常场景带入", duration: 8 }, + { id: "c2", order: 2, clipType: "产品体验", description: "使用过程展示", duration: 20 }, + { id: "c3", order: 3, clipType: "效果对比", description: "使用前后对比", duration: 10 }, + { id: "c4", order: 4, clipType: "总结推荐", description: "使用心得总结", duration: 7 }, + ], + recommendedDuration: 45, + tags: ["种草", "美妆", "体验分享"], }, { id: "tpl-3", name: "新品发布展示模板", - category: "产品", + type: "产品", + description: "适合新品发布、产品升级等场景,突出产品亮点和技术优势。", usageCount: 74, isFavorite: false, - gradient: "linear-gradient(135deg, #0ea5e9, #0284c7)", - codeSnippet: - "# 新品发布\n## 产品概览\n全新升级,性能提升50%...\n## 核心功能\nAI智能识别,一键优化...", + thumbnailGradient: "linear-gradient(135deg, #0ea5e9, #0284c7)", + scriptContent: "# 新品发布\n## 产品概览\n全新升级,性能提升50%...\n## 核心功能\nAI智能识别,一键优化...\n## 技术参数\n详细技术规格展示...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "悬念开场", description: "产品悬念引入", duration: 5 }, + { id: "c2", order: 2, clipType: "产品全景", description: "产品360度展示", duration: 10 }, + { id: "c3", order: 3, clipType: "功能演示", description: "核心功能实操", duration: 25 }, + { id: "c4", order: 4, clipType: "技术规格", description: "参数对比展示", duration: 10 }, + { id: "c5", order: 5, clipType: "结尾", description: "购买引导", duration: 5 }, + ], + recommendedDuration: 55, + tags: ["产品", "发布", "科技"], }, { id: "tpl-4", name: "品牌故事宣传片", - category: "品牌", + type: "品牌", + description: "讲述品牌故事,传递品牌理念,适合品牌形象建设和宣传。", usageCount: 52, isFavorite: true, - gradient: "linear-gradient(135deg, #f59e0b, #d97706)", - codeSnippet: - "# 品牌故事\n## 品牌起源\n2020年,我们从一个想法开始...\n## 品牌理念\n让创作更简单...", + thumbnailGradient: "linear-gradient(135deg, #f59e0b, #d97706)", + scriptContent: "# 品牌故事\n## 品牌起源\n2020年,我们从一个想法开始...\n## 品牌理念\n让创作更简单...\n## 团队风采\n我们的团队...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "品牌起源", description: "创业故事引入", duration: 15 }, + { id: "c2", order: 2, clipType: "发展历程", description: "里程碑事件", duration: 15 }, + { id: "c3", order: 3, clipType: "核心理念", description: "品牌价值主张", duration: 10 }, + { id: "c4", order: 4, clipType: "未来展望", description: "品牌愿景", duration: 10 }, + ], + recommendedDuration: 50, + tags: ["品牌", "宣传", "故事"], }, { id: "tpl-5", name: "知识分享口播模板", - category: "口播", + type: "口播", + description: "适合知识博主、教程类内容,结构清晰,信息密度高。", usageCount: 215, isFavorite: false, - gradient: "linear-gradient(135deg, #8b5cf6, #7c3aed)", - codeSnippet: - "# 知识分享\n## 主题引入\n今天我们来聊一个很多人问的问题...\n## 核心内容\n第一点,第二点,第三点...", + thumbnailGradient: "linear-gradient(135deg, #8b5cf6, #7c3aed)", + scriptContent: "# 知识分享\n## 主题引入\n今天我们来聊一个很多人问的问题...\n## 核心内容\n第一点,第二点,第三点...\n## 总结\n希望对你有帮助...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "开场", description: "话题引入", duration: 5 }, + { id: "c2", order: 2, clipType: "知识点1", description: "第一个要点", duration: 15 }, + { id: "c3", order: 3, clipType: "知识点2", description: "第二个要点", duration: 15 }, + { id: "c4", order: 4, clipType: "总结", description: "内容回顾", duration: 5 }, + ], + recommendedDuration: 40, + tags: ["知识", "教程", "口播"], }, { id: "tpl-6", name: "好物推荐种草模板", - category: "种草", + type: "种草", + description: "以痛点引入,展示解决方案,适合日常好物推荐。", usageCount: 183, isFavorite: true, - gradient: "linear-gradient(135deg, #059669, #047857)", - codeSnippet: - "# 好物推荐\n## 痛点引入\n你是不是也有这样的困扰...\n## 解决方案\n直到我发现了这个神器...", + thumbnailGradient: "linear-gradient(135deg, #059669, #047857)", + scriptContent: "# 好物推荐\n## 痛点引入\n你是不是也有这样的困扰...\n## 解决方案\n直到我发现了这个神器...\n## 使用效果\n一个月后的变化...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "痛点", description: "用户痛点共鸣", duration: 8 }, + { id: "c2", order: 2, clipType: "产品引入", description: "产品出场", duration: 10 }, + { id: "c3", order: 3, clipType: "使用展示", description: "实际使用过程", duration: 15 }, + { id: "c4", order: 4, clipType: "效果", description: "使用效果展示", duration: 10 }, + ], + recommendedDuration: 43, + tags: ["种草", "好物", "推荐"], }, { id: "tpl-7", name: "产品对比评测模板", - category: "产品", + type: "产品", + description: "多维度对比评测,客观公正,适合数码、家电等产品。", usageCount: 67, isFavorite: false, - gradient: "linear-gradient(135deg, #0284c7, #0369a1)", - codeSnippet: - "# 产品对比\n## 对比维度\n外观、性能、价格、体验...\n## 总结推荐\n综合来看,A适合...B适合...", + thumbnailGradient: "linear-gradient(135deg, #0284c7, #0369a1)", + scriptContent: "# 产品对比\n## 对比维度\n外观、性能、价格、体验...\n## 详细对比\n逐项分析...\n## 总结推荐\n综合来看,A适合...B适合...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "产品亮相", description: "两款产品展示", duration: 8 }, + { id: "c2", order: 2, clipType: "外观对比", description: "外观设计对比", duration: 12 }, + { id: "c3", order: 3, clipType: "性能测试", description: "性能数据对比", duration: 15 }, + { id: "c4", order: 4, clipType: "总结", description: "购买建议", duration: 10 }, + ], + recommendedDuration: 45, + tags: ["评测", "对比", "产品"], }, { id: "tpl-8", name: "品牌活动预热模板", - category: "品牌", + type: "品牌", + description: "适合品牌活动、促销预热,制造期待感。", usageCount: 41, isFavorite: false, - gradient: "linear-gradient(135deg, #d97706, #b45309)", - codeSnippet: - "# 活动预热\n## 悬念引入\n倒计时3天,惊喜即将揭晓...\n## 活动亮点\n福利一、福利二、福利三...", + thumbnailGradient: "linear-gradient(135deg, #d97706, #b45309)", + scriptContent: "# 活动预热\n## 悬念引入\n倒计时3天,惊喜即将揭晓...\n## 活动亮点\n福利一、福利二、福利三...\n## 参与方式\n如何参与活动...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "悬念", description: "倒计时悬念", duration: 5 }, + { id: "c2", order: 2, clipType: "亮点", description: "活动亮点展示", duration: 15 }, + { id: "c3", order: 3, clipType: "福利", description: "优惠福利说明", duration: 10 }, + { id: "c4", order: 4, clipType: "引导", description: "参与方式引导", duration: 5 }, + ], + recommendedDuration: 35, + tags: ["品牌", "活动", "预热"], + }, + { + id: "tpl-9", + name: "多素材混剪模板", + type: "混剪", + description: "适合多段素材拼接,自动匹配节奏,适合混剪类视频。", + usageCount: 89, + isFavorite: false, + thumbnailGradient: "linear-gradient(135deg, #8b5cf6, #6d28d9)", + scriptContent: "# 混剪模板\n## 素材1\n开场高能片段...\n## 素材2\n过渡衔接...\n## 素材3\n高潮部分...\n## 结尾\n精彩回顾...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "高能开场", description: "吸引眼球的开场", duration: 5 }, + { id: "c2", order: 2, clipType: "素材A", description: "第一段素材", duration: 10 }, + { id: "c3", order: 3, clipType: "过渡", description: "转场衔接", duration: 3 }, + { id: "c4", order: 4, clipType: "素材B", description: "第二段素材", duration: 10 }, + { id: "c5", order: 5, clipType: "高潮", description: "高潮片段", duration: 8 }, + { id: "c6", order: 6, clipType: "结尾", description: "精彩回顾", duration: 4 }, + ], + recommendedDuration: 40, + tags: ["混剪", "多素材", "节奏"], + }, + { + id: "tpl-10", + name: "日常Vlog模板", + type: "Vlog", + description: "记录日常生活,轻松自然的Vlog风格模板。", + usageCount: 156, + isFavorite: true, + thumbnailGradient: "linear-gradient(135deg, #ec4899, #db2777)", + scriptContent: "# 日常Vlog\n## 早安\n今天又是美好的一天...\n## 出门\n准备出门啦...\n## 日常\n记录精彩瞬间...\n## 晚安\n今天也很充实...", + clipConfigs: [ + { id: "c1", order: 1, clipType: "早安", description: "起床日常", duration: 8 }, + { id: "c2", order: 2, clipType: "出门", description: "准备出门", duration: 10 }, + { id: "c3", order: 3, clipType: "日常", description: "日常活动记录", duration: 20 }, + { id: "c4", order: 4, clipType: "晚安", description: "一天总结", duration: 7 }, + ], + recommendedDuration: 45, + tags: ["Vlog", "日常", "生活"], }, ]; /* ============================================================ - * 组件 + * 辅助函数 * ============================================================ */ + +/** 获取类型对应颜色 */ +const getTypeColor = (type: EditTemplateType): string => { + const found = TEMPLATE_TYPES.find((t) => t.type === type); + return found?.color ?? "#6366f1"; +}; + +/** 获取片段类型标签 */ +const getClipTypeLabel = (clipType: string): string => clipType; + +/** 获取片段类型颜色 */ +const getClipTypeColor = (clipType: string): string => { + const colorMap: Record = { + 开场: "#6366f1", + 产品展示: "#0ea5e9", + 卖点讲解: "#10b981", + 结尾: "#f59e0b", + 场景引入: "#8b5cf6", + 产品体验: "#ec4899", + 效果对比: "#14b8a6", + 总结推荐: "#f59e0b", + 悬念开场: "#6366f1", + 产品全景: "#0ea5e9", + 功能演示: "#10b981", + 技术规格: "#64748b", + 品牌起源: "#f59e0b", + 发展历程: "#0ea5e9", + 核心理念: "#8b5cf6", + 未来展望: "#10b981", + 知识点1: "#6366f1", + 知识点2: "#8b5cf6", + 痛点: "#ef4444", + 产品引入: "#10b981", + 使用展示: "#0ea5e9", + 效果: "#f59e0b", + 产品亮相: "#6366f1", + 外观对比: "#0ea5e9", + 性能测试: "#10b981", + 总结: "#f59e0b", + 悬念: "#6366f1", + 亮点: "#10b981", + 福利: "#f59e0b", + 引导: "#0ea5e9", + 高能开场: "#ef4444", + 过渡: "#64748b", + 高潮: "#ec4899", + 早安: "#f59e0b", + 出门: "#10b981", + 日常: "#0ea5e9", + 晚安: "#8b5cf6", + }; + return colorMap[clipType] ?? "#6366f1"; +}; + +/** 格式化时长 */ +const formatDuration = (seconds: number): string => { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + if (m === 0) return `${s}秒`; + return `${m}分${s > 0 ? `${s}秒` : ""}`; +}; + +/* ============================================================ + * 预览弹窗组件 + * ============================================================ */ + +interface TemplatePreviewModalProps { + template: EditTemplate; + isFavorite: boolean; + onClose: () => void; + onToggleFavorite: (id: string) => void; + onUse: (template: EditTemplate) => void; +} + +const TemplatePreviewModal: React.FC = ({ + template, + isFavorite, + onClose, + onToggleFavorite, + onUse, +}) => { + const totalDuration = template.clipConfigs.reduce( + (sum, c) => sum + c.duration, + 0, + ); + + return ( +
+
e.stopPropagation()} + > + {/* 关闭按钮 */} + + + {/* 预览区域 */} +
+
+ + {TEMPLATE_TYPES.find((t) => t.type === template.type)?.icon ?? "📋"} + + + {template.name} + +
+
+ + {/* 内容区域 */} +
+ {/* 标题行 */} +
+

{template.name}

+ + {TEMPLATE_TYPES.find((t) => t.type === template.type)?.icon}{" "} + {template.type} + +
+ + {/* 描述 */} +

{template.description}

+ + {/* 标签 */} +
+ {template.tags.map((tag) => ( + + #{tag} + + ))} +
+ + {/* 脚本内容 */} +
+

📝 脚本内容

+
+              {template.scriptContent}
+            
+
+ + {/* 视频结构 */} +
+

🎬 视频结构

+
+ {template.clipConfigs + .sort((a, b) => a.order - b.order) + .map((clip) => ( +
+ + {getClipTypeLabel(clip.clipType)} + + + {clip.description} + + + {clip.duration}秒 + +
+ ))} +
+
+ 总时长:{formatDuration(totalDuration)} + {template.recommendedDuration !== totalDuration && ( + + {" "} + · 推荐时长:{formatDuration(template.recommendedDuration)} + + )} +
+
+ + {/* 统计信息 */} +
+ 已使用 {template.usageCount} 次 + +
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ ); +}; + +/* ============================================================ + * 模板卡片组件 + * ============================================================ */ + +interface TemplateCardProps { + template: EditTemplate; + isFavorite: boolean; + onPreview: (template: EditTemplate) => void; + onToggleFavorite: (id: string, e: React.MouseEvent) => void; + onUse: (template: EditTemplate) => void; +} + +const TemplateCard: React.FC = ({ + template, + isFavorite, + onPreview, + onToggleFavorite, + onUse, +}) => { + return ( +
onPreview(template)} + > + {/* 缩略图 */} +
+
+ {template.scriptContent.slice(0, 80)}... +
+
+
点击预览
+ +
+ + {/* 信息区 */} +
+
+

{template.name}

+ + {template.type} + +
+

{template.description}

+
+ + 已使用 {template.usageCount} 次 + + +
+
+
+ ); +}; + +/* ============================================================ + * 主组件 + * ============================================================ */ + const TemplateLibrary: React.FC = () => { + const navigate = useNavigate(); const [searchText, setSearchText] = useState(""); - const [activeCategory, setActiveCategory] = useState< - "全部" | TemplateCategory - >("全部"); + const [activeType, setActiveType] = useState("全部"); + const [previewTemplate, setPreviewTemplate] = useState(null); const [favorites, setFavorites] = useState>( () => new Set(mockTemplates.filter((t) => t.isFavorite).map((t) => t.id)), ); /** 切换收藏 */ - const toggleFavorite = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); + 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); - } + if (next.has(id)) next.delete(id); + else next.add(id); return next; }); }; /** 过滤模板 */ - const filtered = mockTemplates.filter((t) => { - const matchSearch = - !searchText || - t.name.toLowerCase().includes(searchText.toLowerCase()) || - t.codeSnippet.toLowerCase().includes(searchText.toLowerCase()); - const matchCategory = - activeCategory === "全部" || t.category === activeCategory; - return matchSearch && matchCategory; - }); + const filtered = useMemo(() => { + return mockTemplates.filter((t) => { + const matchSearch = + !searchText || + t.name.toLowerCase().includes(searchText.toLowerCase()) || + t.description.toLowerCase().includes(searchText.toLowerCase()) || + t.tags.some((tag) => + tag.toLowerCase().includes(searchText.toLowerCase()), + ); + const matchType = activeType === "全部" || t.type === activeType; + return matchSearch && matchType; + }); + }, [searchText, activeType]); + + /** 按类型分组 */ + const groupedTemplates = useMemo(() => { + const groups: Record = {}; + for (const tpl of filtered) { + if (!groups[tpl.type]) groups[tpl.type] = []; + groups[tpl.type].push(tpl); + } + return groups; + }, [filtered]); + + /** 使用模板 → 创建 EditPlan */ + const handleUseTemplate = async (template: EditTemplate) => { + try { + const result = await createEditPlan({ + templateId: template.id, + planName: `基于「${template.name}」的剪辑计划`, + }); + console.log("[TemplateLibrary] EditPlan created:", result.planId); + navigate("/app/editing-planner"); + } catch (err) { + console.error("[TemplateLibrary] createEditPlan failed:", err); + } + }; return (
@@ -156,96 +663,112 @@ const TemplateLibrary: React.FC = () => {

模板库

-

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

+

选择模板快速创建剪辑计划,支持自定义修改

- {/* ── 工具栏:搜索 + 分类按钮组 ─────────────────────────── */} + {/* ── 工具栏:搜索 + 类型按钮组 ─────────────────────────── */}
🔍 setSearchText(e.target.value)} />
- {categories.map((cat) => ( + {TEMPLATE_TYPES.map((cat) => ( ))}
- {/* ── 模板卡片网格 ──────────────────────────────────────── */} + {/* ── 模板展示区 ────────────────────────────────────────── */} {filtered.length === 0 ? (
📭

- {searchText || activeCategory !== "全部" + {searchText || activeType !== "全部" ? "未找到匹配的模板" : "暂无模板"}

- {searchText || activeCategory !== "全部" - ? "试试调整搜索条件或切换分类" + {searchText || activeType !== "全部" + ? "试试调整搜索条件或切换类型" : "点击上方「创建模板」开始创作"}

+ ) : activeType === "全部" ? ( + /* 全部类型 → 按类型分组展示 */ +
+ {Object.entries(groupedTemplates).map(([type, templates]) => { + const typeConfig = TEMPLATE_TYPES.find((t) => t.type === type); + return ( +
+
+ + {typeConfig?.icon ?? "📋"} + +

{type}

+ + {templates.length} 个模板 + +
+
+ {templates.map((tpl) => ( + + ))} +
+
+ ); + })} +
) : ( + /* 单类型 → 平铺网格 */
{filtered.map((tpl) => ( -
- {/* 缩略图 */} -
-
- {tpl.codeSnippet} -
-
- -
- - {/* 信息区 */} -
-
-

{tpl.name}

- - {tpl.category} - -
-
- - 已使用 {tpl.usageCount} 次 - - -
-
-
+ ))}
)} + + {/* ── 预览弹窗 ──────────────────────────────────────────── */} + {previewTemplate && ( + setPreviewTemplate(null)} + onToggleFavorite={toggleFavorite} + onUse={handleUseTemplate} + /> + )}
); }; diff --git a/apps/web/src/pages/templates/templates.css b/apps/web/src/pages/templates/templates.css index 64530e50b..2e812ed21 100644 --- a/apps/web/src/pages/templates/templates.css +++ b/apps/web/src/pages/templates/templates.css @@ -1,6 +1,6 @@ /** - * 模板库页面 - V21 设计系统样式 - * 页面头部 + 工具栏(搜索+分类按钮组)+ 4列模板卡片网格 + 空状态 + * 模板库页面 - V21 设计系统样式(升级版) + * 任务 2.13:新增分组展示、预览弹窗、卡片描述等样式 * 统一使用 CSS 变量,支持深色/浅色主题 */ @import "../../styles/global.css"; @@ -106,6 +106,9 @@ cursor: pointer; transition: var(--transition-all); white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 4px; } .xx-templates-cat-btn:hover { @@ -120,6 +123,43 @@ font-weight: 600; } +.xx-templates-cat-icon { + font-size: 14px; +} + +/* ============================================================ + 分组展示 + ============================================================ */ +.xx-templates-grouped { + display: flex; + flex-direction: column; + gap: var(--space-xl); +} + +.xx-templates-group-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: var(--space-md); +} + +.xx-templates-group-icon { + font-size: 20px; +} + +.xx-templates-group-header h3 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.xx-templates-group-count { + font-size: var(--font-size-sm); + color: var(--text-secondary); + margin-left: 4px; +} + /* ============================================================ 模板卡片网格 ============================================================ */ @@ -183,6 +223,27 @@ pointer-events: none; } +/* 预览提示(hover 显示) */ +.xx-template-preview-hint { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(2px); + color: #fff; + font-size: 14px; + font-weight: 500; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 1; +} + +.xx-template-card:hover .xx-template-preview-hint { + opacity: 1; +} + /* 收藏按钮 */ .xx-template-fav-btn { position: absolute; @@ -224,7 +285,7 @@ align-items: center; justify-content: space-between; gap: 8px; - margin-bottom: 10px; + margin-bottom: 8px; } .xx-template-name { @@ -239,6 +300,18 @@ min-width: 0; } +/* 卡片描述 */ +.xx-template-desc { + margin: 0 0 10px; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + /* 分类药丸 */ .xx-template-category-pill { display: inline-flex; @@ -251,26 +324,6 @@ flex-shrink: 0; } -.xx-template-category-pill--口播 { - color: var(--primary-color); - background: var(--primary-soft); -} - -.xx-template-category-pill--种草 { - color: var(--success-color); - background: var(--success-soft); -} - -.xx-template-category-pill--产品 { - color: var(--info-color, #0ea5e9); - background: var(--info-soft, #e0f2fe); -} - -.xx-template-category-pill--品牌 { - color: var(--warning-color); - background: var(--warning-soft); -} - /* 使用次数 */ .xx-template-meta { display: flex; @@ -333,6 +386,253 @@ color: var(--text-secondary); } +/* ============================================================ + 预览弹窗 + ============================================================ */ +.xx-template-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.xx-template-modal { + background: var(--bg-primary); + border-radius: var(--radius-lg); + max-width: 720px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + animation: slideUp 0.25s ease; +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.xx-template-modal-close { + position: absolute; + top: 12px; + right: 12px; + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(4px); + color: #fff; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + transition: var(--transition-all); +} + +.xx-template-modal-close:hover { + background: rgba(0, 0, 0, 0.5); + transform: scale(1.1); +} + +/* 预览区域 */ +.xx-template-modal-preview { + aspect-ratio: 16 / 9; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + overflow: hidden; +} + +.xx-template-modal-preview-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; +} + +/* 内容区域 */ +.xx-template-modal-content { + padding: 24px; +} + +.xx-template-modal-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.xx-template-modal-title-row h3 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + color: var(--text-primary); +} + +.xx-template-modal-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.xx-template-modal-desc { + margin: 0 0 12px; + font-size: var(--font-size-sm); + color: var(--text-secondary); + line-height: 1.6; +} + +.xx-template-modal-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 16px; +} + +.xx-template-modal-tag { + padding: 2px 8px; + border-radius: 4px; + background: var(--bg-surface, var(--bg-secondary)); + color: var(--text-secondary); + font-size: 11px; +} + +/* 脚本内容 */ +.xx-template-modal-section { + margin-bottom: 16px; +} + +.xx-template-modal-section h4 { + margin: 0 0 8px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.xx-template-modal-script { + margin: 0; + padding: 12px; + background: var(--bg-surface, var(--bg-secondary)); + border-radius: var(--radius-sm); + font-family: "SF Mono", "Fira Code", monospace; + font-size: 12px; + line-height: 1.6; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; + max-height: 160px; + overflow-y: auto; +} + +/* 片段列表 */ +.xx-template-modal-clip-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.xx-template-modal-clip-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--bg-surface, var(--bg-secondary)); + border-radius: var(--radius-sm); +} + +.xx-template-modal-clip-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; + white-space: nowrap; +} + +.xx-template-modal-clip-desc { + flex: 1; + font-size: 12px; + color: var(--text-primary); +} + +.xx-template-modal-clip-duration { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; +} + +.xx-template-modal-total-duration { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + font-size: 12px; + color: var(--text-secondary); + text-align: right; +} + +/* 统计信息 */ +.xx-template-modal-stats { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + margin-bottom: 16px; + font-size: 13px; + color: var(--text-secondary); +} + +.xx-template-modal-fav-btn { + padding: 4px 12px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: transparent; + color: var(--text-secondary); + font-size: 12px; + cursor: pointer; + transition: var(--transition-all); +} + +.xx-template-modal-fav-btn:hover { + border-color: #fbbf24; + color: #fbbf24; +} + +.xx-template-modal-fav-btn.is-favorite { + border-color: #fbbf24; + color: #fbbf24; + background: #fbbf2418; +} + +/* 操作按钮 */ +.xx-template-modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + /* ============================================================ 响应式 ============================================================ */ @@ -368,6 +668,19 @@ .xx-templates-categories { justify-content: flex-start; } + + /* 弹窗移动端适配 */ + .xx-template-modal-content { + padding: 16px; + } + + .xx-template-modal-actions { + flex-direction: column-reverse; + } + + .xx-template-modal-actions button { + width: 100%; + } } @media (max-width: 480px) {