feat: 开发采集、采集箱和商品编辑功能
This commit is contained in:
@@ -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.params,studio 里再映射为 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;
|
||||
}
|
||||
Reference in New Issue
Block a user