chore: checkpoint intake and warehouse workspace state

This commit is contained in:
Codex 2026-04-14 10:00:51 +08:00 committed by WuYongJie
parent 995030a11d
commit ce85aa8def
3 changed files with 428 additions and 259 deletions

View File

@ -86,7 +86,7 @@ const nextConfig = {
turbopack: {},
// 本地开发允许 localhost 和 127.0.0.1 复用同一套 dev 资源,避免 UI 改动只在其中一个地址生效
allowedDevOrigins: ['localhost', '127.0.0.1'],
allowedDevOrigins: ['localhost', '127.0.0.1', '192.168.42.134'],
// 图像优化配置
images: {

View File

@ -266,22 +266,33 @@ function FormalIntakeWorkbench({
onToggle={() => onToggle('pending')}
>
<div className="space-y-3">
{skuList.slice(0, 3).map((sku) => (
<div
key={sku.id}
className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-apple-semibold text-apple-label-primary">{sku.code}</p>
<p className="mt-1 text-xs text-apple-label-secondary">{sku.name}</p>
</div>
<Badge type="info" variant="subtle">
{sku.status}
</Badge>
</div>
{skuList.length === 0 ? (
<div className="rounded-apple-lg border border-dashed border-apple-separator px-3 py-4 text-sm leading-7 text-apple-label-secondary">
当前还没有待执行的 SKU。
</div>
))}
) : (
skuList.slice(0, 3).map((sku) => (
<div
key={sku.id}
className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-apple-semibold text-apple-label-primary">
SKU编码:{sku.code}
</p>
<p className="mt-1 text-xs text-apple-label-secondary">SKU名称:{sku.name}</p>
</div>
<Badge type="info" variant="subtle">
{sku.status}
</Badge>
</div>
<div className="mt-3 flex justify-end">
<Button size="small">执行</Button>
</div>
</div>
))
)}
</div>
</AccordionSection>
@ -292,7 +303,17 @@ function FormalIntakeWorkbench({
expanded={activeSection === 'components'}
onToggle={() => onToggle('components')}
>
<div className="text-sm text-apple-label-secondary">点击后在左侧查看详细配件内容。</div>
<div className="space-y-2">
{COMPONENT_LABELS.map((label) => (
<div
key={label}
className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2"
>
<p className="text-sm text-apple-label-primary">{label}:编码</p>
<p className="mt-1 text-xs text-apple-label-secondary">点击后在左侧查看详细内容</p>
</div>
))}
</div>
</AccordionSection>
<AccordionSection
@ -302,7 +323,17 @@ function FormalIntakeWorkbench({
expanded={activeSection === 'picking'}
onToggle={() => onToggle('picking')}
>
<div className="text-sm text-apple-label-secondary">点击后在左侧查看拣货打印预览。</div>
<div className="space-y-3">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3 text-xs text-apple-label-secondary">
这里会显示 SKU编码、配件编码、库位、数量。先看表格预览,没问题再打印。
</div>
<div className="flex gap-2">
<Button variant="plain" className="flex-1">
表格预览
</Button>
<Button className="flex-1">打印按钮</Button>
</div>
</div>
</AccordionSection>
<AccordionSection
@ -312,7 +343,23 @@ function FormalIntakeWorkbench({
expanded={activeSection === 'brand'}
onToggle={() => onToggle('brand')}
>
<div className="text-sm text-apple-label-secondary">点击后在左侧查看品牌物料明细。</div>
<div className="space-y-2">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm text-apple-label-primary">品牌识别结果</p>
<p className="mt-1 text-xs text-apple-label-secondary">根据 SKU 前缀自动识别</p>
</div>
{BRAND_FILES.map((item) => (
<div
key={item}
className="flex items-center justify-between rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2"
>
<span className="text-sm text-apple-label-primary">{item}</span>
<Button variant="plain" size="small">
上传
</Button>
</div>
))}
</div>
</AccordionSection>
<AccordionSection
@ -322,7 +369,18 @@ function FormalIntakeWorkbench({
expanded={activeSection === 'packaging'}
onToggle={() => onToggle('packaging')}
>
<div className="text-sm text-apple-label-secondary">点击后左侧切换成 3D 包装预览。</div>
<div className="space-y-3">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm text-apple-label-primary">包装方案状态</p>
<p className="mt-1 text-xs text-apple-label-secondary">点击后左侧切换成 3D 包装预览</p>
</div>
<div className="grid gap-2">
<Button variant="plain">包装示意图</Button>
<Button variant="plain">打印包装图</Button>
<Button>开始打包</Button>
<Button variant="plain">完成打包</Button>
</div>
</div>
</AccordionSection>
</div>
)

View File

@ -1,18 +1,28 @@
'use client'
import { startTransition, useState } from 'react'
import Link from 'next/link'
import { ArrowRight, Boxes, MapPinned, Package, Plus, Warehouse as WarehouseIcon } from 'lucide-react'
import {
Boxes,
MapPinned,
Package,
Plus,
Printer,
Search,
Warehouse as WarehouseIcon,
} from 'lucide-react'
import { Badge } from '@/components/ui/Badge'
import Button from '@/components/ui/Button'
import Input, { Textarea } from '@/components/ui/Input'
import Modal from '@/components/ui/Modal'
import Select from '@/components/ui/Select'
import { PlatformSplitLayout } from '@/components/platform/PlatformSplitLayout'
import { AccordionSection } from '@/components/platform/AccordionSection'
import type { PlatformMetricItem } from '@/components/platform/types'
import { createWarehouseLocation } from '@/lib/open-product/platform-api'
import type { WarehouseOverviewDTO } from '@/server/services'
type WarehouseSection = 'location' | 'parts-sku' | 'product-sku' | 'brand' | null
interface WarehouseClientProps {
initialOverview: WarehouseOverviewDTO
user: {
@ -26,8 +36,13 @@ interface WarehouseClientProps {
metrics: PlatformMetricItem[]
}
function toggleSection(current: WarehouseSection, section: Exclude<WarehouseSection, null>): WarehouseSection {
return current === section ? null : section
}
export default function WarehouseClient({ initialOverview, user, metrics }: WarehouseClientProps) {
const [overview, setOverview] = useState(initialOverview)
const [activeSection, setActiveSection] = useState<WarehouseSection>(null)
const [locationForm, setLocationForm] = useState({
code: '',
name: '',
@ -44,37 +59,32 @@ export default function WarehouseClient({ initialOverview, user, metrics }: Ware
text: string
} | null>(null)
// 配件 SKU 搜索
const [partsSearchMode, setPartsSearchMode] = useState<'barcode' | 'location'>('barcode')
const [partsSearchValue, setPartsSearchValue] = useState('')
// 成品 SKU 搜索
const [productSkuSearch, setProductSkuSearch] = useState('')
// 品牌选择
const [selectedBrand, setSelectedBrand] = useState('')
async function handleCreateLocation() {
setSubmitting(true)
setFeedback(null)
try {
const location = await createWarehouseLocation(locationForm)
startTransition(() => {
setOverview((current) => ({
...current,
locations: [...current.locations, location],
}))
setLocationForm({
code: '',
name: '',
zone: '',
aisle: '',
shelf: '',
bin: '',
notes: '',
})
setLocationForm({ code: '', name: '', zone: '', aisle: '', shelf: '', bin: '', notes: '' })
setLocationModalOpen(false)
setFeedback({
tone: 'success',
text: '库位已创建,后续正式入库时即可直接选用。',
})
setFeedback({ tone: 'success', text: '库位已创建,后续正式入库时即可直接选用。' })
})
} catch (error) {
setFeedback({
tone: 'error',
text: error instanceof Error ? error.message : '库位创建失败,请稍后重试',
})
setFeedback({ tone: 'error', text: error instanceof Error ? error.message : '库位创建失败,请稍后重试' })
} finally {
setSubmitting(false)
}
@ -87,210 +97,343 @@ export default function WarehouseClient({ initialOverview, user, metrics }: Ware
overview.skuStock.reduce((sum, item) => sum + item.stock, 0) +
overview.packagedItems.reduce((sum, item) => sum + item.stockQuantity, 0)
return (
<PlatformSplitLayout
title="仓库"
metrics={metrics}
feedback={feedback}
leftContent={
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-apple-xl border border-apple-separator bg-apple-bg-primary p-5 shadow-apple-xs">
<h3 className="text-sm font-apple-semibold tracking-apple-tight text-apple-label-primary">
统计概览
</h3>
<p className="text-xs leading-5 text-apple-label-secondary">
一期只管理正式 SKU、包装成品、库存数量和基础库位。复杂出入库流水、调拨和盘点等完整 WMS 能力,留到后续阶段再进入。
</p>
// ========== 左侧详情内容 ==========
<div className="mt-2 grid grid-cols-2 gap-3">
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">库位数</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-label-primary">{locationCount}</p>
</div>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">正式 SKU</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-blue">{skuCount}</p>
</div>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">包装成品</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-green">{packagedCount}</p>
</div>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">总库存</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-label-primary">{totalStock}</p>
</div>
let leftContent: React.ReactNode = (
<div className="flex h-full items-center justify-center rounded-apple-xl border border-dashed border-apple-separator bg-apple-bg-secondary/40 p-8">
<div className="text-center">
<p className="text-sm text-apple-label-tertiary">点击右侧模块查看详情</p>
</div>
</div>
)
if (activeSection === 'location') {
leftContent = (
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-apple-xl border border-apple-separator bg-apple-bg-primary p-5 shadow-apple-xs">
<div className="grid grid-cols-2 gap-3">
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">库位数</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-label-primary">{locationCount}</p>
</div>
<div className="mt-auto pt-4">
<p className="text-xs leading-5 text-apple-label-tertiary">
右侧面板可展开各模块查看详情或新增库位。
</p>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">正式 SKU</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-blue">{skuCount}</p>
</div>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">包装成品</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-green">{packagedCount}</p>
</div>
<div className="rounded-apple-xl border border-apple-separator bg-apple-bg-secondary px-4 py-4">
<p className="text-xs font-apple-medium text-apple-label-tertiary">总库存</p>
<p className="mt-1 text-2xl font-apple-semibold text-apple-label-primary">{totalStock}</p>
</div>
</div>
}
rightContent={
<>
{/* 库位管理:新增表单 + 现有列表,默认展开 */}
<AccordionSection
title="库位管理"
description="新增库位 + 现有库位列表。供正式入库时直接挂接。"
icon={<MapPinned className="h-4 w-4" />}
defaultExpanded
aside={
<Badge type="info" variant="subtle">
{locationCount} 个库位
</Badge>
}
<div className="flex items-center justify-between">
<h3 className="text-sm font-apple-semibold text-apple-label-primary">库位列表</h3>
<Button
size="small"
onClick={() => {
setLocationForm({ code: '', name: '', zone: '', aisle: '', shelf: '', bin: '', notes: '' })
setLocationModalOpen(true)
}}
>
<div className="space-y-4">
{/* 新增库位按钮 */}
<Plus className="h-3.5 w-3.5" />
新增库位
</Button>
</div>
{overview.locations.length === 0 ? (
<div className="rounded-apple-xl border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
还没有库位。点击「新增库位」创建一个正式库位,后续入库即可挂接。
</div>
) : (
overview.locations.map((location) => (
<article key={location.id} className="rounded-apple-xl border border-apple-separator bg-apple-bg-primary px-4 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-sm font-apple-semibold text-apple-label-primary">{location.name}</p>
<p className="text-xs text-apple-label-secondary">{location.code}</p>
</div>
<Badge type={location.isActive ? 'success' : 'warning'} variant="subtle">
{location.isActive ? '可用' : '停用'}
</Badge>
</div>
<p className="mt-2 text-xs leading-5 text-apple-label-secondary">
{location.zone || '未分区'} / {location.aisle || '未设巷道'} / {location.shelf || '未设货架'} / {location.bin || '未设箱位'}
</p>
</article>
))
)}
</div>
)
} else if (activeSection === 'parts-sku') {
leftContent = (
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-apple-xl border border-apple-separator bg-apple-bg-primary p-5 shadow-apple-xs">
<h3 className="text-sm font-apple-semibold text-apple-label-primary">配件 SKU 库位管理</h3>
<p className="text-xs text-apple-label-secondary">管理板材、钢架、五金、纸箱等配件的条码和库位绑定关系,方便仓库按条码拣货。</p>
{/* 搜索模式切换 */}
<div className="flex gap-2">
<Button
variant={partsSearchMode === 'barcode' ? 'filled' : 'plain'}
size="small"
onClick={() => setPartsSearchMode('barcode')}
>
按条码查库位
</Button>
<Button
variant={partsSearchMode === 'location' ? 'filled' : 'plain'}
size="small"
onClick={() => setPartsSearchMode('location')}
>
按库位看条码
</Button>
</div>
{/* 搜索框 */}
<div className="flex gap-2">
<div className="flex-1">
<Input
placeholder={partsSearchMode === 'barcode' ? '输入条码编码搜索' : '输入库位编码搜索'}
value={partsSearchValue}
onChange={(e) => setPartsSearchValue(e.target.value)}
/>
</div>
<Button size="regular">
<Search className="h-3.5 w-3.5" />
查询
</Button>
</div>
{/* 配件主数据列表 */}
<div className="mt-2">
<div className="flex items-center justify-between mb-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary">配件主数据</p>
<Button variant="plain" size="small">
<Plus className="h-3.5 w-3.5" />
新增绑定
</Button>
</div>
{['台面', '桌腿', '拉杆', '五金', '纸箱'].map((part) => (
<div key={part} className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 mb-2">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-apple-medium text-apple-label-primary">{part}</p>
<p className="text-xs text-apple-label-secondary">编码:{part}-001 · 库位:A-01-01</p>
</div>
<Badge type="info" variant="subtle">已绑定</Badge>
</div>
</div>
))}
</div>
</div>
)
} else if (activeSection === 'product-sku') {
leftContent = (
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-apple-xl border border-apple-separator bg-apple-bg-primary p-5 shadow-apple-xs">
<h3 className="text-sm font-apple-semibold text-apple-label-primary">成品 SKU 库位管理</h3>
<p className="text-xs text-apple-label-secondary">组合后的 SKU 打包成成品后,放在哪个成品库位。支持查询和绑定。</p>
{/* 成品 SKU 搜索 */}
<Input
placeholder="输入 SKU 编码或产品名搜索"
value={productSkuSearch}
onChange={(e) => setProductSkuSearch(e.target.value)}
/>
{/* 成品 SKU 列表 */}
<div className="mt-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary mb-2">成品 SKU 列表</p>
<div className="rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
成品 SKU 列表将从正式入库数据中自动获取。功能开发中。
</div>
</div>
{/* 成品库位绑定 */}
<div className="mt-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary mb-2">成品库位绑定</p>
<div className="rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
选择成品 SKU 后,可绑定或修改其存放的成品库位。功能开发中。
</div>
</div>
{/* 打包执行入口 */}
<div className="mt-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary mb-2">打包执行</p>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm font-apple-medium text-apple-label-primary">查 SKU</p>
<p className="text-xs text-apple-label-secondary">输入编码或扫码进入</p>
</div>
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm font-apple-medium text-apple-label-primary">打印条码明细</p>
<p className="text-xs text-apple-label-secondary">配件条码 + 拣货单</p>
</div>
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm font-apple-medium text-apple-label-primary">选品牌</p>
<p className="text-xs text-apple-label-secondary">自动带出品牌物料</p>
</div>
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-3">
<p className="text-sm font-apple-medium text-apple-label-primary">3D 打包</p>
<p className="text-xs text-apple-label-secondary">对照方案装箱</p>
</div>
</div>
</div>
</div>
)
} else if (activeSection === 'brand') {
leftContent = (
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-apple-xl border border-apple-separator bg-apple-bg-primary p-5 shadow-apple-xs">
<h3 className="text-sm font-apple-semibold text-apple-label-primary">品牌物料库位管理</h3>
<p className="text-xs text-apple-label-secondary">根据品牌自动带出对应的说明书、铭牌、合格证、外箱印刷等物料,维护库位。</p>
{/* 品牌选择 */}
<Select
label="选择品牌"
value={selectedBrand}
onChange={(e) => setSelectedBrand(e.target.value)}
placeholder="请选择品牌"
options={[
{ value: 'suofeiya', label: '索菲亚' },
{ value: 'gujia', label: '顾家' },
{ value: 'baipai', label: '白牌' },
{ value: 'pumei', label: '朴美' },
]}
/>
{/* 品牌物料 */}
<div className="mt-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary mb-2">品牌物料</p>
{['说明书', '铭牌', '合格证', '外箱印刷'].map((item) => (
<div key={item} className="flex items-center justify-between rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 mb-2">
<div>
<Button
size="small"
onClick={() => {
setLocationForm({ code: '', name: '', zone: '', aisle: '', shelf: '', bin: '', notes: '' })
setLocationModalOpen(true)
}}
>
<Plus className="h-3.5 w-3.5" />
新增库位
<p className="text-sm text-apple-label-primary">{item}</p>
<p className="text-xs text-apple-label-secondary">库位:待绑定</p>
</div>
<div className="flex gap-2">
<Button variant="plain" size="small">绑定库位</Button>
<Button variant="plain" size="small">
<Printer className="h-3.5 w-3.5" />
</Button>
</div>
</div>
))}
</div>
{/* 现有库位列表 */}
{/* 品牌资料库 */}
<div className="mt-2">
<p className="text-xs font-apple-medium text-apple-label-tertiary mb-2">品牌资料库</p>
<div className="rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
品牌资料库维护功能开发中。
</div>
</div>
</div>
)
}
return (
<>
<PlatformSplitLayout
title="仓库"
metrics={metrics}
feedback={feedback}
leftContent={leftContent}
rightContent={
<div className="space-y-4">
{/* 库位管理 */}
<AccordionSection
title="库位管理"
description="新增库位 + 现有库位列表。供正式入库时直接挂接。"
icon={<MapPinned className="h-4 w-4" />}
expanded={activeSection === 'location'}
onToggle={() => setActiveSection((current) => toggleSection(current, 'location'))}
aside={<Badge type="info" variant="subtle">{locationCount} 个库位</Badge>}
>
<div className="space-y-3">
<p className="text-xs font-apple-medium text-apple-label-tertiary">现有库位</p>
{overview.locations.length === 0 ? (
<div className="rounded-apple-xl border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
还没有库位。先在上方新增一个正式库位,后续入库即可挂接。
<div className="rounded-apple-lg border border-dashed border-apple-separator px-3 py-4 text-sm leading-7 text-apple-label-secondary">
还没有库位。展开后点击左侧详情区的「新增库位」。
</div>
) : (
overview.locations.map((location) => (
<article
key={location.id}
className="rounded-apple-xl border border-apple-separator bg-apple-bg-primary px-4 py-3"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-sm font-apple-semibold text-apple-label-primary">
{location.name}
</p>
<p className="text-xs text-apple-label-secondary">{location.code}</p>
</div>
<div key={location.id} className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-apple-medium text-apple-label-primary">{location.name}</p>
<Badge type={location.isActive ? 'success' : 'warning'} variant="subtle">
{location.isActive ? '可用' : '停用'}
</Badge>
</div>
<p className="mt-2 text-xs leading-5 text-apple-label-secondary">
{location.zone || '未分区'} / {location.aisle || '未设巷道'} /{' '}
{location.shelf || '未设货架'} / {location.bin || '未设箱位'}
</p>
</article>
<p className="mt-1 text-xs text-apple-label-secondary">{location.code}</p>
</div>
))
)}
</div>
</div>
</AccordionSection>
</AccordionSection>
{/* 正式 SKU 库存 */}
<AccordionSection
title="正式 SKU 库存"
description="正式 SKU 在首次入库时已挂好库位,这里只做查看和基础检索。"
icon={<WarehouseIcon className="h-4 w-4" />}
aside={
<Badge type="info" variant="subtle">
{skuCount} 条
</Badge>
}
>
<div className="space-y-3">
{overview.skuStock.length === 0 ? (
<div className="rounded-apple-xl border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
还没有正式 SKU 库存。先从正式入库页完成首入库。
{/* 配件 SKU 库位 */}
<AccordionSection
title="配件 SKU 库位"
description="按条码查库位,按库位看条码。服务拣货。"
icon={<Boxes className="h-4 w-4" />}
expanded={activeSection === 'parts-sku'}
onToggle={() => setActiveSection((current) => toggleSection(current, 'parts-sku'))}
>
<div className="space-y-2">
{['台面', '桌腿', '拉杆', '五金', '纸箱'].map((part) => (
<div key={part} className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2">
<p className="text-sm text-apple-label-primary">{part}</p>
<p className="mt-1 text-xs text-apple-label-secondary">条码:{part}-001 · 库位:A-01-01</p>
</div>
))}
</div>
</AccordionSection>
{/* 成品 SKU 库位 */}
<AccordionSection
title="成品 SKU 库位"
description="成品打包后的存放库位管理与打包执行入口。"
icon={<WarehouseIcon className="h-4 w-4" />}
expanded={activeSection === 'product-sku'}
onToggle={() => setActiveSection((current) => toggleSection(current, 'product-sku'))}
>
<div className="space-y-3">
<div className="rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary px-3 py-4 text-sm text-apple-label-secondary">
成品 SKU 列表将从正式入库数据中自动获取。
</div>
) : (
overview.skuStock.map((sku) => (
<article
key={sku.skuId}
className="rounded-apple-xl border border-apple-separator bg-apple-bg-primary px-4 py-3"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-sm font-apple-semibold text-apple-label-primary">
{sku.skuName}
</p>
<p className="text-xs text-apple-label-secondary">
{sku.productModelNumber} · {sku.skuCode}
</p>
</div>
<Badge type="info" variant="subtle">
库存 {sku.stock}
</Badge>
</div>
<p className="mt-2 text-xs leading-5 text-apple-label-secondary">
库位:{sku.warehouseLocationName || '未指定'}
</p>
</article>
))
)}
</div>
</AccordionSection>
{/* 包装成品库存 */}
<AccordionSection
title="包装成品库存"
description="包装结果按准 SKU 保存,入库后转成包装成品对象。"
icon={<Package className="h-4 w-4" />}
aside={
<Badge type="success" variant="subtle">
{packagedCount} 条
</Badge>
}
>
<div className="space-y-3">
{overview.packagedItems.length === 0 ? (
<div className="rounded-apple-xl border border-dashed border-apple-separator bg-apple-bg-secondary px-4 py-6 text-sm leading-7 text-apple-label-secondary">
还没有包装成品对象。草稿组里先保存包装占位,入库后会自动带过来。
<div className="grid gap-2">
<Button variant="plain" size="small">
<Printer className="h-3.5 w-3.5" />
打印拣货条码明细单
</Button>
<Button variant="plain" size="small">
<Printer className="h-3.5 w-3.5" />
打印包装方案示意图
</Button>
</div>
) : (
overview.packagedItems.map((item) => (
<article
key={item.id}
className="rounded-apple-xl border border-apple-separator bg-apple-bg-primary px-4 py-3"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-sm font-apple-semibold text-apple-label-primary">
{item.name}
</p>
<p className="text-xs text-apple-label-secondary">{item.code}</p>
</div>
<Badge type="success" variant="subtle">
库存 {item.stockQuantity}
</Badge>
</div>
<p className="mt-2 text-xs leading-5 text-apple-label-secondary">
尺寸:{item.dimensions.length || '-'} × {item.dimensions.width || '-'} ×{' '}
{item.dimensions.height || '-'},库位:{item.warehouseLocationName || '未指定'}
</p>
</article>
))
)}
</div>
</AccordionSection>
</div>
</AccordionSection>
{/* 循环引导:回到资产中心开新一批 */}
<Link
href="/asset-center"
className="group flex items-center justify-between rounded-apple-lg border border-apple-separator bg-apple-bg-primary px-4 py-3 transition hover:border-apple-blue/40 hover:bg-apple-blue/5"
>
<div>
<p className="text-xs font-apple-medium text-apple-label-secondary">开始新一批</p>
<p className="text-sm font-apple-semibold text-apple-label-primary">
回到资产中心,继续创建新资产
</p>
</div>
<ArrowRight className="h-4 w-4 text-apple-label-tertiary transition group-hover:translate-x-0.5 group-hover:text-apple-blue" />
</Link>
{/* 品牌物料库位 */}
<AccordionSection
title="品牌物料库位"
description="按品牌带出说明书、铭牌、合格证、外箱印刷等物料。"
icon={<Package className="h-4 w-4" />}
expanded={activeSection === 'brand'}
onToggle={() => setActiveSection((current) => toggleSection(current, 'brand'))}
>
<div className="space-y-2">
{['索菲亚', '顾家', '白牌', '朴美'].map((brand) => (
<div key={brand} className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2">
<p className="text-sm text-apple-label-primary">{brand}</p>
<p className="mt-1 text-xs text-apple-label-secondary">说明书 · 铭牌 · 合格证 · 外箱印刷</p>
</div>
))}
</div>
</AccordionSection>
</div>
}
/>
{/* 新增库位弹窗 */}
<Modal
@ -313,62 +456,30 @@ export default function WarehouseClient({ initialOverview, user, metrics }: Ware
<Input
label="库位编码"
value={locationForm.code}
onChange={(event) =>
setLocationForm((current) => ({ ...current, code: event.target.value }))
}
onChange={(event) => setLocationForm((current) => ({ ...current, code: event.target.value }))}
placeholder="例如 A-01-02"
/>
<Input
label="库位名称"
value={locationForm.name}
onChange={(event) =>
setLocationForm((current) => ({ ...current, name: event.target.value }))
}
onChange={(event) => setLocationForm((current) => ({ ...current, name: event.target.value }))}
placeholder="例如 成品区 A01"
/>
</div>
<div className="mt-3 grid gap-3 grid-cols-2 md:grid-cols-4">
<Input
label="区域"
value={locationForm.zone}
onChange={(event) =>
setLocationForm((current) => ({ ...current, zone: event.target.value }))
}
/>
<Input
label="巷道"
value={locationForm.aisle}
onChange={(event) =>
setLocationForm((current) => ({ ...current, aisle: event.target.value }))
}
/>
<Input
label="货架"
value={locationForm.shelf}
onChange={(event) =>
setLocationForm((current) => ({ ...current, shelf: event.target.value }))
}
/>
<Input
label="箱位"
value={locationForm.bin}
onChange={(event) =>
setLocationForm((current) => ({ ...current, bin: event.target.value }))
}
/>
<Input label="区域" value={locationForm.zone} onChange={(event) => setLocationForm((current) => ({ ...current, zone: event.target.value }))} />
<Input label="巷道" value={locationForm.aisle} onChange={(event) => setLocationForm((current) => ({ ...current, aisle: event.target.value }))} />
<Input label="货架" value={locationForm.shelf} onChange={(event) => setLocationForm((current) => ({ ...current, shelf: event.target.value }))} />
<Input label="箱位" value={locationForm.bin} onChange={(event) => setLocationForm((current) => ({ ...current, bin: event.target.value }))} />
</div>
<div className="mt-3">
<Textarea
label="备注"
value={locationForm.notes}
onChange={(event) =>
setLocationForm((current) => ({ ...current, notes: event.target.value }))
}
onChange={(event) => setLocationForm((current) => ({ ...current, notes: event.target.value }))}
/>
</div>
</Modal>
</>
}
/>
</>
)
}