feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理
- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段 - 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移 - 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制 - 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑 - 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑 - 新增临时 brainstorm UI对比草稿与开发临时状态文件
This commit is contained in:
1 parent
96bcc7c032
commit
4e04feda5b
27 files changed
+2695
-377
No files matched your search
@@ -8,19 +8,40 @@
|
||||
:current="heroIndex"
|
||||
@change="onSlideChange"
|
||||
>
|
||||
<swiper-item v-for="slide in slides" :key="slide.id || slide.image">
|
||||
<swiper-item v-for="(slide, index) in props.slides" :key="slide.id || slide.image">
|
||||
<view
|
||||
class="relative m-0 h-full w-full overflow-hidden rounded-none p-0 text-left"
|
||||
@click="$emit('select', slide.title)"
|
||||
>
|
||||
<image class="absolute inset-0 h-full w-full" :src="slide.image" mode="aspectFill" />
|
||||
<image
|
||||
class="absolute inset-0 h-full w-full"
|
||||
:src="getSlideImage(slide, index)"
|
||||
mode="aspectFill"
|
||||
@error="onImageError(slide, index)"
|
||||
/>
|
||||
<view class="hero-scrim absolute inset-0 flex items-end px-6 pb-[68px]">
|
||||
<view class="min-w-0 text-white">
|
||||
<text v-if="slide.kicker" class="block text-[12px] font-medium leading-5 tracking-[0.12em] text-white/80">
|
||||
{{ slide.kicker }}
|
||||
</text>
|
||||
<text v-if="slide.title" class="mt-1 block text-[25px] font-semibold leading-8 tracking-[0.04em]">
|
||||
{{ slide.title }}
|
||||
</text>
|
||||
<text
|
||||
v-if="slide.actionLabel || slide.action"
|
||||
class="mt-3 inline-flex rounded-[8px] border border-white/55 px-3 py-1 text-[12px] leading-5 text-white/95"
|
||||
>
|
||||
{{ slide.actionLabel || slide.action }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<view class="absolute bottom-8 left-0 right-0 flex justify-center gap-2">
|
||||
<text
|
||||
v-for="(_, index) in slides"
|
||||
v-for="(_, index) in props.slides"
|
||||
:key="index"
|
||||
class="h-1.5 rounded-full transition-all"
|
||||
:class="heroIndex === index ? 'w-6 bg-white' : 'w-1.5 bg-white/50'"
|
||||
@@ -31,17 +52,43 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { fallbackData } from "@/lib/data";
|
||||
import { resolveNetworkImage } from "@/content";
|
||||
import type { HeroSlide } from "@/lib/types";
|
||||
|
||||
defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
slides: HeroSlide[];
|
||||
}>();
|
||||
fallbackSlides?: HeroSlide[];
|
||||
}>(), {
|
||||
fallbackSlides: () => fallbackData.heroSlides,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
select: [title: string];
|
||||
}>();
|
||||
|
||||
const heroIndex = ref(0);
|
||||
const failedSlideImages = ref<Record<string, boolean>>({});
|
||||
|
||||
function getSlideKey(slide: HeroSlide, index: number) {
|
||||
return slide.id || `${slide.image}-${index}`;
|
||||
}
|
||||
|
||||
function getFallbackImage(slide: HeroSlide, index: number) {
|
||||
const fallbackSlide = props.fallbackSlides.find((item) => item.title === slide.title) ?? props.fallbackSlides[index];
|
||||
return fallbackSlide?.image ?? "";
|
||||
}
|
||||
|
||||
function getSlideImage(slide: HeroSlide, index: number) {
|
||||
const fallbackImage = getFallbackImage(slide, index);
|
||||
return failedSlideImages.value[getSlideKey(slide, index)] ? fallbackImage : resolveNetworkImage(slide.image, fallbackImage);
|
||||
}
|
||||
|
||||
function onImageError(slide: HeroSlide, index: number) {
|
||||
const fallbackImage = getFallbackImage(slide, index);
|
||||
if (!fallbackImage || fallbackImage === slide.image) return;
|
||||
failedSlideImages.value[getSlideKey(slide, index)] = true;
|
||||
}
|
||||
|
||||
function onSlideChange(event: { detail: { current: number } }) {
|
||||
heroIndex.value = Number(event.detail.current);
|
||||
|
||||
Reference in new issue
Block a user