merge(main): 发布 BOM 商品图片布局修正
This commit is contained in:
commit
452c41bc4d
@ -103,21 +103,25 @@ describe('BomCostingWorkbenchClient', () => {
|
||||
expect(screen.queryByText('成本构成')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('左侧图片按主图、当前 SKU 图和详情图分组切换', async () => {
|
||||
it('主图在左、SKU 图在右、详情图在下并共同切换中间动态图片', async () => {
|
||||
mockCaseResponse(detail())
|
||||
render(<BomCostingWorkbenchClient initialCapabilities={capabilities} initialCases={[summary()]} />)
|
||||
await screen.findByText('约 ¥1,350.00')
|
||||
|
||||
expect(screen.getByRole('tab', { name: '主图 1' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('tab', { name: '当前 SKU 图 1' })).toHaveAttribute('aria-selected', 'true')
|
||||
expect(screen.getByRole('tab', { name: '详情图 1' })).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('主图缩略图')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('当前 SKU 图缩略图')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('详情图缩略图')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('tab')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('img', { name: '黑胡桃色 120cm SKU 图' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '主图 1' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看主图 1' }))
|
||||
expect(screen.getByRole('img', { name: '商品主图' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '详情图 1' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看详情图 1' }))
|
||||
expect(screen.getByRole('img', { name: '商品详情图 1' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看当前 SKU 图 1' }))
|
||||
expect(screen.getByRole('img', { name: '黑胡桃色 120cm SKU 图' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('整件临时价的详情只展示结构事实,不展示另一套结构金额', async () => {
|
||||
|
||||
@ -54,10 +54,9 @@ type RecalculationMode =
|
||||
|
||||
type ProductImageKind = BomCostingSourceAssetDetail['kind']
|
||||
|
||||
const PRODUCT_IMAGE_KINDS: ProductImageKind[] = ['MAIN', 'SKU', 'DETAIL']
|
||||
const PRODUCT_IMAGE_KIND_LABELS: Record<ProductImageKind, string> = {
|
||||
MAIN: '主图',
|
||||
SKU: '当前 SKU 图',
|
||||
SKU: 'SKU 图',
|
||||
DETAIL: '详情图',
|
||||
}
|
||||
|
||||
@ -477,16 +476,16 @@ function ResultCanvas({
|
||||
SKU: skuAssets,
|
||||
DETAIL: detailAssets,
|
||||
}
|
||||
const [preferredImageKind, setPreferredImageKind] = useState<ProductImageKind>('SKU')
|
||||
const activeImageKind = imageGroups[preferredImageKind].length > 0
|
||||
? preferredImageKind
|
||||
: PRODUCT_IMAGE_KINDS.find((kind) => imageGroups[kind].length > 0) ?? preferredImageKind
|
||||
const galleryAssets = imageGroups[activeImageKind]
|
||||
const [selectedAssetId, setSelectedAssetId] = useState('')
|
||||
const selectedAsset = galleryAssets.find((asset) => asset.id === selectedAssetId) ??
|
||||
galleryAssets[0] ?? null
|
||||
const displayAssets = [...skuAssets, ...mainAssets, ...detailAssets]
|
||||
const [selectedAssetSelection, setSelectedAssetSelection] = useState({ skuId: '', assetId: '' })
|
||||
const selectedAssetId = selectedAssetSelection.skuId === selectedSku.id
|
||||
? selectedAssetSelection.assetId
|
||||
: ''
|
||||
const selectedAsset = displayAssets.find((asset) => asset.id === selectedAssetId) ??
|
||||
skuAssets[0] ?? mainAssets[0] ?? detailAssets[0] ?? null
|
||||
const selectedAssetGroup = selectedAsset ? imageGroups[selectedAsset.kind] : []
|
||||
const selectedAssetIndex = selectedAsset
|
||||
? galleryAssets.findIndex((asset) => asset.id === selectedAsset.id)
|
||||
? selectedAssetGroup.findIndex((asset) => asset.id === selectedAsset.id)
|
||||
: -1
|
||||
const [detailsOpen, setDetailsOpen] = useState(false)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
@ -517,6 +516,9 @@ function ResultCanvas({
|
||||
const latestRun = detail.analysisRuns?.[0] ?? null
|
||||
const canResume = latestRun?.revision > 1 && latestRun.status === 'FAILED'
|
||||
const selectedAssetIdSet = new Set(selectedAssetIds)
|
||||
const selectDisplayedAsset = (assetId: string) => {
|
||||
setSelectedAssetSelection({ skuId: selectedSku.id, assetId })
|
||||
}
|
||||
|
||||
const toggleAsset = (assetId: string) => {
|
||||
setSelectedAssetIds((current) => current.includes(assetId)
|
||||
@ -573,86 +575,107 @@ function ResultCanvas({
|
||||
) : null}
|
||||
|
||||
<div className="grid flex-1 items-center gap-8 lg:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)] lg:gap-12">
|
||||
<section id="bom-product-gallery" className="min-w-0">
|
||||
<div className="mb-3 flex items-end justify-between gap-4 border-b border-black/[0.08]">
|
||||
<div className="flex min-w-0 gap-4 overflow-x-auto" role="tablist" aria-label="商品图片分类">
|
||||
{PRODUCT_IMAGE_KINDS.map((kind) => {
|
||||
const assets = imageGroups[kind]
|
||||
const active = activeImageKind === kind
|
||||
return (
|
||||
<section id="bom-product-gallery" className="min-w-0" aria-label="商品图片">
|
||||
<div className="grid grid-cols-[48px_minmax(0,1fr)_48px] items-start gap-2 sm:grid-cols-[64px_minmax(0,1fr)_64px] sm:gap-3">
|
||||
<aside className="min-w-0" aria-label="主图缩略图">
|
||||
<div className="mb-2 text-center text-[10px] font-semibold text-[#687376] sm:text-[11px]">
|
||||
主图 <span className="tabular-nums text-[#8a9395]">{mainAssets.length}</span>
|
||||
</div>
|
||||
<div className="flex max-h-[280px] flex-col gap-2 overflow-y-auto sm:max-h-[420px]">
|
||||
{mainAssets.map((asset, index) => (
|
||||
<button
|
||||
key={kind}
|
||||
key={asset.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
aria-controls="bom-product-gallery-panel"
|
||||
disabled={assets.length === 0}
|
||||
onClick={() => {
|
||||
setPreferredImageKind(kind)
|
||||
setSelectedAssetId(assets[0]?.id ?? '')
|
||||
}}
|
||||
onClick={() => selectDisplayedAsset(asset.id)}
|
||||
aria-label={`查看主图 ${index + 1}`}
|
||||
aria-pressed={selectedAsset?.id === asset.id}
|
||||
className={cn(
|
||||
'flex h-9 shrink-0 items-center gap-1.5 border-b-2 px-0 text-[11px] font-semibold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[#526a70] focus-visible:ring-offset-2',
|
||||
active
|
||||
? 'border-[#526a70] text-[#2f3d40]'
|
||||
: 'border-transparent text-[#6f797b] hover:text-[#3f4d50]',
|
||||
assets.length === 0 && 'cursor-not-allowed text-[#a3aaac] hover:text-[#a3aaac]',
|
||||
'relative aspect-square w-full shrink-0 overflow-hidden border bg-[#f2f4f3] outline-none focus-visible:ring-2 focus-visible:ring-[#526a70] focus-visible:ring-offset-2',
|
||||
selectedAsset?.id === asset.id ? 'border-[#526a70]' : 'border-black/[0.08] hover:border-black/25',
|
||||
)}
|
||||
>
|
||||
<span>{PRODUCT_IMAGE_KIND_LABELS[kind]}</span>
|
||||
<span className="tabular-nums text-[10px] text-current/70">{assets.length}</span>
|
||||
<Image src={asset.storedUrl!} alt="" fill sizes="64px" unoptimized className="object-contain" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
{mainAssets.length === 0 ? <span className="py-3 text-center text-[11px] text-[#a3aaac]">—</span> : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="relative aspect-[4/3] min-h-[280px] overflow-hidden bg-[#f2f4f3] sm:min-h-[420px]">
|
||||
{selectedAsset?.storedUrl ? (
|
||||
<>
|
||||
<Image
|
||||
src={selectedAsset.storedUrl}
|
||||
alt={selectedAsset.kind === 'MAIN'
|
||||
? '商品主图'
|
||||
: selectedAsset.kind === 'SKU'
|
||||
? `${selectedSku.name} SKU 图`
|
||||
: `商品详情图 ${selectedAssetIndex + 1}`}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 54vw, 74vw"
|
||||
priority
|
||||
unoptimized
|
||||
className="object-contain"
|
||||
/>
|
||||
<span className="absolute right-2 top-2 bg-white/90 px-2 py-1 text-[10px] font-semibold text-[#526064] shadow-sm backdrop-blur-sm">
|
||||
{PRODUCT_IMAGE_KIND_LABELS[selectedAsset.kind]} {selectedAssetIndex + 1}/{selectedAssetGroup.length}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-[12px] text-[#7a8486]">暂无商品图片</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{galleryAssets.length > 0 ? (
|
||||
<span className="mb-2.5 shrink-0 text-[10px] tabular-nums text-[#7a8486]">
|
||||
{selectedAssetIndex + 1} / {galleryAssets.length}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<aside className="min-w-0" aria-label="当前 SKU 图缩略图">
|
||||
<div className="mb-2 text-center text-[10px] font-semibold text-[#687376] sm:text-[11px]">
|
||||
SKU <span className="tabular-nums text-[#8a9395]">{skuAssets.length}</span>
|
||||
</div>
|
||||
<div className="flex max-h-[280px] flex-col gap-2 overflow-y-auto sm:max-h-[420px]">
|
||||
{skuAssets.map((asset, index) => (
|
||||
<button
|
||||
key={asset.id}
|
||||
type="button"
|
||||
onClick={() => selectDisplayedAsset(asset.id)}
|
||||
aria-label={`查看当前 SKU 图 ${index + 1}`}
|
||||
aria-pressed={selectedAsset?.id === asset.id}
|
||||
className={cn(
|
||||
'relative aspect-square w-full shrink-0 overflow-hidden border bg-[#f2f4f3] outline-none focus-visible:ring-2 focus-visible:ring-[#526a70] focus-visible:ring-offset-2',
|
||||
selectedAsset?.id === asset.id ? 'border-[#526a70]' : 'border-black/[0.08] hover:border-black/25',
|
||||
)}
|
||||
>
|
||||
<Image src={asset.storedUrl!} alt="" fill sizes="64px" unoptimized className="object-contain" />
|
||||
</button>
|
||||
))}
|
||||
{skuAssets.length === 0 ? <span className="py-3 text-center text-[11px] text-[#a3aaac]">—</span> : null}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="bom-product-gallery-panel"
|
||||
role="tabpanel"
|
||||
aria-label={PRODUCT_IMAGE_KIND_LABELS[activeImageKind]}
|
||||
className="relative aspect-[4/3] min-h-[280px] overflow-hidden bg-[#f2f4f3] sm:min-h-[420px]"
|
||||
>
|
||||
{selectedAsset?.storedUrl ? (
|
||||
<Image
|
||||
src={selectedAsset.storedUrl}
|
||||
alt={selectedAsset.kind === 'MAIN'
|
||||
? '商品主图'
|
||||
: selectedAsset.kind === 'SKU'
|
||||
? `${selectedSku.name} SKU 图`
|
||||
: `商品详情图 ${selectedAssetIndex + 1}`}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 62vw, 100vw"
|
||||
priority
|
||||
unoptimized
|
||||
className="object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-[12px] text-[#7a8486]">暂无商品图片</div>
|
||||
)}
|
||||
</div>
|
||||
{galleryAssets.length > 1 ? (
|
||||
<div className="mt-3 flex gap-2 overflow-x-auto pb-1" aria-label={`${PRODUCT_IMAGE_KIND_LABELS[activeImageKind]}缩略图`}>
|
||||
{galleryAssets.map((asset, index) => (
|
||||
<button
|
||||
key={asset.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedAssetId(asset.id)}
|
||||
aria-label={`查看${PRODUCT_IMAGE_KIND_LABELS[activeImageKind]} ${index + 1}`}
|
||||
aria-pressed={selectedAsset?.id === asset.id}
|
||||
className={cn(
|
||||
'relative h-14 w-14 shrink-0 overflow-hidden border bg-[#f2f4f3] outline-none focus-visible:ring-2 focus-visible:ring-[#526a70]',
|
||||
selectedAsset?.id === asset.id ? 'border-[#526a70]' : 'border-black/[0.08]',
|
||||
)}
|
||||
>
|
||||
<Image src={asset.storedUrl!} alt="" fill sizes="56px" unoptimized className="object-contain" />
|
||||
</button>
|
||||
))}
|
||||
{detailAssets.length > 0 ? (
|
||||
<div className="mt-3" aria-label="详情图缩略图">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-semibold text-[#687376] sm:text-[11px]">
|
||||
<span>详情图</span>
|
||||
<span className="tabular-nums text-[#8a9395]">{detailAssets.length}</span>
|
||||
</div>
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{detailAssets.map((asset, index) => (
|
||||
<button
|
||||
key={asset.id}
|
||||
type="button"
|
||||
onClick={() => selectDisplayedAsset(asset.id)}
|
||||
aria-label={`查看详情图 ${index + 1}`}
|
||||
aria-pressed={selectedAsset?.id === asset.id}
|
||||
className={cn(
|
||||
'relative h-14 w-14 shrink-0 overflow-hidden border bg-[#f2f4f3] outline-none focus-visible:ring-2 focus-visible:ring-[#526a70] focus-visible:ring-offset-2',
|
||||
selectedAsset?.id === asset.id ? 'border-[#526a70]' : 'border-black/[0.08] hover:border-black/25',
|
||||
)}
|
||||
>
|
||||
<Image src={asset.storedUrl!} alt="" fill sizes="56px" unoptimized className="object-contain" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user