gongxiangp2/components/EditMaterialModal.tsx

541 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useRef, useEffect } from 'react'
import Image from 'next/image'
import { requiresCategoryAndScene, requiresRenderer, requiresProductCategoryName, shouldPassRenderer } from '@/lib/material-validation'
interface EditMaterialModalProps {
material: {
id: string
previewUrl: string
productModel: string
categoryName: string
sceneName: string
platformName?: string | null
productCategoryName?: string | null
materialType: string
renderer: string
authorName: string
}
onClose: () => void
onSuccess: () => void
}
export default function EditMaterialModal({ material, onClose, onSuccess }: EditMaterialModalProps) {
const [previewImage, setPreviewImage] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string>(material.previewUrl)
const [productModel, setProductModel] = useState(material.productModel)
const [categoryName, setCategoryName] = useState(material.categoryName)
const [sceneName, setSceneName] = useState(material.sceneName)
const [platformName, setPlatformName] = useState(material.platformName || '')
const [productCategoryName, setProductCategoryName] = useState(material.productCategoryName || '')
const [materialType, setMaterialType] = useState(material.materialType || '3D场景')
const [renderer, setRenderer] = useState(material.renderer || 'CR')
const [authorName, setAuthorName] = useState(material.authorName)
const [categories, setCategories] = useState<Array<{ id: string; name: string }>>([])
const [scenes, setScenes] = useState<Array<{ id: string; name: string }>>([])
const [platforms, setPlatforms] = useState<Array<{ id: string; name: string; count: number }>>([])
const [productCategories, setProductCategories] = useState<Array<{ id: string; name: string; count: number }>>([])
const [showNewCategoryInput, setShowNewCategoryInput] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
const [showNewSceneInput, setShowNewSceneInput] = useState(false)
const [newSceneName, setNewSceneName] = useState('')
const [updating, setUpdating] = useState(false)
const [error, setError] = useState('')
const previewInputRef = useRef<HTMLInputElement>(null)
// 加载分类和场景列表
useEffect(() => {
fetchCategories()
fetchScenes()
fetchPlatforms()
fetchProductCategories()
}, [])
const fetchCategories = async () => {
try {
const response = await fetch('/api/categories')
if (response.ok) {
const data = await response.json()
setCategories(data)
}
} catch (error) {
console.error('Error fetching categories:', error)
}
}
const fetchScenes = async () => {
try {
const response = await fetch('/api/scenes')
if (response.ok) {
const data = await response.json()
setScenes(data)
}
} catch (error) {
console.error('Error fetching scenes:', error)
}
}
const fetchPlatforms = async () => {
try {
const response = await fetch('/api/platforms')
if (response.ok) {
const data = await response.json()
setPlatforms(data)
}
} catch (error) {
console.error('Error fetching platforms:', error)
}
}
const fetchProductCategories = async () => {
try {
const response = await fetch('/api/product-categories')
if (response.ok) {
const data = await response.json()
setProductCategories(data)
}
} catch (error) {
console.error('Error fetching product categories:', error)
}
}
const handleAddNewCategory = async () => {
if (!newCategoryName.trim()) {
setError('请输入一级分类名称')
return
}
try {
const response = await fetch('/api/categories', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newCategoryName.trim() }),
})
if (response.ok) {
const newCategory = await response.json()
await fetchCategories()
setCategoryName(newCategory.name)
setNewCategoryName('')
setShowNewCategoryInput(false)
setError('')
} else {
const data = await response.json()
setError(data.error || '添加一级分类失败')
}
} catch (error) {
console.error('Error adding category:', error)
setError('添加一级分类失败')
}
}
const handleAddNewScene = async () => {
if (!newSceneName.trim()) {
setError('请输入场景名称')
return
}
try {
const response = await fetch('/api/scenes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newSceneName.trim() }),
})
if (response.ok) {
const newScene = await response.json()
await fetchScenes()
setSceneName(newScene.name)
setNewSceneName('')
setShowNewSceneInput(false)
setError('')
} else {
const data = await response.json()
setError(data.error || '添加场景失败')
}
} catch (error) {
console.error('Error adding scene:', error)
setError('添加场景失败')
}
}
const handlePreviewChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!file.type.startsWith('image/')) {
setError('请选择图片文件')
return
}
setPreviewImage(file)
setPreviewUrl(URL.createObjectURL(file))
setError('')
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
// 基础必填字段验证
if (!productModel || !materialType || !authorName) {
setError('请填写所有必填字段')
return
}
// 使用统一的验证逻辑
if (requiresCategoryAndScene(materialType) && (!categoryName || !sceneName)) {
setError('请填写所有必填字段(店铺、场景)')
return
}
if (requiresProductCategoryName(materialType) && !productCategoryName) {
setError('标准模型类型必须选择品类')
return
}
if (requiresRenderer(materialType) && !renderer) {
setError('3D场景类型必须指定渲染器')
return
}
setUpdating(true)
try {
const formData = new FormData()
formData.append('id', material.id)
formData.append('productModel', productModel)
formData.append('categoryName', categoryName)
formData.append('sceneName', sceneName)
if (platformName) formData.append('platformName', platformName)
if (productCategoryName) formData.append('productCategoryName', productCategoryName)
formData.append('materialType', materialType)
// 统一处理renderer字段:根据素材类型判断是否需要传递
const rendererInfo = shouldPassRenderer(materialType, renderer)
if (rendererInfo.shouldPass) {
formData.append('renderer', rendererInfo.value)
}
formData.append('authorName', authorName)
if (previewImage) {
formData.append('previewImage', previewImage)
}
const response = await fetch('/api/materials/update', {
method: 'PUT',
body: formData,
})
if (response.ok) {
onSuccess()
} else {
const data = await response.json()
setError(data.error || '更新失败')
}
} catch (error) {
console.error('Update error:', error)
setError('更新失败,请稍后再试')
} finally {
setUpdating(false)
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-apple max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-apple-lg">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">编辑素材</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
>
×
</button>
</div>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-apple">
{error}
</div>
)}
{/* Preview Image */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
预览图
</label>
<div
onClick={() => previewInputRef.current?.click()}
className="border-2 border-dashed border-gray-300 rounded-apple p-4 text-center cursor-pointer hover:border-primary-500 transition-colors"
>
<div className="relative w-full h-48">
<Image
src={previewUrl}
alt="预览"
fill
unoptimized
className="object-contain"
/>
</div>
<p className="text-xs text-gray-400 mt-2">
点击更换预览图(可选)
</p>
</div>
<input
ref={previewInputRef}
type="file"
accept="image/*"
onChange={handlePreviewChange}
className="hidden"
/>
</div>
{/* Product Model */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
商品型号 *
</label>
<input
type="text"
value={productModel}
onChange={(e) => {
// 只允许英文、数字和常见符号(- _ .),过滤中文,自动转大写
const value = e.target.value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase()
setProductModel(value)
}}
className="input-apple"
placeholder="例如:XY-2024-001"
required
/>
</div>
{/* Category Name (一级分类) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
一级分类(使用店铺) *
</label>
{!showNewCategoryInput ? (
<div className="space-y-2">
<select
value={categoryName}
onChange={(e) => {
if (e.target.value === '__add_new__') {
setShowNewCategoryInput(true)
setCategoryName('')
} else {
setCategoryName(e.target.value)
}
}}
className="input-apple"
required
>
<option value="">请选择一级分类</option>
{categories.map((category) => (
<option key={category.id} value={category.name}>
{category.name}
</option>
))}
<option value="__add_new__">➕ 添加新分类</option>
</select>
</div>
) : (
<div className="space-y-2">
<div className="flex gap-2">
<input
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
className="input-apple flex-1"
placeholder="输入新的一级分类名称"
autoFocus
/>
<button
type="button"
onClick={handleAddNewCategory}
className="px-4 py-2 bg-blue-600 text-white rounded-apple hover:bg-blue-700 transition-colors"
>
确认
</button>
<button
type="button"
onClick={() => {
setShowNewCategoryInput(false)
setNewCategoryName('')
setError('')
}}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-apple hover:bg-gray-200 transition-colors"
>
取消
</button>
</div>
</div>
)}
</div>
{/* Scene Name (二级分类) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
二级分类(场景名称) *
</label>
{!showNewSceneInput ? (
<div className="space-y-2">
<select
value={sceneName}
onChange={(e) => {
if (e.target.value === '__add_new__') {
setShowNewSceneInput(true)
setSceneName('')
} else {
setSceneName(e.target.value)
}
}}
className="input-apple"
required
>
<option value="">请选择场景</option>
{scenes.map((scene) => (
<option key={scene.id} value={scene.name}>
{scene.name}
</option>
))}
<option value="__add_new__">+ 添加新场景</option>
</select>
</div>
) : (
<div className="space-y-2">
<div className="flex space-x-2">
<input
type="text"
value={newSceneName}
onChange={(e) => setNewSceneName(e.target.value)}
className="input-apple"
placeholder="输入新场景名称"
autoFocus
/>
<button
type="button"
onClick={handleAddNewScene}
className="btn-primary whitespace-nowrap"
>
确定
</button>
<button
type="button"
onClick={() => {
setShowNewSceneInput(false)
setNewSceneName('')
setError('')
}}
className="btn-secondary whitespace-nowrap"
>
取消
</button>
</div>
<p className="text-xs text-gray-500">
添加后将自动加入到场景列表中
</p>
</div>
)}
</div>
{/* Material Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
素材类型 *
</label>
<select
value={materialType}
onChange={(e) => setMaterialType(e.target.value)}
className="input-apple"
required
>
<option value="3D场景">3D场景</option>
<option value="详情页">详情页</option>
<option value="主图模版">主图模版</option>
<option value="SKU模版">SKU模版</option>
<option value="主图视频">主图视频</option>
<option value="安装视频">安装视频</option>
<option value="种草视频">种草视频</option>
<option value="模特">模特</option>
<option value="实拍图片">实拍图片</option>
<option value="买家秀图片">买家秀图片</option>
<option value="小红书种草">小红书种草</option>
<option value="音乐">音乐</option>
<option value="C4D模型">C4D模型</option>
<option value="标准饰品">标准饰品</option>
<option value="品牌VI灵感">品牌VI灵感</option>
<option value="矢量文件">矢量文件</option>
<option value="CAD">CAD</option>
<option value="标准模型">标准模型</option>
</select>
</div>
{/* Renderer (仅3D场景显示) */}
{requiresRenderer(materialType) && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
渲染器 *
</label>
<select
value={renderer}
onChange={(e) => setRenderer(e.target.value)}
className="input-apple"
required
>
<option value="CR">CR</option>
<option value="VR">VR</option>
<option value="其它">其它</option>
</select>
</div>
)}
{/* Author Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
作者花名 *
</label>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
className="input-apple"
placeholder="作者花名"
required
/>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onClose}
className="btn-secondary"
disabled={updating}
>
取消
</button>
<button
type="submit"
className="btn-primary"
disabled={updating}
>
{updating ? '更新中...' : '保存修改'}
</button>
</div>
</form>
</div>
</div>
)
}