From 720c163a6be8fdd8fb65455e425f8d576fa06c06 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Mon, 25 May 2026 16:45:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A8=A1=E6=9D=BF=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=89=8D=E7=AB=AF=E4=BA=A4=E4=BA=92=E5=B9=B6=E6=94=B6?= =?UTF-8?q?=E6=95=9B=E9=A2=84=E8=A7=88=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/EditMaterialModal.tsx | 146 +++--- components/MaterialPreviewModal.tsx | 77 +-- components/UploadModal.tsx | 137 +++-- .../__tests__/EditMaterialModal.test.tsx | 490 ++++++++++++++++++ components/__tests__/UploadModal.test.tsx | 247 +++++++++ 5 files changed, 958 insertions(+), 139 deletions(-) create mode 100644 components/__tests__/UploadModal.test.tsx diff --git a/components/EditMaterialModal.tsx b/components/EditMaterialModal.tsx index f180b30..e3ecd58 100644 --- a/components/EditMaterialModal.tsx +++ b/components/EditMaterialModal.tsx @@ -1,9 +1,10 @@ 'use client' -import { useState, useRef, useEffect, useCallback } from 'react' +import { useState, useRef, useEffect, useCallback, useMemo } from 'react' import Image from 'next/image' import Modal from '@/components/ui/Modal' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { filterVisibleFieldValues, getVisibleTemplateFields, hasFieldValue } from '@/lib/field-conditions' interface EditMaterialModalProps { material: { @@ -24,6 +25,7 @@ interface EditMaterialModalProps { colorMode?: string | null templateId?: string | null platformName?: string | null + templateFields?: Record // 音频相关字段 audioDuration?: string | null audioCategory?: string | null @@ -46,6 +48,11 @@ type UploadTemplateOption = { export default function EditMaterialModal({ material, onClose, onSuccess, isOpen = true }: EditMaterialModalProps) { const styles = useThemeStyles() const theme = useTheme() + const controlClass = + 'w-full px-4 py-2.5 text-sm rounded-lg outline-none transition-colors border bg-white dark:bg-slate-800 text-slate-900 dark:text-white border-slate-300 dark:border-slate-700 focus-visible:ring-2 focus-visible:ring-slate-200 dark:focus-visible:ring-slate-700 focus-visible:border-slate-400 dark:focus-visible:border-slate-500 placeholder:text-slate-400 dark:placeholder:text-slate-500' + const requiredControlClass = 'bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600' + const requiredBadgeClass = + 'inline-flex items-center px-2 py-0.5 rounded-md border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-[10px] font-medium text-slate-600 dark:text-slate-300' const [previewImage, setPreviewImage] = useState(null) const [previewUrl, setPreviewUrl] = useState(material.previewUrl) const [templateFieldValues, setTemplateFieldValues] = useState>({}) @@ -57,10 +64,15 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen const [success, setSuccess] = useState('') const previewInputRef = useRef(null) + const visibleTemplateFields = useMemo(() => { + if (!selectedTemplate?.fields?.length) return [] + return getVisibleTemplateFields(selectedTemplate.fields, templateFieldValues) + }, [selectedTemplate, templateFieldValues]) // 初始化字段值 useEffect(() => { const initialValues: Record = { + ...(material.templateFields || {}), productModel: material.productModel, brandName: material.brandName || '', seriesName: material.seriesName || '', @@ -103,39 +115,48 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen setTemplateCategoryOptions(map) } - // 如果有templateId,加载模板 + const setTemplateFromData = (templateData: any) => { + setSelectedTemplate({ + id: templateData.id, + name: templateData.name, + description: templateData.description, + materialType: templateData.materialType, + fields: templateData.fields || [], + isBuiltin: templateData.isBuiltin, + }) + } + + const loadPublicTemplates = async () => { + const templatesResponse = await fetch('/api/upload-templates') + if (!templatesResponse.ok) { + return [] + } + const templatesData = await templatesResponse.json() + return Array.isArray(templatesData) ? templatesData : [] + } + + // 如果有templateId,优先从普通用户可访问的模板列表加载 if (material.templateId) { - const templateResponse = await fetch(`/api/admin/upload-templates/${material.templateId}`) - if (templateResponse.ok) { - const templateData = await templateResponse.json() - setSelectedTemplate({ - id: templateData.id, - name: templateData.name, - description: templateData.description, - materialType: templateData.materialType, - fields: templateData.fields || [], - isBuiltin: templateData.isBuiltin, - }) + const templates = await loadPublicTemplates() + const matchingTemplate = templates.find((t: any) => t.id === material.templateId) + + if (matchingTemplate) { + setTemplateFromData(matchingTemplate) + } else { + const templateResponse = await fetch(`/api/admin/upload-templates/${material.templateId}`) + if (templateResponse.ok) { + const templateData = await templateResponse.json() + setTemplateFromData(templateData) + } } } else { // 如果没有templateId,尝试根据materialType查找匹配的模板 - const templatesResponse = await fetch('/api/upload-templates') - if (templatesResponse.ok) { - const templatesData = await templatesResponse.json() - const templates = Array.isArray(templatesData) ? templatesData : [] - const matchingTemplate = templates.find( - (t: any) => t.materialType === material.materialType && t.status === 'active' - ) - if (matchingTemplate) { - setSelectedTemplate({ - id: matchingTemplate.id, - name: matchingTemplate.name, - description: matchingTemplate.description, - materialType: matchingTemplate.materialType, - fields: matchingTemplate.fields || [], - isBuiltin: matchingTemplate.isBuiltin, - }) - } + const templates = await loadPublicTemplates() + const matchingTemplate = templates.find( + (t: any) => t.materialType === material.materialType && t.status === 'active' + ) + if (matchingTemplate) { + setTemplateFromData(matchingTemplate) } } } catch (error) { @@ -166,10 +187,10 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen setSuccess('') // 验证必填字段 - const requiredFields = selectedTemplate?.fields.filter((f: any) => f.required) || [] + const requiredFields = visibleTemplateFields.filter((f: any) => f.required) for (const field of requiredFields) { const value = templateFieldValues[field.key] - if (!value || (typeof value === 'string' && value.trim() === '')) { + if (!hasFieldValue(value, field.type)) { setError(`请填写必填字段:${field.label}`) return } @@ -180,13 +201,21 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen try { const formData = new FormData() formData.append('id', material.id) + if (selectedTemplate?.id) { + formData.append('templateId', selectedTemplate.id) + } // 添加所有模板字段值 - Object.entries(templateFieldValues).forEach(([key, value]) => { + const visibleTemplateFieldValues = selectedTemplate?.fields?.length + ? filterVisibleFieldValues(selectedTemplate.fields, templateFieldValues) + : templateFieldValues + + Object.entries(visibleTemplateFieldValues).forEach(([key, value]) => { if (value !== null && value !== undefined) { formData.append(key, String(value)) } }) + formData.append('templateFields', JSON.stringify(visibleTemplateFieldValues)) if (previewImage) { formData.append('previewImage', previewImage) @@ -219,6 +248,7 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen const renderTemplateField = (field: any) => { const fieldValue = templateFieldValues[field.key] || '' const isHalfWidth = field.layoutWidth === 'half' + const fieldInputId = `edit-template-field-${field.id || field.key}` return (
-
{field.type === 'text' || field.type === 'uppercase' ? ( { @@ -256,15 +290,14 @@ export default function EditMaterialModal({ material, onClose, onSuccess, isOpen : `请输入${field.label || field.key}` } className={cn( - 'w-full px-4 py-2.5 text-sm rounded-2xl outline-none transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] border bg-white/90 dark:bg-slate-800/80 dark:text-white border-slate-200/70 dark:border-slate-700/60 shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] dark:shadow-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:border-blue-400/60', - field.required - ? 'bg-gradient-to-br from-white via-blue-50/90 to-blue-100/60 dark:from-slate-800/80 dark:via-blue-900/30 dark:to-blue-900/20 border-blue-200/80 dark:border-blue-500/50 shadow-[0_18px_35px_-25px_rgba(37,99,235,1)]' - : 'bg-white/90 dark:bg-slate-800/80', + controlClass, + field.required ? requiredControlClass : null, field.type === 'uppercase' && 'uppercase' )} /> ) : field.type === 'textarea' ? (