Files
yeuimu 6c61a4e871 feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
2026-08-26 14:23:09 +08:00

166 lines
4.4 KiB
TypeScript

interface CategoryChild {
id: number;
name: string;
parentId: number;
parentName: string;
tag: string;
productNum: number;
children: CategoryChild[];
}
interface Category {
id: number;
name: string;
tag: string;
productNum: number;
children: CategoryChild[];
}
interface ProductItem {
id: number;
name: string;
psd_img_url: string;
thumbImgUrl: string;
currentPrice: number;
color_str: string[];
}
interface ProductPage {
totalCount: number;
items: ProductItem[];
page: number;
size: number;
}
interface CountryGroup {
id: number;
name: string;
products: CategoryChild[];
}
const COUNTRY_NAME_MAP: Record<string, string> = {
'美国工厂直发': '美国',
'日本本地工厂直发': '日本',
'墨西哥工厂本地直发': '墨西哥',
'巴西本地工厂直发': '巴西',
'中东本地工厂直发': '中东',
'欧洲波兰工厂直发': '波兰',
'欧洲西班牙工厂直发': '西班牙',
'欧洲德国工厂本地直发': '德国',
'欧洲意大利工厂直发': '意大利',
'英国本地直发': '英国',
'加拿大本地工厂直发': '加拿大',
'澳大利亚本地工厂直发': '澳大利亚',
'韩国本地直发': '韩国',
};
export function usePodProducts() {
const categories = ref<Category[]>([]);
const countryGroups = ref<CountryGroup[]>([]);
const activeCountryIdx = ref(0);
const productImages = ref<Map<number, string>>(new Map());
const loading = ref(true);
const loadingProducts = ref(false);
const countryPage = ref(0);
const visibleCountryCount = 6;
const fetchCategories = async () => {
try {
loading.value = true;
const data = await $fetch<Category[]>('/api/pod/categories', {
method: 'POST',
});
categories.value = data;
countryGroups.value = data.map((cat) => ({
id: cat.id,
name: COUNTRY_NAME_MAP[cat.name] ?? cat.name,
products: cat.children,
}));
if (countryGroups.value.length > 0) {
await fetchProductsForCountry(0);
}
} catch (error) {
console.error('Failed to fetch categories:', error);
} finally {
loading.value = false;
}
};
const fetchProductsForCountry = async (idx: number) => {
const group = countryGroups.value[idx];
if (!group) return;
loadingProducts.value = true;
try {
const children = group.products.slice(0, 10);
const promises = children.map((child) =>
$fetch<ProductPage>(`/api/pod/products/${child.id}`).then((res) => ({
id: child.id,
name: child.name,
image: res.items?.[0]?.psd_img_url ?? res.items?.[0]?.thumbImgUrl ?? '',
})).catch(() => ({
id: child.id,
name: child.name,
image: '',
})),
);
const results = await Promise.all(promises);
const newMap = new Map(productImages.value);
for (const r of results) {
newMap.set(r.id, r.image);
}
productImages.value = newMap;
} catch (error) {
console.error('Failed to fetch products:', error);
} finally {
loadingProducts.value = false;
}
};
const selectCountry = (idx: number) => {
activeCountryIdx.value = idx;
countryPage.value = Math.floor(idx / visibleCountryCount);
fetchProductsForCountry(idx);
};
const visibleCountries = computed(() => {
const start = countryPage.value * visibleCountryCount;
return countryGroups.value.slice(start, start + visibleCountryCount);
});
const canPrevCountry = computed(() => countryPage.value > 0);
const canNextCountry = computed(
() =>
(countryPage.value + 1) * visibleCountryCount <
countryGroups.value.length,
);
const currentProducts = computed(() => {
const group = countryGroups.value[activeCountryIdx.value];
if (!group) return [];
return group.products.slice(0, 10).map((p) => ({
id: p.id,
name: p.name,
image: productImages.value.get(p.id) ?? '',
}));
});
return {
categories,
countryGroups,
activeCountryIdx,
productImages,
loading,
loadingProducts,
countryPage,
visibleCountryCount,
visibleCountries,
canPrevCountry,
canNextCountry,
currentProducts,
fetchCategories,
selectCountry,
};
}