gongxiangp2/components/upload/UploadForm.tsx

363 lines
13 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.

/**
* 上传表单字段组件
* 包含 SPU、店铺、场景、平台、品类、渲染器等表单字段
*/
'use client'
import { useState } from 'react'
import { requiresCategoryAndScene, requiresProductCategoryName, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
/**
* 分类数据项
*/
interface CategoryItem {
id: string
name: string
}
/**
* 表单字段 Props
*/
export interface UploadFormProps {
/** 素材类型 */
materialType: string
/** SPU型号 */
productModel: string
/** 店铺名称 */
categoryName: string
/** 场景名称 */
sceneName: string
/** 平台名称 */
platformName: string
/** 品类名称 */
productCategoryName: string
/** 渲染器 */
renderer: string
/** 店铺列表 */
categories: CategoryItem[]
/** 场景列表 */
scenes: CategoryItem[]
/** 平台列表 */
platforms: CategoryItem[]
/** 品类列表 */
productCategories: CategoryItem[]
/** 是否批量模式(批量模式下隐藏部分字段) */
isBatchMode?: boolean
/** SPU变更回调 */
onProductModelChange: (value: string) => void
/** 店铺变更回调 */
onCategoryChange: (value: string) => void
/** 场景变更回调 */
onSceneChange: (value: string) => void
/** 平台变更回调 */
onPlatformChange: (value: string) => void
/** 品类变更回调 */
onProductCategoryChange: (value: string) => void
/** 渲染器变更回调 */
onRendererChange: (value: string) => void
/** 添加店铺回调 */
onAddCategory: (name: string) => Promise<void>
/** 添加场景回调 */
onAddScene: (name: string) => Promise<void>
}
/**
* 上传表单字段组件
*/
export default function UploadForm({
materialType,
productModel,
categoryName,
sceneName,
platformName,
productCategoryName,
renderer,
categories,
scenes,
platforms,
productCategories,
isBatchMode = false,
onProductModelChange,
onCategoryChange,
onSceneChange,
onPlatformChange,
onProductCategoryChange,
onRendererChange,
onAddCategory,
onAddScene,
}: UploadFormProps) {
// 新建店铺/场景输入状态
const [showNewCategoryInput, setShowNewCategoryInput] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
const [showNewSceneInput, setShowNewSceneInput] = useState(false)
const [newSceneName, setNewSceneName] = useState('')
const isAutoGeneralType = shouldAutoFillGeneral(materialType)
// 处理添加新店铺
const handleAddNewCategory = async () => {
if (!newCategoryName.trim()) return
await onAddCategory(newCategoryName.trim())
setNewCategoryName('')
setShowNewCategoryInput(false)
}
// 处理添加新场景
const handleAddNewScene = async () => {
if (!newSceneName.trim()) return
await onAddScene(newSceneName.trim())
setNewSceneName('')
setShowNewSceneInput(false)
}
// 批量模式下不显示这些字段
if (isBatchMode) {
return null
}
// 品牌VI灵感类型:只显示SPU和品类
const isBrandVI = materialType === '品牌VI灵感'
return (
<>
{/* SPU 或材质名称 */}
<div>
<label htmlFor="productModel" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{materialType === '材质' ? '材质名称 *' : 'SPU型号 *'}
</label>
<input
id="productModel"
type="text"
value={productModel}
onChange={(e) => {
// 材质类型支持中文,其他类型只允许英文、数字和常见符号
if (materialType === '材质') {
onProductModelChange(e.target.value)
} else {
const value = e.target.value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase()
onProductModelChange(value)
}
}}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
placeholder="例如:XY-2024-001"
/>
</div>
{/* 品类分类(品牌VI灵感必填,标准模型必填,其他类型可选) */}
{productCategories.length > 0 && (
<div>
<label htmlFor="productCategoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
品类{isBrandVI || requiresProductCategoryName(materialType) ? ' *' : '(可选)'}
</label>
<select
id="productCategoryName"
value={productCategoryName}
onChange={(e) => onProductCategoryChange(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
>
<option value="">{isBrandVI || requiresProductCategoryName(materialType) ? '请选择品类' : '不选择品类'}</option>
{productCategories.map((category) => (
<option key={category.id} value={category.name}>
{category.name}
</option>
))}
</select>
</div>
)}
{/* 使用店铺(品牌VI灵感不显示,其他类型按需显示) */}
{!isBrandVI && requiresCategoryAndScene(materialType) && (
<div>
<label htmlFor="categoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
使用店铺 *
</label>
{showNewCategoryInput ? (
<div className="flex space-x-2">
<input
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
placeholder="输入新店铺名称"
/>
<button
type="button"
onClick={handleAddNewCategory}
className="btn-primary"
>
添加
</button>
<button
type="button"
onClick={() => {
setShowNewCategoryInput(false)
setNewCategoryName('')
}}
className="btn-secondary"
>
取消
</button>
</div>
) : (
<div className="flex space-x-2">
<select
id="categoryName"
value={categoryName}
onChange={(e) => onCategoryChange(e.target.value)}
disabled={isAutoGeneralType}
className={`flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none ${isAutoGeneralType ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isAutoGeneralType ? (
<option value="通用">通用</option>
) : (
<>
<option value="">选择使用店铺</option>
{categories.map((category) => (
<option key={category.id} value={category.name}>
{category.name}
</option>
))}
</>
)}
</select>
<button
type="button"
onClick={() => setShowNewCategoryInput(true)}
className="btn-secondary whitespace-nowrap"
disabled={isAutoGeneralType}
>
+ 新建店铺
</button>
</div>
)}
</div>
)}
{/* 场景类别(品牌VI灵感、材质类型和标准模型不需要) */}
{!isBrandVI && requiresCategoryAndScene(materialType) && (
<div>
<label htmlFor="sceneName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
场景类别 *
</label>
{showNewSceneInput ? (
<div className="flex space-x-2">
<input
type="text"
value={newSceneName}
onChange={(e) => setNewSceneName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
placeholder="输入新场景类别"
/>
<button
type="button"
onClick={handleAddNewScene}
className="btn-primary"
>
添加
</button>
<button
type="button"
onClick={() => {
setShowNewSceneInput(false)
setNewSceneName('')
}}
className="btn-secondary"
>
取消
</button>
</div>
) : (
<div className="flex space-x-2">
<select
id="sceneName"
value={sceneName}
onChange={(e) => onSceneChange(e.target.value)}
disabled={isAutoGeneralType}
className={`flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none ${isAutoGeneralType ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isAutoGeneralType ? (
<option value="通用">通用</option>
) : (
<>
<option value="">选择场景类别</option>
{scenes.map((scene) => (
<option key={scene.id} value={scene.name}>
{scene.name}
</option>
))}
</>
)}
</select>
<button
type="button"
onClick={() => setShowNewSceneInput(true)}
className="btn-secondary whitespace-nowrap"
disabled={isAutoGeneralType}
>
+ 新建场景类别
</button>
</div>
)}
</div>
)}
{/* 平台分类(可选,品牌VI灵感不显示,其他类型按需显示) */}
{!isBrandVI && platforms.length > 0 && !requiresProductCategoryName(materialType) && (
<div>
<label htmlFor="platformName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
平台分类(可选)
</label>
<select
id="platformName"
value={platformName}
onChange={(e) => onPlatformChange(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
>
<option value="">不选择平台</option>
{platforms.map((platform) => (
<option key={platform.id} value={platform.name}>
{platform.name}
</option>
))}
</select>
</div>
)}
{/* 渲染器(仅3D场景显示) */}
{requiresRenderer(materialType) && (
<div>
<label htmlFor="renderer" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
渲染器 *
</label>
<select
id="renderer"
value={renderer}
onChange={(e) => onRendererChange(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
>
<option value="CR">CR</option>
<option value="VR">VR</option>
<option value="其它">其它</option>
</select>
</div>
)}
{/* 标准模型说明 */}
{requiresProductCategoryName(materialType) && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-apple p-4">
<p className="text-sm text-blue-700 dark:text-blue-300 font-medium mb-2">
💡 标准模型类型说明:
</p>
<ul className="text-xs text-blue-600 dark:text-blue-400 space-y-1 list-disc list-inside">
<li>以产品型号(SPU)为基准创建模型合集</li>
<li>上传时只需填写 SPU 型号和预览图</li>
<li>创建后在详情页添加各个 SKU 的模型文件</li>
<li>每个 SKU 需要单独的预览图和压缩包</li>
</ul>
</div>
)}
</>
)
}