'use client'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { Plus, Pencil, Trash2, Tags, Loader2 } from 'lucide-react'; import { AdminHeader } from '@/components/admin/AdminHeader'; import { PaginationTable, type Column } from '@/components/admin/PaginationTable'; import { TableToolbar } from '@/components/admin/TableToolbar'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { ImageUpload } from '@/components/admin/ImageUpload'; import { RichEditor } from '@/components/admin/RichEditor'; import { MarkdownImport } from '@/components/admin/MarkdownImport'; import { adminApi } from '@/lib/admin-services'; import { useAdminStore } from '@/store/adminStore'; import { cn, resolveUploadUrl } from '@/lib/utils'; import { isHTMLContent } from '@/lib/content'; import type { Product, ProductCategory } from '@/lib/types'; type ContentMode = 'rich' | 'markdown'; interface FormState { categoryId: number; name: string; cover: string; desc: string; content: string; sort: number; isShow: number; } export default function ProductAdminPage() { const [hydrated, setHydrated] = useState(false); const [page, setPage] = useState(1); const [categoryId, setCategoryId] = useState(); const [keyword, setKeyword] = useState(''); const [searchKw, setSearchKw] = useState(''); const [open, setOpen] = useState(false); const [editId, setEditId] = useState(null); const [saving, setSaving] = useState(false); const [contentMode, setContentMode] = useState('rich'); const [form, setForm] = useState({ categoryId: 0, name: '', cover: '', desc: '', content: '', sort: 0, isShow: 1, }); const isSuperAdmin = useAdminStore((s) => s.admin?.role) === 'super_admin'; // 分类管理弹窗 const [catOpen, setCatOpen] = useState(false); const [catName, setCatName] = useState(''); const [catSort, setCatSort] = useState(0); useEffect(() => setHydrated(true), []); const { data: categories, mutate: mutateCats } = useSWR( hydrated ? '/admin/product-category' : null, () => adminApi.productCategoryAll(), ); const { data, isLoading, mutate } = useSWR( hydrated ? ['/admin/product', page, categoryId, searchKw] : null, () => adminApi.productPaginate({ page, pageSize: 10, categoryId, keyword: searchKw || undefined, }), ); const openCreate = () => { if (!categories || categories.length === 0) { alert('请先创建产品分类'); setCatOpen(true); return; } setEditId(null); setContentMode('rich'); setForm({ categoryId: categories[0].id, name: '', cover: '', desc: '', content: '', sort: 0, isShow: 1, }); setOpen(true); }; const openEdit = async (id: number) => { const detail = await adminApi.productDetail(id); setEditId(id); setContentMode(isHTMLContent(detail.content) ? 'rich' : 'markdown'); setForm({ categoryId: detail.categoryId, name: detail.name, cover: detail.cover, desc: detail.desc ?? '', content: detail.content ?? '', sort: detail.sort, isShow: detail.isShow, }); setOpen(true); }; const onSave = async () => { if (!form.categoryId) { alert('请选择分类'); return; } if (!form.name || !form.cover) { alert('请填写名称并上传封面'); return; } setSaving(true); try { const payload = { ...form, categoryId: Number(form.categoryId) }; if (editId) { await adminApi.productUpdate(editId, payload); } else { await adminApi.productCreate(payload); } setOpen(false); await mutate(); } catch (e) { alert((e as Error).message); } finally { setSaving(false); } }; const onDelete = async (id: number) => { if (!confirm('确认删除该产品?')) return; try { await adminApi.productDelete(id); await mutate(); } catch (e) { alert((e as Error).message); } }; // 分类 CRUD const onAddCategory = async () => { if (!catName.trim()) { alert('请输入分类名称'); return; } try { await adminApi.productCategoryCreate({ name: catName, sort: catSort, isShow: 1, }); setCatName(''); setCatSort(0); await mutateCats(); } catch (e) { alert((e as Error).message); } }; const onDeleteCategory = async (id: number) => { if (!confirm('删除分类后,关联产品仍保留但分类无效。确认删除?')) return; try { await adminApi.productCategoryDelete(id); await mutateCats(); } catch (e) { alert((e as Error).message); } }; const columns: Column[] = [ { key: 'id', title: 'ID', width: 60 }, { key: 'cover', title: '封面', width: 100, render: (r) => ( // eslint-disable-next-line @next/next/no-img-element {r.name} ), }, { key: 'name', title: '产品名称' }, { key: 'category', title: '分类', width: 120, render: (r) => r.category?.name ?? '-', }, { key: 'desc', title: '描述', render: (r) => ( {r.desc ?? '-'} ), }, { key: 'isShow', title: '状态', width: 80, render: (r) => r.isShow === 1 ? ( 上架 ) : ( 下架 ), }, { key: '_op', title: '操作', width: 150, render: (r) => (
{isSuperAdmin && ( )}
), }, ]; return (
setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { setSearchKw(keyword); setPage(1); } }} className="max-w-xs" /> } right={ <> } /> columns={columns} rows={data?.list ?? []} total={data?.total ?? 0} page={page} pageSize={10} loading={isLoading} onPageChange={setPage} rowKey={(r) => String(r.id)} /> {/* 产品表单 */} {editId ? '编辑产品' : '新增产品'}
setForm({ ...form, name: e.target.value })} />
setForm({ ...form, cover: url })} />