flnxi-Wu/app/admin/_components/template-manager/CustomTemplateSection.tsx
WuYongJie e4dcb6260a
Some checks failed
CI Check / check (push) Has been cancelled
feat: Admin页面重构 & UI三层架构系统
- 创建统一UI三层架构系统(基础层、颜色层、组合层)
- 拆分Admin页面为独立Tab组件(MonitorTab、UsersTab、DepartmentsTab等)
- 提取公共逻辑到Hooks(useToast、useDataFetch、useUserOperations等)
- 提取工具函数(validators、formatters、api等)
- 创建统一弹窗组件(Modal)和useModal Hook
- 添加错误边界和统一状态组件
- 完善类型定义和可访问性支持
- 更新文档(优化记录、优化计划、更新日志)
2025-11-22 15:17:10 +08:00

164 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { cardStyles, buttonStyles, buttonModifiers, cn } from '@/lib/ui-styles'
import type { DisplayTemplate } from './types'
interface CustomTemplateSectionProps {
loading: boolean
error: string | null
templates: DisplayTemplate[]
filteredTemplates: DisplayTemplate[]
deletingId: string | null
onCreate: () => void
onPreview: (template: DisplayTemplate) => void
onActivate: (id: string) => void
onEdit: (template: DisplayTemplate) => void
onDuplicate: (template: DisplayTemplate) => void
onDelete: (id: string) => void
}
export default function CustomTemplateSection({
loading,
error,
templates,
filteredTemplates,
deletingId,
onCreate,
onPreview,
onActivate,
onEdit,
onDuplicate,
onDelete,
}: CustomTemplateSectionProps) {
return (
<div className="px-6 pt-5 pb-4">
<div className="flex items-center justify-between gap-4 mb-3">
<div>
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">自定义模板</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">这里列出你自己创建的模板,可继续编辑、复制或删除</p>
<p className="text-[11px] text-slate-400 dark:text-slate-500">这些模板才会被真实上传面板使用,确保状态为“启用”后用户才能看到。</p>
</div>
</div>
{loading ? (
<div className="p-8 text-center text-slate-500 dark:text-slate-400">加载中...</div>
) : error ? (
<div className="p-8 text-center text-red-500 dark:text-red-300">{error}</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<button
type="button"
onClick={onCreate}
className={cn(cardStyles.flat, 'border-dashed border-2 border-slate-300 dark:border-slate-600 flex items-center justify-center gap-3')}
>
<div className="w-14 h-14 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center flex-shrink-0 group-hover:bg-slate-200 dark:group-hover:bg-slate-700 transition-colors">
<svg className="w-8 h-8 text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-200 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</div>
<div className="text-left">
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200">新建自定义模板</p>
<p className="text-[11px] text-slate-500 dark:text-slate-400 mt-1 leading-snug">从空白模板起步,完全按照业务需求自由配置字段</p>
</div>
</button>
{templates.length === 0 ? (
<div className="col-span-2 p-8 text-center text-slate-500 dark:text-slate-400">暂无自定义模板,点击上方卡片开始创建。</div>
) : filteredTemplates.length === 0 ? (
<div className="col-span-2 p-8 text-center text-slate-500 dark:text-slate-400">未找到匹配的模板名称,请调整搜索关键词。</div>
) : (
filteredTemplates.map((tpl) => (
<div
key={tpl.id}
onClick={() => onPreview(tpl)}
className={cn(cardStyles.hover, 'group relative overflow-hidden cursor-pointer')}
>
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-purple-500/70 via-blue-500/60 to-purple-500/70 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-slate-900 dark:text-white tracking-tight">{tpl.name}</p>
{tpl.description && (
<p className="text-xs text-slate-500 dark:text-slate-400 leading-snug line-clamp-2">{tpl.description}</p>
)}
</div>
<div className="flex flex-col items-end gap-1 text-right">
<span
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-all duration-200 ${
tpl.status === 'active'
? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300'
: tpl.status === 'draft'
? 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
}`}
>
{tpl.statusLabel}
</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-blue-500/80 dark:text-blue-300/80 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
预览
</span>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-2 border-t border-slate-200/70 dark:border-slate-700/70 pt-3">
{tpl.status !== 'active' && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onActivate(tpl.id)
}}
className={cn(buttonStyles.success, buttonModifiers.compactXs, buttonModifiers.pill)}
>
启用
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onEdit(tpl)
}}
className={cn(buttonStyles.secondary, buttonModifiers.compactXs, buttonModifiers.pill)}
>
编辑
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onDuplicate(tpl)
}}
className={cn(buttonStyles.ghost, buttonModifiers.compactXs, buttonModifiers.pill)}
>
复制
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onDelete(tpl.id)
}}
disabled={deletingId === tpl.id}
className={cn(
buttonStyles.danger,
buttonModifiers.compactXs,
buttonModifiers.pill,
deletingId === tpl.id && 'opacity-70 cursor-wait',
)}
>
{deletingId === tpl.id ? '删除中...' : '删除'}
</button>
<span className="ml-auto text-[11px] text-slate-400 dark:text-slate-500">
{tpl.updatedLabel} · v{tpl.version}
</span>
</div>
</div>
))
)}
</div>
)}
</div>
)
}