feat(基础资料库): 移除系统名称、优化花色筛选和表单布局
- 全栈移除 SubstrateSystemNameOption(字典管理、表单、API、数据库) - 花色改为单选 + 自动搜索下拉,排除五金油漆/漆面颜色/五金漆面色 - 基材名称自动生成格式:尺寸+环保等级+材质+花色+贴面 - 材质改为纯下拉选择,贴面始终显示 - 花色框体与 Select 组件宽度/高度统一 - 厚度规格字典只保留厚度数值+排序值
This commit is contained in:
parent
c1606f9af6
commit
8d6eed4583
@ -446,8 +446,8 @@ model Substrate {
|
||||
thicknessSpec SubstrateThicknessSpec @relation(fields: [thicknessSpecId], references: [id])
|
||||
surfaceOptionId String?
|
||||
surfaceOption SubstrateSurfaceOption? @relation(fields: [surfaceOptionId], references: [id])
|
||||
systemNameOptionId String
|
||||
systemNameOption SubstrateSystemNameOption @relation(fields: [systemNameOptionId], references: [id])
|
||||
systemNameOptionId String?
|
||||
systemNameOption SubstrateSystemNameOption? @relation(fields: [systemNameOptionId], references: [id])
|
||||
suggestedPrice Decimal? @db.Decimal(10, 2)
|
||||
weightKg Float?
|
||||
metadata Json?
|
||||
|
||||
@ -49,8 +49,7 @@ export async function DELETE(request: NextRequest, context: RouteParams) {
|
||||
type !== 'CATEGORY' &&
|
||||
type !== 'ENVIRONMENT_GRADE' &&
|
||||
type !== 'THICKNESS_SPEC' &&
|
||||
type !== 'SURFACE_OPTION' &&
|
||||
type !== 'SYSTEM_NAME_OPTION'
|
||||
type !== 'SURFACE_OPTION'
|
||||
) {
|
||||
return jsonError(ErrorCode.VALIDATION_FAILED, '缺少有效的字典类型', 400, ctx.requestId)
|
||||
}
|
||||
|
||||
@ -104,7 +104,6 @@ export const substrateCreateSchema = z.object({
|
||||
environmentGradeId: z.string().trim().min(1),
|
||||
thicknessSpecId: z.string().trim().min(1),
|
||||
surfaceOptionId: z.string().trim().min(1).optional().nullable(),
|
||||
systemNameOptionId: z.string().trim().min(1),
|
||||
suggestedPrice: z.number().nonnegative().optional().nullable(),
|
||||
weightKg: z.number().nonnegative().optional().nullable(),
|
||||
finishIds: z.array(z.string().trim().min(1)).optional(),
|
||||
@ -118,7 +117,6 @@ export const substrateUpdateSchema = z.object({
|
||||
environmentGradeId: z.string().trim().min(1).optional(),
|
||||
thicknessSpecId: z.string().trim().min(1).optional(),
|
||||
surfaceOptionId: z.string().trim().min(1).optional().nullable(),
|
||||
systemNameOptionId: z.string().trim().min(1).optional(),
|
||||
suggestedPrice: z.number().nonnegative().optional().nullable(),
|
||||
weightKg: z.number().nonnegative().optional().nullable(),
|
||||
finishIds: z.array(z.string().trim().min(1)).optional(),
|
||||
@ -131,7 +129,6 @@ export const substrateDictionaryTypeSchema = z.enum([
|
||||
'ENVIRONMENT_GRADE',
|
||||
'THICKNESS_SPEC',
|
||||
'SURFACE_OPTION',
|
||||
'SYSTEM_NAME_OPTION',
|
||||
])
|
||||
|
||||
export const substrateDictionaryCreateSchema = z.object({
|
||||
|
||||
@ -54,7 +54,6 @@ const dictionaries: SubstrateDictionaryBundleDTO = {
|
||||
environmentGrades: [{ id: 'grade-e1', type: 'ENVIRONMENT_GRADE', name: 'E1', sortOrder: 0, isActive: true, createdAt: new Date(), updatedAt: new Date() }],
|
||||
thicknessSpecs: [{ id: 'th-18', type: 'THICKNESS_SPEC', name: '18', sortOrder: 0, isActive: true, thicknessMm: 18, sheetLengthMm: 2440, sheetWidthMm: 1220, createdAt: new Date(), updatedAt: new Date() }],
|
||||
surfaceOptions: [{ id: 'surface-double', type: 'SURFACE_OPTION', name: '双贴', sortOrder: 0, isActive: true, createdAt: new Date(), updatedAt: new Date() }],
|
||||
systemNameOptions: [{ id: 'sys-18e1', type: 'SYSTEM_NAME_OPTION', name: '18E1级刨花板', sortOrder: 0, isActive: true, createdAt: new Date(), updatedAt: new Date() }],
|
||||
}
|
||||
|
||||
function renderClient() {
|
||||
@ -76,8 +75,6 @@ function renderClient() {
|
||||
sheetWidthMm: 1220,
|
||||
surfaceOptionId: 'surface-double',
|
||||
surfaceOptionName: '双贴',
|
||||
systemNameOptionId: 'sys-18e1',
|
||||
systemNameOptionName: '18E1级刨花板',
|
||||
suggestedPrice: 180,
|
||||
weightKg: null,
|
||||
finishes: [],
|
||||
@ -118,8 +115,6 @@ describe('MaterialLibraryClient', () => {
|
||||
sheetWidthMm: 1220,
|
||||
surfaceOptionId: 'surface-double',
|
||||
surfaceOptionName: '双贴',
|
||||
systemNameOptionId: 'sys-18e1',
|
||||
systemNameOptionName: '18E1级刨花板',
|
||||
suggestedPrice: 200,
|
||||
weightKg: null,
|
||||
finishes: [],
|
||||
@ -145,7 +140,6 @@ describe('MaterialLibraryClient', () => {
|
||||
environmentGradeId: 'grade-e1',
|
||||
thicknessSpecId: 'th-18',
|
||||
surfaceOptionId: 'surface-double',
|
||||
systemNameOptionId: 'sys-18e1',
|
||||
suggestedPrice: 200,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ import { startTransition, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ImagePlus,
|
||||
Package2,
|
||||
Pencil,
|
||||
@ -72,7 +73,6 @@ type SubstrateFormState = {
|
||||
environmentGradeId: string
|
||||
thicknessSpecId: string
|
||||
surfaceOptionId: string
|
||||
systemNameOptionId: string
|
||||
suggestedPrice: string
|
||||
weightKg: string
|
||||
finishIds: string[]
|
||||
@ -144,7 +144,6 @@ function emptySubstrateForm(dict: SubstrateDictionaryBundleDTO): SubstrateFormSt
|
||||
environmentGradeId: dict.environmentGrades[0]?.id ?? '',
|
||||
thicknessSpecId: dict.thicknessSpecs[0]?.id ?? '',
|
||||
surfaceOptionId: dict.surfaceOptions[0]?.id ?? '',
|
||||
systemNameOptionId: dict.systemNameOptions[0]?.id ?? '',
|
||||
suggestedPrice: '',
|
||||
weightKg: '',
|
||||
finishIds: [],
|
||||
@ -170,8 +169,6 @@ function dictionaryOptionsByType(bundle: SubstrateDictionaryBundleDTO, type: Sub
|
||||
return bundle.thicknessSpecs
|
||||
case 'SURFACE_OPTION':
|
||||
return bundle.surfaceOptions
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
return bundle.systemNameOptions
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,8 +182,6 @@ function dictionaryLabel(type: SubstrateDictionaryType) {
|
||||
return '厚度规格'
|
||||
case 'SURFACE_OPTION':
|
||||
return '贴面类型'
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
return '系统名称'
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,7 +220,8 @@ export default function MaterialLibraryClient({
|
||||
const [substrateForm, setSubstrateForm] = useState<SubstrateFormState>(
|
||||
emptySubstrateForm(initialSubstrateDictionaries),
|
||||
)
|
||||
const [finishPickerOpen, setFinishPickerOpen] = useState(false)
|
||||
const [finishSearch, setFinishSearch] = useState('')
|
||||
const [finishDropdownOpen, setFinishDropdownOpen] = useState(false)
|
||||
|
||||
// Dictionary management state
|
||||
const [dictionaryModalOpen, setDictionaryModalOpen] = useState(false)
|
||||
@ -241,7 +237,6 @@ export default function MaterialLibraryClient({
|
||||
...dictionaries.environmentGrades,
|
||||
...dictionaries.thicknessSpecs,
|
||||
...dictionaries.surfaceOptions,
|
||||
...dictionaries.systemNameOptions,
|
||||
]
|
||||
const map = new Map<string, { name: string }>()
|
||||
for (const item of all) map.set(item.id, item)
|
||||
@ -255,7 +250,6 @@ export default function MaterialLibraryClient({
|
||||
ENVIRONMENT_GRADE: new Set(substrates.map((item) => item.environmentGradeId).filter(Boolean)),
|
||||
THICKNESS_SPEC: new Set(substrates.map((item) => item.thicknessSpecId).filter(Boolean)),
|
||||
SURFACE_OPTION: new Set(substrates.map((item) => item.surfaceOptionId).filter(Boolean)),
|
||||
SYSTEM_NAME_OPTION: new Set(substrates.map((item) => item.systemNameOptionId).filter(Boolean)),
|
||||
} as Record<SubstrateDictionaryType, Set<string>>
|
||||
}, [substrates])
|
||||
|
||||
@ -284,14 +278,20 @@ export default function MaterialLibraryClient({
|
||||
)
|
||||
}, [substrates, searchText])
|
||||
|
||||
const isSolidWood = substrateForm.categoryId === 'cat-solid' || substrateForm.categoryText === '实木'
|
||||
const isSolidWood = (() => {
|
||||
const catName = dictById.get(substrateForm.categoryId)?.name ?? substrateForm.categoryText
|
||||
if (!catName) return false
|
||||
const solidKeywords = ['实木', '橡胶木', '橡木', '黑胡桃', '白蜡', '榉木', '桦木', '松木', '柞木']
|
||||
return solidKeywords.some((kw) => catName.includes(kw))
|
||||
})()
|
||||
|
||||
// Filter finishes by material type
|
||||
// Filter finishes by material type, exclude 五金油漆/漆面颜色/五金漆面色 (these are for hardware accessories only)
|
||||
const filteredFinishesForSubstrate = useMemo(() => {
|
||||
const excludeCategories = ['五金油漆', '漆面颜色', '五金漆面色']
|
||||
if (isSolidWood) {
|
||||
return finishes.filter((f) => f.categoryName === '实木花色')
|
||||
return finishes.filter((f) => f.categoryName === '实木花色' && !excludeCategories.includes(f.categoryName ?? ''))
|
||||
}
|
||||
return finishes.filter((f) => f.categoryName !== '实木花色')
|
||||
return finishes.filter((f) => f.categoryName !== '实木花色' && !excludeCategories.includes(f.categoryName ?? ''))
|
||||
}, [finishes, isSolidWood])
|
||||
|
||||
// Get the default finish category for inline creation
|
||||
@ -302,20 +302,26 @@ export default function MaterialLibraryClient({
|
||||
return categories.find((c) => c.name === '木纹贴面花色')?.id ?? categories[0]?.id ?? ''
|
||||
}, [categories, isSolidWood])
|
||||
|
||||
const finishAutoSuggestions = useMemo(() => {
|
||||
const kw = finishSearch.trim().toLowerCase()
|
||||
if (!kw) return []
|
||||
return filteredFinishesForSubstrate
|
||||
.filter((f) => f.name.toLowerCase().includes(kw))
|
||||
.slice(0, 8)
|
||||
}, [finishSearch, filteredFinishesForSubstrate])
|
||||
|
||||
const previewSubstrateName = useMemo(() => {
|
||||
const cat = substrateForm.categoryText.trim() || dictById.get(substrateForm.categoryId)?.name || ''
|
||||
const cat = dictById.get(substrateForm.categoryId)?.name || ''
|
||||
const env = dictById.get(substrateForm.environmentGradeId)?.name ?? ''
|
||||
const thick = dictionaries.thicknessSpecs.find((t) => t.id === substrateForm.thicknessSpecId)
|
||||
const surface = isSolidWood ? '' : (dictById.get(substrateForm.surfaceOptionId)?.name ?? '')
|
||||
const finishNames = substrateForm.finishIds
|
||||
.map((fid) => finishes.find((f) => f.id === fid)?.name)
|
||||
.filter(Boolean) as string[]
|
||||
const newFinish = substrateForm.newFinishName.trim()
|
||||
if (newFinish) finishNames.push(newFinish)
|
||||
const surface = dictById.get(substrateForm.surfaceOptionId)?.name ?? ''
|
||||
const finishName = substrateForm.finishIds.length > 0
|
||||
? finishes.find((f) => f.id === substrateForm.finishIds[0])?.name
|
||||
: substrateForm.newFinishName.trim() || ''
|
||||
if (!cat || !env || !thick) return ''
|
||||
const thickness = thick.name.toUpperCase()
|
||||
const finishPart = finishNames.length > 0 ? finishNames.join(' ') : ''
|
||||
return [thickness, env, cat, finishPart, surface].filter(Boolean).join(' ')
|
||||
const thickness = `${thick.thicknessMm ?? thick.name}`
|
||||
const envPart = env.endsWith('级') ? env : `${env}级`
|
||||
return [thickness, envPart, cat, finishName, surface].filter(Boolean).join(' ')
|
||||
}, [substrateForm, dictById, dictionaries.thicknessSpecs, finishes, isSolidWood])
|
||||
|
||||
// Selected items
|
||||
@ -371,7 +377,6 @@ export default function MaterialLibraryClient({
|
||||
environmentGradeId: s.environmentGradeId,
|
||||
thicknessSpecId: s.thicknessSpecId,
|
||||
surfaceOptionId: s.surfaceOptionId ?? '',
|
||||
systemNameOptionId: s.systemNameOptionId,
|
||||
suggestedPrice: s.suggestedPrice != null ? String(s.suggestedPrice) : '',
|
||||
weightKg: s.weightKg != null ? String(s.weightKg) : '',
|
||||
finishIds: s.finishes.map((f) => f.id),
|
||||
@ -420,7 +425,7 @@ export default function MaterialLibraryClient({
|
||||
|
||||
async function handleSaveSubstrate() {
|
||||
const solidWood = substrateForm.categoryId === 'cat-solid' || substrateForm.categoryText.trim() === '实木'
|
||||
if (!substrateForm.categoryText.trim() || !substrateForm.environmentGradeId || !substrateForm.thicknessSpecId || !substrateForm.systemNameOptionId) {
|
||||
if (!substrateForm.categoryText.trim() || !substrateForm.environmentGradeId || !substrateForm.thicknessSpecId) {
|
||||
toast.error('请补齐必填选项'); return
|
||||
}
|
||||
if (!solidWood && !substrateForm.surfaceOptionId) {
|
||||
@ -470,7 +475,6 @@ export default function MaterialLibraryClient({
|
||||
environmentGradeId: substrateForm.environmentGradeId,
|
||||
thicknessSpecId: substrateForm.thicknessSpecId,
|
||||
surfaceOptionId: solidWood ? undefined : substrateForm.surfaceOptionId,
|
||||
systemNameOptionId: substrateForm.systemNameOptionId,
|
||||
suggestedPrice: toNum(substrateForm.suggestedPrice),
|
||||
weightKg: toNum(substrateForm.weightKg),
|
||||
finishIds: finishIds.length > 0 ? finishIds : undefined,
|
||||
@ -533,11 +537,29 @@ export default function MaterialLibraryClient({
|
||||
}
|
||||
|
||||
async function handleSaveDictionary() {
|
||||
if (!dictionaryForm.name.trim()) {
|
||||
if (activeDictionaryType === 'THICKNESS_SPEC') {
|
||||
if (!dictionaryForm.thicknessMm.trim() || !Number.isFinite(Number(dictionaryForm.thicknessMm))) {
|
||||
toast.error('请填写有效的厚度数值')
|
||||
return
|
||||
}
|
||||
} else if (!dictionaryForm.name.trim()) {
|
||||
toast.error(`请填写${dictionaryLabel(activeDictionaryType)}名称`)
|
||||
return
|
||||
}
|
||||
|
||||
// Uniqueness check for non-THICKNESS_SPEC types
|
||||
if (activeDictionaryType !== 'THICKNESS_SPEC') {
|
||||
const trimmedName = dictionaryForm.name.trim()
|
||||
const existingOptions = dictionaryOptionsByType(dictionaries, activeDictionaryType)
|
||||
const duplicate = existingOptions.find(
|
||||
(opt) => opt.name === trimmedName && opt.id !== editingDictionaryId,
|
||||
)
|
||||
if (duplicate) {
|
||||
toast.error(`已存在同名「${trimmedName}」,不可重复创建`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (dictionaryModalMode === 'create') {
|
||||
@ -583,7 +605,6 @@ export default function MaterialLibraryClient({
|
||||
environmentGrades: patch(current.environmentGrades),
|
||||
thicknessSpecs: patch(current.thicknessSpecs),
|
||||
surfaceOptions: patch(current.surfaceOptions),
|
||||
systemNameOptions: patch(current.systemNameOptions),
|
||||
}
|
||||
})
|
||||
setDictionaryModalOpen(false)
|
||||
@ -619,7 +640,6 @@ export default function MaterialLibraryClient({
|
||||
environmentGrades: current.environmentGrades.filter((item) => item.id !== deleteTarget.id),
|
||||
thicknessSpecs: current.thicknessSpecs.filter((item) => item.id !== deleteTarget.id),
|
||||
surfaceOptions: current.surfaceOptions.filter((item) => item.id !== deleteTarget.id),
|
||||
systemNameOptions: current.systemNameOptions.filter((item) => item.id !== deleteTarget.id),
|
||||
}))
|
||||
setDeleteTarget(null)
|
||||
toast.success(`${dictionaryLabel(deleteTarget.dictionaryType)}已删除`)
|
||||
@ -820,122 +840,137 @@ export default function MaterialLibraryClient({
|
||||
{/* Auto-generated name preview */}
|
||||
<Input label="基材名称(自动生成)" value={previewSubstrateName} readOnly placeholder="按选项自动生成" />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="mb-1.5 text-sm font-apple-medium text-apple-label-primary">材质</p>
|
||||
<input
|
||||
list="substrate-category-list"
|
||||
value={substrateForm.categoryText}
|
||||
onChange={(e) => {
|
||||
const text = e.target.value
|
||||
const match = dictionaries.categories.find((d) => d.name === text)
|
||||
setSubstrateForm((c) => ({
|
||||
...c,
|
||||
categoryText: text,
|
||||
categoryId: match ? match.id : '',
|
||||
}))
|
||||
}}
|
||||
placeholder="选择或输入材质名称"
|
||||
className="w-full rounded-apple-md border border-apple-separator bg-apple-bg-primary px-3 py-1.5 text-sm text-apple-label-primary placeholder:text-apple-label-tertiary"
|
||||
/>
|
||||
<datalist id="substrate-category-list">
|
||||
{dictionaries.categories.map((d) => (
|
||||
<option key={d.id} value={d.name} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<Select label="材质" value={substrateForm.categoryId} onChange={(e) => {
|
||||
const cat = dictionaries.categories.find((d) => d.id === e.target.value)
|
||||
setSubstrateForm((c) => ({
|
||||
...c,
|
||||
categoryId: e.target.value,
|
||||
categoryText: cat?.name ?? '',
|
||||
}))
|
||||
}} options={dictionaries.categories.map((d) => ({ value: d.id, label: d.name }))} />
|
||||
<Select label="环保等级" value={substrateForm.environmentGradeId} onChange={(e) => setSubstrateForm((c) => ({ ...c, environmentGradeId: e.target.value }))} options={dictionaries.environmentGrades.map((d) => ({ value: d.id, label: d.name }))} />
|
||||
<Select label="尺寸" value={substrateForm.thicknessSpecId} onChange={(e) => setSubstrateForm((c) => ({ ...c, thicknessSpecId: e.target.value }))} options={dictionaries.thicknessSpecs.map((d) => ({ value: d.id, label: `${d.thicknessMm}MM (${d.sheetLengthMm ?? 2440}×${d.sheetWidthMm ?? 1220})` }))} />
|
||||
{!isSolidWood && <Select label="贴面" value={substrateForm.surfaceOptionId} onChange={(e) => setSubstrateForm((c) => ({ ...c, surfaceOptionId: e.target.value }))} options={dictionaries.surfaceOptions.map((d) => ({ value: d.id, label: d.name }))} />}
|
||||
<Select label="贴面" value={substrateForm.surfaceOptionId} onChange={(e) => setSubstrateForm((c) => ({ ...c, surfaceOptionId: e.target.value }))} options={dictionaries.surfaceOptions.map((d) => ({ value: d.id, label: d.name }))} />
|
||||
<Input label="售价" value={substrateForm.suggestedPrice} onChange={(e) => setSubstrateForm((c) => ({ ...c, suggestedPrice: e.target.value }))} placeholder="例如 180" />
|
||||
<Input label="重量 (kg)" value={substrateForm.weightKg} onChange={(e) => setSubstrateForm((c) => ({ ...c, weightKg: e.target.value }))} placeholder="例如 32" />
|
||||
</div>
|
||||
{/* Finish multi-select */}
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<p className="text-sm font-apple-medium text-apple-label-primary">花色</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="plain" size="small" onClick={() => setFinishPickerOpen(!finishPickerOpen)}>
|
||||
{finishPickerOpen ? '收起' : '选择已有花色'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Inline new finish input */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={substrateForm.newFinishName}
|
||||
onChange={(e) => setSubstrateForm((c) => ({ ...c, newFinishName: e.target.value }))}
|
||||
placeholder="输入新花色名称,保存时自动创建"
|
||||
className="flex-1 rounded-apple-md border border-apple-separator bg-apple-bg-primary px-3 py-1.5 text-sm text-apple-label-primary placeholder:text-apple-label-tertiary"
|
||||
/>
|
||||
{substrateForm.newFinishName.trim() && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const name = substrateForm.newFinishName.trim()
|
||||
setSubstrateForm((c) => ({ ...c, newFinishName: '' }))
|
||||
// Check if already exists
|
||||
const existing = filteredFinishesForSubstrate.find((f) => f.name === name)
|
||||
if (existing) {
|
||||
if (!substrateForm.finishIds.includes(existing.id)) {
|
||||
setSubstrateForm((c) => ({ ...c, finishIds: [...c.finishIds, existing.id] }))
|
||||
}
|
||||
} else {
|
||||
// Will be created on save via newFinishName
|
||||
setSubstrateForm((c) => ({ ...c, newFinishName: name }))
|
||||
}
|
||||
}}
|
||||
className="shrink-0 rounded-apple-md bg-apple-blue px-3 py-1.5 text-xs text-white hover:bg-apple-blue/90"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
{/* Finish single-select inside grid */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-apple-medium text-apple-label-secondary mb-1.5">花色</label>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between border rounded-apple-md bg-apple-bg-secondary cursor-text h-11 px-4 text-sm',
|
||||
finishDropdownOpen ? 'border-apple-blue border-2' : 'border-apple-separator hover:border-apple-label-tertiary',
|
||||
)}
|
||||
</div>
|
||||
{/* Pending new finish badge */}
|
||||
{substrateForm.newFinishName.trim() && (
|
||||
<div className="flex flex-wrap gap-1 pb-2">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-apple-blue/30 bg-apple-blue/10 px-2 py-0.5 text-[11px] text-apple-blue">
|
||||
+ {substrateForm.newFinishName.trim()}(新建)
|
||||
<button type="button" onClick={() => setSubstrateForm((c) => ({ ...c, newFinishName: '' }))} className="hover:text-apple-red"><X className="h-3 w-3" /></button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{substrateForm.finishIds.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 pb-2">
|
||||
{substrateForm.finishIds.map((fid) => {
|
||||
const f = finishes.find((x) => x.id === fid)
|
||||
if (!f) return null
|
||||
onClick={() => { setFinishDropdownOpen(true); setFinishSearch('') }}
|
||||
>
|
||||
{(() => {
|
||||
const selectedFinish = substrateForm.finishIds.length > 0
|
||||
? finishes.find((x) => x.id === substrateForm.finishIds[0])
|
||||
: null
|
||||
const pendingNew = substrateForm.newFinishName.trim()
|
||||
if (pendingNew && !selectedFinish) {
|
||||
return (
|
||||
<span key={fid} className="inline-flex items-center gap-1 rounded-full border border-apple-separator bg-apple-bg-secondary px-2 py-0.5 text-[11px] text-apple-label-secondary">
|
||||
{f.name}
|
||||
<button type="button" onClick={() => setSubstrateForm((c) => ({ ...c, finishIds: c.finishIds.filter((id) => id !== fid) }))} className="text-apple-label-tertiary hover:text-apple-red"><X className="h-3 w-3" /></button>
|
||||
<span className="flex flex-1 items-center gap-1.5 px-3 py-1.5 text-sm text-apple-blue overflow-hidden">
|
||||
<Plus className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{pendingNew}(新建)</span>
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setSubstrateForm((c) => ({ ...c, newFinishName: '' })) }} className="ml-1 shrink-0 hover:text-apple-red"><X className="h-3 w-3" /></button>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{finishPickerOpen && (
|
||||
<div className="max-h-40 space-y-1 overflow-y-auto rounded-apple-md border border-apple-separator bg-apple-bg-primary p-2">
|
||||
{filteredFinishesForSubstrate.length === 0 ? (
|
||||
<p className="px-2 py-1 text-xs text-apple-label-tertiary">暂无花色,请在上方输入框新建</p>
|
||||
) : (
|
||||
filteredFinishesForSubstrate.map((f) => {
|
||||
const selected = substrateForm.finishIds.includes(f.id)
|
||||
return (
|
||||
<button key={f.id} type="button" onClick={() => setSubstrateForm((c) => ({ ...c, finishIds: selected ? c.finishIds.filter((id) => id !== f.id) : [...c.finishIds, f.id] }))}
|
||||
className={cn('flex w-full items-center gap-2 rounded-apple-sm px-2 py-1.5 text-left text-sm', selected ? 'bg-apple-blue/10 text-apple-blue' : 'text-apple-label-primary hover:bg-apple-bg-secondary')}>
|
||||
<div className={cn('flex h-4 w-4 items-center justify-center rounded border', selected ? 'border-apple-blue bg-apple-blue text-white' : 'border-apple-separator')}>
|
||||
{selected && <Check className="h-3 w-3" />}
|
||||
</div>
|
||||
{f.thumbnailUrl && <img src={f.thumbnailUrl} alt="" className="h-5 w-5 rounded-sm object-cover" />}
|
||||
<span className="flex-1 truncate">{f.name}</span>
|
||||
<span className="text-xs text-apple-label-tertiary">{f.code}</span>
|
||||
}
|
||||
if (selectedFinish) {
|
||||
return (
|
||||
<span className="flex flex-1 items-center gap-1.5 px-3 py-1.5 text-sm text-apple-label-primary overflow-hidden">
|
||||
{selectedFinish.thumbnailUrl && <img src={selectedFinish.thumbnailUrl} alt="" className="h-4 w-4 shrink-0 rounded-sm object-cover" />}
|
||||
<span className="truncate">{selectedFinish.name}</span>
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setSubstrateForm((c) => ({ ...c, finishIds: [] })) }} className="ml-1 shrink-0 text-apple-label-tertiary hover:text-apple-red"><X className="h-3.5 w-3.5" /></button>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={finishSearch}
|
||||
onChange={(e) => { setFinishSearch(e.target.value); setFinishDropdownOpen(true) }}
|
||||
onFocus={() => setFinishDropdownOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const name = finishSearch.trim()
|
||||
if (!name) return
|
||||
const existing = filteredFinishesForSubstrate.find((f) => f.name === name)
|
||||
if (existing) {
|
||||
setSubstrateForm((c) => ({ ...c, finishIds: [existing.id], newFinishName: '' }))
|
||||
} else {
|
||||
setSubstrateForm((c) => ({ ...c, finishIds: [], newFinishName: name }))
|
||||
}
|
||||
setFinishSearch('')
|
||||
setFinishDropdownOpen(false)
|
||||
}
|
||||
}}
|
||||
placeholder="输入花色名称搜索"
|
||||
className="flex-1 bg-transparent px-3 py-1.5 text-sm text-apple-label-primary placeholder:text-apple-label-tertiary focus:outline-none"
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setFinishDropdownOpen(!finishDropdownOpen); setFinishSearch('') }}
|
||||
className="flex shrink-0 items-center justify-center px-2.5 py-1.5 text-apple-label-tertiary hover:text-apple-label-secondary"
|
||||
>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Dropdown */}
|
||||
{finishDropdownOpen && (
|
||||
<div className="absolute left-0 z-10 mt-1 w-full max-h-60 overflow-y-auto rounded-apple-lg border border-apple-separator bg-apple-bg-primary/95 backdrop-blur-xl shadow-apple-lg">
|
||||
<div className="py-1">
|
||||
{finishSearch.trim() ? (
|
||||
finishAutoSuggestions.length > 0 ? (
|
||||
finishAutoSuggestions.map((f) => (
|
||||
<button key={f.id} type="button" onClick={() => {
|
||||
setSubstrateForm((c) => ({ ...c, finishIds: [f.id], newFinishName: '' }))
|
||||
setFinishSearch('')
|
||||
setFinishDropdownOpen(false)
|
||||
}} className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-apple-label-primary hover:bg-apple-bg-secondary">
|
||||
{f.thumbnailUrl && <img src={f.thumbnailUrl} alt="" className="h-5 w-5 shrink-0 rounded-sm object-cover" />}
|
||||
<span className="flex-1 truncate">{f.name}</span>
|
||||
<span className="shrink-0 text-[11px] text-apple-label-tertiary">{f.code}</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<button type="button" onClick={() => {
|
||||
const name = finishSearch.trim()
|
||||
if (name) setSubstrateForm((c) => ({ ...c, finishIds: [], newFinishName: name }))
|
||||
setFinishSearch('')
|
||||
setFinishDropdownOpen(false)
|
||||
}} className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-apple-blue hover:bg-apple-blue/5">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span>创建「{finishSearch.trim()}」</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
) : (
|
||||
filteredFinishesForSubstrate.slice(0, 15).map((f) => {
|
||||
const selected = substrateForm.finishIds.includes(f.id)
|
||||
return (
|
||||
<button key={f.id} type="button" onClick={() => {
|
||||
setSubstrateForm((c) => ({ ...c, finishIds: selected ? [] : [f.id], newFinishName: '' }))
|
||||
setFinishSearch('')
|
||||
setFinishDropdownOpen(false)
|
||||
}} className={cn('flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors', selected ? 'text-apple-blue bg-apple-blue/5' : 'text-apple-label-primary hover:bg-apple-bg-secondary')}>
|
||||
{selected && <Check className="h-3.5 w-3.5 shrink-0" />}
|
||||
{f.thumbnailUrl && <img src={f.thumbnailUrl} alt="" className="h-5 w-5 shrink-0 rounded-sm object-cover" />}
|
||||
<span className="flex-1 truncate">{f.name}</span>
|
||||
<span className="shrink-0 text-[11px] text-apple-label-tertiary">{f.code}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button size="small" onClick={handleSaveSubstrate} loading={submitting}>
|
||||
@ -1017,7 +1052,7 @@ export default function MaterialLibraryClient({
|
||||
{/* Dictionary management buttons (substrate tab only) */}
|
||||
{activeTab === 'substrate' && (
|
||||
<div className="flex gap-2 overflow-x-auto whitespace-nowrap pb-1">
|
||||
{(['CATEGORY', 'ENVIRONMENT_GRADE', 'THICKNESS_SPEC', 'SURFACE_OPTION', 'SYSTEM_NAME_OPTION'] as SubstrateDictionaryType[]).map((type) => (
|
||||
{(['CATEGORY', 'ENVIRONMENT_GRADE', 'THICKNESS_SPEC', 'SURFACE_OPTION'] as SubstrateDictionaryType[]).map((type) => (
|
||||
<Button key={type} variant="plain" size="small" onClick={() => openDictionaryModal(type)} className="shrink-0">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
管理{dictionaryLabel(type)}
|
||||
@ -1085,12 +1120,13 @@ export default function MaterialLibraryClient({
|
||||
<div key={item.id} className="flex items-center gap-2 rounded-apple-sm border border-apple-separator/70 px-2 py-1.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm text-apple-label-primary">
|
||||
{item.name}
|
||||
{item.type === 'THICKNESS_SPEC' && item.thicknessMm != null
|
||||
? `${item.thicknessMm}MM`
|
||||
: item.name}
|
||||
{isLegacySeedOptionId(item.id) ? '(历史默认)' : ''}
|
||||
</p>
|
||||
<p className="text-[11px] text-apple-label-tertiary">
|
||||
排序 {item.sortOrder}
|
||||
{item.type === 'THICKNESS_SPEC' && item.thicknessMm != null ? ` · ${item.thicknessMm}mm` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => openDictionaryModal(activeDictionaryType, item)} className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary" title="编辑">
|
||||
@ -1102,9 +1138,17 @@ export default function MaterialLibraryClient({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Input label="名称" value={dictionaryForm.name} onChange={(event) => setDictionaryForm((current) => ({ ...current, name: event.target.value }))} />
|
||||
<Input label="排序值" value={dictionaryForm.sortOrder} onChange={(event) => setDictionaryForm((current) => ({ ...current, sortOrder: event.target.value }))} />
|
||||
{activeDictionaryType === 'THICKNESS_SPEC' ? <Input label="厚度 mm" value={dictionaryForm.thicknessMm} onChange={(event) => setDictionaryForm((current) => ({ ...current, thicknessMm: event.target.value }))} /> : null}
|
||||
{activeDictionaryType === 'THICKNESS_SPEC' ? (
|
||||
<>
|
||||
<Input label="厚度(MM)" value={dictionaryForm.thicknessMm} onChange={(event) => setDictionaryForm((current) => ({ ...current, thicknessMm: event.target.value, name: event.target.value }))} placeholder="例如 12、15、18" />
|
||||
<Input label="排序值" value={dictionaryForm.sortOrder} onChange={(event) => setDictionaryForm((current) => ({ ...current, sortOrder: event.target.value }))} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input label="名称" value={dictionaryForm.name} onChange={(event) => setDictionaryForm((current) => ({ ...current, name: event.target.value }))} />
|
||||
<Input label="排序值" value={dictionaryForm.sortOrder} onChange={(event) => setDictionaryForm((current) => ({ ...current, sortOrder: event.target.value }))} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
@ -15,17 +15,23 @@ function toFinitePositive(value: number | null | undefined): number | null {
|
||||
}
|
||||
|
||||
export function buildSubstrateDisplayName(params: {
|
||||
thicknessLabel: string
|
||||
thicknessMm: number
|
||||
environmentGradeLabel: string
|
||||
systemNameLabel: string
|
||||
categoryLabel: string
|
||||
finishLabels?: string[]
|
||||
surfaceLabel?: string
|
||||
}): string {
|
||||
const envLabel = params.environmentGradeLabel.trim()
|
||||
const envWithSuffix = envLabel ? (envLabel.endsWith('级') ? envLabel : `${envLabel}级`) : ''
|
||||
return [
|
||||
params.thicknessLabel.trim(),
|
||||
params.environmentGradeLabel.trim(),
|
||||
params.systemNameLabel.trim(),
|
||||
`${params.thicknessMm}`,
|
||||
envWithSuffix,
|
||||
params.categoryLabel.trim(),
|
||||
...(params.finishLabels ?? []),
|
||||
params.surfaceLabel?.trim() ?? '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function buildSubstrateSummaryLabel(params: {
|
||||
|
||||
@ -40,7 +40,6 @@ export interface SubstrateWithRelations extends Substrate {
|
||||
environmentGrade: { id: string; name: string }
|
||||
thicknessSpec: { id: string; name: string; thicknessMm: number; sheetLengthMm: number; sheetWidthMm: number }
|
||||
surfaceOption: { id: string; name: string } | null
|
||||
systemNameOption: { id: string; name: string }
|
||||
finishes: {
|
||||
finish: {
|
||||
id: string
|
||||
@ -89,7 +88,6 @@ const substrateRelationInclude = {
|
||||
},
|
||||
},
|
||||
surfaceOption: { select: { id: true, name: true } },
|
||||
systemNameOption: { select: { id: true, name: true } },
|
||||
finishes: {
|
||||
select: {
|
||||
finish: {
|
||||
|
||||
@ -103,8 +103,6 @@ describe('assetCenterService.createSubstrate', () => {
|
||||
},
|
||||
surfaceOptionId: null,
|
||||
surfaceOption: null,
|
||||
systemNameOptionId: 'sys-solid',
|
||||
systemNameOption: { id: 'sys-solid', name: '实木板' },
|
||||
suggestedPrice: 320,
|
||||
weightKg: 32,
|
||||
metadata: null,
|
||||
@ -124,7 +122,6 @@ describe('assetCenterService.createSubstrate', () => {
|
||||
environmentGradeId: 'grade-e1',
|
||||
thicknessSpecId: 'th-18',
|
||||
surfaceOptionId: null,
|
||||
systemNameOptionId: 'sys-solid',
|
||||
suggestedPrice: 320,
|
||||
weightKg: 32,
|
||||
finishIds: ['finish-oak', 'finish-walnut'],
|
||||
|
||||
@ -234,8 +234,6 @@ function toSubstrateDTO(record: SubstrateWithRelations): SubstrateDTO {
|
||||
sheetWidthMm: record.thicknessSpec.sheetWidthMm,
|
||||
surfaceOptionId: record.surfaceOptionId,
|
||||
surfaceOptionName: record.surfaceOption?.name ?? null,
|
||||
systemNameOptionId: record.systemNameOptionId,
|
||||
systemNameOptionName: record.systemNameOption.name,
|
||||
suggestedPrice: decimalToNumber(record.suggestedPrice),
|
||||
weightKg: record.weightKg ?? null,
|
||||
finishes: (record.finishes ?? []).map((item) => ({
|
||||
@ -279,8 +277,6 @@ function getDictionaryLabel(type: SubstrateDictionaryType): string {
|
||||
return '厚度规格'
|
||||
case 'SURFACE_OPTION':
|
||||
return '贴面类型'
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
return '系统名称'
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,9 +345,8 @@ async function loadSubstrateParts(input: {
|
||||
environmentGradeId: string
|
||||
thicknessSpecId: string
|
||||
surfaceOptionId: string | null
|
||||
systemNameOptionId: string
|
||||
}) {
|
||||
const [category, environmentGrade, thicknessSpec, surfaceOption, systemNameOption] =
|
||||
const [category, environmentGrade, thicknessSpec, surfaceOption] =
|
||||
await Promise.all([
|
||||
prisma.substrateCategory.findUnique({ where: { id: input.categoryId } }),
|
||||
prisma.substrateEnvironmentGrade.findUnique({ where: { id: input.environmentGradeId } }),
|
||||
@ -359,7 +354,6 @@ async function loadSubstrateParts(input: {
|
||||
input.surfaceOptionId
|
||||
? prisma.substrateSurfaceOption.findUnique({ where: { id: input.surfaceOptionId } })
|
||||
: Promise.resolve(null),
|
||||
prisma.substrateSystemNameOption.findUnique({ where: { id: input.systemNameOptionId } }),
|
||||
])
|
||||
|
||||
if (!category) {
|
||||
@ -374,16 +368,12 @@ async function loadSubstrateParts(input: {
|
||||
if (input.surfaceOptionId && !surfaceOption) {
|
||||
throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '贴面类型不存在')
|
||||
}
|
||||
if (!systemNameOption) {
|
||||
throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '系统名称不存在')
|
||||
}
|
||||
|
||||
return {
|
||||
category,
|
||||
environmentGrade,
|
||||
thicknessSpec,
|
||||
surfaceOption,
|
||||
systemNameOption,
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,13 +382,12 @@ async function buildSubstratePayload(input: {
|
||||
environmentGradeId: string
|
||||
thicknessSpecId: string
|
||||
surfaceOptionId: string | null
|
||||
systemNameOptionId: string
|
||||
}) {
|
||||
const parts = await loadSubstrateParts(input)
|
||||
const generatedName = buildSubstrateDisplayName({
|
||||
thicknessLabel: parts.thicknessSpec.name,
|
||||
thicknessMm: parts.thicknessSpec.thicknessMm,
|
||||
environmentGradeLabel: parts.environmentGrade.name,
|
||||
systemNameLabel: parts.systemNameOption.name,
|
||||
categoryLabel: parts.category.name,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -438,7 +427,6 @@ async function buildSubstratePayloadWithFinishes(input: {
|
||||
environmentGradeId: string
|
||||
thicknessSpecId: string
|
||||
surfaceOptionId: string | null
|
||||
systemNameOptionId: string
|
||||
finishIds: string[]
|
||||
}) {
|
||||
const parts = await loadSubstrateParts(input)
|
||||
@ -452,15 +440,13 @@ async function buildSubstratePayloadWithFinishes(input: {
|
||||
: []
|
||||
|
||||
const finishPart = finishRows.map((item) => item.name).join(' ')
|
||||
const generatedName = [
|
||||
parts.thicknessSpec.name.toUpperCase(),
|
||||
parts.environmentGrade.name,
|
||||
parts.category.name,
|
||||
finishPart,
|
||||
parts.surfaceOption?.name ?? '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
const generatedName = buildSubstrateDisplayName({
|
||||
thicknessMm: parts.thicknessSpec.thicknessMm,
|
||||
environmentGradeLabel: parts.environmentGrade.name,
|
||||
categoryLabel: parts.category.name,
|
||||
finishLabels: finishPart ? [finishPart] : [],
|
||||
surfaceLabel: parts.surfaceOption?.name ?? '',
|
||||
})
|
||||
|
||||
return {
|
||||
generatedName,
|
||||
@ -529,13 +515,12 @@ class AssetCenterService {
|
||||
}
|
||||
|
||||
async getSubstrateDictionaryBundle(): Promise<SubstrateDictionaryBundleDTO> {
|
||||
const [categories, environmentGrades, thicknessSpecs, surfaceOptions, systemNameOptions] =
|
||||
const [categories, environmentGrades, thicknessSpecs, surfaceOptions] =
|
||||
await Promise.all([
|
||||
prisma.substrateCategory.findMany({ orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }] }),
|
||||
prisma.substrateEnvironmentGrade.findMany({ orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }] }),
|
||||
prisma.substrateThicknessSpec.findMany({ orderBy: [{ sortOrder: 'asc' }, { thicknessMm: 'asc' }] }),
|
||||
prisma.substrateSurfaceOption.findMany({ orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }] }),
|
||||
prisma.substrateSystemNameOption.findMany({ orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }] }),
|
||||
])
|
||||
|
||||
return {
|
||||
@ -543,7 +528,6 @@ class AssetCenterService {
|
||||
environmentGrades: environmentGrades.map((item) => toDictionaryOptionDTO('ENVIRONMENT_GRADE', item)),
|
||||
thicknessSpecs: thicknessSpecs.map((item) => toDictionaryOptionDTO('THICKNESS_SPEC', item)),
|
||||
surfaceOptions: surfaceOptions.map((item) => toDictionaryOptionDTO('SURFACE_OPTION', item)),
|
||||
systemNameOptions: systemNameOptions.map((item) => toDictionaryOptionDTO('SYSTEM_NAME_OPTION', item)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,7 +610,6 @@ class AssetCenterService {
|
||||
environmentGradeId: input.environmentGradeId,
|
||||
thicknessSpecId: input.thicknessSpecId,
|
||||
surfaceOptionId: input.surfaceOptionId ?? null,
|
||||
systemNameOptionId: input.systemNameOptionId,
|
||||
isActive: true,
|
||||
},
|
||||
include: {
|
||||
@ -650,7 +633,6 @@ class AssetCenterService {
|
||||
environmentGradeId: existing.environmentGradeId,
|
||||
thicknessSpecId: existing.thicknessSpecId,
|
||||
surfaceOptionId: existing.surfaceOptionId ?? null,
|
||||
systemNameOptionId: existing.systemNameOptionId,
|
||||
finishIds: mergedFinishIds,
|
||||
})
|
||||
|
||||
@ -683,7 +665,6 @@ class AssetCenterService {
|
||||
environmentGradeId: input.environmentGradeId,
|
||||
thicknessSpecId: input.thicknessSpecId,
|
||||
surfaceOptionId: input.surfaceOptionId ?? null,
|
||||
systemNameOptionId: input.systemNameOptionId,
|
||||
finishIds,
|
||||
})
|
||||
|
||||
@ -698,7 +679,6 @@ class AssetCenterService {
|
||||
surfaceOption: input.surfaceOptionId
|
||||
? { connect: { id: input.surfaceOptionId } }
|
||||
: undefined,
|
||||
systemNameOption: { connect: { id: input.systemNameOptionId } },
|
||||
suggestedPrice: input.suggestedPrice ?? null,
|
||||
weightKg: input.weightKg ?? null,
|
||||
finishes:
|
||||
@ -720,7 +700,6 @@ class AssetCenterService {
|
||||
select: { id: true, name: true, thicknessMm: true, sheetLengthMm: true, sheetWidthMm: true },
|
||||
},
|
||||
surfaceOption: { select: { id: true, name: true } },
|
||||
systemNameOption: { select: { id: true, name: true } },
|
||||
finishes: {
|
||||
include: {
|
||||
finish: {
|
||||
@ -760,7 +739,6 @@ class AssetCenterService {
|
||||
const nextThicknessSpecId = input.thicknessSpecId ?? existing.thicknessSpecId
|
||||
const nextSurfaceOptionId =
|
||||
input.surfaceOptionId !== undefined ? input.surfaceOptionId : existing.surfaceOptionId
|
||||
const nextSystemNameOptionId = input.systemNameOptionId ?? existing.systemNameOptionId
|
||||
const nextFinishIds =
|
||||
input.finishIds !== undefined
|
||||
? normalizeFinishIds(input.finishIds)
|
||||
@ -774,13 +752,12 @@ class AssetCenterService {
|
||||
environmentGradeId: nextEnvironmentGradeId,
|
||||
thicknessSpecId: nextThicknessSpecId,
|
||||
surfaceOptionId: nextSurfaceOptionId ?? null,
|
||||
systemNameOptionId: nextSystemNameOptionId,
|
||||
isActive: true,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (duplicate) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '已存在相同 5 字段规格的基材,请直接维护已有基材')
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '已存在相同规格的基材,请直接维护已有基材')
|
||||
}
|
||||
|
||||
const payload = await buildSubstratePayloadWithFinishes({
|
||||
@ -788,7 +765,6 @@ class AssetCenterService {
|
||||
environmentGradeId: nextEnvironmentGradeId,
|
||||
thicknessSpecId: nextThicknessSpecId,
|
||||
surfaceOptionId: nextSurfaceOptionId ?? null,
|
||||
systemNameOptionId: nextSystemNameOptionId,
|
||||
finishIds: nextFinishIds,
|
||||
})
|
||||
|
||||
@ -815,10 +791,6 @@ class AssetCenterService {
|
||||
: input.surfaceOptionId
|
||||
? { connect: { id: input.surfaceOptionId } }
|
||||
: { disconnect: true },
|
||||
systemNameOption:
|
||||
input.systemNameOptionId === undefined
|
||||
? undefined
|
||||
: { connect: { id: input.systemNameOptionId } },
|
||||
suggestedPrice: input.suggestedPrice === undefined ? undefined : input.suggestedPrice,
|
||||
weightKg: input.weightKg === undefined ? undefined : input.weightKg,
|
||||
finishes:
|
||||
@ -840,7 +812,6 @@ class AssetCenterService {
|
||||
select: { id: true, name: true, thicknessMm: true, sheetLengthMm: true, sheetWidthMm: true },
|
||||
},
|
||||
surfaceOption: { select: { id: true, name: true } },
|
||||
systemNameOption: { select: { id: true, name: true } },
|
||||
finishes: {
|
||||
include: {
|
||||
finish: {
|
||||
@ -913,11 +884,6 @@ class AssetCenterService {
|
||||
'SURFACE_OPTION',
|
||||
await prisma.substrateSurfaceOption.create({ data: dataBase }),
|
||||
)
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
return toDictionaryOptionDTO(
|
||||
'SYSTEM_NAME_OPTION',
|
||||
await prisma.substrateSystemNameOption.create({ data: dataBase }),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
@ -988,18 +954,6 @@ class AssetCenterService {
|
||||
},
|
||||
}),
|
||||
)
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
return toDictionaryOptionDTO(
|
||||
'SYSTEM_NAME_OPTION',
|
||||
await prisma.substrateSystemNameOption.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name?.trim(),
|
||||
sortOrder: input.sortOrder,
|
||||
isActive: input.isActive,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
@ -1024,7 +978,6 @@ class AssetCenterService {
|
||||
ENVIRONMENT_GRADE: { environmentGradeId: id },
|
||||
THICKNESS_SPEC: { thicknessSpecId: id },
|
||||
SURFACE_OPTION: { surfaceOptionId: id },
|
||||
SYSTEM_NAME_OPTION: { systemNameOptionId: id },
|
||||
}
|
||||
const referenceCount = await prisma.substrate.count({ where: whereByType[type] })
|
||||
if (referenceCount > 0) {
|
||||
@ -1045,9 +998,6 @@ class AssetCenterService {
|
||||
case 'SURFACE_OPTION':
|
||||
await prisma.substrateSurfaceOption.delete({ where: { id } })
|
||||
return
|
||||
case 'SYSTEM_NAME_OPTION':
|
||||
await prisma.substrateSystemNameOption.delete({ where: { id } })
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
|
||||
@ -100,8 +100,6 @@ export interface SubstrateDTO {
|
||||
sheetWidthMm: number
|
||||
surfaceOptionId: string | null
|
||||
surfaceOptionName: string | null
|
||||
systemNameOptionId: string
|
||||
systemNameOptionName: string
|
||||
suggestedPrice: number | null
|
||||
weightKg: number | null
|
||||
finishes: {
|
||||
@ -121,7 +119,6 @@ export type SubstrateDictionaryType =
|
||||
| 'ENVIRONMENT_GRADE'
|
||||
| 'THICKNESS_SPEC'
|
||||
| 'SURFACE_OPTION'
|
||||
| 'SYSTEM_NAME_OPTION'
|
||||
|
||||
export interface SubstrateDictionaryOptionDTO {
|
||||
id: string
|
||||
@ -141,7 +138,6 @@ export interface SubstrateDictionaryBundleDTO {
|
||||
environmentGrades: SubstrateDictionaryOptionDTO[]
|
||||
thicknessSpecs: SubstrateDictionaryOptionDTO[]
|
||||
surfaceOptions: SubstrateDictionaryOptionDTO[]
|
||||
systemNameOptions: SubstrateDictionaryOptionDTO[]
|
||||
}
|
||||
|
||||
export interface CreateAssetFamilyInput {
|
||||
@ -223,7 +219,6 @@ export interface CreateSubstrateInput {
|
||||
environmentGradeId: string
|
||||
thicknessSpecId: string
|
||||
surfaceOptionId?: string | null
|
||||
systemNameOptionId: string
|
||||
suggestedPrice?: number | null
|
||||
weightKg?: number | null
|
||||
finishIds?: string[]
|
||||
@ -238,7 +233,6 @@ export interface UpdateSubstrateInput {
|
||||
environmentGradeId?: string
|
||||
thicknessSpecId?: string
|
||||
surfaceOptionId?: string | null
|
||||
systemNameOptionId?: string
|
||||
suggestedPrice?: number | null
|
||||
weightKg?: number | null
|
||||
finishIds?: string[]
|
||||
|
||||
@ -341,7 +341,7 @@ async function ensureSubstrate(creatorId: string) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const [category, environmentGrade, thicknessSpec, surfaceOption, systemNameOption] = await Promise.all([
|
||||
const [category, environmentGrade, thicknessSpec, surfaceOption] = await Promise.all([
|
||||
prisma.substrateCategory.upsert({
|
||||
where: { name: '颗粒板' },
|
||||
update: {},
|
||||
@ -362,11 +362,6 @@ async function ensureSubstrate(creatorId: string) {
|
||||
update: {},
|
||||
create: { name: '双贴', sortOrder: 0, isActive: true },
|
||||
}),
|
||||
prisma.substrateSystemNameOption.upsert({
|
||||
where: { name: '18E1级颗粒板' },
|
||||
update: {},
|
||||
create: { name: '18E1级颗粒板', sortOrder: 0, isActive: true },
|
||||
}),
|
||||
])
|
||||
|
||||
return assetCenterService.createSubstrate({
|
||||
@ -376,7 +371,6 @@ async function ensureSubstrate(creatorId: string) {
|
||||
environmentGradeId: environmentGrade.id,
|
||||
thicknessSpecId: thicknessSpec.id,
|
||||
surfaceOptionId: surfaceOption.id,
|
||||
systemNameOptionId: systemNameOption.id,
|
||||
suggestedPrice: 180,
|
||||
metadata: {
|
||||
seedKey: OPEN_PLATFORM_DEMO_SEED_MARKER,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user