142 lines
10 KiB
TypeScript
142 lines
10 KiB
TypeScript
'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>
|
||
)
|
||
}
|