Compare commits
7 Commits
home3.0
...
duohua-new
| Author | SHA1 | Date | |
|---|---|---|---|
| a37eaa6e4c | |||
| 44b1056ba7 | |||
| 163f433970 | |||
| 07a7a0f516 | |||
| 9ed03459c2 | |||
| a04d7e267d | |||
| 6ec590b324 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appid": "wx5e79df5996572539",
|
||||
"appid": "wx23f86d809ae80259",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.8.10",
|
||||
"packOptions": {
|
||||
|
||||
@@ -12,7 +12,7 @@ import rawConfigs from "../../client-configs.json" with { type: "json" };
|
||||
export const CLIENT_CONFIGS = rawConfigs;
|
||||
|
||||
// 获取当前用户端配置
|
||||
export const getCurrentConfig = () => CLIENT_CONFIGS.zhinian;
|
||||
export const getCurrentConfig = () => CLIENT_CONFIGS.duohua;
|
||||
export const clientId = getCurrentConfig().clientId;
|
||||
export const appId = getCurrentConfig().appId;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getCurrentConfig, currentClientType, ClientType } from "@/constant/base
|
||||
|
||||
export const chatInputPlaceholder = () => {
|
||||
const config = getCurrentConfig();
|
||||
return `快告诉${config.name}您在想什么~`;
|
||||
return "龙里有什么必吃美食?";// `快告诉${config.name}您在想什么~`;
|
||||
}
|
||||
|
||||
export const chatGuideSubTitle = () => {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
"quickapp" : {},
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx5e79df5996572539",
|
||||
"appid" : "wx23f86d809ae80259",
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"minified" : true
|
||||
|
||||
@@ -81,6 +81,20 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/DuohuaDiscovery/catalog",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/DuohuaDiscovery/detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/test/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</view>
|
||||
|
||||
<view class="discovery-panel" :class="{ 'is-collapsed': isDiscoveryPanelCollapsed }">
|
||||
<Discovery :active="tabIndex === 0" @content-scroll="handleDiscoveryContentScroll" />
|
||||
<DuohuaDiscovery :active="tabIndex === 0" @content-scroll="handleDiscoveryContentScroll" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -163,7 +163,7 @@ import { MessageRole, MessageType, CompName, Command } from "@/model/ChatModel";
|
||||
|
||||
import HomeWelcome from "../HomeWelcome/index.vue";
|
||||
import AiTabSwitch from "@/components/AiTabSwitch/index.vue";
|
||||
import Discovery from "../../Discovery/index.vue";
|
||||
import DuohuaDiscovery from "../../DuohuaDiscovery/index.vue";
|
||||
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
|
||||
import ChatGuide from "../ChatGuide/index.vue";
|
||||
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 92px;
|
||||
height: 42px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-panel {
|
||||
position: absolute;
|
||||
top: 92px;
|
||||
top: 42px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
min-height: 0;
|
||||
|
||||
@@ -25,7 +25,7 @@ const AIGC_HOME_URL = "/pages-aigc/home/home";
|
||||
const itemList = ref([
|
||||
{
|
||||
icon: "",
|
||||
title: "快速预订",
|
||||
title: "快速领取",
|
||||
type: Command.quickBooking,
|
||||
},
|
||||
{
|
||||
@@ -60,8 +60,9 @@ const sendReply = (item) => {
|
||||
// 快速预订
|
||||
if (item.type === Command.quickBooking) {
|
||||
checkToken().then(() => {
|
||||
uni.navigateTo({ url: "/pages/DuohuaDiscovery/catalog?section=tourism" });
|
||||
// uni.navigateTo({ url: "/pages-quick/list" });
|
||||
uni.navigateTo({ url: "/pages-service/tickets/index" });
|
||||
// uni.navigateTo({ url: "/pages-service/tickets/index" });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<view class="home-welcome-panel">
|
||||
<view class="home-welcome-header w-full min-w-0">
|
||||
<text class="home-welcome-title block w-0 min-w-0 truncate">
|
||||
{{ props.mainPageDataModel.welcomeContent || "小七欢迎你~" }}
|
||||
欢迎来到龙里!
|
||||
</text>
|
||||
<text v-if="weatherText" class="home-welcome-weather">
|
||||
{{ weatherText }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<NoticeMessage :tipsMessage="props.mainPageDataModel.tipsMessage" />
|
||||
<!-- <NoticeMessage :tipsMessage="props.mainPageDataModel.tipsMessage" /> -->
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view v-for="item in themeDTOList" :key="item.title">
|
||||
<RecommendPostsList :recommendTheme="item" />
|
||||
<RecommendPostsList v-if="item.recommendPostsList.lenght > 0" :recommendTheme="item" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -39,20 +39,21 @@ const popup = ref(null);
|
||||
const list = ref([
|
||||
{
|
||||
icon: "https://oss.nianxx.cn/mp/static/version_101/home/ksyd.png",
|
||||
title: "快速预订",
|
||||
content: "预定门票、房间、餐食",
|
||||
btnText: "去预定",
|
||||
title: "快速领取",
|
||||
content: "领取票根优惠",
|
||||
btnText: "去领取",
|
||||
type: Command.quickBooking,
|
||||
path: "/pages-quick/list",
|
||||
// path: "/pages-quick/list",
|
||||
path: "/pages/DuohuaDiscovery/catalog?section=tourism",
|
||||
},
|
||||
{
|
||||
icon: "https://oss.nianxx.cn/mp/static/version_101/home/tsfx.png",
|
||||
title: "探索发现",
|
||||
content: "发现景点、活动、特色内容",
|
||||
btnText: "去探索",
|
||||
type: Command.discovery,
|
||||
path: "",
|
||||
},
|
||||
// {
|
||||
// icon: "https://oss.nianxx.cn/mp/static/version_101/home/tsfx.png",
|
||||
// title: "探索发现",
|
||||
// content: "发现景点、活动、特色内容",
|
||||
// btnText: "去探索",
|
||||
// type: Command.discovery,
|
||||
// path: "",
|
||||
// },
|
||||
{
|
||||
icon: "https://oss.nianxx.cn/mp/static/version_101/home/mddd.png",
|
||||
title: "我的订单",
|
||||
@@ -85,14 +86,14 @@ const list = ref([
|
||||
type: Command.feedbackCard,
|
||||
path: "",
|
||||
},
|
||||
{
|
||||
icon: "https://oss.nianxx.cn/mp/static/version_101/home/hjfw.png",
|
||||
title: "呼叫热线",
|
||||
content: "我们将全程为您提供细致解答与暖心服务。",
|
||||
btnText: "去呼叫",
|
||||
type: Command.callMobileService,
|
||||
path: "",
|
||||
},
|
||||
// {
|
||||
// icon: "https://oss.nianxx.cn/mp/static/version_101/home/hjfw.png",
|
||||
// title: "呼叫热线",
|
||||
// content: "我们将全程为您提供细致解答与暖心服务。",
|
||||
// btnText: "去呼叫",
|
||||
// type: Command.callMobileService,
|
||||
// path: "",
|
||||
// },
|
||||
]);
|
||||
|
||||
const open = () => {
|
||||
|
||||
38
src/pages/DuohuaDiscovery/catalog.vue
Normal file
38
src/pages/DuohuaDiscovery/catalog.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<view class="h-screen min-w-0 overflow-hidden bg-[#f5f7fa] text-[#172033]">
|
||||
<DuohuaCatalog
|
||||
v-if="sectionData"
|
||||
:section="sectionData"
|
||||
@back="goBack"
|
||||
@open-detail="openDetail"
|
||||
/>
|
||||
<view v-else class="flex h-full flex-col">
|
||||
<TopNavBar title="" @back="goBack" />
|
||||
<view class="flex min-h-0 flex-1 items-center justify-center">
|
||||
<text class="text-[14px] text-[#808997]">暂无对应内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import DuohuaCatalog from "./components/DuohuaCatalog.vue";
|
||||
import { getSection } from "./data";
|
||||
|
||||
const sectionData = ref(null);
|
||||
|
||||
onLoad((options) => {
|
||||
sectionData.value = getSection(options?.section || "");
|
||||
});
|
||||
|
||||
const goBack = () => uni.navigateBack();
|
||||
|
||||
const openDetail = ({ collection, itemId }) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/DuohuaDiscovery/detail?collection=${collection}&id=${itemId}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
266
src/pages/DuohuaDiscovery/components/DuohuaCatalog.vue
Normal file
266
src/pages/DuohuaDiscovery/components/DuohuaCatalog.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<view class="flex h-screen flex-col overflow-hidden bg-[#06111f]">
|
||||
<view class="relative z-0 h-[34vh] shrink-0 overflow-hidden bg-[#173a34]">
|
||||
<DuohuaMediaSwiper
|
||||
:media-list="heroMediaList"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
<view class="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(6,17,31,0.15)_18%,rgba(6,17,31,0.1)_45%,rgba(6,17,31,0.92)_100%)]" />
|
||||
|
||||
<view class="absolute inset-x-0 top-0 z-20">
|
||||
<TopNavBar
|
||||
title=""
|
||||
background="transparent"
|
||||
title-color="#ffffff"
|
||||
back-icon-color="#ffffff"
|
||||
@back="emit('back')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="absolute bottom-[42px] left-[20px] right-[20px]">
|
||||
<view class="flex items-center gap-[10px]">
|
||||
<text class="text-[28px] font-bold leading-[1.2] text-white">
|
||||
{{ section.title }}
|
||||
</text>
|
||||
<view
|
||||
class="flex h-[27px] shrink-0 items-center rounded-full border border-white/55 bg-black/20 px-[10px]"
|
||||
@tap.stop="previewPanorama"
|
||||
>
|
||||
<text class="text-[11px] text-white">查看全景图</text>
|
||||
<uni-icons class="ml-[2px]" type="right" size="12" color="#FFFFFF" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="mt-[7px] block text-[13px] leading-[19px] text-white/85">
|
||||
{{ region }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="relative z-10 -mt-[28px] min-h-0 flex-1 overflow-hidden rounded-t-[28px] bg-[#f4f6f8] shadow-[0_-8px_24px_rgba(6,17,31,0.12)]">
|
||||
<view class="flex h-full min-h-0">
|
||||
<scroll-view
|
||||
class="h-full w-[86px] shrink-0 bg-white"
|
||||
scroll-y
|
||||
:scroll-into-view="leftScrollIntoViewId"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view
|
||||
v-for="group in groups"
|
||||
:id="`duohua-nav-${group.groupId}`"
|
||||
:key="group.groupId"
|
||||
class="relative flex min-h-[68px] items-center justify-center px-[10px] py-[12px] text-center"
|
||||
:class="isActiveGroup(group.groupId) ? 'bg-[#ecf8f2] text-[#11795c]' : 'bg-white text-[#5f6875]'"
|
||||
@click="selectGroup(group.groupId)"
|
||||
>
|
||||
<view
|
||||
v-if="isActiveGroup(group.groupId)"
|
||||
class="absolute left-0 h-[28px] w-[3px] rounded-r-full bg-[#149474]"
|
||||
/>
|
||||
<text
|
||||
class="text-[13px] leading-[18px]"
|
||||
:class="isActiveGroup(group.groupId) ? 'font-semibold' : 'font-normal'"
|
||||
>
|
||||
{{ group.type }}
|
||||
</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view
|
||||
id="duohua-content-scroll"
|
||||
class="h-full min-w-0 flex-1 bg-[#f4f6f8]"
|
||||
scroll-y
|
||||
:scroll-into-view="scrollIntoViewId"
|
||||
scroll-with-animation
|
||||
@scroll="handleContentScroll"
|
||||
@touchstart="enableContentScrollSync"
|
||||
>
|
||||
<view class="px-[12px] pb-[24px]">
|
||||
<view
|
||||
v-for="group in groups"
|
||||
:id="`duohua-group-${group.groupId}`"
|
||||
:key="group.groupId"
|
||||
class="pt-[14px]"
|
||||
>
|
||||
<text class="block text-[18px] font-bold text-[#202733]">
|
||||
{{ group.type }}
|
||||
</text>
|
||||
|
||||
<view class="mt-[10px] flex flex-col gap-[10px]">
|
||||
<view
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="flex rounded-[14px] bg-white p-[10px]"
|
||||
@click="openDetail(group.collection, item.id)"
|
||||
>
|
||||
<image
|
||||
class="h-[100px] w-[100px] shrink-0 rounded-[10px] bg-[#e8edf0]"
|
||||
:src="item.main_image_url"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="ml-[10px] flex min-w-0 flex-1 flex-col">
|
||||
<view class="flex min-w-0 items-start">
|
||||
<text class="line-clamp-2 min-w-0 flex-1 text-[14px] font-semibold leading-[20px] text-[#242b36]">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<view class="flex h-[20px] shrink-0 items-center">
|
||||
<uni-icons type="right" size="14" color="#242b36" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.tags?.length" class="mt-[5px] flex flex-wrap gap-[4px]">
|
||||
<text
|
||||
v-for="tag in item.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
class="rounded-[4px] bg-[#e8f2ff] px-[5px] py-[2px] text-[10px] text-[#3474c7]"
|
||||
>
|
||||
{{ tag }}
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="item.description"
|
||||
class="mt-[6px] line-clamp-2 text-[11px] leading-[17px] text-[#808997]"
|
||||
>
|
||||
{{ item.description }}
|
||||
</text>
|
||||
<view v-if="addressText(item)" class="mt-auto flex items-center pt-[5px]">
|
||||
<uni-icons type="location" size="12" color="#9aa2ad" />
|
||||
<text class="ml-[2px] min-w-0 flex-1 truncate text-[10px] text-[#9aa2ad]">
|
||||
{{ addressText(item) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import DuohuaMediaSwiper from "./DuohuaMediaSwiper.vue";
|
||||
import { sourceData } from "../data";
|
||||
|
||||
const props = defineProps({
|
||||
section: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["back", "open-detail"]);
|
||||
const groups = computed(() => props.section.groups || []);
|
||||
const region = sourceData.metadata?.region || "";
|
||||
const activeGroupId = ref(groups.value[0]?.groupId || "");
|
||||
const scrollIntoViewId = ref("");
|
||||
const leftScrollIntoViewId = computed(
|
||||
() => `duohua-nav-${activeGroupId.value}`
|
||||
);
|
||||
const groupOffsets = ref([]);
|
||||
const currentScrollTop = ref(0);
|
||||
const shouldSyncFromScroll = ref(true);
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const heroMediaList = computed(() =>
|
||||
groups.value
|
||||
.map((group) =>
|
||||
group.items.find((item) => item.main_image_url)?.main_image_url
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const isActiveGroup = (groupId) => groupId === activeGroupId.value;
|
||||
const addressText = (item) =>
|
||||
item?.address?.raw || item?.address?.normalized || "";
|
||||
|
||||
const selectGroup = (groupId) => {
|
||||
shouldSyncFromScroll.value = false;
|
||||
activeGroupId.value = groupId;
|
||||
scrollIntoViewId.value = "";
|
||||
nextTick(() => {
|
||||
scrollIntoViewId.value = `duohua-group-${groupId}`;
|
||||
});
|
||||
};
|
||||
|
||||
const enableContentScrollSync = () => {
|
||||
shouldSyncFromScroll.value = true;
|
||||
scrollIntoViewId.value = "";
|
||||
};
|
||||
|
||||
const measureGroupOffsets = () => {
|
||||
if (!groups.value.length) return;
|
||||
|
||||
nextTick(() => {
|
||||
const query = uni.createSelectorQuery().in(instance?.proxy);
|
||||
query.select("#duohua-content-scroll").boundingClientRect();
|
||||
groups.value.forEach((group) => {
|
||||
query
|
||||
.select(`#duohua-group-${group.groupId}`)
|
||||
.boundingClientRect();
|
||||
});
|
||||
query.exec((rects = []) => {
|
||||
const containerRect = rects[0];
|
||||
if (!containerRect) return;
|
||||
groupOffsets.value = groups.value
|
||||
.map((group, index) => ({
|
||||
id: group.groupId,
|
||||
top: Math.max(
|
||||
0,
|
||||
Number(rects[index + 1]?.top || 0) -
|
||||
Number(containerRect.top || 0) +
|
||||
currentScrollTop.value
|
||||
),
|
||||
}))
|
||||
.sort((left, right) => left.top - right.top);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleContentScroll = (event) => {
|
||||
currentScrollTop.value = Number(event.detail?.scrollTop || 0);
|
||||
if (!groupOffsets.value.length) {
|
||||
measureGroupOffsets();
|
||||
return;
|
||||
}
|
||||
if (!shouldSyncFromScroll.value) return;
|
||||
|
||||
const activationLine = currentScrollTop.value + 24;
|
||||
const activeGroup = groupOffsets.value.reduce(
|
||||
(current, group) => (group.top <= activationLine ? group : current),
|
||||
groupOffsets.value[0]
|
||||
);
|
||||
if (activeGroup?.id) activeGroupId.value = activeGroup.id;
|
||||
};
|
||||
|
||||
const previewPanorama = () => {
|
||||
if (!heroMediaList.value.length) return;
|
||||
uni.previewImage({
|
||||
current: heroMediaList.value[0],
|
||||
urls: heroMediaList.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openDetail = (collection, itemId) => {
|
||||
emit("open-detail", { collection, itemId });
|
||||
};
|
||||
|
||||
onMounted(measureGroupOffsets);
|
||||
watch(
|
||||
() => groups.value.map((group) => group.groupId).join(","),
|
||||
() => {
|
||||
activeGroupId.value = groups.value[0]?.groupId || "";
|
||||
groupOffsets.value = [];
|
||||
measureGroupOffsets();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
124
src/pages/DuohuaDiscovery/components/DuohuaHome.vue
Normal file
124
src/pages/DuohuaDiscovery/components/DuohuaHome.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<view class="box-border w-full px-[12px] pb-[20px] pt-[12px]">
|
||||
<view
|
||||
v-if="mainSection"
|
||||
class="relative h-[162px] w-full overflow-hidden rounded-[18px] border border-white bg-[#17463d]"
|
||||
hover-class="opacity-90"
|
||||
hover-stay-time="80"
|
||||
@tap="selectSection(mainSection)"
|
||||
>
|
||||
<image
|
||||
class="absolute inset-0 h-full w-full"
|
||||
:src="mainSection.coverImage"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="absolute inset-0 bg-[linear-gradient(90deg,rgba(3,39,33,0.78)_0%,rgba(3,39,33,0.34)_55%,rgba(3,39,33,0.08)_100%)]" />
|
||||
<view class="absolute inset-0 box-border flex flex-col items-start justify-center px-[20px] py-[16px]">
|
||||
<text class="block text-[27px] font-bold leading-[1.15] tracking-[1px] text-white">
|
||||
{{ mainSection.title }}
|
||||
</text>
|
||||
<text class="mt-[7px] block text-[12px] leading-[1.4] tracking-[0.5px] text-white/90">
|
||||
{{ mainSection.subtitle }}
|
||||
</text>
|
||||
<view class="mt-[12px] flex h-[36px] items-center rounded-full bg-white/95 px-[16px]">
|
||||
<text class="mr-[4px] text-[14px] font-semibold text-[#149474]">开始探索</text>
|
||||
<uni-icons type="arrow-right" size="16" color="#149474" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="lowerSections.length" class="mt-[12px] grid grid-cols-12 gap-[10px]">
|
||||
<view
|
||||
v-if="leftSection"
|
||||
class="relative col-span-6 row-span-2 min-h-0 overflow-hidden rounded-[18px] border border-white bg-[#173a34]"
|
||||
hover-class="opacity-90"
|
||||
hover-stay-time="80"
|
||||
@tap="selectSection(leftSection)"
|
||||
>
|
||||
<swiper
|
||||
class="h-full w-full"
|
||||
circular
|
||||
autoplay
|
||||
:interval="3500"
|
||||
:duration="500"
|
||||
@change="handleLodgingSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="imageUrl in lodgingImages" :key="imageUrl">
|
||||
<image class="h-full w-full" :src="imageUrl" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(5,24,24,0.72)_0%,rgba(5,24,24,0.18)_46%,rgba(5,24,24,0.34)_100%)]" />
|
||||
<view class="pointer-events-none absolute left-[16px] right-[14px] top-[17px]">
|
||||
<text class="block text-[21px] font-bold leading-[1.2] tracking-[0.5px] text-white">
|
||||
{{ leftSection.title }}
|
||||
</text>
|
||||
<text class="mt-[7px] block text-[11px] leading-[1.45] tracking-[0.2px] text-white/85">
|
||||
{{ leftSection.subtitle }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pointer-events-none absolute bottom-[16px] left-[16px] flex h-[46px] w-[46px] items-center justify-center rounded-full border border-[#149474] bg-[rgba(0,0,0,0.3)]">
|
||||
<uni-icons type="arrow-right" size="22" color="#FFFFFF" />
|
||||
</view>
|
||||
<view
|
||||
v-if="lodgingImages.length > 1"
|
||||
class="pointer-events-none absolute bottom-[20px] right-[14px] flex items-center gap-[5px]"
|
||||
>
|
||||
<view
|
||||
v-for="(imageUrl, index) in lodgingImages"
|
||||
:key="`${imageUrl}-dot`"
|
||||
class="h-[5px] rounded-full transition-all duration-300"
|
||||
:class="index === activeLodgingIndex ? 'w-[14px] bg-white' : 'w-[5px] bg-white/45'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="section in rightSections"
|
||||
:key="section.key"
|
||||
class="relative col-span-6 h-[130px] min-h-0 overflow-hidden rounded-[18px] border border-white bg-[#173a34]"
|
||||
hover-class="opacity-90"
|
||||
hover-stay-time="80"
|
||||
@tap="selectSection(section)"
|
||||
>
|
||||
<image class="h-full w-full" :src="section.coverImage" mode="aspectFill" />
|
||||
<view class="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(5,24,24,0.72)_0%,rgba(5,24,24,0.18)_46%,rgba(5,24,24,0.34)_100%)]" />
|
||||
<view class="pointer-events-none absolute left-[12px] right-[10px] top-[11px]">
|
||||
<text class="block text-[17px] font-bold leading-[1.2] tracking-[0.5px] text-white">
|
||||
{{ section.title }}
|
||||
</text>
|
||||
<text class="mt-[3px] block text-[9px] leading-[1.45] tracking-[0.2px] text-white/85">
|
||||
{{ section.subtitle }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pointer-events-none absolute bottom-[10px] left-[12px] flex h-[46px] w-[46px] items-center justify-center rounded-full border border-white/20 bg-[rgba(0,0,0,0.3)]">
|
||||
<uni-icons type="arrow-right" size="22" color="#FFFFFF" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { sections } from "../data";
|
||||
|
||||
const emit = defineEmits(["select"]);
|
||||
const mainSection = computed(() => sections[0] || null);
|
||||
const lowerSections = computed(() => sections.slice(1));
|
||||
const leftSection = computed(() => lowerSections.value[0] || null);
|
||||
const rightSections = computed(() => lowerSections.value.slice(1, 3));
|
||||
const activeLodgingIndex = ref(0);
|
||||
const lodgingImages = computed(() =>
|
||||
(leftSection.value?.groups || [])
|
||||
.map((group) =>
|
||||
group.items.find((item) => item.main_image_url)?.main_image_url
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const handleLodgingSwiperChange = (event) => {
|
||||
activeLodgingIndex.value = Number(event.detail?.current || 0);
|
||||
};
|
||||
|
||||
const selectSection = (section) => emit("select", section);
|
||||
</script>
|
||||
55
src/pages/DuohuaDiscovery/components/DuohuaMediaSwiper.vue
Normal file
55
src/pages/DuohuaDiscovery/components/DuohuaMediaSwiper.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="relative h-full w-full overflow-hidden">
|
||||
<swiper
|
||||
v-if="images.length"
|
||||
class="h-full w-full"
|
||||
circular
|
||||
:indicator-dots="false"
|
||||
@change="handleChange"
|
||||
>
|
||||
<swiper-item v-for="imageUrl in images" :key="imageUrl">
|
||||
<image
|
||||
class="h-full w-full"
|
||||
:src="imageUrl"
|
||||
mode="aspectFill"
|
||||
@tap="previewImage(imageUrl)"
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-else class="flex h-full w-full items-center justify-center bg-[#dfe8e5]">
|
||||
<text class="text-[12px] text-[#73827d]">暂无图片</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="images.length > 1"
|
||||
class="absolute bottom-[12px] right-[14px] rounded-full bg-black/45 px-[9px] py-[4px]"
|
||||
>
|
||||
<text class="text-[11px] text-white">{{ activeIndex + 1 }}/{{ images.length }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
mediaList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const activeIndex = ref(0);
|
||||
const images = computed(() =>
|
||||
props.mediaList
|
||||
.filter((item) => typeof item === "string" && item.trim())
|
||||
.filter((item, index, list) => list.indexOf(item) === index)
|
||||
);
|
||||
|
||||
const handleChange = (event) => {
|
||||
activeIndex.value = Number(event.detail?.current || 0);
|
||||
};
|
||||
|
||||
const previewImage = (current) => {
|
||||
uni.previewImage({ current, urls: images.value });
|
||||
};
|
||||
</script>
|
||||
109
src/pages/DuohuaDiscovery/data/index.js
Normal file
109
src/pages/DuohuaDiscovery/data/index.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import sourceData from "./miniprogram_data.json";
|
||||
|
||||
const SECTION_DEFINITIONS = [
|
||||
{
|
||||
key: "tourism",
|
||||
title: "龙里票根经济",
|
||||
subtitle: "领取龙里旅游优惠",
|
||||
coverImage:
|
||||
"https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/longli/miniprogram/attractions/attraction-001/01.jpg",
|
||||
collections: ["attractions", "attraction_subprojects"],
|
||||
supportAssetIds: ["tourism-overview-map", "tourism-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "lodging",
|
||||
title: "旅居服务",
|
||||
subtitle: "酒店 · 民宿别院 · 旅居公寓 · 露营基地",
|
||||
collections: ["accommodations"],
|
||||
supportAssetIds: ["lodging-location-schematic", "lodging-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "food",
|
||||
title: "全域美食",
|
||||
subtitle: "特色菜肴 · 地方小吃 · 甜品饮品",
|
||||
collections: ["foods"],
|
||||
supportAssetIds: ["food-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "specialties",
|
||||
title: "龙里特产",
|
||||
subtitle: "刺梨制品 · 地方食品 · 农产品",
|
||||
collections: ["specialties"],
|
||||
supportAssetIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeImageList = (item) => {
|
||||
const imageUrls = Array.isArray(item?.images)
|
||||
? item.images.map((image) => image?.oss_url)
|
||||
: [];
|
||||
|
||||
return [item?.main_image_url, ...imageUrls, ...(item?.gallery_urls || [])]
|
||||
.filter((url) => typeof url === "string" && url.trim())
|
||||
.filter((url, index, list) => list.indexOf(url) === index);
|
||||
};
|
||||
|
||||
const getCollectionItems = (collection) =>
|
||||
Array.isArray(sourceData?.[collection]) ? sourceData[collection] : [];
|
||||
|
||||
const getSectionGroups = (sectionKey) => {
|
||||
const section = SECTION_DEFINITIONS.find((item) => item.key === sectionKey);
|
||||
if (!section) return [];
|
||||
|
||||
return section.collections.flatMap((collection) => {
|
||||
const groups = Array.isArray(sourceData?.catalog_by_type?.[collection])
|
||||
? sourceData.catalog_by_type[collection]
|
||||
: [];
|
||||
|
||||
return groups.map((group) => ({
|
||||
...group,
|
||||
collection,
|
||||
groupId: `${collection}-${group.type_code}`,
|
||||
items: Array.isArray(group.items) ? group.items : [],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const getSection = (sectionKey) => {
|
||||
const definition = SECTION_DEFINITIONS.find(
|
||||
(item) => item.key === sectionKey
|
||||
);
|
||||
if (!definition) return null;
|
||||
|
||||
const groups = getSectionGroups(sectionKey);
|
||||
const items = groups.flatMap((group) => group.items);
|
||||
const supportAssets = (sourceData?.assets || []).filter((asset) =>
|
||||
definition.supportAssetIds.includes(asset.entity_id)
|
||||
);
|
||||
|
||||
return {
|
||||
...definition,
|
||||
groups,
|
||||
items,
|
||||
supportAssets,
|
||||
coverImage:
|
||||
definition.coverImage ||
|
||||
items.find((item) => item.main_image_url)?.main_image_url ||
|
||||
"",
|
||||
};
|
||||
};
|
||||
|
||||
const getItem = (collection, itemId) =>
|
||||
getCollectionItems(collection).find((item) => item.id === itemId) || null;
|
||||
|
||||
const getSectionForCollection = (collection) =>
|
||||
SECTION_DEFINITIONS.find((section) =>
|
||||
section.collections.includes(collection)
|
||||
) || null;
|
||||
|
||||
const sections = SECTION_DEFINITIONS.map((section) => getSection(section.key));
|
||||
|
||||
export {
|
||||
getItem,
|
||||
getSection,
|
||||
getSectionForCollection,
|
||||
getSectionGroups,
|
||||
normalizeImageList,
|
||||
sections,
|
||||
sourceData,
|
||||
};
|
||||
19958
src/pages/DuohuaDiscovery/data/miniprogram_data.json
Normal file
19958
src/pages/DuohuaDiscovery/data/miniprogram_data.json
Normal file
File diff suppressed because it is too large
Load Diff
218
src/pages/DuohuaDiscovery/detail.vue
Normal file
218
src/pages/DuohuaDiscovery/detail.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<view class="flex h-screen min-w-0 flex-col overflow-hidden bg-[#f4f6f8]">
|
||||
<TopNavBar
|
||||
:title="item?.name || ''"
|
||||
:subtitle="sectionTitle"
|
||||
title-align="left"
|
||||
background="#ffffff"
|
||||
/>
|
||||
|
||||
<scroll-view v-if="item" class="min-h-0 flex-1" scroll-y>
|
||||
<view class="h-[240px] overflow-hidden bg-[#dfe8e5]">
|
||||
<DuohuaMediaSwiper :media-list="imageList" />
|
||||
</view>
|
||||
|
||||
<view class="bg-white px-[16px] pb-[18px] pt-[16px]">
|
||||
<view class="flex items-center gap-[8px]">
|
||||
<text class="min-w-0 text-[22px] font-bold leading-[30px] text-[#172033]">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text
|
||||
v-if="item.grade"
|
||||
class="shrink-0 rounded-[5px] bg-[#e9f7f1] px-[7px] py-[3px] text-[11px] font-semibold text-[#149474]"
|
||||
>
|
||||
{{ item.grade }}
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="item.name_en"
|
||||
class="mt-[3px] block text-[12px] leading-[18px] text-[#808997]"
|
||||
>
|
||||
{{ item.name_en }}
|
||||
</text>
|
||||
<view v-if="displayAddress" class="mt-[10px] flex items-center">
|
||||
<uni-icons class="shrink-0" type="location" size="15" color="#808997" />
|
||||
<text class="ml-[4px] min-w-0 flex-1 text-[12px] leading-[19px] text-[#6f7884]">
|
||||
{{ displayAddress }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="contactText || canOpenLocation" class="mt-[14px] flex items-center gap-[10px]">
|
||||
<view v-if="contactText" class="flex min-w-0 items-center gap-[8px]">
|
||||
<text class="min-w-0 text-[13px] font-medium text-[#4f5966]">
|
||||
{{ contactText }}
|
||||
</text>
|
||||
<view
|
||||
class="flex h-[36px] shrink-0 items-center rounded-full bg-[#ecf8f2] px-[14px]"
|
||||
@click="makePhoneCall"
|
||||
>
|
||||
<uni-icons type="phone" size="15" color="#149474" />
|
||||
<text class="ml-[5px] text-[12px] font-medium text-[#149474]">拨打电话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="canOpenLocation"
|
||||
class="flex h-[36px] items-center rounded-full bg-[#ecf8f2] px-[14px]"
|
||||
@click="openLocation"
|
||||
>
|
||||
<uni-icons type="navigate" size="15" color="#149474" />
|
||||
<text class="ml-[5px] text-[12px] font-medium text-[#149474]">地图导航</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.tags?.length" class="mt-[14px] flex flex-wrap gap-[6px]">
|
||||
<text
|
||||
v-for="tag in item.tags"
|
||||
:key="tag"
|
||||
class="rounded-[4px] bg-[#e8f2ff] px-[6px] py-[3px] text-[10px] text-[#3474c7]"
|
||||
>
|
||||
{{ tag }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="item.description" class="mx-[12px] mt-[12px] rounded-[16px] bg-white p-[16px]">
|
||||
<text class="block text-[16px] font-semibold text-[#202733]">介绍</text>
|
||||
<text class="mt-[10px] block text-[14px] leading-[23px] text-[#5f6875]">
|
||||
{{ item.description }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="section in arraySections"
|
||||
:key="section.title"
|
||||
class="mx-[12px] mt-[12px] rounded-[16px] bg-white p-[16px]"
|
||||
>
|
||||
<text class="block text-[16px] font-semibold text-[#202733]">
|
||||
{{ section.title }}
|
||||
</text>
|
||||
<view class="mt-[9px] flex flex-wrap gap-[7px]">
|
||||
<text
|
||||
v-for="(value, index) in section.values"
|
||||
:key="`${section.title}-${index}`"
|
||||
class="rounded-[6px] bg-[#f2f6f5] px-[9px] py-[6px] text-[12px] leading-[18px] text-[#52605c]"
|
||||
>
|
||||
{{ formatValue(value) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="h-[24px]" />
|
||||
</scroll-view>
|
||||
|
||||
<view v-else class="flex min-h-0 flex-1 items-center justify-center">
|
||||
<text class="text-[14px] text-[#808997]">未找到对应内容</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="item"
|
||||
class="shrink-0 border-t border-[#edf0f2] bg-white px-[16px] pb-[calc(env(safe-area-inset-bottom)+10px)] pt-[10px] shadow-[0_-4px_16px_rgba(23,32,51,0.06)]"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="flex min-w-0 flex-1 items-center">
|
||||
<view class="flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-full bg-[#fff1e8]">
|
||||
<uni-icons type="gift" size="20" color="#f26b32" />
|
||||
</view>
|
||||
<text class="ml-[10px] text-[16px] font-semibold text-[#202733]">领取票根优惠</text>
|
||||
</view>
|
||||
<view
|
||||
class="flex h-[36px] shrink-0 items-center justify-center rounded-full bg-[#f26b32] px-[18px]"
|
||||
@click="claimCoupon"
|
||||
>
|
||||
<text class="text-[12px] font-medium text-white">立即领取</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import DuohuaMediaSwiper from "./components/DuohuaMediaSwiper.vue";
|
||||
import {
|
||||
getItem,
|
||||
getSectionForCollection,
|
||||
normalizeImageList,
|
||||
} from "./data";
|
||||
|
||||
const item = ref(null);
|
||||
const collection = ref("");
|
||||
const sectionTitle = computed(
|
||||
() => getSectionForCollection(collection.value)?.title || ""
|
||||
);
|
||||
const imageList = computed(() => normalizeImageList(item.value));
|
||||
const contactText = computed(
|
||||
() => item.value?.contact?.normalized || item.value?.contact?.raw || ""
|
||||
);
|
||||
const displayAddress = computed(() => {
|
||||
const address = item.value?.address;
|
||||
if (!address) return "";
|
||||
return address.normalized || "";
|
||||
});
|
||||
const canOpenLocation = computed(() => {
|
||||
const latitudeValue = item.value?.address?.latitude;
|
||||
const longitudeValue = item.value?.address?.longitude;
|
||||
if (
|
||||
latitudeValue === null ||
|
||||
latitudeValue === undefined ||
|
||||
latitudeValue === "" ||
|
||||
longitudeValue === null ||
|
||||
longitudeValue === undefined ||
|
||||
longitudeValue === ""
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const latitude = Number(latitudeValue);
|
||||
const longitude = Number(longitudeValue);
|
||||
return Number.isFinite(latitude) && Number.isFinite(longitude);
|
||||
});
|
||||
|
||||
const arraySections = computed(() => {
|
||||
if (!item.value) return [];
|
||||
return [
|
||||
{ title: "房型", values: item.value.room_types },
|
||||
{ title: "设施服务", values: item.value.facilities },
|
||||
{ title: "主要食材", values: item.value.ingredients },
|
||||
{ title: "烹饪方式", values: item.value.cooking_methods },
|
||||
{ title: "营养信息", values: item.value.nutrition_facts },
|
||||
].filter((section) => Array.isArray(section.values) && section.values.length);
|
||||
});
|
||||
|
||||
const formatValue = (value) => {
|
||||
if (value && typeof value === "object") {
|
||||
return Object.entries(value)
|
||||
.filter(([, itemValue]) => itemValue !== "" && itemValue !== null)
|
||||
.map(([key, itemValue]) => `${key}:${itemValue}`)
|
||||
.join(";");
|
||||
}
|
||||
return String(value ?? "");
|
||||
};
|
||||
|
||||
const makePhoneCall = () => {
|
||||
if (!contactText.value) return;
|
||||
uni.makePhoneCall({ phoneNumber: contactText.value });
|
||||
};
|
||||
|
||||
const claimCoupon = () => {
|
||||
uni.showToast({
|
||||
title: "领取成功",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
const openLocation = () => {
|
||||
if (!canOpenLocation.value) return;
|
||||
uni.openLocation({
|
||||
latitude: Number(item.value.address.latitude),
|
||||
longitude: Number(item.value.address.longitude),
|
||||
name: item.value.name,
|
||||
address: item.value.address.raw || item.value.address.normalized || "",
|
||||
});
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
collection.value = options?.collection || "";
|
||||
item.value = getItem(collection.value, options?.id || "");
|
||||
});
|
||||
</script>
|
||||
37
src/pages/DuohuaDiscovery/index.vue
Normal file
37
src/pages/DuohuaDiscovery/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="flex h-full min-h-0 w-full flex-col bg-[#F0F8F3]">
|
||||
<scroll-view
|
||||
class="h-0 min-h-0 w-full flex-1 overflow-hidden"
|
||||
scroll-y
|
||||
@scroll="handleContentScroll"
|
||||
>
|
||||
<DuohuaHome @select="openSection" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DuohuaHome from "./components/DuohuaHome.vue";
|
||||
|
||||
defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["content-scroll"]);
|
||||
|
||||
const handleContentScroll = (event) => {
|
||||
emit("content-scroll", {
|
||||
scrollTop: Number(event.detail?.scrollTop || 0),
|
||||
});
|
||||
};
|
||||
|
||||
const openSection = (section) => {
|
||||
if (!section?.key) return;
|
||||
uni.navigateTo({
|
||||
url: `/pages/DuohuaDiscovery/catalog?section=${section.key}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
20
src/uni.scss
20
src/uni.scss
@@ -13,16 +13,16 @@
|
||||
*/
|
||||
|
||||
/* 主题颜色(由 switch-client 命令自动更新) */
|
||||
$theme-color-900: #19428F;
|
||||
$theme-color-800: #174BB6;
|
||||
$theme-color-700: #145EE1;
|
||||
$theme-color-600: #1B74F5;
|
||||
$theme-color-500: #2D91FF;
|
||||
$theme-color-400: #59B4FF;
|
||||
$theme-color-300: #8ED0FF;
|
||||
$theme-color-200: #BCE1FF;
|
||||
$theme-color-100: #D9EEFF;
|
||||
$theme-color-50: #EEF8FF;
|
||||
$theme-color-900: #0B5C2D;
|
||||
$theme-color-800: #0B7034;
|
||||
$theme-color-700: #0B5C2D;
|
||||
$theme-color-600: #02C34E;
|
||||
$theme-color-500: #0CCD58;
|
||||
$theme-color-400: #35F37F;
|
||||
$theme-color-300: #77FEAB;
|
||||
$theme-color-200: #A0E5BA;
|
||||
$theme-color-100: #E8FFF1;
|
||||
$theme-color-50: #F0F8F3;
|
||||
|
||||
// text 颜色
|
||||
$text-color-900: #181b25;
|
||||
|
||||
Reference in New Issue
Block a user