Files
xiaoxia-saas/apps/web/src/pages/templates/TemplateLibrary.tsx
T
CI Test f0eed0ca3f
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 192h26m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 192h27m37s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 192h27m46s
style: fix prettier formatting issues
2026-07-01 12:26:48 +08:00

254 lines
8.4 KiB
TypeScript

/**
* 模板库页面 — V21 设计系统
* 页面头部 + 工具栏(搜索+分类胶囊按钮组)+ 4列模板卡片网格 + 空状态
* 使用 mock 数据,CSS 变量,V21 组件
*/
import React, { useState } from "react";
import { Button } from "@/components/ui";
import "./templates.css";
/* ============================================================
* 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 [searchText, setSearchText] = useState("");
const [activeCategory, setActiveCategory] = useState<
"全部" | TemplateCategory
>("全部");
const [favorites, setFavorites] = useState<Set<string>>(
() => new Set(mockTemplates.filter((t) => t.isFavorite).map((t) => t.id)),
);
/** 切换收藏 */
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 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;
});
return (
<div className="xx-templates-page">
{/* ── 页面头部 ──────────────────────────────────────────── */}
<div className="xx-templates-header">
<div className="xx-templates-header-text">
<h2>模板库</h2>
<p>选择模板快速开始创作,支持自定义修改</p>
</div>
<Button buttonType="primary" buttonSize="md">
+ 创建模板
</Button>
</div>
{/* ── 工具栏:搜索 + 分类按钮组 ─────────────────────────── */}
<div className="xx-templates-toolbar">
<div className="xx-templates-search">
<span className="xx-templates-search-icon">🔍</span>
<input
className="xx-templates-search-input"
type="text"
placeholder="搜索模板名称或内容..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
<div className="xx-templates-categories">
{categories.map((cat) => (
<button
key={cat}
className={`xx-templates-cat-btn${activeCategory === cat ? " active" : ""}`}
onClick={() => setActiveCategory(cat)}
>
{cat}
</button>
))}
</div>
</div>
{/* ── 模板卡片网格 ──────────────────────────────────────── */}
{filtered.length === 0 ? (
<div className="xx-templates-empty">
<div className="xx-templates-empty-icon">📭</div>
<h3>
{searchText || activeCategory !== "全部"
? "未找到匹配的模板"
: "暂无模板"}
</h3>
<p>
{searchText || activeCategory !== "全部"
? "试试调整搜索条件或切换分类"
: "点击上方「创建模板」开始创作"}
</p>
</div>
) : (
<div className="xx-templates-grid">
{filtered.map((tpl) => (
<div key={tpl.id} className="xx-template-card">
{/* 缩略图 */}
<div className="xx-template-thumb">
<div
className="xx-template-thumb-bg"
style={{ background: tpl.gradient }}
>
{tpl.codeSnippet}
</div>
<div className="xx-template-thumb-overlay" />
<button
className={`xx-template-fav-btn${favorites.has(tpl.id) ? " is-favorite" : ""}`}
onClick={(e) => toggleFavorite(tpl.id, e)}
title={favorites.has(tpl.id) ? "取消收藏" : "收藏"}
>
{favorites.has(tpl.id) ? "★" : "☆"}
</button>
</div>
{/* 信息区 */}
<div className="xx-template-info">
<div className="xx-template-info-top">
<h4 className="xx-template-name">{tpl.name}</h4>
<span
className={`xx-template-category-pill xx-template-category-pill--${tpl.category}`}
>
{tpl.category}
</span>
</div>
<div className="xx-template-meta">
<span className="xx-template-usage">
已使用 {tpl.usageCount}
</span>
<button className="xx-template-use-btn">使用此模板</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};
export default TemplateLibrary;