feat: 修复二级页面头图展示问题

This commit is contained in:
zoujing committed 2026-09-17 11:37:49 +08:00
1 parent d2dab27afa
commit 87cf7dfbaa
4 files changed
+67 -130

No files matched your search

+20 -13
View File
@@ -34,27 +34,34 @@ const buildGroups = (section) => {
});
};
/** 板块列表(4 个);coverImage 缺失时用该板块首个条目的主图(只读字段,不拼路径) */
export const getSections = async () => {
if (!USE_MOCK_CONTENT) return contentApi.fetchSections();
return clone(
sectionsMock.map((section) => {
if (section.coverImage) return section;
const firstGroup = (catalogMock[section.collections[0]] || [])[0];
const firstId = firstGroup?.item_ids?.[0];
return { ...section, coverImage: itemsMock[firstId]?.main_image_url || "" };
})
);
/**
* 板块封面兜底:`coverImage` 缺失时取该板块**首个分组首个条目**的主图(只读字段,不拼路径)。
*
* ⚠️ 兜底必须抽出来给 `getSections` 与 `getSection` **共用**:两处口径一旦不一致,就会出现
* 「首页板块卡有图、点进板块列表页头图空白」—— `sections.json` 里只有 `tourism` 自带
* `coverImage`,`lodging` / `food` / `specialties` 三个都没这个字段。
*/
const withCoverFallback = (section) => {
if (!section || section.coverImage) return section;
const firstGroup = (catalogMock[section.collections[0]] || [])[0];
const firstId = firstGroup?.item_ids?.[0];
return { ...section, coverImage: itemsMock[firstId]?.main_image_url || "" };
};
/** 板块详情:定义 + 分组(含条目)+ 素材 */
/** 板块列表(4 个) */
export const getSections = async () => {
if (!USE_MOCK_CONTENT) return contentApi.fetchSections();
return clone(sectionsMock.map(withCoverFallback));
};
/** 板块详情:定义 + 分组(含条目)+ 素材;封面同样走兜底,供列表页头图使用 */
export const getSection = async (sectionKey) => {
if (!USE_MOCK_CONTENT) return contentApi.fetchSection(sectionKey);
const section = sectionsMock.find((s) => s.key === sectionKey);
if (!section) return null;
const groups = buildGroups(section);
const supportAssets = (assetsMock || []).filter((a) => section.supportAssetIds.includes(a.entity_id));
return clone({ ...section, groups, supportAssets });
return clone({ ...withCoverFallback(section), groups, supportAssets });
};
/** 条目详情 */
+14 -28
View File
@@ -105,12 +105,8 @@ const onTap = () => {
</script>
<template>
<view
class="relative flex h-[130px] bg-white border-[0.5px] rounded-card mt-[11px]"
:class="rootStateCls"
hover-class="opacity-90"
@click="onTap"
>
<view class="relative flex h-[130px] bg-white border-[0.5px] rounded-card mt-[11px]" :class="rootStateCls"
hover-class="opacity-90" @click="onTap">
<!-- 左:信息区(四行固定顺序) -->
<view class="flex-1 min-w-0 rounded-l-card px-[14px] pt-[13px] pb-3 flex flex-col" :class="sideStateCls">
<!-- ① 类型 -->
@@ -126,25 +122,19 @@ const onTap = () => {
<text class="text-[15px] font-bold" :class="priceNumCls">¥</text>
<text class="text-display font-extrabold tracking-[-1px]" :class="priceNumCls">{{ deal }}</text>
<text v-if="original" class="text-[11.5px] font-medium line-through" :class="wasCls">¥{{ original }}</text>
<text
v-if="pct"
class="ml-auto text-[10px] font-bold tracking-[0.2px] rounded-[6px] px-[6px] py-[3px]"
:class="pctCls"
>
<text v-if="pct" class="ml-auto text-[10px] font-bold tracking-[0.2px] rounded-[6px] px-[6px] py-[3px]"
:class="pctCls">
{{ pct }}
</text>
</view>
<!-- ③ 规格胶囊 + 适用范围 -->
<view class="flex items-center gap-[6px] mt-[7px] text-[11px] whitespace-nowrap" :class="scopeCls">
<text
v-if="spec"
class="shrink-0 text-hint font-semibold rounded-[6px] px-[6px] py-[2px] border-[0.5px]"
:class="specCls"
>
<text v-if="spec" class="shrink-0 text-hint font-semibold rounded-[6px] px-[6px] py-[2px] border-[0.5px]"
:class="specCls">
{{ spec }}
</text>
<text class="min-w-0 truncate">{{ b.scope_desc || b.merchant_name || "" }}</text>
<view class="min-w-0 flex-1 line-clamp-1">{{ b.scope_desc || b.merchant_name || "" }}</view>
</view>
<!-- ④ 有效期 + 第二项随态 -->
@@ -153,10 +143,8 @@ const onTap = () => {
<AppIcon name="clock" tone="ink3" :size="12" />
<text>{{ validText }}</text>
</view>
<view
class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap"
:class="secondMeta.ok ? 'text-[#00A46C]' : ''"
>
<view class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap"
:class="secondMeta.ok ? 'text-[#00A46C]' : ''">
<AppIcon :name="secondMeta.icon" :tone="secondMeta.ok ? 'brand' : 'ink3'" :size="12" />
<text>{{ secondMeta.text }}</text>
</view>
@@ -165,20 +153,18 @@ const onTap = () => {
<!-- 右:配图区(62%/38% 撕口) -->
<view
class="relative w-[38%] min-w-[112px] shrink-0 overflow-hidden rounded-r-card border-l border-dashed border-[rgba(255,138,61,0.45)]"
>
class="relative w-[38%] min-w-[112px] shrink-0 overflow-hidden rounded-r-card border-l border-dashed border-[rgba(255,138,61,0.45)]">
<image class="absolute inset-0 w-full h-full" :class="coverImgCls" :src="b.image_url" mode="aspectFill" />
<view class="absolute inset-0 scrim-cover" />
<view class="absolute top-[11px] right-[11px] z-[2] text-right text-[11.5px] font-bold leading-[1.5] text-white tracking-[0.6px] [rotate:-8deg] [text-shadow:0_1px_7px_rgba(0,0,0,0.5)]">
<view
class="absolute top-[11px] right-[11px] z-[2] text-right text-[11.5px] font-bold leading-[1.5] text-white tracking-[0.6px] [rotate:-8deg] [text-shadow:0_1px_7px_rgba(0,0,0,0.5)]">
<text v-for="(line, i) in slogan" :key="i" class="block">{{ line }}</text>
</view>
<view class="absolute left-0 right-0 bottom-3 z-[2] flex justify-center">
<view
class="h-[30px] px-4 rounded-full text-white text-cap font-semibold flex items-center whitespace-nowrap"
:class="ctaStateCls"
>
<view class="h-[30px] px-4 rounded-full text-white text-cap font-semibold flex items-center whitespace-nowrap"
:class="ctaStateCls">
{{ ctaText }}
</view>
</view>
+7 -13
View File
@@ -25,17 +25,10 @@ const onTap = () => {
</script>
<template>
<view
class="flex gap-[11px] bg-white rounded-card p-[10px] shadow-elev1 border-[0.5px] border-line"
hover-class="opacity-90"
@click="onTap"
>
<image
class="shrink-0 rounded-[12px] bg-alt"
:src="item.main_image_url"
mode="aspectFill"
:style="{ width: thumbSize + 'px', height: thumbSize + 'px' }"
/>
<view class="flex gap-[11px] bg-white rounded-card p-[10px] shadow-elev1 border-[0.5px] border-line"
hover-class="opacity-90" @click="onTap">
<image class="shrink-0 rounded-[12px] bg-alt" :src="item.main_image_url" mode="aspectFill"
:style="{ width: thumbSize + 'px', height: thumbSize + 'px' }" />
<view class="min-w-0 flex-1 flex flex-col">
<view class="text-[13.5px] font-semibold leading-[1.35] text-ink line-clamp-2">{{ item.name }}</view>
@@ -44,9 +37,10 @@ const onTap = () => {
{{ item.description }}
</view>
<view v-if="showAddress && item.address_text" class="mt-auto pt-[5px] flex items-center gap-[3px] text-[10px] text-ink3">
<view v-if="showAddress && item.address_text"
class="mt-auto pt-[5px] flex items-center gap-[3px] text-[10px] text-ink3">
<AppIcon name="pin" tone="ink3" :size="11" />
<text class="truncate">{{ item.address_text }}</text>
<view class="min-w-0 flex-1 line-clamp-1">{{ item.address_text }}</view>
</view>
</view>
</view>
+26 -76
View File
@@ -33,10 +33,6 @@ const businessHours = computed(() => item.value?.business_hours || "");
/**
* 卡片通用底:白卡 + 22px 圆角 + 0.5px 描边 + 双侧柔影
* ⚠️ `border-[0.5px]` **必须紧跟颜色类**:Tailwind v4 里 `border-<宽度>` 的默认色是
* `currentColor`,只写宽度会让卡片描边吃掉继承的文字色 → 渲染成近黑色。
* 普通卡与「到店权益」染色卡写成两套**互斥完整类串**,不走「CARD + 覆盖」拼接 ——
* 同类选择器优先级相同时,谁生效取决于样式表顺序而非 class 书写顺序,拼接不可靠。
*/
const CARD = "bg-white rounded-hero p-[18px] mx-3 mt-3 border-[0.5px] border-line shadow-block";
/** 品牌染色卡(到店权益) */
@@ -131,33 +127,15 @@ onShareAppMessage(() => ({
<view class="absolute inset-0 bg-brand-soft" :style="{ opacity: navProgress }" />
<view class="relative">
<AppStatusBar />
<AppNavBar
:theme="navSolid ? 'dark' : 'light'"
bare
:glass="!navSolid"
:title="item.name"
:subtitle="item.type"
show-back
/>
<AppNavBar :theme="navSolid ? 'dark' : 'light'" bare :glass="!navSolid" :title="item.name"
:subtitle="item.type" show-back />
</view>
</view>
<!-- 头图:多图自动轮播(≥2 张才启用),点击任意位置预览大图。
⚠️ 点击必须绑在**最外层容器**上,不能只绑 <image>:上下两条 scrim 渐隐层与
指示点都是绝对定位的 <view>,会吃掉落在图片上的 tap,只绑 image 的话
只有中段一条窄带能点开预览。tap 默认冒泡,所以只在最外层绑一次即可
(image 上不要再绑,否则会连触两次)。 -->
<view class="relative overflow-hidden bg-[#0E4432]">
<view class="relative h-[316px]" @click="onPreview(current)">
<swiper
class="w-full h-[316px] bg-[#0E4432]"
:current="current"
:circular="images.length > 1"
:autoplay="images.length > 1"
:interval="3500"
:duration="500"
@change="onSwiperChange"
>
<swiper class="w-full h-[316px] bg-[#0E4432]" :current="current" :circular="images.length > 1"
:autoplay="images.length > 1" :interval="3500" :duration="500" @change="onSwiperChange">
<swiper-item v-for="(url, i) in images" :key="i">
<image class="block w-full h-[316px]" :src="url" mode="aspectFill" />
</swiper-item>
@@ -166,20 +144,14 @@ onShareAppMessage(() => ({
<view class="absolute left-0 right-0 top-0 h-[140px] scrim-dtop" />
<view class="absolute left-0 right-0 bottom-0 h-[170px] scrim-dbottom" />
<view
v-if="images.length > 1"
class="absolute right-[14px] bottom-[52px] h-[22px] px-[10px] rounded-full flex items-center text-[11px] text-white bg-[rgba(6,26,19,0.44)] border-[0.5px] border-[rgba(255,255,255,0.24)]"
>
<view v-if="images.length > 1"
class="absolute right-[14px] bottom-[52px] h-[22px] px-[10px] rounded-full flex items-center text-[11px] text-white bg-[rgba(6,26,19,0.44)] border-[0.5px] border-[rgba(255,255,255,0.24)]">
{{ current + 1 }} / {{ images.length }}
</view>
<view v-if="images.length > 1" class="absolute left-0 right-0 bottom-[52px] flex justify-center gap-[5px]">
<view
v-for="(url, i) in images"
:key="i"
class="rounded-full"
:class="i === current ? 'w-4 h-[5px] bg-white' : 'w-[5px] h-[5px] bg-[rgba(255,255,255,0.5)]'"
/>
<view v-for="(url, i) in images" :key="i" class="rounded-full"
:class="i === current ? 'w-4 h-[5px] bg-white' : 'w-[5px] h-[5px] bg-[rgba(255,255,255,0.5)]'" />
</view>
</view>
@@ -189,10 +161,8 @@ onShareAppMessage(() => ({
<view class="relative z-[4] -mt-[34px] rounded-t-panel bg-white pt-7 px-4 pb-[18px] shadow-wcard">
<view class="flex items-center gap-[9px]">
<text class="text-[24px] font-bold tracking-[-0.6px] text-ink">{{ item.name }}</text>
<text
v-if="badge"
class="shrink-0 text-hint font-semibold px-2 py-1 rounded-[7px] bg-[rgba(0,181,120,0.1)] text-brand-deep border-[0.5px] border-[rgba(0,181,120,0.2)]"
>
<text v-if="badge"
class="shrink-0 text-hint font-semibold px-2 py-1 rounded-[7px] bg-[rgba(0,181,120,0.1)] text-brand-deep border-[0.5px] border-[rgba(0,181,120,0.2)]">
{{ badge }}
</text>
</view>
@@ -200,22 +170,20 @@ onShareAppMessage(() => ({
<view v-if="item.name_en" class="mt-px text-[11.5px] text-ink3">{{ item.name_en }}</view>
<!-- 营业时间(字段待运营方补充,一期 mock) -->
<view v-if="businessHours" class="mt-2 flex items-center gap-[5px] text-[12px] font-medium text-brand-deep tracking-[0.1px]">
<view v-if="businessHours"
class="mt-2 flex items-center gap-[5px] text-[12px] font-medium text-brand-deep tracking-[0.1px]">
<AppIcon name="clock" tone="brand" :size="12" />
<text>营业时间 {{ businessHours }}</text>
</view>
<!-- 地址条(无经纬度 → 不做地图导航,点击复制) -->
<view
v-if="addressText"
<view v-if="addressText"
class="mt-3 flex items-center gap-[7px] px-[11px] py-[10px] rounded-[14px] grad-address border-[0.5px] border-[rgba(0,181,120,0.14)] text-[12px] leading-[1.55] text-ink2"
@click="onCopyAddress"
>
@click="onCopyAddress">
<AppIcon name="pin" tone="ink3" :size="14" />
<text class="flex-1 min-w-0">{{ addressText }}</text>
<view
class="shrink-0 ml-auto w-7 h-7 rounded-full flex items-center justify-center bg-white border-[0.5px] border-[rgba(0,181,120,0.22)] shadow-[0_3px_8px_rgba(0,181,120,0.14)]"
>
class="shrink-0 ml-auto w-7 h-7 rounded-full flex items-center justify-center bg-white border-[0.5px] border-[rgba(0,181,120,0.22)] shadow-[0_3px_8px_rgba(0,181,120,0.14)]">
<AppIcon name="copy" tone="brandDeep" :size="14" />
</view>
</view>
@@ -241,9 +209,7 @@ onShareAppMessage(() => ({
</view>
<view
class="shrink-0 w-11 h-11 rounded-full flex items-center justify-center grad-call shadow-[0_8px_18px_rgba(0,181,120,0.32)]"
hover-class="opacity-90"
@click="onCall"
>
hover-class="opacity-90" @click="onCall">
<AppIcon name="phone" tone="white" :size="20" />
</view>
</view>
@@ -254,13 +220,8 @@ onShareAppMessage(() => ({
<view class="flex items-center gap-2 text-[16px] font-bold tracking-[-0.3px] text-ink deco-titledot">
到店权益
</view>
<BenefitCard
v-for="benefit in relatedBenefits"
:key="benefit.id"
:benefit="benefit"
mode="detail"
@action="onClaimBenefit"
/>
<BenefitCard v-for="benefit in relatedBenefits" :key="benefit.id" :benefit="benefit" mode="detail"
@action="onClaimBenefit" />
</view>
<!-- 附近同类推荐(图上压字卡) -->
@@ -268,27 +229,16 @@ onShareAppMessage(() => ({
<view class="flex items-center gap-2 text-[16px] font-bold tracking-[-0.3px] text-ink deco-titledot">
附近同类推荐
</view>
<!-- ⚠️ scroll-x 必须开 enable-flex,否则 display:flex 在 WebView 下不生效 → 子项竖排。
内层包裹需 shrink-0,否则成为 flex item 后会被压到容器宽度。
scroll-view 内不要用 gap-*(小程序实测不生效),条间距用子项 mr-[11px]。 -->
<scroll-view
class="mt-[11px] flex flex-row overflow-hidden"
scroll-x
enable-flex
:show-scrollbar="false"
>
<scroll-view class="mt-[11px] flex flex-row overflow-hidden" scroll-x enable-flex :show-scrollbar="false">
<view class="flex shrink-0">
<view v-for="rec in related" :key="rec.id" class="relative shrink-0 w-[154px] mr-[11px]" @click="goRelated(rec)">
<image
class="w-[154px] h-[102px] rounded-[16px] shadow-[0_8px_20px_rgba(8,30,22,0.14)]"
:src="rec.main_image_url"
mode="aspectFill"
/>
<text
class="absolute left-0 right-0 bottom-0 px-[10px] pt-[22px] pb-[9px] rounded-b-[16px] text-white text-[11.5px] font-semibold truncate scrim-rec"
>
<view v-for="rec in related" :key="rec.id" class="relative shrink-0 w-[154px] h-[102px] mr-[11px]"
@click="goRelated(rec)">
<image class="block w-[154px] h-[102px] rounded-[16px] shadow-[0_8px_20px_rgba(8,30,22,0.14)]"
:src="rec.main_image_url" mode="aspectFill" />
<view
class="absolute left-0 right-0 bottom-0 px-[10px] pt-[22px] pb-[9px] rounded-b-[16px] text-white text-[11.5px] font-semibold line-clamp-1 scrim-rec">
{{ rec.name }}
</text>
</view>
</view>
</view>
</scroll-view>