feat: 开发采集、采集箱和商品编辑功能

This commit is contained in:
Joey
2026-08-15 22:17:26 +08:00
parent c61d1a3154
commit 36357843d0
130 changed files with 18005 additions and 12 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* 后端 HTTP 客户端 —— 仅 background 使用(有 host_permissions,不受 CORS 约束)。
* 契约对齐 server 端 /api/materials(见 docs/v2/api.md)。
*/
import type { ScanResult } from '../collector/scan';
import type { TextEdits } from '../export/builder';
export interface MaterialsPayload {
product_id: string | null;
source: {
platform: string;
itemId: string | null;
url: string;
collectedAt: number;
};
texts: Array<{ kind: string; content: string; pairs?: Array<{ key: string; value: string }> | null }>;
images: Array<{
groupKey: string;
groupName: string;
variantName?: string | null;
url: string;
index: number;
type: string;
dedupeKey?: string | null;
}>;
refererOrigin?: string;
}
/** 用(可能已二次修改的)文本 + 已勾选图片,组装 /api/materials 请求体 */
export function buildMaterialsPayload(
result: ScanResult,
selectedKeys: Set<string>,
edits?: TextEdits,
): MaterialsPayload {
const text = (kind: string) => result.texts.find((t) => t.kind === kind)?.content ?? '';
const title = edits?.title ?? text('title');
const price = edits?.price ?? text('price');
const brand = edits?.brand ?? text('brand');
const desc = edits?.desc ?? text('desc');
const sellingPoints = edits?.sellingPoints ?? text('selling_point');
const params: Array<{ key: string; value: string }> = [
...(edits?.params ?? result.texts.find((t) => t.kind === 'params')?.pairs ?? []),
];
// 包装重量 / 包装尺寸合并进参数(后端存到 raw.paramsstudio 里再映射为 Ozon 字段)
if (edits?.weight) params.push({ key: '包装重量', value: edits.weight });
const dimSuffix = edits?.dimsUnit === 'mm' ? ' mm' : ' cm';
if (edits?.dims?.l) params.push({ key: '包装长度', value: `${edits.dims.l}${dimSuffix}` });
if (edits?.dims?.w) params.push({ key: '包装宽度', value: `${edits.dims.w}${dimSuffix}` });
if (edits?.dims?.h) params.push({ key: '包装高度', value: `${edits.dims.h}${dimSuffix}` });
const texts: MaterialsPayload['texts'] = [];
if (title) texts.push({ kind: 'title', content: title });
if (price) texts.push({ kind: 'price', content: price });
if (brand) texts.push({ kind: 'brand', content: brand });
if (params.length) texts.push({ kind: 'params', content: '', pairs: params });
if (sellingPoints) texts.push({ kind: 'selling_point', content: sellingPoints });
if (desc) texts.push({ kind: 'desc', content: desc });
const images = result.images
.filter((img) => selectedKeys.has(img.key))
.map((img) => ({
groupKey: img.groupKey,
groupName: img.groupName,
variantName: img.variantName ?? null,
url: img.url,
index: img.index,
type: img.type,
dedupeKey: img.url,
}));
return {
product_id: null,
source: {
platform: result.platform,
itemId: result.itemId,
url: result.url,
collectedAt: result.scannedAt,
},
texts,
images,
refererOrigin: 'https://www.ozon.ru',
};
}
export async function uploadMaterials(
baseUrl: string,
token: string,
payload: MaterialsPayload,
): Promise<{ product_id: string; stage: string; assets_queued: number }> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`; // 单用户宽松模式:token 可空
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/materials`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data?.detail || `上传失败 HTTP ${res.status}`);
}
return data;
}