refactor(包材资料库): 四栏可拖拽布局 L1|L2|列表|详情,顶部工具栏
This commit is contained in:
parent
fce4793f68
commit
41a9c06f0e
@ -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)} />
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user