fix: use report title as certificate material name

This commit is contained in:
WuYongJie 2026-09-28 20:28:48 +08:00
parent d2337b960e
commit 846b8c601b
9 changed files with 66 additions and 29 deletions

View File

@ -63,6 +63,13 @@ describe('检测报告初始化', () => {
expect(prisma.material.create).not.toHaveBeenCalled()
})
test('成品即使有 SPU,上传时也必须填写报告名称', async () => {
const response = await POST(request({ ...input, reportKind: '成品', productModel: 'SPU-001', reportTitle: ' ' }))
expect(response.status).toBe(400)
await expect(response.json()).resolves.toEqual({ error: '请填写报告名称 / 检测对象' })
expect(prisma.material.create).not.toHaveBeenCalled()
})
test('到期时间早于检测日期时不写入素材', async () => {
const response = await POST(request({ ...input, certificateExpiresOn: '2027-09-22' }))
expect(response.status).toBe(400)

View File

@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { CERTIFICATE_REPORT_MATERIAL_TYPE, normalizeReportTitle, validateCertificateReportMetadata } from '@/lib/certificate-report'
import { CERTIFICATE_REPORT_KIND, CERTIFICATE_REPORT_MATERIAL_TYPE, normalizeReportTitle, validateCertificateReportMetadata } from '@/lib/certificate-report'
import { INCOMPLETE_SHELL_CREATE_DATA } from '@/lib/material-query'
export async function POST(request: NextRequest) {
@ -17,6 +17,9 @@ export async function POST(request: NextRequest) {
const reportTitle = normalizeReportTitle(body.reportTitle)
const productCategoryName = String(body.productCategoryName || '').trim()
const platformName = String(body.platformName || '').trim()
if (body.reportKind === CERTIFICATE_REPORT_KIND.FINISHED && !reportTitle) {
return NextResponse.json({ error: '请填写报告名称 / 检测对象' }, { status: 400 })
}
const metadata = validateCertificateReportMetadata({
reportKind: body.reportKind,
productModel,

View File

@ -594,7 +594,6 @@ export default function SharePage() {
? getCertificateReportDisplayTitle({
productModel: material.productModel,
reportTitle: material.reportTitle,
reportKind: material.reportKind,
})
: ''

View File

@ -156,6 +156,14 @@ describe('MaterialCard', () => {
expect(notice.parentElement).not.toHaveClass('bg-[#fafafa]/55')
})
test.each([
['成品检测报告', '成品检测报告'],
[null, 'SPU-001'],
])('检测报告卡片优先使用报告名称,缺失时保留旧 SPU (%s)', (reportTitle, expectedTitle) => {
render(<MaterialCard {...defaultProps} material={{ ...baseMaterial, materialType: '证书-检测报告', reportKind: '成品', reportTitle }} />)
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(expectedTitle)
})
test('视频卡片初始只加载封面,悬停时才挂载视频预览', () => {
const { container } = render(
<MaterialCard

View File

@ -175,7 +175,6 @@ export default function MaterialCard({
const materialDisplayTitle = getCertificateReportDisplayTitle({
productModel: material.productModel,
reportTitle: material.reportTitle,
reportKind: material.reportKind,
})
// 素材切换时回到封面态;视频只在用户悬停时加载,避免列表首屏并发预载全部视频。

View File

@ -5,6 +5,20 @@ import { DEFAULT_CERTIFICATE_REPORT_USAGE_RULES } from '@/lib/certificate-report
jest.mock('@/lib/chunk-upload', () => ({ chunkUpload: jest.fn() }))
test('成品只填写 SPU 时不能上传,须先填写报告名称', () => {
const originalFetch = global.fetch
global.fetch = jest.fn()
try {
render(<CertificateReportUploader onSuccess={jest.fn()} onCancel={jest.fn()} productCategories={[]} />)
fireEvent.change(screen.getByLabelText('SPU'), { target: { value: 'SPU-001' } })
fireEvent.click(screen.getByRole('button', { name: '创建分享素材' }))
expect(screen.getByText('请填写报告名称 / 检测对象')).toBeInTheDocument()
expect(global.fetch).not.toHaveBeenCalled()
} finally {
global.fetch = originalFetch
}
})
test.each([['成品', true], ['成品', false], ['原材料', true], ['原材料', false]] as const)('%s上传说明与日期:手动到期时间 %s', async (kind, manualExpiry) => {
const originalFetch = global.fetch
jest.useFakeTimers().setSystemTime(new Date('2026-09-27T12:00:00'))
@ -12,6 +26,12 @@ test.each([['成品', true], ['成品', false], ['原材料', true], ['原材料
try {
expect(screen.getByLabelText('成品')).toBeChecked()
fireEvent.click(screen.getByLabelText(kind))
const reportTitleInput = screen.getByLabelText('报告名称 / 检测对象 *')
expect(reportTitleInput).toBeRequired()
if (kind === '成品') {
expect(reportTitleInput.compareDocumentPosition(screen.getByLabelText('SPU')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
}
expect(screen.getByText('上传后将作为素材名称显示')).toBeInTheDocument()
fireEvent.change(screen.getByLabelText(/报告名称 \/ 检测对象/), { target: { value: '板材检测报告' } })
fireEvent.change(screen.getByLabelText('品类 *'), { target: { value: '板材' } })
fireEvent.change(screen.getByLabelText('证书编号 *'), { target: { value: 'CMA-001' } })

View File

@ -122,6 +122,10 @@ export default function CertificateReportUploader({
const handleSubmit = async () => {
const submittedProductModel = reportKind === CERTIFICATE_REPORT_KIND.RAW ? '' : productModel.trim()
if (reportKind === CERTIFICATE_REPORT_KIND.FINISHED && !reportTitle.trim()) {
setError('请填写报告名称 / 检测对象')
return
}
const metadata = validateCertificateReportMetadata({
reportKind,
productModel: submittedProductModel,
@ -280,6 +284,23 @@ export default function CertificateReportUploader({
</fieldset>
<div className={`grid grid-cols-1 gap-4 ${reportKind === CERTIFICATE_REPORT_KIND.FINISHED ? 'md:grid-cols-2' : ''}`}>
<div>
<label htmlFor="certificate-report-title" className={fieldLabelClass}>
报告名称 / 检测对象 *
</label>
<input
id="certificate-report-title"
type="text"
value={reportTitle}
onChange={(event) => setReportTitle(event.target.value)}
placeholder="例如:E0级颗粒板检测报告"
className={fieldControlClass}
required
/>
<p className="mt-1 text-xs text-gray-500">
上传后将作为素材名称显示
</p>
</div>
{reportKind === CERTIFICATE_REPORT_KIND.FINISHED && (
<div>
<label htmlFor="certificate-report-spu" className={fieldLabelClass}>SPU</label>
@ -293,24 +314,6 @@ export default function CertificateReportUploader({
/>
</div>
)}
<div>
<label htmlFor="certificate-report-title" className={fieldLabelClass}>
报告名称 / 检测对象{reportKind === CERTIFICATE_REPORT_KIND.RAW ? ' *' : ''}
</label>
<input
id="certificate-report-title"
type="text"
value={reportTitle}
onChange={(event) => setReportTitle(event.target.value)}
placeholder="例如:E0级颗粒板检测报告"
className={fieldControlClass}
required={reportKind === CERTIFICATE_REPORT_KIND.RAW}
/>
<p className="mt-1 text-xs text-gray-500">
{reportKind === CERTIFICATE_REPORT_KIND.RAW ? '原材料检测报告必须填写报告名称' : 'SPU 和这个字段至少填写一个'}
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@ -41,12 +41,16 @@ describe('检测报告字段规则', () => {
reportTitle: '板材检测报告',
})).toHaveProperty('value.reportKind', '原材料')
expect(getCertificateReportDisplayTitle({
reportKind: '原材料',
productModel: '旧 SPU',
reportTitle: '板材检测报告',
})).toBe('板材检测报告')
})
test('成品有报告名称时优先显示,旧记录缺少名称时显示 SPU', () => {
expect(getCertificateReportDisplayTitle({ productModel: 'SPU-001', reportTitle: '成品检测报告' })).toBe('成品检测报告')
expect(getCertificateReportDisplayTitle({ productModel: 'SPU-001', reportTitle: null })).toBe('SPU-001')
})
test('检测日期必须是真实的 YYYY-MM-DD 日期', () => {
expect(parseCertificateDate('2027-02-28')?.toISOString().slice(0, 10)).toBe('2027-02-28')
expect(parseCertificateDate('2027-02-30')).toBeNull()

View File

@ -153,21 +153,15 @@ export function validateCertificateReportMetadata(input: {
export function getCertificateReportDisplayTitle(input: {
productModel?: string | null
reportTitle?: string | null
reportKind?: string | null
}): string {
const productModel = String(input.productModel || '').trim()
const reportTitle = String(input.reportTitle || '').trim()
if (input.reportKind === CERTIFICATE_REPORT_KIND.RAW && reportTitle) {
if (reportTitle) {
return reportTitle
}
if (productModel) {
return productModel
}
if (reportTitle) {
return reportTitle
}
return '未命名报告'
}