363 lines
13 KiB
TypeScript
363 lines
13 KiB
TypeScript
/**
|
||
* 上传表单字段组件
|
||
* 包含 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>
|
||
)}
|
||
</>
|
||
)
|
||
}
|