gongxiangp2/components/department-dashboard/BrandKnowledgeDashboard.tsx
2026-09-08 17:46:49 +08:00

142 lines
10 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BookOpen, ChevronDown, Download, FileText, Search, Upload } from 'lucide-react'
import DepartmentShell from './DepartmentShell'
import BrandKnowledgeUploadDialog from './BrandKnowledgeUploadDialog'
const KNOWLEDGE_TYPES = ['全部', '品牌规范', '产品知识', '内容规范', '流程文档']
type BrandKnowledgeItem = { id: string; title: string; knowledgeType: string; brand: string; fileName: string; mimeType: string; fileSize: number; createdAt: string; uploadedBy: string; downloadUrl: string }
function formatFileSize(size: number) { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`; return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB` }
function formatDate(value: string) { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).format(date) }
export default function BrandKnowledgeDashboard() {
const [selectedType, setSelectedType] = useState('全部')
const [searchKeyword, setSearchKeyword] = useState('')
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false)
const [items, setItems] = useState<BrandKnowledgeItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [loadError, setLoadError] = useState('')
const [notice, setNotice] = useState('')
const uploadButtonRef = useRef<HTMLButtonElement>(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 || '品牌知识列表读取失败'); setItems(Array.isArray(payload.data) ? payload.data : []) }
catch (error) { setLoadError(error instanceof Error ? error.message : '品牌知识列表读取失败,请稍后重试') }
finally { setIsLoading(false) }
}, [])
useEffect(() => { void loadKnowledge() }, [loadKnowledge])
const visibleItems = useMemo(() => { const keyword = searchKeyword.trim().toLowerCase(); return items.filter((item) => (selectedType === '全部' || item.knowledgeType === selectedType) && (!keyword || [item.title, item.fileName, item.brand, 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 (
<DepartmentShell>
<main className="min-h-[calc(100dvh-72px)] bg-[#f2f4f7] text-[#101828]">
<div className="mx-auto max-w-7xl px-4 pb-16 pt-6 sm:px-6 lg:px-8">
<header className="mb-8 flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[#e9f8f2] text-[#087052]">
<BookOpen className="h-5 w-5" aria-hidden="true" />
</span>
<div className="min-w-0">
<h1 className="truncate text-2xl font-bold tracking-[-0.03em] text-[#101828] sm:text-3xl">品牌知识库</h1>
<p className="mt-1 truncate text-sm text-[#8a99ad]">品牌中心部门工作台</p>
</div>
</div>
<span className="inline-flex shrink-0 items-center gap-2 rounded-lg border border-[#b8e5d5] bg-[#f0fbf6] px-3 py-2 text-xs font-semibold text-[#087052]">
<span className="h-2 w-2 rounded-full bg-[#31d0aa]" aria-hidden="true" />
可上传
</span>
</div>
<div className="flex items-center justify-between gap-4">
<p className="text-sm text-[#66758b]" role="status" aria-live="polite">{notice}</p>
<button
ref={uploadButtonRef}
type="button"
onClick={() => { setNotice(''); setIsUploadDialogOpen(true) }}
className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-[#247fd3] px-5 text-sm font-bold text-white shadow-[0_8px_22px_rgba(36,127,211,0.16)] transition-colors hover:bg-[#176fc1] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2"
>
<Upload className="h-4 w-4" aria-hidden="true" />
上传知识
</button>
</div>
</header>
<section aria-label="品牌知识库筛选">
<label className="relative block">
<Search className="pointer-events-none absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-[#6b7075]" aria-hidden="true" />
<span className="sr-only">搜索品牌知识</span>
<input
type="search"
value={searchKeyword}
onChange={(event) => setSearchKeyword(event.currentTarget.value)}
placeholder="搜索品牌知识、产品、场景或关键词"
className="h-14 w-full rounded-lg border border-[#e2e8f0] bg-white pl-14 pr-5 text-base text-[#101828] outline-none transition-shadow placeholder:text-[#8a8f97] focus:border-[#1677ff] focus:ring-2 focus:ring-[#1677ff]/20"
/>
</label>
<nav className="mt-4 flex gap-7 overflow-x-auto border-b border-[#dfe4ea]" aria-label="知识类型">
{KNOWLEDGE_TYPES.map((type) => {
const active = selectedType === type
return (
<button
key={type}
type="button"
aria-pressed={active}
onClick={() => setSelectedType(type)}
className={`relative min-h-11 shrink-0 whitespace-nowrap px-0.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2 ${active ? 'text-[#101828]' : 'text-[#6b7075] hover:text-[#101828]'}`}
>
{type}
{active && <span className="absolute inset-x-0 -bottom-px h-0.5 bg-[#101828]" aria-hidden="true" />}
</button>
)
})}
</nav>
</section>
<section className="mt-6" aria-labelledby="brand-knowledge-content-title">
<div className="flex items-center justify-between gap-4 border-b border-[#dfe4ea] pb-4">
<div>
<h2 id="brand-knowledge-content-title" className="text-lg font-bold text-[#101828]">知识内容</h2>
<p className="mt-1 text-sm text-[#8a8f97]">人员上传的知识文件将按类型保存,并可在此下载</p>
</div>
<button
type="button"
disabled
className="inline-flex items-center gap-1.5 text-sm font-semibold text-[#8a8f97]"
title="内容接入后开放排序"
>
最近更新
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</button>
</div>
{isLoading ? <div className="border-t border-[#e2e8f0] py-24 text-center text-sm text-[#66758b]" role="status">正在读取知识内容…</div> : visibleItems.length > 0 ? <ul className="mt-4 grid gap-3 md:grid-cols-2" aria-label="品牌知识内容列表">{visibleItems.map((item) => <li key={item.id} className="rounded-xl border border-[#e2e8f0] bg-white p-5 shadow-[0_6px_18px_rgba(16,24,40,0.04)]"><div className="flex items-start justify-between gap-4"><div className="flex min-w-0 gap-3"><span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[#eef6ff] text-[#247fd3]"><FileText className="h-5 w-5" aria-hidden="true" /></span><div className="min-w-0"><h3 className="truncate font-bold text-[#101828]" title={item.title}>{item.title}</h3><p className="mt-1 truncate text-sm text-[#66758b]" title={item.fileName}>{item.fileName}</p></div></div><a href={item.downloadUrl} className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-[#247fd3] hover:bg-[#eef6ff] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]" aria-label={`下载${item.title}`} title="下载"><Download className="h-4 w-4" aria-hidden="true" /></a></div><div className="mt-4 flex flex-wrap items-center gap-2 text-xs text-[#66758b]"><span className="rounded-full bg-[#f0fbf6] px-2.5 py-1 font-semibold text-[#087052]">{item.knowledgeType}</span>{item.brand && item.brand !== '未关联品牌' && <span className="rounded-full bg-[#f5f7fa] px-2.5 py-1">{item.brand}</span>}<span>{formatFileSize(item.fileSize)}</span><span>上传人:{item.uploadedBy}</span><span>{formatDate(item.createdAt)}</span></div></li>)}</ul> : <div className="border-t border-[#e2e8f0] py-24 text-center">
<BookOpen className="mx-auto h-8 w-8 text-[#aab5c3]" aria-hidden="true" />
<p className="mt-4 text-base font-semibold text-[#4f6077]">{emptyMessage}</p>
<p className="mt-2 text-sm text-[#8a8f97]">点击右上角“上传知识”,即可将文件保存到品牌知识库。</p>
</div>}
</section>
</div>
</main>
{isUploadDialogOpen && (
<BrandKnowledgeUploadDialog
onClose={() => setIsUploadDialogOpen(false)}
onUploaded={async () => { setNotice('知识文件已上传到品牌知识库'); await loadKnowledge() }}
returnFocusRef={uploadButtonRef}
/>
)}
</DepartmentShell>
)
}