Files
atlas-seller-kit/apps/web/src/pages/collection/CollectionInspectionPage.tsx
T
2026-09-07 17:51:11 +08:00

232 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Alert, Card, Collapse, Descriptions, Image, Spin, Table, Tag, Typography } from 'antd';
import type { CollectionItemInspection } from '@atlas/contracts';
import { collectionApi } from '@/services/collection';
import { apiErrorMessage } from '@/services/api';
import { mediaApi } from '@/services/media';
/** Temporary parser inspection surface. This is deliberately read-only: it lets
* product and parser work be checked before these values become editor fields. */
export default function CollectionInspectionPage() {
const { id } = useParams<{ id: string }>();
const [record, setRecord] = useState<CollectionItemInspection>();
const [error, setError] = useState<string>();
const [previewUrls, setPreviewUrls] = useState<Record<string, string>>({});
useEffect(() => {
if (!id) return;
void collectionApi
.inspection(id)
.then(setRecord)
.catch((reason) => setError(apiErrorMessage(reason)));
}, [id]);
const assetIds = useMemo(
() => [
...new Set(
(record?.asset_transfers ?? [])
.filter((transfer) => transfer.status === 'succeeded' && !!transfer.asset_id)
.map((transfer) => transfer.asset_id as string)
),
],
[record]
);
useEffect(() => {
let disposed = false;
setPreviewUrls({});
if (!assetIds.length) return;
void loadPreviewUrls(assetIds).then((urls) => {
if (!disposed) setPreviewUrls(urls);
});
return () => {
disposed = true;
};
}, [assetIds]);
const variantsById = useMemo(
() => new Map((record?.variants ?? []).map((variant) => [variant.id, variant])),
[record]
);
if (error)
return <Alert type="error" message="无法读取采集数据" description={error} showIcon />;
if (!record)
return (
<div style={{ padding: 64, textAlign: 'center' }}>
<Spin size="large" />
</div>
);
return (
<Card title="采集数据测试页" extra={<Tag color="blue">只读测试</Tag>}>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="这里展示扩展实际提交、服务端实际保存的数据,不代表已确认的商品编辑字段。"
/>
<Descriptions
bordered
size="small"
column={2}
items={[
{ key: 'title', label: '商品标题', children: record.title ?? '未命名' },
{
key: 'source',
label: '采集来源',
children: `${record.source.platform_code ?? '手动'} · ${record.source.url ?? '—'}`,
},
{ key: 'status', label: '采集状态', children: record.collection_status },
{ key: 'parser', label: '解析器版本', children: record.parser_version ?? '—' },
{ key: 'sku', label: '已创建 SKU', children: record.variants.length },
{ key: 'assets', label: '素材任务', children: record.asset_transfers.length },
]}
/>
<Typography.Title level={5} style={{ marginTop: 24 }}>
SKU 草稿
</Typography.Title>
<Table
size="small"
pagination={false}
rowKey="id"
dataSource={record.variants}
columns={[
{
title: '来源 SKU ID',
dataIndex: 'source_variant_key',
render: (value) => value ?? '默认 SKU(待确认)',
},
{
title: '来源货号',
dataIndex: 'source_sku',
render: (value) => value ?? '—',
},
{
title: '名称',
dataIndex: 'display_name',
render: (value) => value ?? '—',
},
]}
/>
<Typography.Title level={5} style={{ marginTop: 24 }}>
素材转存任务
</Typography.Title>
<Table
size="small"
pagination={false}
rowKey="id"
dataSource={record.asset_transfers}
columns={[
{
title: '素材预览',
width: 108,
render: (_, transfer) => {
const url = transfer.asset_id
? previewUrls[transfer.asset_id]
: undefined;
if (url)
return (
<Image
width={72}
height={72}
src={url}
alt="已转存采集素材"
style={{ objectFit: 'cover' }}
/>
);
return (
<Typography.Text type="secondary">
{transfer.status === 'succeeded' ? '预览加载中…' : '待转存'}
</Typography.Text>
);
},
},
{ title: '类型', dataIndex: 'role' },
{
title: '关联 SKU',
render: (_, transfer) => {
const variant = transfer.source_variant_id
? variantsById.get(transfer.source_variant_id)
: undefined;
return variant?.source_sku ?? variant?.source_variant_key ?? '商品级';
},
},
{ title: '状态', dataIndex: 'status' },
{
title: '来源 URL',
dataIndex: 'source_url',
ellipsis: true,
render: (value) => (
<Typography.Link href={value} target="_blank">
查看来源
</Typography.Link>
),
},
{
title: '错误',
dataIndex: 'error_payload',
render: (value) =>
value ? (
<Typography.Text type="danger">
{JSON.stringify(value)}
</Typography.Text>
) : (
'—'
),
},
]}
/>
<Collapse
style={{ marginTop: 24 }}
items={[
{
key: 'normalized',
label: '规范化采集载荷(normalized_payload)',
children: <Json value={record.normalized_payload} />,
},
{
key: 'evidence',
label: '原始证据(raw_evidence)',
children: <Json value={record.raw_evidence} />,
},
]}
/>
</Card>
);
}
async function loadPreviewUrls(assetIds: string[]) {
const urls: Record<string, string> = {};
let cursor = 0;
const worker = async () => {
while (cursor < assetIds.length) {
const assetId = assetIds[cursor++];
try {
urls[assetId] = (await mediaApi.createPreviewUrl(assetId)).url;
} catch {
// The task row remains inspectable through its source URL and error state.
}
}
};
await Promise.all(Array.from({ length: Math.min(4, assetIds.length) }, () => worker()));
return urls;
}
function Json({ value }: { value: unknown }) {
return (
<pre
style={{
margin: 0,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontSize: 12,
}}
>
{JSON.stringify(value, null, 2)}
</pre>
);
}