diff --git a/components/department-dashboard/BrandKnowledgeDashboard.test.tsx b/components/department-dashboard/BrandKnowledgeDashboard.test.tsx index 80776ad..24ecf2d 100644 --- a/components/department-dashboard/BrandKnowledgeDashboard.test.tsx +++ b/components/department-dashboard/BrandKnowledgeDashboard.test.tsx @@ -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', diff --git a/components/department-dashboard/BrandKnowledgeDashboard.tsx b/components/department-dashboard/BrandKnowledgeDashboard.tsx index 61c8e4d..95204ee 100644 --- a/components/department-dashboard/BrandKnowledgeDashboard.tsx +++ b/components/department-dashboard/BrandKnowledgeDashboard.tsx @@ -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 (
  • -
    -
    +
    +
    {item.previewUrl ? ( {`预览图:${item.title}`} ) : ( -
    -
    -

    +

    {item.title}

    -

    +

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

    -

    - {item.fileName} -

    + -
    - {item.knowledgeType} - {item.brand && item.brand !== '未关联品牌' && ( - <>{item.brand} - )} - {item.productCategoryName && ( - <>{item.productCategoryName} - )} -
    - -
    + @@ -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() {
    - {isLoading ?
    正在读取知识内容…
    : visibleItems.length > 0 ?
      {visibleItems.map((item) => )}
    :
    + {isLoading ?
    正在读取知识内容…
    : visibleItems.length > 0 ?
      {visibleItems.map((item) => )}
    :