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:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
+77 -77
View File
@@ -1,77 +1,77 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
const activeFeature = ref(0);
const FEATURE_INTERVAL_MS = 2000;
const features = [
{ title: '设计工具', desc: '轻松设计,快速定制', image: '/设计工具.png' },
{ title: '自动化履约', desc: '从生产到配送,全程无忧,准时交付', image: '/自动化履约.png' },
{ title: '全球配送', desc: '全球20+国家本土工厂', image: '/全球配送.png' },
{ title: '店铺集成', desc: '与Shopify、Amazon等平台无缝对接', image: '/店铺集成.png' },
];
const selectFeature = (idx: number) => {
activeFeature.value = idx;
};
let featureTimer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
featureTimer = setInterval(() => {
activeFeature.value = (activeFeature.value + 1) % features.length;
}, FEATURE_INTERVAL_MS);
});
onBeforeUnmount(() => {
if (featureTimer) clearInterval(featureTimer);
});
</script>
<template>
<section class="feature-section w-full flex flex-col gap-8 lg:gap-10 items-center p-6 lg:p-10">
<div class="text-[48px] font-extrabold md:text-4xl lg:text-5xl text-center">用强大功能开启盈利快车道</div>
<div class="flex flex-col lg:flex-row gap-5 w-full max-w-7xl items-stretch">
<div class="feature-list flex flex-row gap-3 overflow-x-auto lg:overflow-visible">
<div
v-for="(feat, idx) in features"
:key="idx"
@click="selectFeature(idx)"
@mouseenter="activeFeature = idx"
class="flex-shrink-0 flex flex-col justify-center gap-2 lg:gap-4 pl-4 lg:pl-5 w-40 lg:w-100 h-20 lg:h-auto rounded-lg cursor-pointer transition-colors duration-300"
:class="activeFeature === idx ? 'bg-primary text-white' : 'bg-inkreach-homepage-2'"
>
<p class="feature-item-title text-xl lg:text-3xl">{{ feat.title }}</p>
<p class="text-sm lg:text-base indent-1">{{ feat.desc }}</p>
</div>
</div>
<div class="flex-1 overflow-hidden rounded-xl relative">
<img
v-for="(feat, idx) in features"
:key="'feat-img-' + idx"
:src="feat.image"
:alt="feat.title"
:data-testid="`feature-image-${idx}`"
class="w-full h-full object-contain transition-opacity duration-500 absolute inset-0"
:class="activeFeature === idx ? 'opacity-100' : 'opacity-0 pointer-events-none'"
/>
</div>
</div>
</section>
</template>
<style scoped>
@media (min-width: 1024px) {
.feature-section { height: 754px; padding-top: 54px; }
.feature-section > div:nth-child(2) { height: 550px; }
.feature-list {
display: grid;
grid-template-rows: repeat(4, minmax(0, 1fr));
width: 400px;
height: 100%;
gap: 12px;
}
}
</style>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
const activeFeature = ref(0);
const FEATURE_INTERVAL_MS = 2000;
const features = [
{ title: '设计工具', desc: '轻松设计,快速定制', image: '/设计工具.png' },
{ title: '自动化履约', desc: '从生产到配送,全程无忧,准时交付', image: '/自动化履约.png' },
{ title: '全球配送', desc: '全球20+国家本土工厂', image: '/全球配送.png' },
{ title: '店铺集成', desc: '与Shopify、Amazon等平台无缝对接', image: '/店铺集成.png' },
];
const selectFeature = (idx: number) => {
activeFeature.value = idx;
};
let featureTimer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
featureTimer = setInterval(() => {
activeFeature.value = (activeFeature.value + 1) % features.length;
}, FEATURE_INTERVAL_MS);
});
onBeforeUnmount(() => {
if (featureTimer) clearInterval(featureTimer);
});
</script>
<template>
<section class="feature-section w-full flex flex-col gap-8 lg:gap-10 items-center p-6 lg:p-10">
<div class="text-[48px] font-extrabold md:text-4xl lg:text-5xl text-center">用强大功能开启盈利快车道</div>
<div class="flex flex-col lg:flex-row gap-5 w-full max-w-7xl items-stretch">
<div class="feature-list flex flex-row gap-3 overflow-x-auto lg:overflow-visible">
<div
v-for="(feat, idx) in features"
:key="idx"
@click="selectFeature(idx)"
@mouseenter="activeFeature = idx"
class="flex-shrink-0 flex flex-col justify-center gap-2 lg:gap-4 pl-4 lg:pl-5 w-40 lg:w-100 h-20 lg:h-auto rounded-lg cursor-pointer transition-colors duration-300"
:class="activeFeature === idx ? 'bg-primary text-white' : 'bg-inkreach-homepage-2'"
>
<p class="feature-item-title text-xl lg:text-3xl">{{ feat.title }}</p>
<p class="text-sm lg:text-base indent-1">{{ feat.desc }}</p>
</div>
</div>
<div class="flex-1 overflow-hidden rounded-xl relative">
<img
v-for="(feat, idx) in features"
:key="'feat-img-' + idx"
:src="feat.image"
:alt="feat.title"
:data-testid="`feature-image-${idx}`"
class="w-full h-full object-contain transition-opacity duration-500 absolute inset-0"
:class="activeFeature === idx ? 'opacity-100' : 'opacity-0 pointer-events-none'"
/>
</div>
</div>
</section>
</template>
<style scoped>
@media (min-width: 1024px) {
.feature-section { height: 754px; padding-top: 54px; }
.feature-section > div:nth-child(2) { height: 550px; }
.feature-list {
display: grid;
grid-template-rows: repeat(4, minmax(0, 1fr));
width: 400px;
height: 100%;
gap: 12px;
}
}
</style>