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
This commit is contained in:
@@ -1,165 +1,165 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user