feat(edit): 商品编辑页开发

This commit is contained in:
R524809
2026-09-10 17:45:46 +08:00
parent 130a005231
commit 946af649dd
22 changed files with 7034 additions and 518 deletions
+104 -3
View File
@@ -12,6 +12,34 @@ export class OzonRequestError extends Error {
}
export class OzonRetryableError extends Error {}
export type OzonCategoryNode = {
description_category_id: number | string;
category_name: string;
type_id?: number | string | null;
type_name?: string | null;
disabled?: boolean;
children?: OzonCategoryNode[];
};
export type OzonCategoryAttribute = {
id: number | string;
name: string;
type?: string | null;
is_collection?: boolean;
is_required?: boolean;
is_aspect?: boolean;
max_value_count?: number | null;
dictionary_id?: number | string | null;
complex_id?: number | string | null;
[key: string]: unknown;
};
export type OzonDictionaryValue = {
id: number | string;
value: string;
[key: string]: unknown;
};
/**
* Uses the category-tree read endpoint as the credential probe. It has no write side effect and,
* unlike warehouse-list, is verified against the current Ozon account integration.
@@ -20,22 +48,80 @@ export async function verifyOzonCredentials(
clientId: string,
apiKey: string,
): Promise<void> {
await requestOzon(clientId, apiKey, '/v1/description-category/tree', {
language: 'ZH_HANS',
});
}
/** 平台目录同步只在 Worker 中使用,密钥仅在此函数调用期间留在内存。 */
export async function fetchOzonCategoryTree(clientId: string, apiKey: string) {
const body = await requestOzon(clientId, apiKey, '/v1/description-category/tree', {
language: 'ZH_HANS',
});
return readResultArray<OzonCategoryNode>(body, '类目树');
}
export async function fetchOzonCategoryAttributes(
clientId: string,
apiKey: string,
categoryId: string,
typeId: string,
) {
const body = await requestOzon(clientId, apiKey, '/v1/description-category/attribute', {
description_category_id: numberOrText(categoryId),
type_id: numberOrText(typeId),
language: 'ZH_HANS',
});
return readResultArray<OzonCategoryAttribute>(body, '类目属性');
}
export async function fetchOzonDictionaryValues(
clientId: string,
apiKey: string,
input: {
attributeId: string;
categoryId: string;
typeId: string;
query?: string;
},
) {
const query = input.query?.trim() ?? '';
const path = query
? '/v1/description-category/attribute/values/search'
: '/v1/description-category/attribute/values';
const body = await requestOzon(clientId, apiKey, path, {
attribute_id: numberOrText(input.attributeId),
description_category_id: numberOrText(input.categoryId),
type_id: numberOrText(input.typeId),
language: 'ZH_HANS',
limit: query ? 100 : 1_000,
...(query ? { value: query } : { last_value_id: 0 }),
});
return readResultArray<OzonDictionaryValue>(body, '属性字典');
}
async function requestOzon(
clientId: string,
apiKey: string,
path: string,
body: Record<string, unknown>,
): Promise<unknown> {
let response: Response;
try {
response = await fetch('https://api-seller.ozon.ru/v1/description-category/tree', {
response = await fetch(`https://api-seller.ozon.ru${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Client-Id': clientId,
'Api-Key': apiKey,
},
body: JSON.stringify({ language: 'ZH_HANS' }),
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
} catch {
throw new OzonRetryableError('Ozon request could not be completed');
}
if (response.ok) return;
if (response.ok) return response.json() as Promise<unknown>;
if (response.status === 401 || response.status === 403) {
throw new OzonCredentialError(response.status);
}
@@ -44,3 +130,18 @@ export async function verifyOzonCredentials(
}
throw new OzonRequestError(response.status);
}
function readResultArray<T>(body: unknown, resource: string): T[] {
if (
!body ||
typeof body !== 'object' ||
!Array.isArray((body as { result?: unknown }).result)
) {
throw new OzonRetryableError(`Ozon ${resource} 响应格式异常`);
}
return (body as { result: T[] }).result;
}
function numberOrText(value: string) {
return /^\d+$/.test(value) ? Number(value) : value;
}