refactor(bom-costing): 收紧空态工作台布局
This commit is contained in:
parent
026fab35c9
commit
e909cbdc94
@ -6,7 +6,7 @@ jest.mock('@/components/platform/AssetWorkbenchEmptyBackdrop', () => ({
|
||||
}))
|
||||
|
||||
describe('BomCostingWorkbenchClient', () => {
|
||||
it('生产服务未接入时保留完整工作台,但明确禁用分析按钮', () => {
|
||||
it('生产服务未接入且没有案例时只展示紧凑入口', () => {
|
||||
render(
|
||||
<BomCostingWorkbenchClient
|
||||
initialCapabilities={{
|
||||
@ -25,7 +25,11 @@ describe('BomCostingWorkbenchClient', () => {
|
||||
expect(screen.getByText('分析服务未接入')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /开始核算/ })).toBeDisabled()
|
||||
expect(screen.getByText(/目标误差 ≤10%/)).toBeInTheDocument()
|
||||
expect(screen.getByText('概念模型服务未接入')).toBeInTheDocument()
|
||||
expect(screen.getByText('竞品分析服务尚未接入')).toBeInTheDocument()
|
||||
expect(screen.queryByText('核算记录')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('成本汇总')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('概念模型服务未接入')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('empty-backdrop')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('开发 fixture 结果支持 SKU 切换并联动 BOM 与成本', async () => {
|
||||
@ -196,6 +200,8 @@ describe('BomCostingWorkbenchClient', () => {
|
||||
)
|
||||
|
||||
expect(await screen.findByText('桌面板')).toBeInTheDocument()
|
||||
expect(screen.getByText('成本汇总')).toBeInTheDocument()
|
||||
expect(screen.getByText('概念模型服务未接入')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: '白色 120cm' }))
|
||||
expect(screen.getByText('白色桌面板')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('¥92.00').length).toBeGreaterThan(0)
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
Clock3,
|
||||
ExternalLink,
|
||||
Link2,
|
||||
PackageSearch,
|
||||
@ -173,12 +172,16 @@ export default function BomCostingWorkbenchClient({
|
||||
}
|
||||
}
|
||||
|
||||
const modelStatus = initialCapabilities.modelingConfigured
|
||||
? '概念模型服务可用'
|
||||
: '概念模型服务未接入'
|
||||
const hasCases = cases.length > 0
|
||||
const hasSelectedSku = selectedSku !== null
|
||||
const workbenchGridClassName = hasSelectedSku
|
||||
? 'lg:grid-cols-[248px_minmax(0,1fr)_310px]'
|
||||
: hasCases
|
||||
? 'lg:grid-cols-[248px_minmax(0,1fr)]'
|
||||
: 'grid-cols-1'
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-[#f3f5f5] text-[#2d3335]">
|
||||
<div className="flex h-full min-h-0 flex-col bg-[#eef1f1] text-[#252b2d]">
|
||||
<ToastContainer
|
||||
toasts={toasts}
|
||||
onClose={removeToast}
|
||||
@ -186,20 +189,41 @@ export default function BomCostingWorkbenchClient({
|
||||
/>
|
||||
|
||||
<header className="shrink-0 border-b border-black/[0.06] bg-white px-4 py-3 lg:px-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center">
|
||||
<div className="flex min-w-[190px] items-center gap-3">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="flex min-w-0 items-center gap-3 lg:min-w-[300px]">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-[8px] bg-[#2d3335] text-white">
|
||||
<Calculator size={18} strokeWidth={1.8} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-[16px] font-bold">BOM核算</h1>
|
||||
<p className="text-[11px] text-[#8a9092]">
|
||||
预计出厂成本 · 目标误差 ≤{initialCapabilities.targetErrorPercent}%
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h1 className="shrink-0 text-[16px] font-bold">BOM核算</h1>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-0 items-center gap-1 rounded-[5px] px-1.5 py-1 text-[10px] font-bold',
|
||||
initialCapabilities.analysisConfigured
|
||||
? 'bg-emerald-50 text-emerald-700'
|
||||
: 'bg-amber-50 text-amber-700',
|
||||
)}
|
||||
>
|
||||
{initialCapabilities.analysisConfigured ? (
|
||||
<CheckCircle2 size={12} strokeWidth={2} />
|
||||
) : (
|
||||
<CircleAlert size={12} strokeWidth={2} />
|
||||
)}
|
||||
<span className="truncate">
|
||||
{initialCapabilities.analysisConfigured
|
||||
? '分析服务可用'
|
||||
: '分析服务未接入'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="truncate text-[11px] text-[#747c7e]">
|
||||
出厂成本预估 · 目标误差 ≤{initialCapabilities.targetErrorPercent}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2 lg:max-w-[860px]">
|
||||
<Input
|
||||
value={sourceUrl}
|
||||
onChange={(event) => setSourceUrl(event.target.value)}
|
||||
@ -223,35 +247,24 @@ export default function BomCostingWorkbenchClient({
|
||||
开始核算
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex shrink-0 items-center gap-2 text-[12px] font-semibold',
|
||||
initialCapabilities.analysisConfigured
|
||||
? 'text-emerald-700'
|
||||
: 'text-amber-700',
|
||||
)}
|
||||
>
|
||||
{initialCapabilities.analysisConfigured ? (
|
||||
<CheckCircle2 size={16} strokeWidth={1.8} />
|
||||
) : (
|
||||
<CircleAlert size={16} strokeWidth={1.8} />
|
||||
)}
|
||||
{initialCapabilities.analysisConfigured
|
||||
? '分析服务已接入'
|
||||
: '分析服务未接入'}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="grid min-h-0 flex-1 grid-cols-1 overflow-y-auto lg:grid-cols-[248px_minmax(0,1fr)_310px] lg:overflow-hidden">
|
||||
<aside className="min-h-[180px] border-b border-black/[0.06] bg-[#f8f9f9] p-3 lg:min-h-0 lg:overflow-y-auto lg:border-b-0 lg:border-r">
|
||||
<div className="mb-3 flex items-center justify-between px-1">
|
||||
<span className="text-[12px] font-bold">核算记录</span>
|
||||
<span className="text-[11px] text-[#929799]">{cases.length}</span>
|
||||
</div>
|
||||
{cases.length ? (
|
||||
<div className="space-y-1.5">
|
||||
<main
|
||||
className={cn(
|
||||
'grid min-h-0 flex-1 overflow-y-auto lg:overflow-hidden',
|
||||
workbenchGridClassName,
|
||||
)}
|
||||
>
|
||||
{hasCases ? (
|
||||
<aside className="min-h-0 border-b border-black/[0.07] bg-[#f6f7f7] p-2 lg:overflow-y-auto lg:border-b-0 lg:border-r lg:p-3">
|
||||
<div className="mb-2 flex items-center justify-between px-1 lg:mb-3">
|
||||
<span className="text-[12px] font-bold">核算记录</span>
|
||||
<span className="text-[11px] font-semibold text-[#767e80]">
|
||||
{cases.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 lg:block lg:space-y-1.5 lg:overflow-x-visible lg:pb-0">
|
||||
{cases.map((item) => {
|
||||
const active = item.id === selectedCaseId
|
||||
return (
|
||||
@ -263,10 +276,10 @@ export default function BomCostingWorkbenchClient({
|
||||
setSelectedSkuId(null)
|
||||
}}
|
||||
className={cn(
|
||||
'w-full rounded-[8px] border px-3 py-3 text-left transition',
|
||||
'min-w-[220px] rounded-[8px] border px-3 py-2.5 text-left transition lg:w-full lg:min-w-0 lg:py-3',
|
||||
active
|
||||
? 'border-[#47646b]/25 bg-white shadow-sm'
|
||||
: 'border-transparent bg-transparent hover:bg-white/80',
|
||||
? 'border-[#47646b]/30 bg-white shadow-sm'
|
||||
: 'border-transparent bg-[#eef1f1] hover:bg-white',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
@ -299,17 +312,12 @@ export default function BomCostingWorkbenchClient({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[130px] flex-col items-center justify-center text-center">
|
||||
<Clock3 size={22} className="mb-2 text-[#9aa0a2]" strokeWidth={1.5} />
|
||||
<span className="text-[12px] font-semibold text-[#747a7c]">暂无核算记录</span>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</aside>
|
||||
) : null}
|
||||
|
||||
<section className="min-w-0 bg-white lg:min-h-0 lg:overflow-hidden">
|
||||
{selectedSku ? (
|
||||
<div className="flex h-full min-h-[620px] flex-col lg:min-h-0">
|
||||
<div className="flex min-h-[560px] flex-col lg:h-full lg:min-h-0">
|
||||
<div className="shrink-0 border-b border-black/[0.06] px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
@ -428,101 +436,133 @@ export default function BomCostingWorkbenchClient({
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : hasCases ? (
|
||||
<div className="flex min-h-[320px] items-center justify-center px-6 py-12 lg:h-full lg:min-h-0">
|
||||
<div className="flex max-w-[320px] flex-col items-center text-center">
|
||||
{loadingCaseId ? (
|
||||
<RefreshCw
|
||||
size={24}
|
||||
strokeWidth={1.7}
|
||||
className="animate-spin text-[#536367]"
|
||||
/>
|
||||
) : (
|
||||
<PackageSearch size={24} strokeWidth={1.7} className="text-[#536367]" />
|
||||
)}
|
||||
<h2 className="mt-3 text-[14px] font-bold">
|
||||
{loadingCaseId ? '正在加载核算结果' : '选择核算记录'}
|
||||
</h2>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#747c7e]">
|
||||
{loadingCaseId
|
||||
? '正在读取 SKU、BOM 与成本数据'
|
||||
: '从记录中选择一项,查看对应 SKU 的 BOM 与成本'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-full min-h-[480px] items-center justify-center overflow-hidden">
|
||||
<AssetWorkbenchEmptyBackdrop className="opacity-70" />
|
||||
<div className="relative z-10 flex max-w-[360px] flex-col items-center px-6 text-center">
|
||||
<PackageSearch size={30} strokeWidth={1.5} className="text-[#647174]" />
|
||||
<h2 className="mt-3 text-[15px] font-bold">选择核算记录</h2>
|
||||
<p className="mt-1 text-[12px] leading-5 text-[#7f8587]">
|
||||
分析完成后,这里按 SKU 展开部件、用量与资料库价格匹配结果。
|
||||
<div className="flex h-full min-h-[320px] items-center justify-center px-6 py-12">
|
||||
<div className="flex max-w-[420px] flex-col items-center text-center">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-[8px] bg-[#e8eded] text-[#46575a]">
|
||||
<PackageSearch size={22} strokeWidth={1.7} />
|
||||
</div>
|
||||
<h2 className="mt-4 text-[15px] font-bold">
|
||||
{initialCapabilities.analysisConfigured
|
||||
? '粘贴竞品链接开始核算'
|
||||
: '竞品分析服务尚未接入'}
|
||||
</h2>
|
||||
<p className="mt-1.5 text-[12px] leading-5 text-[#6f7779]">
|
||||
{initialCapabilities.analysisConfigured
|
||||
? '完成后将按 SKU 展开部件用量、资料库价格匹配与预计出厂成本'
|
||||
: '服务接入后,可按 SKU 核算材料、加工、人工与包材成本'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<aside className="border-t border-black/[0.06] bg-[#f8f9f9] p-3 lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0">
|
||||
<section className="border-b border-black/[0.06] px-1 pb-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-[12px] font-bold">成本汇总</h3>
|
||||
<ShieldCheck size={15} className="text-[#657174]" strokeWidth={1.7} />
|
||||
</div>
|
||||
<div className="space-y-2 text-[11px]">
|
||||
{[
|
||||
['材料', selectedSku?.costSummary.material ?? null],
|
||||
['加工', selectedSku?.costSummary.processing ?? null],
|
||||
['人工', selectedSku?.costSummary.labor ?? null],
|
||||
['包材', selectedSku?.costSummary.packaging ?? null],
|
||||
].map(([label, value]) => (
|
||||
<div key={String(label)} className="flex items-center justify-between">
|
||||
<span className="text-[#747b7d]">{label}</span>
|
||||
<span className="font-semibold">{formatMoney(value as number | null)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex items-end justify-between border-t border-black/[0.06] pt-3">
|
||||
<span className="text-[11px] font-bold">预计出厂成本</span>
|
||||
<span className="text-[20px] font-black">
|
||||
{formatMoney(selectedSku?.estimatedCost)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] leading-4 text-[#909597]">
|
||||
含材料、加工、人工、包材和损耗,不含税费、物流、平台佣金及营销费用。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-black/[0.06] px-1 py-4">
|
||||
<h3 className="mb-3 text-[12px] font-bold">规格摘要</h3>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px]">
|
||||
<span className="text-[#8b9193]">成品尺寸</span>
|
||||
<span className="text-right font-semibold">
|
||||
{selectedSku
|
||||
? [selectedSku.lengthMm, selectedSku.widthMm, selectedSku.heightMm]
|
||||
.map((value) => value ?? '--')
|
||||
.join(' × ')
|
||||
: '--'}
|
||||
</span>
|
||||
<span className="text-[#8b9193]">产品重量</span>
|
||||
<span className="text-right font-semibold">
|
||||
{formatMetric(selectedSku?.weightKg ?? null, 'kg')}
|
||||
</span>
|
||||
<span className="text-[#8b9193]">包装尺寸</span>
|
||||
<span className="text-right font-semibold">
|
||||
{selectedSku
|
||||
? [
|
||||
selectedSku.packageLengthMm,
|
||||
selectedSku.packageWidthMm,
|
||||
selectedSku.packageHeightMm,
|
||||
]
|
||||
.map((value) => value ?? '--')
|
||||
.join(' × ')
|
||||
: '--'}
|
||||
</span>
|
||||
<span className="text-[#8b9193]">包装重量</span>
|
||||
<span className="text-right font-semibold">
|
||||
{formatMetric(selectedSku?.packageWeightKg ?? null, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-1 pt-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-[12px] font-bold">概念模型</h3>
|
||||
<span className="text-[10px] text-[#8b9193]">{modelStatus}</span>
|
||||
</div>
|
||||
<div className="relative h-[210px] overflow-hidden rounded-[8px] border border-black/[0.06] bg-[#f1f4f4]">
|
||||
<AssetWorkbenchEmptyBackdrop className="opacity-55" />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
|
||||
<Box size={28} strokeWidth={1.4} className="text-[#647174]" />
|
||||
<span className="mt-2 text-[11px] font-bold">概念模型待接入</span>
|
||||
<span className="mt-1 max-w-[190px] text-[10px] leading-4 text-[#8b9193]">
|
||||
仅用于运营决策预览,不作为工程尺寸或生产依据
|
||||
{selectedSku ? (
|
||||
<aside className="border-t border-black/[0.07] bg-[#f6f7f7] p-3 lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0">
|
||||
<section className="border-b border-black/[0.07] px-1 pb-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-[12px] font-bold">成本汇总</h3>
|
||||
<ShieldCheck size={15} className="text-[#586669]" strokeWidth={1.7} />
|
||||
</div>
|
||||
<div className="space-y-2 text-[11px]">
|
||||
{[
|
||||
['材料', selectedSku.costSummary.material],
|
||||
['加工', selectedSku.costSummary.processing],
|
||||
['人工', selectedSku.costSummary.labor],
|
||||
['包材', selectedSku.costSummary.packaging],
|
||||
].map(([label, value]) => (
|
||||
<div key={String(label)} className="flex items-center justify-between">
|
||||
<span className="text-[#687174]">{label}</span>
|
||||
<span className="font-semibold">
|
||||
{formatMoney(value as number | null)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex items-end justify-between border-t border-black/[0.07] pt-3">
|
||||
<span className="text-[11px] font-bold">预计出厂成本</span>
|
||||
<span className="text-[20px] font-black">
|
||||
{formatMoney(selectedSku.estimatedCost)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
<p className="mt-2 text-[10px] leading-4 text-[#747c7e]">
|
||||
含材料、加工、人工、包材和损耗,不含税费、物流、平台佣金及营销费用。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-black/[0.07] px-1 py-4">
|
||||
<h3 className="mb-3 text-[12px] font-bold">规格摘要</h3>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px]">
|
||||
<span className="text-[#747c7e]">成品尺寸</span>
|
||||
<span className="text-right font-semibold">
|
||||
{[selectedSku.lengthMm, selectedSku.widthMm, selectedSku.heightMm]
|
||||
.map((value) => value ?? '--')
|
||||
.join(' × ')}
|
||||
</span>
|
||||
<span className="text-[#747c7e]">产品重量</span>
|
||||
<span className="text-right font-semibold">
|
||||
{formatMetric(selectedSku.weightKg, 'kg')}
|
||||
</span>
|
||||
<span className="text-[#747c7e]">包装尺寸</span>
|
||||
<span className="text-right font-semibold">
|
||||
{[
|
||||
selectedSku.packageLengthMm,
|
||||
selectedSku.packageWidthMm,
|
||||
selectedSku.packageHeightMm,
|
||||
]
|
||||
.map((value) => value ?? '--')
|
||||
.join(' × ')}
|
||||
</span>
|
||||
<span className="text-[#747c7e]">包装重量</span>
|
||||
<span className="text-right font-semibold">
|
||||
{formatMetric(selectedSku.packageWeightKg, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-1 pt-4">
|
||||
<h3 className="mb-3 text-[12px] font-bold">概念模型</h3>
|
||||
{initialCapabilities.modelingConfigured ? (
|
||||
<div className="relative h-[170px] overflow-hidden rounded-[8px] border border-black/[0.07] bg-[#edf1f1]">
|
||||
<AssetWorkbenchEmptyBackdrop className="opacity-45" />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
|
||||
<Box size={26} strokeWidth={1.5} className="text-[#586669]" />
|
||||
<span className="mt-2 text-[11px] font-bold">概念模型待生成</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 rounded-[8px] border border-black/[0.07] bg-[#edf1f1] px-3 py-2.5">
|
||||
<Box size={17} strokeWidth={1.7} className="text-[#586669]" />
|
||||
<span className="text-[11px] font-semibold text-[#687174]">
|
||||
概念模型服务未接入
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</aside>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user