refactor(包材资料库): 四栏可拖拽布局 L1|L2|列表|详情,顶部工具栏

This commit is contained in:
Codex 2026-04-02 15:01:51 +08:00
parent fce4793f68
commit 41a9c06f0e

View File

@ -1,6 +1,6 @@
'use client'
import { useCallback, useMemo, useState, useRef, useEffect } from 'react'
import { useCallback, useMemo, useState, useRef } from 'react'
import Link from 'next/link'
import { Badge } from '@/components/ui/Badge'
import Button from '@/components/ui/Button'
@ -12,7 +12,6 @@ import {
Search,
ArrowLeft,
ChevronRight,
Trash2,
Copy,
ToggleLeft,
ToggleRight,
@ -146,7 +145,7 @@ function getStatusLabel(status: string) {
}
/* ============================================================
* Drag-to-resize hook
* Drag-to-resize hook — supports N columns
* ============================================================ */
function useColumnResize(initialWidths: number[]) {
@ -163,7 +162,7 @@ function useColumnResize(initialWidths: number[]) {
const i = dragging.current.index
setWidths((prev) => {
const next = [...prev]
const minW = 120
const minW = 100
next[i] = Math.max(minW, dragging.current!.startWidths[i] + dx)
if (i + 1 < next.length) {
next[i + 1] = Math.max(minW, dragging.current!.startWidths[i + 1] - dx)
@ -185,6 +184,19 @@ function useColumnResize(initialWidths: number[]) {
return { widths, onMouseDown }
}
/* ============================================================
* Resize Handle component
* ============================================================ */
function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
return (
<div
className="w-[3px] shrink-0 cursor-col-resize hover:bg-apple-blue/40 active:bg-apple-blue/60 transition-colors bg-black/[0.06]"
onMouseDown={onMouseDown}
/>
)
}
/* ============================================================
* Main Client Component
* ============================================================ */
@ -212,28 +224,8 @@ export default function PackagingMaterialsClient({
const [newFamilyLevel1, setNewFamilyLevel1] = useState<Level1CategoryKey>('outer')
const [newFamilyLevel2, setNewFamilyLevel2] = useState('carton')
// Resizable columns: [L1, L2, List+Detail]
const { widths: colWidths, onMouseDown: onColResize } = useColumnResize([150, 150, 600])
// Vertical split within column 3: [list%, detail%] (as pixel heights)
const [listHeight, setListHeight] = useState(320)
const vDragging = useRef<{ startY: number; startH: number } | null>(null)
const onVResizeDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
vDragging.current = { startY: e.clientY, startH: listHeight }
const onMove = (ev: MouseEvent) => {
if (!vDragging.current) return
const dy = ev.clientY - vDragging.current.startY
setListHeight(Math.max(120, vDragging.current.startH + dy))
}
const onUp = () => {
vDragging.current = null
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
}, [listHeight])
// 4 resizable columns: [L1, L2, List, Detail]
const { widths: colWidths, onMouseDown: onColResize } = useColumnResize([140, 140, 400, 400])
// ---- Filtered data ----
const filteredFamilies = useMemo(() => {
@ -245,7 +237,6 @@ export default function PackagingMaterialsClient({
[filteredFamilies],
)
// L2 list for selected L1
const l2Categories = useMemo(
() => LEVEL2_BY_LEVEL1[selectedLevel1] ?? [],
[selectedLevel1],
@ -420,7 +411,6 @@ export default function PackagingMaterialsClient({
[],
)
// ---- Selected variant for preview ----
const selectedVariant = useMemo(
() => variants.find((v) => v.id === selectedVariantId) ?? null,
[variants, selectedVariantId],
@ -428,7 +418,7 @@ export default function PackagingMaterialsClient({
return (
<div className="flex flex-col h-screen bg-gradient-to-br from-slate-50 to-blue-50/30">
{/* ====== Top Header ====== */}
{/* ====== Top Header & Toolbar ====== */}
<header className="shrink-0 border-b border-black/5 bg-white/80 backdrop-blur-xl z-30">
<div className="px-5 py-2.5 flex items-center justify-between">
<div className="flex items-center gap-3">
@ -442,13 +432,10 @@ export default function PackagingMaterialsClient({
<ChevronRight className="h-3.5 w-3.5 text-apple-label-tertiary" />
<div className="flex items-center gap-2">
<Package2 className="h-5 w-5 text-apple-blue" />
<h1 className="text-lg font-apple-semibold text-apple-label-primary">
包材资料库
</h1>
<h1 className="text-lg font-apple-semibold text-apple-label-primary">包材资料库</h1>
</div>
</div>
{/* Action toolbar */}
<div className="flex items-center gap-2">
<div className="hidden md:flex items-center gap-3 text-xs text-apple-label-secondary mr-3">
<span><strong className="text-apple-label-primary">{stats.families}</strong> 品类</span>
@ -475,11 +462,11 @@ export default function PackagingMaterialsClient({
</div>
</header>
{/* ====== Three-column body ====== */}
{/* ====== Four-column body ====== */}
<div className="flex flex-1 min-h-0">
{/* Column 1: L1 Categories */}
{/* Column 1: L1 */}
<div
className="shrink-0 border-r border-black/5 bg-white/70 overflow-y-auto"
className="shrink-0 overflow-y-auto bg-white/70"
style={{ width: colWidths[0] }}
>
<div className="px-3 py-3 space-y-1">
@ -494,10 +481,7 @@ export default function PackagingMaterialsClient({
return (
<button
key={cat.key}
onClick={() => {
setSelectedLevel1(cat.key)
setSelectedFamilyId(null)
}}
onClick={() => { setSelectedLevel1(cat.key); setSelectedFamilyId(null) }}
className={cn(
'w-full text-left px-2.5 py-2 rounded-apple-md text-sm transition flex items-center justify-between',
selectedLevel1 === cat.key
@ -513,15 +497,11 @@ export default function PackagingMaterialsClient({
</div>
</div>
{/* Resize handle 1 */}
<div
className="w-1 shrink-0 cursor-col-resize hover:bg-apple-blue/30 active:bg-apple-blue/50 transition bg-black/5"
onMouseDown={(e) => onColResize(0, e)}
/>
<ResizeHandle onMouseDown={(e) => onColResize(0, e)} />
{/* Column 2: L2 Categories */}
{/* Column 2: L2 */}
<div
className="shrink-0 border-r border-black/5 bg-white/70 overflow-y-auto"
className="shrink-0 overflow-y-auto bg-white/70"
style={{ width: colWidths[1] }}
>
<div className="px-3 py-3 space-y-1">
@ -562,7 +542,6 @@ export default function PackagingMaterialsClient({
</button>
)
})}
{l2Categories.length === 0 && (
<p className="text-xs text-apple-label-tertiary px-2 py-4 text-center">
请先选择一级分类
@ -571,186 +550,163 @@ export default function PackagingMaterialsClient({
</div>
</div>
{/* Resize handle 2 */}
<ResizeHandle onMouseDown={(e) => onColResize(1, e)} />
{/* Column 3: Material List */}
<div
className="w-1 shrink-0 cursor-col-resize hover:bg-apple-blue/30 active:bg-apple-blue/50 transition bg-black/5"
onMouseDown={(e) => onColResize(1, e)}
/>
{/* Column 3: List + Detail (vertical split) */}
<div className="flex-1 flex flex-col min-w-0 bg-white/50">
{/* ---- List area ---- */}
<div className="shrink-0 flex flex-col overflow-hidden" style={{ height: listHeight }}>
{/* Search bar */}
<div className="px-4 py-2.5 border-b border-black/5 flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-apple-label-tertiary" />
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索编码或名称..."
className="w-full rounded-apple-md border border-black/10 bg-white/80 pl-8 pr-3 py-1.5 text-sm placeholder:text-apple-label-tertiary focus:outline-none focus:ring-1 focus:ring-apple-blue/40"
/>
</div>
<Select
size="small"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
options={[
{ value: 'all', label: '全部状态' },
{ value: 'AVAILABLE', label: '启用中' },
{ value: 'DISABLED', label: '停用中' },
{ value: 'DRAFT', label: '草稿' },
]}
className="shrink-0 flex flex-col overflow-hidden bg-white/50"
style={{ width: colWidths[2] }}
>
{/* Search bar */}
<div className="px-4 py-2.5 border-b border-black/5 flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-apple-label-tertiary" />
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索编码或名称..."
className="w-full rounded-apple-md border border-black/10 bg-white/80 pl-8 pr-3 py-1.5 text-sm placeholder:text-apple-label-tertiary focus:outline-none focus:ring-1 focus:ring-apple-blue/40"
/>
<span className="text-xs text-apple-label-tertiary shrink-0">{filteredVariants.length} 条</span>
</div>
{/* List */}
<div className="flex-1 overflow-y-auto px-4 py-2 space-y-1">
{filteredVariants.length === 0 ? (
<div className="py-12 text-center">
<Package2 className="h-10 w-10 mx-auto mb-2 text-apple-label-tertiary" strokeWidth={1.2} />
<p className="text-sm text-apple-label-secondary">暂无包材记录</p>
<p className="text-xs text-apple-label-tertiary mt-1">选择品类后点击"新增包材"</p>
</div>
) : (
filteredVariants.map((variant) => {
const isSelected = selectedVariantId === variant.id
const family = families.find((f) => f.id === variant.assetFamilyId)
const level2Key = getLevel2FromFamily(family?.metadata ?? null)
const level2Label = getLevel2Label(level2Key)
const dims = (variant.dimensions ?? {}) as Record<string, number>
const hasDims = dims.length || dims.width || dims.height || dims.thickness
return (
<div
key={variant.id}
onClick={() => setSelectedVariantId(variant.id)}
onDoubleClick={() => handleEdit(variant)}
className={cn(
'rounded-apple-lg border px-3 py-2 text-left transition cursor-pointer group',
isSelected
? 'border-apple-blue/30 bg-apple-blue/6'
: 'border-transparent hover:border-black/5 hover:bg-white/80',
)}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-sm text-apple-label-primary truncate font-apple-medium">
{variant.name}
</p>
<div className="flex items-center gap-2 mt-0.5 text-xs text-apple-label-secondary">
<span className="font-mono">{variant.code}</span>
{level2Label !== '未分类' && (
<Badge type="default" variant="subtle" size="small">
{level2Label}
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge
type={getStatusTone(variant.status)}
variant="subtle"
size="small"
>
{getStatusLabel(variant.status)}
</Badge>
</div>
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-apple-label-tertiary">
{hasDims && (
<span>{dims.length ?? 0} × {dims.width ?? 0} × {dims.height ?? 0} mm</span>
)}
{variant.costPrice != null && <span>¥{variant.costPrice}</span>}
{variant.weightKg != null && <span>{variant.weightKg} kg</span>}
</div>
</div>
)
})
)}
</div>
<Select
size="small"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
options={[
{ value: 'all', label: '全部' },
{ value: 'AVAILABLE', label: '启用中' },
{ value: 'DISABLED', label: '停用中' },
{ value: 'DRAFT', label: '草稿' },
]}
/>
<span className="text-xs text-apple-label-tertiary shrink-0">{filteredVariants.length} 条</span>
</div>
{/* Vertical resize handle */}
<div
className="h-1 shrink-0 cursor-row-resize hover:bg-apple-blue/30 active:bg-apple-blue/50 transition bg-black/5"
onMouseDown={onVResizeDown}
/>
{/* ---- Detail area ---- */}
<div className="flex-1 overflow-y-auto bg-white/60">
{editingMaterial ? (
<DetailEditor
material={editingMaterial}
isNew={isNew}
saving={saving}
error={error}
templateFields={templateFields}
families={filteredFamilies.length > 0 ? filteredFamilies : families}
onChange={(patch) =>
setEditingMaterial((cur) => (cur ? { ...cur, ...patch } : cur))
}
onTemplateFieldChange={(key, value) =>
setEditingMaterial((cur) =>
cur
? { ...cur, templateFields: { ...cur.templateFields, [key]: value } }
: cur,
)
}
onAddCustomAttribute={() =>
setEditingMaterial((cur) =>
cur
? {
...cur,
customAttributes: [...cur.customAttributes, { key: '', value: '' }],
}
: cur,
)
}
onUpdateCustomAttribute={(index, field, value) =>
setEditingMaterial((cur) => {
if (!cur) return cur
const attrs = [...cur.customAttributes]
attrs[index] = { ...attrs[index], [field]: value }
return { ...cur, customAttributes: attrs }
})
}
onRemoveCustomAttribute={(index) =>
setEditingMaterial((cur) => {
if (!cur) return cur
return {
...cur,
customAttributes: cur.customAttributes.filter((_, i) => i !== index),
}
})
}
onSave={handleSave}
onCancel={() => {
setEditingMaterial(null)
setIsNew(false)
setError(null)
}}
/>
) : selectedVariant ? (
<VariantPreview
variant={selectedVariant}
family={families.find((f) => f.id === selectedVariant.assetFamilyId) ?? null}
onEdit={() => handleEdit(selectedVariant)}
onCopy={() => handleCopyNew(selectedVariant)}
onToggleStatus={() => handleToggleStatus(selectedVariant)}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-center px-6">
<Package2 className="h-12 w-12 text-apple-label-tertiary mb-3" strokeWidth={1.0} />
<p className="text-sm text-apple-label-secondary mb-1">单击列表项查看详情</p>
<p className="text-xs text-apple-label-tertiary">双击进入编辑</p>
{/* List */}
<div className="flex-1 overflow-y-auto px-3 py-2 space-y-1">
{filteredVariants.length === 0 ? (
<div className="py-12 text-center">
<Package2 className="h-10 w-10 mx-auto mb-2 text-apple-label-tertiary" strokeWidth={1.2} />
<p className="text-sm text-apple-label-secondary">暂无包材记录</p>
<p className="text-xs text-apple-label-tertiary mt-1">选择品类后点击"新增包材"</p>
</div>
) : (
filteredVariants.map((variant) => {
const isSelected = selectedVariantId === variant.id
const family = families.find((f) => f.id === variant.assetFamilyId)
const level2Key = getLevel2FromFamily(family?.metadata ?? null)
const level2Label = getLevel2Label(level2Key)
const dims = (variant.dimensions ?? {}) as Record<string, number>
const hasDims = dims.length || dims.width || dims.height || dims.thickness
return (
<div
key={variant.id}
onClick={() => setSelectedVariantId(variant.id)}
onDoubleClick={() => handleEdit(variant)}
className={cn(
'rounded-apple-lg border px-3 py-2 text-left transition cursor-pointer group',
isSelected
? 'border-apple-blue/30 bg-apple-blue/6'
: 'border-transparent hover:border-black/5 hover:bg-white/80',
)}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-sm text-apple-label-primary truncate font-apple-medium">
{variant.name}
</p>
<div className="flex items-center gap-2 mt-0.5 text-xs text-apple-label-secondary">
<span className="font-mono">{variant.code}</span>
{level2Label !== '未分类' && (
<Badge type="default" variant="subtle" size="small">
{level2Label}
</Badge>
)}
</div>
</div>
<Badge type={getStatusTone(variant.status)} variant="subtle" size="small">
{getStatusLabel(variant.status)}
</Badge>
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-apple-label-tertiary">
{hasDims && (
<span>{dims.length ?? 0} × {dims.width ?? 0} × {dims.height ?? 0} mm</span>
)}
{variant.costPrice != null && <span>¥{variant.costPrice}</span>}
{variant.weightKg != null && <span>{variant.weightKg} kg</span>}
</div>
</div>
)
})
)}
</div>
</div>
<ResizeHandle onMouseDown={(e) => onColResize(2, e)} />
{/* Column 4: Detail */}
<div
className="flex-1 min-w-0 overflow-y-auto bg-white/60"
>
{editingMaterial ? (
<DetailEditor
material={editingMaterial}
isNew={isNew}
saving={saving}
error={error}
templateFields={templateFields}
families={filteredFamilies.length > 0 ? filteredFamilies : families}
onChange={(patch) =>
setEditingMaterial((cur) => (cur ? { ...cur, ...patch } : cur))
}
onTemplateFieldChange={(key, value) =>
setEditingMaterial((cur) =>
cur
? { ...cur, templateFields: { ...cur.templateFields, [key]: value } }
: cur,
)
}
onAddCustomAttribute={() =>
setEditingMaterial((cur) =>
cur
? { ...cur, customAttributes: [...cur.customAttributes, { key: '', value: '' }] }
: cur,
)
}
onUpdateCustomAttribute={(index, field, value) =>
setEditingMaterial((cur) => {
if (!cur) return cur
const attrs = [...cur.customAttributes]
attrs[index] = { ...attrs[index], [field]: value }
return { ...cur, customAttributes: attrs }
})
}
onRemoveCustomAttribute={(index) =>
setEditingMaterial((cur) => {
if (!cur) return cur
return { ...cur, customAttributes: cur.customAttributes.filter((_, i) => i !== index) }
})
}
onSave={handleSave}
onCancel={() => { setEditingMaterial(null); setIsNew(false); setError(null) }}
/>
) : selectedVariant ? (
<VariantPreview
variant={selectedVariant}
family={families.find((f) => f.id === selectedVariant.assetFamilyId) ?? null}
onEdit={() => handleEdit(selectedVariant)}
onCopy={() => handleCopyNew(selectedVariant)}
onToggleStatus={() => handleToggleStatus(selectedVariant)}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-center px-6">
<Package2 className="h-12 w-12 text-apple-label-tertiary mb-3" strokeWidth={1.0} />
<p className="text-sm text-apple-label-secondary mb-1">单击列表项查看详情</p>
<p className="text-xs text-apple-label-tertiary">双击进入编辑</p>
</div>
)}
</div>
</div>
{/* ====== New Family Modal ====== */}
@ -783,33 +739,12 @@ export default function PackagingMaterialsClient({
onChange={(e) => setNewFamilyLevel2(e.target.value)}
/>
<div className="grid grid-cols-2 gap-3">
<Input
label="品类编码"
value={newFamilyCode}
onChange={(e) => setNewFamilyCode(e.target.value)}
placeholder="如 CARTON"
/>
<Input
label="品类名称"
value={newFamilyName}
onChange={(e) => setNewFamilyName(e.target.value)}
placeholder="如 纸箱"
/>
<Input label="品类编码" value={newFamilyCode} onChange={(e) => setNewFamilyCode(e.target.value)} placeholder="如 CARTON" />
<Input label="品类名称" value={newFamilyName} onChange={(e) => setNewFamilyName(e.target.value)} placeholder="如 纸箱" />
</div>
<div className="flex justify-end gap-2 pt-2">
<Button
variant="plain"
size="small"
onClick={() => {
setShowNewFamily(false)
setError(null)
}}
>
取消
</Button>
<Button size="small" onClick={handleCreateFamily} loading={saving}>
创建
</Button>
<Button variant="plain" size="small" onClick={() => { setShowNewFamily(false); setError(null) }}>取消</Button>
<Button size="small" onClick={handleCreateFamily} loading={saving}>创建</Button>
</div>
</div>
</div>
@ -857,9 +792,7 @@ function VariantPreview({
<p className="text-sm font-apple-medium text-apple-label-primary">{variant.name}</p>
<div className="flex items-center gap-2 text-xs text-apple-label-secondary">
<span className="font-mono">{variant.code}</span>
<Badge type={getStatusTone(variant.status)} variant="subtle" size="small">
{getStatusLabel(variant.status)}
</Badge>
<Badge type={getStatusTone(variant.status)} variant="subtle" size="small">{getStatusLabel(variant.status)}</Badge>
</div>
<div className="flex flex-wrap gap-1.5 text-xs text-apple-label-tertiary">
{level1Key && <Badge type="default" variant="subtle" size="small">{getLevel1Label(level1Key)}</Badge>}
@ -974,45 +907,27 @@ function DetailEditor({
</div>
)}
{/* 基础信息 */}
<section className="space-y-2.5">
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">
基础信息
</p>
<Select
label="所属品类"
size="small"
value={material.assetFamilyId}
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">基础信息</p>
<Select label="所属品类" size="small" value={material.assetFamilyId}
options={families.map((f) => ({ value: f.id, label: f.name }))}
onChange={(e) => onChange({ assetFamilyId: e.target.value })}
/>
onChange={(e) => onChange({ assetFamilyId: e.target.value })} />
<div className="grid grid-cols-2 gap-2">
<Input label="编码" size="small" value={material.code} onChange={(e) => onChange({ code: e.target.value })} placeholder="CTN-1200" />
<Input label="名称" size="small" value={material.name} onChange={(e) => onChange({ name: e.target.value })} placeholder="纸箱 1200" />
</div>
<div className="grid grid-cols-2 gap-2">
<Select
label="计量单位"
size="small"
value={material.unit}
<Select label="计量单位" size="small" value={material.unit}
options={UNIT_OPTIONS.map((u) => ({ value: u.value, label: u.label }))}
onChange={(e) => onChange({ unit: e.target.value })}
/>
<Select
label="状态"
size="small"
value={material.status}
onChange={(e) => onChange({ unit: e.target.value })} />
<Select label="状态" size="small" value={material.status}
options={MATERIAL_STATUS_OPTIONS.map((s) => ({ value: s.value, label: s.label }))}
onChange={(e) => onChange({ status: e.target.value })}
/>
onChange={(e) => onChange({ status: e.target.value })} />
</div>
</section>
{/* 通用规格 */}
<section className="space-y-2.5">
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">
通用规格
</p>
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">通用规格</p>
<div className="grid grid-cols-3 gap-2">
<Input label="长(mm)" size="small" type="number" value={material.length} onChange={(e) => onChange({ length: e.target.value })} />
<Input label="宽(mm)" size="small" type="number" value={material.width} onChange={(e) => onChange({ width: e.target.value })} />
@ -1025,45 +940,30 @@ function DetailEditor({
</div>
</section>
{/* 品类模板 */}
{templateFields.length > 0 && (
<section className="space-y-2.5">
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">
品类规格
</p>
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">品类规格</p>
<div className="grid grid-cols-2 gap-2">
{templateFields.map((field) => (
<TemplateFieldInput
key={field.key}
field={field}
value={material.templateFields[field.key] ?? ''}
onChange={(value) => onTemplateFieldChange(field.key, value)}
/>
<TemplateFieldInput key={field.key} field={field} value={material.templateFields[field.key] ?? ''}
onChange={(value) => onTemplateFieldChange(field.key, value)} />
))}
</div>
</section>
)}
{/* 成本 */}
<section className="space-y-2.5">
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">
成本
</p>
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">成本</p>
<div className="grid grid-cols-2 gap-2">
<Input label="采购单价(元)" size="small" type="number" value={material.costPrice} onChange={(e) => onChange({ costPrice: e.target.value })} />
<Input label="尺寸标签" size="small" value={material.sizeLabel} onChange={(e) => onChange({ sizeLabel: e.target.value })} placeholder="1200x600" />
</div>
</section>
{/* 自定义属性 */}
<section className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">
自定义属性
</p>
<button onClick={onAddCustomAttribute} className="text-xs text-apple-blue hover:underline">
+ 添加
</button>
<p className="text-[10px] font-apple-semibold text-apple-label-tertiary uppercase tracking-wider">自定义属性</p>
<button onClick={onAddCustomAttribute} className="text-xs text-apple-blue hover:underline">+ 添加</button>
</div>
{material.customAttributes.map((attr, index) => (
<div key={index} className="flex items-center gap-1.5">
@ -1074,10 +974,8 @@ function DetailEditor({
))}
</section>
{/* 备注 */}
<Textarea label="备注" value={material.notes} onChange={(e) => onChange({ notes: e.target.value })} placeholder="补充说明..." rows={2} />
{/* 操作 */}
<div className="flex justify-end gap-2 pt-2 border-t border-black/5">
<Button variant="plain" size="small" onClick={onCancel} disabled={saving}>取消</Button>
<Button size="small" onClick={onSave} loading={saving}>{saving ? '保存中...' : '保存'}</Button>
@ -1090,11 +988,7 @@ function DetailEditor({
* TemplateFieldInput
* ============================================================ */
function TemplateFieldInput({
field,
value,
onChange,
}: {
function TemplateFieldInput({ field, value, onChange }: {
field: FieldTemplate
value: string
onChange: (value: string) => void
@ -1109,22 +1003,14 @@ function TemplateFieldInput({
}
if (field.type === 'select' && field.options) {
return (
<Select
label={`${field.label}${field.unit ? ` (${field.unit})` : ''}`}
size="small"
value={value}
<Select label={`${field.label}${field.unit ? ` (${field.unit})` : ''}`} size="small" value={value}
onChange={(e) => onChange(e.target.value)}
options={[{ value: '', label: '请选择' }, ...field.options.map((o) => ({ value: o, label: o }))]}
/>
options={[{ value: '', label: '请选择' }, ...field.options.map((o) => ({ value: o, label: o }))]} />
)
}
return (
<Input
label={`${field.label}${field.unit ? ` (${field.unit})` : ''}`}
size="small"
type={field.type === 'number' ? 'number' : 'text'}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
<Input label={`${field.label}${field.unit ? ` (${field.unit})` : ''}`} size="small"
type={field.type === 'number' ? 'number' : 'text'} value={value}
onChange={(e) => onChange(e.target.value)} />
)
}