feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理

- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段
- 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移
- 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制
- 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑
- 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑
- 新增临时 brainstorm UI对比草稿与开发临时状态文件
This commit is contained in:
duanshuwen committed 2026-08-27 11:37:03 +08:00
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);