Compare commits
2
Commits
f23d598328
...
d2dab27afa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2dab27afa | ||
|
|
d4790ba82c |
No files matched your search
@@ -96,6 +96,21 @@ export const getSectionBannerImages = async (sectionKey) => {
|
||||
return unique(urls.filter(Boolean)).slice(0, 3);
|
||||
};
|
||||
|
||||
/**
|
||||
* 首页板块卡轮播图集:**每个分组取一张代表图**(组内首个有主图的条目)。
|
||||
* 与 getSectionBannerImages 的区别:那个是「组内逐个条目」取前 3 张,
|
||||
* 可能全落在同一个分组里;首页旅居卡要的是「酒店 / 民宿别院 / 旅居公寓 / 露营基地」
|
||||
* 四个分组各一张,张数 = 分组数。
|
||||
*/
|
||||
export const getSectionGroupCovers = async (sectionKey) => {
|
||||
const section = await getSection(sectionKey);
|
||||
if (!section) return [];
|
||||
const urls = section.groups
|
||||
.map((g) => g.items.find((it) => it.main_image_url)?.main_image_url)
|
||||
.filter(Boolean);
|
||||
return unique(urls);
|
||||
};
|
||||
|
||||
export default {
|
||||
getSections,
|
||||
getSection,
|
||||
@@ -104,4 +119,5 @@ export default {
|
||||
getFeaturedItems,
|
||||
getOverviewMapAsset,
|
||||
getSectionBannerImages,
|
||||
getSectionGroupCovers,
|
||||
};
|
||||
@@ -1,25 +1,21 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 内容条目卡(原 .item)
|
||||
* - 首页「精选推荐」:显示等级/标签,不显示地址
|
||||
* - 板块列表页:不显示标签(v0.9 已去掉描述性标签),显示地址行
|
||||
* - 首页「精选推荐」:名称 + 简介,不显示地址
|
||||
* - 板块列表页:名称 + 简介 + 地址行
|
||||
* 等级与描述性标签(`grade` / `tags`)全站不展示 —— 字段保留在数据层,页面不渲染。
|
||||
* 图片地址一律取 item.main_image_url(只读字段,不拼路径)
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import { goDetail } from "@/utils/nav";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
showTags: { type: Boolean, default: false },
|
||||
showAddress: { type: Boolean, default: false },
|
||||
thumbSize: { type: Number, default: 92 },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["tap"]);
|
||||
|
||||
const tags = computed(() => (Array.isArray(props.item?.tags) ? props.item.tags.slice(0, 2) : []));
|
||||
const grade = computed(() => props.item?.grade || "");
|
||||
|
||||
const onTap = () => {
|
||||
emit("tap", props.item);
|
||||
if (props.item?.collection && props.item?.id) {
|
||||
@@ -44,22 +40,6 @@ const onTap = () => {
|
||||
<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>
|
||||
|
||||
<view v-if="showTags && (grade || tags.length)" class="flex gap-1 mt-[5px] overflow-hidden">
|
||||
<text
|
||||
v-if="grade"
|
||||
class="shrink-0 whitespace-nowrap text-[9.5px] px-[5px] py-[2px] rounded-[4px] border-[0.5px] border-[rgba(0,181,120,0.2)] bg-brand-soft text-brand-deep"
|
||||
>
|
||||
{{ grade }}
|
||||
</text>
|
||||
<text
|
||||
v-for="(t, i) in tags"
|
||||
:key="i"
|
||||
class="shrink-0 whitespace-nowrap text-[9.5px] px-[5px] py-[2px] rounded-[4px] border-[0.5px] border-[rgba(62,139,240,0.14)] bg-info-soft text-info-deep"
|
||||
>
|
||||
{{ t }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="item.description" class="text-hint leading-[1.5] text-ink3 mt-[5px] line-clamp-2">
|
||||
{{ item.description }}
|
||||
</view>
|
||||
|
||||
@@ -11,16 +11,36 @@
|
||||
* → 因此**高度由组件自持**(sizeCls,按 variant 给),不依赖父级;
|
||||
* **宽度**仍由父级在宿主节点上分配(flex-[1.06] / flex-1)。
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { goCatalog } from "@/utils/nav";
|
||||
|
||||
const props = defineProps({
|
||||
section: { type: Object, required: true },
|
||||
variant: { type: String, default: "short" },
|
||||
/**
|
||||
* 卡内轮播图集。首页旅居卡传「每个分组一张代表图」(4 张),
|
||||
* 传空则回落为板块封面单图(右二半高卡就是这种)。
|
||||
*/
|
||||
covers: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const isTall = computed(() => props.variant === "tall");
|
||||
|
||||
/** 轮播图集:有 covers 用 covers,否则单张封面 —— 统一成一个数组,模板只判长度 */
|
||||
const images = computed(() => {
|
||||
if (props.covers?.length) return props.covers;
|
||||
return props.section?.coverImage ? [props.section.coverImage] : [];
|
||||
});
|
||||
/** ≥2 张才起轮播:单张时 swiper 没有意义,还会白白多一层节点 */
|
||||
const hasSlider = computed(() => images.value.length > 1);
|
||||
|
||||
const activeIndex = ref(0);
|
||||
/** 取模防越界:图集长度变化(如热更新/接口返回变少)时进度点不会全部熄灭 */
|
||||
const activeDot = computed(() => (images.value.length ? activeIndex.value % images.value.length : 0));
|
||||
const onSwiperChange = (e) => {
|
||||
activeIndex.value = Number(e.detail?.current || 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* ⚠️ 卡片高度**由组件自持**,不吃父级传来的尺寸。
|
||||
* 原因:小程序自定义组件是「宿主节点 → 内部根节点」两层,父级写在
|
||||
@@ -66,7 +86,24 @@ const onTap = () => goCatalog(props.section.key);
|
||||
hover-class="opacity-95"
|
||||
@click="onTap"
|
||||
>
|
||||
<image class="absolute inset-0 w-full h-full" :src="section.coverImage" mode="aspectFill" />
|
||||
<!-- 图:≥2 张自动轮播(首页旅居卡 = 酒店/民宿/公寓/露营 四个分组各一张),否则单图。
|
||||
swiper 用 absolute inset-0 填满卡片 —— 根节点高度由 sizeCls 自持(226px),
|
||||
百分比高度链路在小程序里太脆,靠绝对定位拿确定高度最稳。 -->
|
||||
<swiper
|
||||
v-if="hasSlider"
|
||||
class="absolute inset-0 w-full h-full"
|
||||
circular
|
||||
autoplay
|
||||
:interval="3500"
|
||||
:duration="500"
|
||||
:indicator-dots="false"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="url in images" :key="url">
|
||||
<image class="w-full h-full" :src="url" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<image v-else class="absolute inset-0 w-full h-full" :src="section.coverImage" mode="aspectFill" />
|
||||
|
||||
<!-- 遮罩:文字锚顶部 → 顶部最重 -->
|
||||
<view class="absolute inset-0" :class="isTall ? 'scrim-tile-tall' : 'scrim-tile'" />
|
||||
@@ -86,11 +123,14 @@ const onTap = () => goCatalog(props.section.key);
|
||||
<AppIcon name="arrow" tone="white" :size="isTall ? 22 : 17" />
|
||||
</view>
|
||||
|
||||
<!-- 通高卡右下角放轮播指示点 -->
|
||||
<view v-if="isTall" class="absolute left-[15px] bottom-[23px] flex gap-[5px] z-[2]">
|
||||
<view class="w-[14px] h-[5px] rounded-full bg-white" />
|
||||
<view class="w-[5px] h-[5px] rounded-full bg-[rgba(255,255,255,0.45)]" />
|
||||
<view class="w-[5px] h-[5px] rounded-full bg-[rgba(255,255,255,0.45)]" />
|
||||
<!-- 通高卡左下角轮播指示点:张数 = 图集张数(旅居卡 4 个分组 → 4 个点) -->
|
||||
<view v-if="isTall && hasSlider" class="absolute left-[15px] bottom-[23px] flex gap-[5px] z-[2]">
|
||||
<view
|
||||
v-for="(url, i) in images"
|
||||
:key="`${url}-dot`"
|
||||
class="h-[5px] rounded-full transition-all duration-300"
|
||||
:class="i === activeDot ? 'w-[14px] bg-white' : 'w-[5px] bg-[rgba(255,255,255,0.45)]'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -142,12 +142,24 @@ onShareAppMessage(() => ({
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头图 -->
|
||||
<!-- 头图:多图自动轮播(≥2 张才启用),点击任意位置预览大图。
|
||||
⚠️ 点击必须绑在**最外层容器**上,不能只绑 <image>:上下两条 scrim 渐隐层与
|
||||
指示点都是绝对定位的 <view>,会吃掉落在图片上的 tap,只绑 image 的话
|
||||
只有中段一条窄带能点开预览。tap 默认冒泡,所以只在最外层绑一次即可
|
||||
(image 上不要再绑,否则会连触两次)。 -->
|
||||
<view class="relative overflow-hidden bg-[#0E4432]">
|
||||
<view class="relative h-[316px]">
|
||||
<swiper class="w-full h-[316px] bg-[#0E4432]" :current="current" :circular="images.length > 1" @change="onSwiperChange">
|
||||
<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-item v-for="(url, i) in images" :key="i">
|
||||
<image class="block w-full h-[316px]" :src="url" mode="aspectFill" @click="onPreview(i)" />
|
||||
<image class="block w-full h-[316px]" :src="url" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { goCatalog, goDetail, previewImages } from "@/utils/nav";
|
||||
const sections = ref([]);
|
||||
const featured = ref([]);
|
||||
const mapAsset = ref(null);
|
||||
/** 旅居卡轮播图集:住宿 4 个分组各取一张代表图 */
|
||||
const lodgingCovers = ref([]);
|
||||
|
||||
const byKey = (key) => sections.value.find((s) => s.key === key) || {};
|
||||
|
||||
@@ -25,14 +27,16 @@ const tileSpecialties = computed(() => byKey("specialties"));
|
||||
const mapUrl = computed(() => mapAsset.value?.oss_url || "");
|
||||
|
||||
const load = async () => {
|
||||
const [list, feat, map] = await Promise.all([
|
||||
const [list, feat, map, covers] = await Promise.all([
|
||||
contentApi.getSections(),
|
||||
contentApi.getFeaturedItems(),
|
||||
contentApi.getOverviewMapAsset(),
|
||||
contentApi.getSectionGroupCovers("lodging"),
|
||||
]);
|
||||
sections.value = list || [];
|
||||
featured.value = feat || [];
|
||||
mapAsset.value = map;
|
||||
lodgingCovers.value = covers || [];
|
||||
};
|
||||
|
||||
load();
|
||||
@@ -122,7 +126,7 @@ const onFeaturedTap = (item) => goDetail(item.collection, item.id);
|
||||
传不进组件内部,所以高度交给组件自己;这里父级只管**宽度比例**。
|
||||
右列 108×2 + gap 10 = 226,与外层容器自洽。 -->
|
||||
<view class="flex gap-[10px] h-[226px] mt-[2px]">
|
||||
<SectionTile class="flex-[1.06] min-w-0" :section="tileLodging" variant="tall" />
|
||||
<SectionTile class="flex-[1.06] min-w-0" :section="tileLodging" variant="tall" :covers="lodgingCovers" />
|
||||
<view class="flex-1 min-w-0 flex flex-col gap-[10px]">
|
||||
<SectionTile :section="tileFood" variant="short" />
|
||||
<SectionTile :section="tileSpecialties" variant="short" />
|
||||
@@ -154,7 +158,7 @@ const onFeaturedTap = (item) => goDetail(item.collection, item.id);
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(item, i) in featured" :key="item.id" :class="i ? 'mt-[10px]' : ''">
|
||||
<ContentCard :item="item" show-tags @tap="onFeaturedTap" />
|
||||
<ContentCard :item="item" @tap="onFeaturedTap" />
|
||||
</view>
|
||||
|
||||
<view class="mt-5 mx-1 text-[10px] leading-[1.7] text-ink3 text-center">内容依据龙里县官方旅游指南整理</view>
|
||||
|
||||
Reference in new issue
Block a user