feat: align knowledge metadata with material cards
This commit is contained in:
parent
61dade05ed
commit
f5cb86e875
@ -11,6 +11,7 @@ describe('BrandKnowledgeDashboard', () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:test-preview') })
|
||||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn() })
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: jest.fn().mockResolvedValue(undefined) } })
|
||||
global.fetch = jest.fn().mockImplementation((input: RequestInfo | URL) => {
|
||||
if (String(input) === '/api/product-categories') {
|
||||
return Promise.resolve({ ok: true, json: async () => [{ id: 'category-1', name: '床类' }] } as Response)
|
||||
@ -69,13 +70,19 @@ describe('BrandKnowledgeDashboard', () => {
|
||||
|
||||
expect(await screen.findByRole('img', { name: '预览图:索菲亚图像影像规范' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { level: 3, name: '索菲亚图像影像规范' })).toBeInTheDocument()
|
||||
expect(screen.getByText(/637\.9 MB/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/上传人:无邪/)).toBeInTheDocument()
|
||||
expect(screen.getByText('索菲亚图像影像规范.rar')).toBeInTheDocument()
|
||||
expect(screen.getByText(/637\.90 MB/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/无邪/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/2026\.09\.12/)).toBeInTheDocument()
|
||||
const knowledgeList = screen.getByRole('list', { name: '品牌知识内容列表' })
|
||||
expect(within(knowledgeList).getByText('影像规范')).toBeInTheDocument()
|
||||
expect(within(knowledgeList).getByText('索菲亚')).toBeInTheDocument()
|
||||
expect(within(knowledgeList).getByText('衣柜')).toBeInTheDocument()
|
||||
expect(screen.queryByText('索菲亚图像影像规范.rar')).not.toBeInTheDocument()
|
||||
const copyIdButton = screen.getByRole('button', { name: '复制作品ID knowledge-1' })
|
||||
expect(copyIdButton).toHaveTextContent('作品ID knowledge-1')
|
||||
fireEvent.click(copyIdButton)
|
||||
await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith('knowledge-1'))
|
||||
expect(screen.getByRole('status')).toHaveTextContent('作品ID已复制')
|
||||
expect(screen.getByRole('link', { name: '下载索菲亚图像影像规范' })).toHaveAttribute(
|
||||
'href',
|
||||
'/api/department-dashboard/brand-knowledge/knowledge-1/file',
|
||||
|
||||
@ -9,71 +9,92 @@ import { 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; createdAt: string; uploadedBy: string; downloadUrl: string; previewUrl: string | null; previewImageName: string | null }
|
||||
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) }
|
||||
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}` }
|
||||
|
||||
function BrandKnowledgeCard({ item }: { item: BrandKnowledgeItem }) {
|
||||
function BrandKnowledgeCard({ item, onNotice }: { item: BrandKnowledgeItem; onNotice: (message: string) => void }) {
|
||||
const details = [
|
||||
formatFileSize(item.fileSize),
|
||||
item.uploadedBy ? `上传人:${item.uploadedBy}` : '',
|
||||
item.uploadedBy,
|
||||
formatDate(item.createdAt),
|
||||
item.knowledgeType,
|
||||
item.brand && item.brand !== '未关联品牌' ? item.brand : '',
|
||||
item.productCategoryName,
|
||||
].filter(Boolean)
|
||||
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(item.id)
|
||||
onNotice('作品ID已复制')
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = item.id
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.left = '-999999px'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
try {
|
||||
const copied = document.execCommand('copy')
|
||||
onNotice(copied ? '作品ID已复制' : '作品ID复制失败,请手动复制')
|
||||
} catch {
|
||||
onNotice('作品ID复制失败,请手动复制')
|
||||
} finally {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="min-w-0">
|
||||
<article>
|
||||
<div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-[#e9edf2]">
|
||||
<article className="bg-transparent">
|
||||
<div className="relative h-[400px] w-full overflow-hidden rounded-[12px] border border-[#e7e8ea] bg-[#f3f4f5]">
|
||||
{item.previewUrl ? (
|
||||
<Image
|
||||
src={item.previewUrl}
|
||||
alt={`预览图:${item.title}`}
|
||||
fill
|
||||
unoptimized
|
||||
className="object-contain"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 1279px) 50vw, 33vw"
|
||||
className="object-cover"
|
||||
sizes="(max-width: 639px) 100vw, (max-width: 1279px) 50vw, 33vw"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-[#66758b]">
|
||||
<FileText className="h-8 w-8 text-[#247fd3]" aria-hidden="true" />
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 bg-[#eceef0] px-6 text-center text-[#86898e]">
|
||||
<FileText className="h-8 w-8" aria-hidden="true" />
|
||||
<span className="line-clamp-2 text-sm">暂无预览图</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<h3 className="line-clamp-2 text-base font-bold leading-6 text-[#101828]" title={item.title}>
|
||||
<h3 className="truncate text-[16px] font-semibold tracking-[-0.03em] text-[#1a1c1d]" title={item.title}>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-1.5 line-clamp-2 text-xs leading-5 text-[#66758b]" title={details.join(' · ')}>
|
||||
<p className="mt-1.5 truncate text-[12px] leading-relaxed text-[#86898e]" title={details.join(' · ')}>
|
||||
{details.map((detail, index) => (
|
||||
<span key={detail}>
|
||||
<span key={`${detail}-${index}`}>
|
||||
{index > 0 && <span aria-hidden="true"> · </span>}
|
||||
{detail}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs leading-5 text-[#8a8f97]" title={item.fileName}>
|
||||
{item.fileName}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCopyId()}
|
||||
className="mt-1 block max-w-full truncate rounded-sm text-left text-[11px] tracking-[0.04em] text-[#86898e] transition-colors hover:text-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
|
||||
title="点击复制完整作品ID"
|
||||
aria-label={`复制作品ID ${item.id}`}
|
||||
>
|
||||
<span className="font-en tabular-nums">作品ID {item.id}</span>
|
||||
</button>
|
||||
|
||||
<div className="mt-2 flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-[#66758b]">
|
||||
<span className="font-semibold text-[#087052]">{item.knowledgeType}</span>
|
||||
{item.brand && item.brand !== '未关联品牌' && (
|
||||
<><span aria-hidden="true" className="text-[#c3cbd6]">/</span><span>{item.brand}</span></>
|
||||
)}
|
||||
{item.productCategoryName && (
|
||||
<><span aria-hidden="true" className="text-[#c3cbd6]">/</span><span>{item.productCategoryName}</span></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border-t border-[#e2e8f0] pt-3">
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]">
|
||||
<a
|
||||
href={item.downloadUrl}
|
||||
className="inline-flex min-h-10 items-center gap-2 rounded-sm text-sm font-semibold text-[#101828] transition-colors hover:text-[#247fd3] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2"
|
||||
className="inline-flex min-h-10 items-center gap-1.5 rounded-sm text-[#1a1c1d] transition-colors hover:text-[#5a66b8] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
|
||||
aria-label={`下载${item.title}`}
|
||||
title="下载"
|
||||
>
|
||||
<Download className="h-4 w-4" aria-hidden="true" />
|
||||
<Download className="h-3.5 w-3.5" strokeWidth={1.6} aria-hidden="true" />
|
||||
下载
|
||||
</a>
|
||||
</div>
|
||||
@ -101,7 +122,7 @@ export default function BrandKnowledgeDashboard() {
|
||||
}, [])
|
||||
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.productCategoryName, item.uploadedBy].some((value) => value.toLowerCase().includes(keyword)))) }, [items, searchKeyword, selectedType])
|
||||
const visibleItems = useMemo(() => { const keyword = searchKeyword.trim().toLowerCase(); return items.filter((item) => (selectedType === '全部' || item.knowledgeType === selectedType) && (!keyword || [item.id, item.title, item.fileName, item.brand, item.productCategoryName, item.uploadedBy].some((value) => value.toLowerCase().includes(keyword)))) }, [items, searchKeyword, selectedType])
|
||||
|
||||
const emptyMessage = useMemo(() => {
|
||||
if (loadError) return loadError
|
||||
@ -194,7 +215,7 @@ export default function BrandKnowledgeDashboard() {
|
||||
</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-5 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 xl:grid-cols-3" aria-label="品牌知识内容列表">{visibleItems.map((item) => <BrandKnowledgeCard key={item.id} item={item} />)}</ul> : <div className="border-t border-[#e2e8f0] py-24 text-center">
|
||||
{isLoading ? <div className="border-t border-[#e2e8f0] py-24 text-center text-sm text-[#66758b]" role="status">正在读取知识内容…</div> : visibleItems.length > 0 ? <ul className="mt-5 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 xl:grid-cols-3" aria-label="品牌知识内容列表">{visibleItems.map((item) => <BrandKnowledgeCard key={item.id} item={item} onNotice={setNotice} />)}</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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user