feat(product-center): implement Figma-designed UI, upload module, and website tests

- Redesign website homepage & product-center per Figma (fonts, logos, hero/footer/customer cases)
- Add API upload module (multer) with static serving for uploads/public assets
- Add OriginGood.delisted flag and SDS request retry logic
- Add admin ImageUpload component and goods import/upload flows
- Add vitest suite for website components and composables (32 tests)
- Add skills, docs, plans and PRODUCT.md
This commit is contained in:
yeuimu
2026-08-20 14:32:03 +08:00
parent b5fc88f3fa
commit 79fabd85f7
107 changed files with 5364 additions and 2601 deletions
+43 -18
View File
@@ -65,10 +65,39 @@ export class SdsClientService {
'https://mapi.sdspod.com';
}
/**
* Fetches the SDS category tree of type 3 (products category).
* Body matches the legacy inkpod client.
*/
private async request<T>(
method: 'post' | 'get',
url: string,
body?: unknown,
params?: Record<string, unknown>,
): Promise<T> {
const MAX_RETRIES = 3;
let lastError: unknown;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const config = {
headers: POD_HEADERS,
timeout: 30000,
...(params ? { params } : {}),
};
const obs =
method === 'post'
? this.http.post<T>(url, body, config)
: this.http.get<T>(url, config);
const { data } = await firstValueFrom(obs);
return data as T;
} catch (err) {
lastError = err;
const msg = err instanceof Error ? err.message : String(err);
if (attempt < MAX_RETRIES) {
this.logger.warn(`SDS request attempt ${attempt}/${MAX_RETRIES} failed: ${msg}`);
await new Promise((r) => setTimeout(r, 1000 * attempt));
}
}
}
throw lastError;
}
async fetchCategoryTree(): Promise<SdsCategoryTreeNode[]> {
const url = `${this.baseUrl}/category/tree/3`;
const body = {
@@ -76,27 +105,23 @@ export class SdsClientService {
withPrivate: true,
onlyHaveProduct: true,
};
const { data } = await firstValueFrom(
this.http.post<SdsCategoryTreeNode[]>(url, body, { headers: POD_HEADERS }),
);
return Array.isArray(data) ? data : [];
const data = await this.request<unknown>('post', url, body);
if (!Array.isArray(data)) {
throw new Error(`SDS category tree returned ${typeof data}, expected array`);
}
return data as SdsCategoryTreeNode[];
}
/**
* Fetches one page of products for a given SDS category.
*/
async fetchProductsPage(
categoryId: string | number,
page = 1,
size = 50,
): Promise<SdsProductsPage> {
const url = `${this.baseUrl}/products/page`;
const { data } = await firstValueFrom(
this.http.get<SdsProductsPage>(url, {
headers: POD_HEADERS,
params: { categoryId, page, size },
}),
);
return data ?? {};
const data = await this.request<unknown>('get', url, undefined, { categoryId, page, size });
if (!data || typeof data !== 'object') {
throw new Error(`SDS products page returned ${typeof data}, expected object`);
}
return data as SdsProductsPage;
}
}