132 lines
5.2 KiB
Vue
132 lines
5.2 KiB
Vue
<script setup>
|
||
/**
|
||
* 二级 · 板块列表页(需求 §5)
|
||
* 数据链路:GET /v1/categories?section_key= → 左列分类导航;
|
||
* GET /v1/merchants?section_key= → 全部商家,前端按 category_id 归组到各分类下。
|
||
* 250px 头图 + 白面板上浮 30px;面板内「左侧 86px 分类导航 + 右侧卡片列表」双列。
|
||
* 头图与大标题**前端自持**:头图取首页同 section_key 的板块卡图,大标题走 SECTION_TITLE。
|
||
*/
|
||
import { computed, ref } from "vue";
|
||
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
|
||
import contentApi from "@/api/content";
|
||
import { REGION_TEXT, SECTION_TITLE } from "@/constant";
|
||
import { previewImages } from "@/utils/nav";
|
||
|
||
const sectionKey = ref("");
|
||
const categories = ref([]);
|
||
const merchants = ref([]);
|
||
const cover = ref("");
|
||
const bannerImages = ref([]);
|
||
const activeIndex = ref(0);
|
||
const scrollIntoView = ref("");
|
||
|
||
/** 左列分类 + 右列该分类下的商家卡片 */
|
||
const groups = computed(() =>
|
||
categories.value.map((c) => ({
|
||
id: c.id,
|
||
name: c.name,
|
||
items: merchants.value.filter((m) => m.category_id === c.id),
|
||
}))
|
||
);
|
||
|
||
const title = computed(() => SECTION_TITLE[sectionKey.value] || "");
|
||
|
||
onLoad(async (options) => {
|
||
sectionKey.value = options?.section || "scenic";
|
||
const [cats, list, banners, coverUrl] = await Promise.all([
|
||
contentApi.getCategories(sectionKey.value),
|
||
contentApi.getMerchants({ section_key: sectionKey.value }),
|
||
contentApi.getSectionBannerImages(sectionKey.value),
|
||
contentApi.getSectionCover(sectionKey.value),
|
||
]);
|
||
categories.value = cats?.items || [];
|
||
merchants.value = list?.items || [];
|
||
bannerImages.value = banners || [];
|
||
cover.value = coverUrl || "";
|
||
if (groups.value.length) {
|
||
scrollIntoView.value = `g-${groups.value[0].id}`;
|
||
}
|
||
});
|
||
|
||
/** 点左侧分类:右侧列表滚动到对应锚点 */
|
||
const onGroupTap = (index) => {
|
||
activeIndex.value = index;
|
||
const group = groups.value[index];
|
||
if (group) scrollIntoView.value = "";
|
||
setTimeout(() => {
|
||
if (group) scrollIntoView.value = `g-${group.id}`;
|
||
}, 0);
|
||
};
|
||
|
||
/** 点头图预览:优先看板块图集,没有图集时退回预览头图本身 */
|
||
const onCoverPreview = () => {
|
||
const list = bannerImages.value.length
|
||
? bannerImages.value
|
||
: [cover.value].filter(Boolean);
|
||
if (!list.length) return;
|
||
previewImages(list, list[0]);
|
||
};
|
||
|
||
onShareAppMessage(() => ({
|
||
title: `${title.value} · 龙里文旅`,
|
||
path: `/pages-sub/catalog/index?section=${sectionKey.value}`,
|
||
imageUrl: cover.value,
|
||
}));
|
||
</script>
|
||
|
||
<template>
|
||
<view class="relative min-h-screen w-full overflow-hidden">
|
||
<!-- 头图(点击直接预览大图,原「全景图」胶囊按钮已去除) -->
|
||
<view class="relative h-[250px] overflow-hidden">
|
||
<image class="absolute inset-0 w-full h-full" :src="cover" mode="aspectFill" @click="onCoverPreview" />
|
||
<view class="absolute inset-0 scrim-hd" />
|
||
|
||
<view class="relative z-[5]">
|
||
<AppStatusBar />
|
||
<AppNavBar theme="light" />
|
||
</view>
|
||
|
||
<view class="absolute left-5 right-5 bottom-[46px] z-[5]">
|
||
<text class="text-h1 font-bold text-white tracking-[0.3px]">{{ title }}</text>
|
||
<view class="mt-[7px] text-cap text-[rgba(255,255,255,0.86)]">{{ REGION_TEXT }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容面板:分类导航 + 卡片列表(双列) -->
|
||
<view
|
||
class="relative -mt-[30px] rounded-t-panel bg-white h-[calc(100vh_-_220px)] min-h-[460px] overflow-hidden shadow-pan">
|
||
<view class="flex h-full">
|
||
<scroll-view class="w-[86px] shrink-0 bg-alt overflow-hidden pt-3" scroll-y :show-scrollbar="false">
|
||
<view v-for="(group, i) in groups" :key="group.id"
|
||
class="h-16 flex items-center justify-center text-cap relative text-center leading-[1.3]"
|
||
:class="i === activeIndex ? 'bg-white text-brand-deep font-semibold deco-mark' : 'text-ink2'"
|
||
@click="onGroupTap(i)">
|
||
<text>{{ group.name }}</text>
|
||
<text class="ml-[3px] text-[10px]" :class="i === activeIndex ? 'text-brand' : 'text-ink3'">
|
||
({{ group.items.length }})
|
||
</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<scroll-view class="flex-1 min-w-0 bg-white px-3 pt-1 pb-5" scroll-y :show-scrollbar="false"
|
||
:scroll-into-view="scrollIntoView" :scroll-with-animation="true">
|
||
<view v-for="group in groups" :id="`g-${group.id}`" :key="group.id">
|
||
<view class="text-[16px] font-semibold mt-3 mb-[9px] text-ink">{{ group.name }}</view>
|
||
<view v-for="(item, i) in group.items" :key="item.id" :class="i ? 'mt-[10px]' : ''">
|
||
<ContentCard :item="item" show-address show-benefit />
|
||
</view>
|
||
|
||
<view v-if="!group.items.length"
|
||
class="h-[92px] rounded-card bg-alt border-[0.5px] border-line border-dashed flex flex-col items-center justify-center gap-[6px]">
|
||
<AppIcon name="info" tone="ink3" :size="22" />
|
||
<text class="text-hint text-ink3">暂无商家</text>
|
||
</view>
|
||
</view>
|
||
<EmptyBox v-if="!groups.length" text="该板块暂无内容" />
|
||
<view class="h-5" />
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|