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,120 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
countryGroups,
|
||||
loading,
|
||||
loadingProducts,
|
||||
currentProducts,
|
||||
fetchCategories,
|
||||
selectCountry,
|
||||
} = usePodProducts();
|
||||
const { countries: configuredCountries, categories: configuredCategories, loadFilters } = useProductCenter();
|
||||
const selectedCountryId = shallowRef<string>();
|
||||
|
||||
async function initialize(): Promise<void> {
|
||||
await Promise.all([fetchCategories(), loadFilters()]);
|
||||
const firstCountry = configuredCountries.value[0];
|
||||
if (firstCountry) switchCountry(firstCountry);
|
||||
}
|
||||
|
||||
function switchCountry(country: { id: string; name: string }): void {
|
||||
selectedCountryId.value = country.id;
|
||||
const index = countryGroups.value.findIndex(
|
||||
(group) => group.name === country.name || group.name.includes(country.name),
|
||||
);
|
||||
if (index >= 0) selectCountry(index);
|
||||
}
|
||||
|
||||
function categoryLink(categoryName: string) {
|
||||
const categoryId = findCategoryIdForPodName(categoryName, configuredCategories.value);
|
||||
return {
|
||||
path: '/product-center',
|
||||
query: {
|
||||
countryId: selectedCountryId.value,
|
||||
...(categoryId ? { categoryId } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => void initialize());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pod-section w-full flex flex-col items-center p-6 lg:p-10 bg-inkreach-homepage-2">
|
||||
<div class="pod-content w-full max-w-7xl flex flex-col gap-8 lg:gap-10">
|
||||
<div class="text-[48px] font-extrabold md:text-4xl lg:text-5xl text-center">全球POD货盘</div>
|
||||
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<i class="fa-solid fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="country-grid grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 pb-2">
|
||||
<button
|
||||
v-for="country in configuredCountries"
|
||||
:key="country.id"
|
||||
type="button"
|
||||
class="country-button px-4 py-2 rounded-lg transition-colors whitespace-nowrap"
|
||||
:class="selectedCountryId === country.id ? 'bg-primary text-white' : 'bg-white text-gray-600 hover:text-primary'"
|
||||
@click="switchCountry(country)"
|
||||
>
|
||||
<span class="country-flag">
|
||||
<img v-if="country.icon" :src="country.icon" :alt="country.name" />
|
||||
</span>
|
||||
<span>{{ country.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="product-stage flex justify-center items-center min-h-[200px]">
|
||||
<div v-if="loadingProducts" class="flex justify-center py-8">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl text-primary"></i>
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3 lg:gap-5 w-full">
|
||||
<NuxtLink
|
||||
v-for="product in currentProducts"
|
||||
:key="product.id"
|
||||
:to="categoryLink(product.name)"
|
||||
class="bg-white rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="h-[14.25rem] flex items-center justify-center">
|
||||
<img
|
||||
v-if="product.image"
|
||||
:src="product.image"
|
||||
:alt="product.name"
|
||||
class="max-w-full max-h-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="flex items-center justify-center text-gray-300">
|
||||
<i class="fa-solid fa-image text-4xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full text-xs lg:text-sm h-8 lg:h-10 flex justify-center items-center px-1 truncate">
|
||||
<div class="truncate">{{ product.name }}</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="more-products w-full flex justify-center py-6 lg:py-10">
|
||||
<NuxtLink
|
||||
to="/product-center"
|
||||
class="more-products-link text-primary text-base lg:text-lg font-medium hover:underline underline-offset-4"
|
||||
>
|
||||
更多产品
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.country-button { display: grid; grid-template-columns: 22px auto; align-items: center; justify-content: center; column-gap: 8px; min-height: 40px; }
|
||||
.country-flag { display: grid; place-items: center; width: 22px; height: 16px; }
|
||||
.country-flag img { display: block; width: 22px; height: 16px; border-radius: 2px; object-fit: cover; }
|
||||
.more-products { margin-top: auto; }
|
||||
@media (min-width: 1024px) {
|
||||
.pod-section { height: 1007px; padding-top: 54px; }
|
||||
.pod-content { height: 100%; }
|
||||
.product-stage { flex: 1; min-height: 0; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
countryGroups,
|
||||
loading,
|
||||
loadingProducts,
|
||||
currentProducts,
|
||||
fetchCategories,
|
||||
selectCountry,
|
||||
} = usePodProducts();
|
||||
const { countries: configuredCountries, categories: configuredCategories, loadFilters } = useProductCenter();
|
||||
const selectedCountryId = shallowRef<string>();
|
||||
|
||||
async function initialize(): Promise<void> {
|
||||
await Promise.all([fetchCategories(), loadFilters()]);
|
||||
const firstCountry = configuredCountries.value[0];
|
||||
if (firstCountry) switchCountry(firstCountry);
|
||||
}
|
||||
|
||||
function switchCountry(country: { id: string; name: string }): void {
|
||||
selectedCountryId.value = country.id;
|
||||
const index = countryGroups.value.findIndex(
|
||||
(group) => group.name === country.name || group.name.includes(country.name),
|
||||
);
|
||||
if (index >= 0) selectCountry(index);
|
||||
}
|
||||
|
||||
function categoryLink(categoryName: string) {
|
||||
const categoryId = findCategoryIdForPodName(categoryName, configuredCategories.value);
|
||||
return {
|
||||
path: '/product-center',
|
||||
query: {
|
||||
countryId: selectedCountryId.value,
|
||||
...(categoryId ? { categoryId } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => void initialize());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pod-section w-full flex flex-col items-center p-6 lg:p-10 bg-inkreach-homepage-2">
|
||||
<div class="pod-content w-full max-w-7xl flex flex-col gap-8 lg:gap-10">
|
||||
<div class="text-[48px] font-extrabold md:text-4xl lg:text-5xl text-center">全球POD货盘</div>
|
||||
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<i class="fa-solid fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="country-grid grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 pb-2">
|
||||
<button
|
||||
v-for="country in configuredCountries"
|
||||
:key="country.id"
|
||||
type="button"
|
||||
class="country-button px-4 py-2 rounded-lg transition-colors whitespace-nowrap"
|
||||
:class="selectedCountryId === country.id ? 'bg-primary text-white' : 'bg-white text-gray-600 hover:text-primary'"
|
||||
@click="switchCountry(country)"
|
||||
>
|
||||
<span class="country-flag">
|
||||
<img v-if="country.icon" :src="country.icon" :alt="country.name" />
|
||||
</span>
|
||||
<span>{{ country.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="product-stage flex justify-center items-center min-h-[200px]">
|
||||
<div v-if="loadingProducts" class="flex justify-center py-8">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl text-primary"></i>
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3 lg:gap-5 w-full">
|
||||
<NuxtLink
|
||||
v-for="product in currentProducts"
|
||||
:key="product.id"
|
||||
:to="categoryLink(product.name)"
|
||||
class="bg-white rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="h-[14.25rem] flex items-center justify-center">
|
||||
<img
|
||||
v-if="product.image"
|
||||
:src="product.image"
|
||||
:alt="product.name"
|
||||
class="max-w-full max-h-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="flex items-center justify-center text-gray-300">
|
||||
<i class="fa-solid fa-image text-4xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full text-xs lg:text-sm h-8 lg:h-10 flex justify-center items-center px-1 truncate">
|
||||
<div class="truncate">{{ product.name }}</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="more-products w-full flex justify-center py-6 lg:py-10">
|
||||
<NuxtLink
|
||||
to="/product-center"
|
||||
class="more-products-link text-primary text-base lg:text-lg font-medium hover:underline underline-offset-4"
|
||||
>
|
||||
更多产品
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.country-button { display: grid; grid-template-columns: 22px auto; align-items: center; justify-content: center; column-gap: 8px; min-height: 40px; }
|
||||
.country-flag { display: grid; place-items: center; width: 22px; height: 16px; }
|
||||
.country-flag img { display: block; width: 22px; height: 16px; border-radius: 2px; object-fit: cover; }
|
||||
.more-products { margin-top: auto; }
|
||||
@media (min-width: 1024px) {
|
||||
.pod-section { height: 1007px; padding-top: 54px; }
|
||||
.pod-content { height: 100%; }
|
||||
.product-stage { flex: 1; min-height: 0; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user