'use client' import Select from '@/components/ui/Select' import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import Image from 'next/image' import Link from 'next/link' import { BookOpen, ChevronDown, Download, FileText, Heart, Pencil, Search, Share2, Trash2, Upload, X } from 'lucide-react' import DepartmentShell from './DepartmentShell' import BrandKnowledgeUploadDialog from './BrandKnowledgeUploadDialog' import ToastBubble from '@/components/ToastBubble' import { useToast } from '@/hooks/useToast' import { BRAND_KNOWLEDGE_BRAND_OPTIONS, BRAND_KNOWLEDGE_TYPE_OPTIONS } from '@/lib/brand-knowledge-file-types' const KNOWLEDGE_TYPES = ['全部', ...BRAND_KNOWLEDGE_TYPE_OPTIONS] type BrandKnowledgeItem = { id: string; title: string; knowledgeType: string; brand: string; productCategoryName: string; fileName: string; mimeType: string; fileSize: number; downloadCount: number; createdAt: string; uploadedBy: string; downloadUrl: string; previewUrl: string | null; previewImageName: string | null; likeCount: number; isLiked: boolean; isFavorited: boolean } type BrandKnowledgeMetadata = Pick type ProductCategory = { id: string; name: string } function formatFileSize(size: number) { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`; return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB` } function formatDate(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const parts = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date); const values = Object.fromEntries(parts.map((part) => [part.type, part.value])); return `${values.year}.${values.month}.${values.day}` } async function copyText(value: string) { try { await navigator.clipboard.writeText(value) return true } catch { const textarea = document.createElement('textarea') textarea.value = value textarea.style.position = 'fixed' textarea.style.left = '-999999px' document.body.appendChild(textarea) textarea.select() try { return document.execCommand('copy') } catch { return false } finally { document.body.removeChild(textarea) } } } function BrandKnowledgeCard({ item, canManage, onNotice, onShare, onEdit, onDelete, onLike, onFavorite }: { item: BrandKnowledgeItem canManage: boolean onNotice: (message: string, variant?: 'success' | 'error') => void onShare: (item: BrandKnowledgeItem) => void onEdit: (item: BrandKnowledgeItem) => void onDelete: (item: BrandKnowledgeItem) => void onLike: (id: string, active: boolean) => Promise onFavorite: (id: string, active: boolean) => Promise }) { const [busyAction, setBusyAction] = useState<'like' | 'favorite' | null>(null) const details = [ formatFileSize(item.fileSize), item.uploadedBy, formatDate(item.createdAt), item.knowledgeType, item.brand && item.brand !== '未关联品牌' ? item.brand : '', item.productCategoryName, ].filter(Boolean) const handleCopyId = async () => { const copied = await copyText(item.id) onNotice(copied ? '作品ID已复制' : '作品ID复制失败,请手动复制', copied ? 'success' : 'error') } const handleEngagement = async (action: 'like' | 'favorite') => { if (busyAction) return setBusyAction(action) try { if (action === 'like') { await onLike(item.id, !item.isLiked) onNotice(item.isLiked ? '已取消点赞' : '点赞成功') } else { await onFavorite(item.id, !item.isFavorited) onNotice(item.isFavorited ? '已取消收藏' : '收藏成功') } } catch (error) { onNotice(error instanceof Error ? error.message : '操作失败,请稍后重试', 'error') } finally { setBusyAction(null) } } return (
  • {item.previewUrl ? ( {`预览图:${item.title}`} ) : (
    )}

    {item.title}

    {details.map((detail, index) => ( {index > 0 && } {detail} ))}

    {canManage && ( <> )}
  • ) } function ModalFrame({ titleId, children, onClose }: { titleId: string children: ReactNode onClose: () => void }) { const panelRef = useRef(null) const closeButtonRef = useRef(null) const onCloseRef = useRef(onClose) useEffect(() => { onCloseRef.current = onClose }, [onClose]) useEffect(() => { const previousOverflow = document.body.style.overflow const returnFocusElement = document.activeElement instanceof HTMLElement ? document.activeElement : null document.body.style.overflow = 'hidden' closeButtonRef.current?.focus() const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault() onCloseRef.current() return } if (event.key !== 'Tab' || !panelRef.current) return const focusable = Array.from(panelRef.current.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')) if (focusable.length === 0) return const first = focusable[0] const last = focusable[focusable.length - 1] if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() } } document.addEventListener('keydown', handleKeyDown) return () => { document.body.style.overflow = previousOverflow document.removeEventListener('keydown', handleKeyDown) returnFocusElement?.focus() } }, []) return (
    { if (event.target === event.currentTarget) onClose() }}>
    {children}
    ) } function ShareDialog({ item, onClose, onNotice }: { item: BrandKnowledgeItem onClose: () => void onNotice: (message: string, variant?: 'success' | 'error') => void }) { const shareUrl = new URL(`/workspaces/assets/brand-knowledge/${encodeURIComponent(item.id)}`, window.location.origin).toString() const handleCopy = async () => { const copied = await copyText(shareUrl) if (copied) { onNotice('分享链接已复制到剪贴板!') onClose() } else { onNotice('复制失败,请手动复制', 'error') } } return (

    分享链接

    复制“{item.title}”的详情链接,发送给同事查看。

    {shareUrl}
    ) } function EditDialog({ item, onClose, onSave }: { item: BrandKnowledgeItem onClose: () => void onSave: (metadata: BrandKnowledgeMetadata) => Promise }) { const [metadata, setMetadata] = useState({ title: item.title, knowledgeType: item.knowledgeType, brand: item.brand, productCategoryName: item.productCategoryName, }) const [categories, setCategories] = useState([]) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState('') const updateMetadata = ( field: Field, value: BrandKnowledgeMetadata[Field], ) => setMetadata((current) => ({ ...current, [field]: value })) useEffect(() => { let cancelled = false void fetch('/api/product-categories', { cache: 'no-store' }) .then((response) => response.ok ? response.json() : []) .then((payload) => { if (!cancelled && Array.isArray(payload)) setCategories(payload.filter((value): value is ProductCategory => typeof value?.id === 'string' && typeof value?.name === 'string')) }) .catch(() => undefined) return () => { cancelled = true } }, []) const submit = async () => { if (!metadata.title.trim() || isSaving) return setIsSaving(true); setError('') try { await onSave({ ...metadata, title: metadata.title.trim() }) onClose() } catch (saveError) { setError(saveError instanceof Error ? saveError.message : '品牌知识编辑失败,请稍后重试') } finally { setIsSaving(false) } } const categoryOptions = metadata.productCategoryName && !categories.some((category) => category.name === metadata.productCategoryName) ? [{ id: 'current', name: metadata.productCategoryName }, ...categories] : categories return (

    编辑品牌知识

    仅更新展示信息,原知识文件和预览图保持不变。

    {error &&

    {error}

    }
    ) } function DeleteDialog({ item, onClose, onConfirm }: { item: BrandKnowledgeItem onClose: () => void onConfirm: () => Promise }) { const [isDeleting, setIsDeleting] = useState(false) const [error, setError] = useState('') const confirm = async () => { if (isDeleting) return setIsDeleting(true); setError('') try { await onConfirm() onClose() } catch (deleteError) { setError(deleteError instanceof Error ? deleteError.message : '品牌知识删除失败,请稍后重试') } finally { setIsDeleting(false) } } return (

    删除品牌知识

    确定删除“{item.title}”吗?内容会从知识库和详情链接中移除,原文件会保留以便后续恢复。

    {error &&

    {error}

    }
    ) } export default function BrandKnowledgeDashboard() { const [selectedType, setSelectedType] = useState('全部') const [searchKeyword, setSearchKeyword] = useState('') const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false) const [items, setItems] = useState([]) const [totalCount, setTotalCount] = useState(0) const [isLoading, setIsLoading] = useState(true) const [loadError, setLoadError] = useState('') const [canManage, setCanManage] = useState(false) const [shareItem, setShareItem] = useState(null) const [editItem, setEditItem] = useState(null) const [deleteItem, setDeleteItem] = useState(null) const { toast, showToast } = useToast() const uploadButtonRef = useRef(null) const loadKnowledge = useCallback(async () => { setIsLoading(true); setLoadError('') try { const response = await fetch('/api/department-dashboard/brand-knowledge', { cache: 'no-store' }); const payload = await response.json().catch(() => null); if (!response.ok || !payload?.success) throw new Error(payload?.error || '品牌知识列表读取失败'); const nextItems = Array.isArray(payload.data) ? payload.data : []; setItems(nextItems); setTotalCount(Number.isSafeInteger(payload.total) && payload.total >= 0 ? payload.total : nextItems.length); setCanManage(payload.permissions?.canManage === true) } catch (error) { setLoadError(error instanceof Error ? error.message : '品牌知识列表读取失败,请稍后重试') } finally { setIsLoading(false) } }, []) useEffect(() => { void loadKnowledge() }, [loadKnowledge]) useEffect(() => { const sharedKnowledgeId = new URLSearchParams(window.location.search).get('knowledge')?.trim() if (sharedKnowledgeId) setSearchKeyword(sharedKnowledgeId) }, []) const setEngagement = useCallback(async (id: string, action: 'like' | 'favorite', active: boolean) => { const response = await fetch(`/api/department-dashboard/brand-knowledge/${encodeURIComponent(id)}/${action}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active }), }) const payload = await response.json().catch(() => null) if (!response.ok || !payload?.success) throw new Error(payload?.error || '操作失败,请稍后重试') setItems((current) => current.map((item) => item.id === id ? { ...item, ...(action === 'like' ? { isLiked: payload.liked, likeCount: payload.likeCount } : { isFavorited: payload.isFavorited }), } : item)) }, []) const saveMetadata = useCallback(async (id: string, metadata: BrandKnowledgeMetadata) => { const response = await fetch(`/api/department-dashboard/brand-knowledge/${encodeURIComponent(id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(metadata), }) const payload = await response.json().catch(() => null) if (!response.ok || !payload?.success) throw new Error(payload?.error || '品牌知识编辑失败,请稍后重试') setItems((current) => current.map((item) => item.id === id ? { ...item, ...payload.data } : item)) showToast('品牌知识已更新', { variant: 'success' }) }, [showToast]) const deleteKnowledge = useCallback(async (id: string) => { const response = await fetch(`/api/department-dashboard/brand-knowledge/${encodeURIComponent(id)}`, { method: 'DELETE' }) const payload = await response.json().catch(() => null) if (!response.ok || !payload?.success) throw new Error(payload?.error || '品牌知识删除失败,请稍后重试') setItems((current) => current.filter((item) => item.id !== id)) setTotalCount((current) => Math.max(0, current - 1)) showToast('品牌知识已删除,原文件已保留', { variant: 'success' }) }, [showToast]) const visibleItems = useMemo(() => { const keyword = searchKeyword.trim().toLowerCase(); return items.filter((item) => (selectedType === '全部' || item.knowledgeType === selectedType) && (!keyword || [item.id, item.title, item.knowledgeType, item.fileName, item.brand, item.productCategoryName, item.uploadedBy].some((value) => value.toLowerCase().includes(keyword)))) }, [items, searchKeyword, selectedType]) const emptyMessage = useMemo(() => { if (loadError) return loadError if (searchKeyword.trim()) return `没有找到与“${searchKeyword.trim()}”匹配的知识内容` if (selectedType !== '全部') return `${selectedType}暂未上传内容` return '品牌知识库暂未上传内容' }, [loadError, searchKeyword, selectedType]) return (

    共 {isLoading ? '…' : totalCount.toLocaleString('zh-CN')} 条

    {isLoading ?
    正在读取知识内容…
    : visibleItems.length > 0 ?
      {visibleItems.map((item) => showToast(message, { variant })} onShare={setShareItem} onEdit={setEditItem} onDelete={setDeleteItem} onLike={(id, active) => setEngagement(id, 'like', active)} onFavorite={(id, active) => setEngagement(id, 'favorite', active)} />)}
    :
    }
    {isUploadDialogOpen && ( setIsUploadDialogOpen(false)} onUploaded={async () => { showToast('知识文件已上传到品牌知识库'); await loadKnowledge() }} returnFocusRef={uploadButtonRef} /> )} {shareItem && setShareItem(null)} onNotice={(message, variant = 'success') => showToast(message, { variant })} />} {editItem && setEditItem(null)} onSave={(metadata) => saveMetadata(editItem.id, metadata)} />} {deleteItem && setDeleteItem(null)} onConfirm={() => deleteKnowledge(deleteItem.id)} />} {toast?.show &&
    }
    ) }