Compare commits
7
Commits
b36a47b314
...
7d7c235a71
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7c235a71 | ||
|
|
0020c8bb47 | ||
|
|
cc1615162a | ||
|
|
b5206c18d1 | ||
|
|
22b1686c26 | ||
|
|
f4f4c05766 | ||
|
|
f7b08a0600 |
No files matched your search
@@ -44,7 +44,7 @@ test('provides coherent default visitor ticket data', async () => {
|
||||
assert.deepEqual(Object.keys(PRODUCT_IMAGES), visibleProducts.map(({ id }) => id))
|
||||
assert.ok(SCENIC_HERO_IMAGE.endsWith('/hero-bridge.jpg'))
|
||||
assert.notEqual(SCENIC_HERO_IMAGE, PLACEHOLDER_IMAGE)
|
||||
assert.ok(visibleProducts.every((product) => product.images[0] === PRODUCT_IMAGES[product.id] && product.images[0].startsWith('/static/images/tickets/xiaoqikong/')))
|
||||
assert.ok(visibleProducts.every((product) => product.images[0] === PRODUCT_IMAGES[product.id] && product.images[0].startsWith('https://one-feel-config-images-bucket.oss-cn-chengdu.aliyuncs.com/xiaoqi/tickets/')))
|
||||
assert.equal(new Set([SCENIC_HERO_IMAGE, ...visibleProducts.map(({ images }) => images[0])]).size, 10)
|
||||
assert.equal(productsById.get('guide').images[0], PLACEHOLDER_IMAGE)
|
||||
assert.deepEqual(PRODUCTS.map(({ id, salePrice, marketPrice, sold }) => [id, salePrice, marketPrice, sold]), [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="discovery-audio-tool-card" hover-class="is-pressed" hover-stay-time="80" @tap="handleSelect">
|
||||
<view class="audio-visual">
|
||||
<image v-if="normalizedItem.coverImage" class="audio-cover" :src="normalizedItem.coverImage" mode="aspectFill" />
|
||||
<image v-if="normalizedItem.coverUrl" class="audio-cover" :src="normalizedItem.coverUrl" mode="aspectFill" />
|
||||
<view v-else class="audio-cover audio-cover-fallback" />
|
||||
<view class="audio-play">
|
||||
<view class="audio-play-triangle" />
|
||||
@@ -13,12 +13,8 @@
|
||||
<text class="audio-subtitle block w-full truncate">{{ normalizedItem.subTitle }}</text>
|
||||
|
||||
<view class="audio-progress-row w-full min-w-0">
|
||||
<view class="audio-progress-track">
|
||||
<view class="audio-progress-fill" :style="{ width: `${normalizedItem.progress}%` }" />
|
||||
</view>
|
||||
<text class="audio-time block w-[90px] truncate">
|
||||
{{ normalizedItem.currentTime }} / {{ normalizedItem.duration }}
|
||||
</text>
|
||||
<view class="audio-progress-track" />
|
||||
<text class="audio-time block w-[90px] truncate">{{ normalizedItem.duration }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -27,15 +23,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const DEFAULT_ITEM = {
|
||||
id: "audio-guide",
|
||||
title: "卧龙潭语音讲解",
|
||||
subTitle: "解说:响水洞瀑布",
|
||||
progress: 31,
|
||||
currentTime: "02:15",
|
||||
duration: "05:40",
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
@@ -45,24 +32,36 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["didSelectItem"]);
|
||||
|
||||
const clampProgress = (value) => {
|
||||
const progress = Number(value);
|
||||
if (Number.isNaN(progress)) return 0;
|
||||
return Math.min(Math.max(progress, 0), 100);
|
||||
const formatDuration = (durationMs) => {
|
||||
if (durationMs === null || durationMs === undefined || durationMs === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const value = Number(durationMs);
|
||||
if (!Number.isFinite(value) || value < 0) return "";
|
||||
|
||||
const totalSeconds = Math.floor(value / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const normalizedItem = computed(() => {
|
||||
const source = Object.keys(props.item).length > 0 ? props.item : DEFAULT_ITEM;
|
||||
const source = props.item && typeof props.item === "object" ? props.item : {};
|
||||
const resource = source.resource || {};
|
||||
const summary = resource.summary || {};
|
||||
const payload = resource.payload || {};
|
||||
|
||||
return {
|
||||
raw: source,
|
||||
id: source.id ?? source.tabContentId ?? DEFAULT_ITEM.id,
|
||||
title: source.title || DEFAULT_ITEM.title,
|
||||
subTitle: source.subTitle || source.desc || DEFAULT_ITEM.subTitle,
|
||||
coverImage: source.coverImage || source.image || DEFAULT_ITEM.coverImage || "",
|
||||
progress: clampProgress(source.progress ?? DEFAULT_ITEM.progress),
|
||||
currentTime: source.currentTime || DEFAULT_ITEM.currentTime,
|
||||
duration: source.duration || DEFAULT_ITEM.duration,
|
||||
code: resource.code || "",
|
||||
title: summary.title || "",
|
||||
subTitle: summary.subtitle || "",
|
||||
coverUrl: summary.coverUrl || "",
|
||||
url: payload.url || "",
|
||||
mimeType: payload.mimeType || "",
|
||||
durationMs: payload.durationMs,
|
||||
duration: formatDuration(payload.durationMs),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<view class="flex w-full gap-[12px]">
|
||||
<view
|
||||
v-for="goods in goodsList"
|
||||
:key="goods.commodityId"
|
||||
:key="goods.resource?.payload?.commodityId"
|
||||
class="flex h-[88px]"
|
||||
:class="{
|
||||
'w-full': !hasMultipleGoods,
|
||||
@@ -19,23 +19,23 @@
|
||||
<view
|
||||
class="h-[88px] w-[88px] shrink-0 overflow-hidden rounded-[16px] bg-[#eef8f2] shadow-[0_4px_18px_rgba(31,110,74,0.08)]"
|
||||
>
|
||||
<image class="h-full w-full" :src="goods.image" mode="aspectFill" />
|
||||
<image class="h-full w-full" :src="goods.resource?.payload?.commodityPhoto" mode="aspectFill" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="box-border flex h-full min-w-0 flex-1 flex-col rounded-[16px] bg-white px-[12px] py-[8px] shadow-[0_4px_18px_rgba(31,110,74,0.08)]"
|
||||
>
|
||||
<text class="block truncate text-[14px] font-semibold leading-[22px] text-[#1f2937]">
|
||||
{{ goods.title }}
|
||||
{{ goods.resource?.payload?.commodityName }}
|
||||
</text>
|
||||
<text class="mt-[2px] block truncate text-[11px] leading-[18px] text-[#9ca3af]">
|
||||
{{ goods.description }}
|
||||
{{ goods.resource?.payload?.commodityDescription }}
|
||||
</text>
|
||||
|
||||
<view class="mt-auto flex items-end justify-between gap-[8px]">
|
||||
<view class="min-w-0 flex-1 truncate font-bold text-[#111827]">
|
||||
<text class="text-[14px]">¥</text>
|
||||
<text class="text-[20px] leading-[28px]">{{ goods.price }}</text>
|
||||
<text class="text-[20px] leading-[28px]">{{ goods.resource?.payload?.specificationPrice }}</text>
|
||||
</view>
|
||||
<button
|
||||
class="m-0 flex h-[26px] shrink-0 items-center justify-center rounded-full bg-[#20cf6d] px-[12px] text-[14px] leading-none text-white after:border-0"
|
||||
@@ -52,30 +52,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const goodsList = ref([
|
||||
{
|
||||
commodityId: "xiaoqikong-rafting",
|
||||
image: "https://one-feel-config-images-bucket.oss-cn-chengdu.aliyuncs.com/xiaoqi/tickets/product-boat-rafting.jpg",
|
||||
title: "卧龙潭漂游",
|
||||
description: "含防水袋 · 全程约 30 分钟",
|
||||
price: "69.0",
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
{
|
||||
commodityId: "xiaoqikong-classic",
|
||||
image: "https://one-feel-config-images-bucket.oss-cn-chengdu.aliyuncs.com/xiaoqi/tickets/product-classic-forest-path.jpg",
|
||||
title: "小七孔经典畅游",
|
||||
description: "含门票及观光车 · 随买随用",
|
||||
price: "188.0",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const goodsList = computed(() => props.list);
|
||||
|
||||
const hasMultipleGoods = computed(() => goodsList.value.length > 1);
|
||||
|
||||
const handleBuy = (goods) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/goods/index?commodityId=${encodeURIComponent(goods.commodityId)}`,
|
||||
url: `/pages/goods/index?commodityId=${encodeURIComponent(goods.resource?.payload?.commodityId)}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<view class="discovery-guide-map-tool-card" hover-class="is-pressed" hover-stay-time="80" @tap="handleSelect">
|
||||
<view class="guide-map-visual">
|
||||
<view class="guidepost-illustration">
|
||||
<image v-if="normalizedItem.coverUrl" class="h-full w-full" :src="normalizedItem.coverUrl" mode="aspectFill" />
|
||||
<view v-else class="guidepost-illustration">
|
||||
<view class="guidepost-sign is-pink">
|
||||
<view class="guidepost-line" />
|
||||
</view>
|
||||
@@ -25,12 +26,10 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const DEFAULT_ITEM = {
|
||||
id: "guide-map",
|
||||
title: "卧龙潭电子导览图",
|
||||
subTitle: "查看景区平面图与设施位置",
|
||||
};
|
||||
import {
|
||||
getGuideMapNavigationOptions,
|
||||
normalizeGuideMapItem,
|
||||
} from "./model.mjs";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -39,21 +38,27 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["didSelectItem"]);
|
||||
|
||||
const normalizedItem = computed(() => {
|
||||
const source = Object.keys(props.item).length > 0 ? props.item : DEFAULT_ITEM;
|
||||
|
||||
return {
|
||||
raw: source,
|
||||
id: source.id ?? source.tabContentId ?? DEFAULT_ITEM.id,
|
||||
title: source.title || DEFAULT_ITEM.title,
|
||||
subTitle: source.subTitle || source.desc || DEFAULT_ITEM.subTitle,
|
||||
};
|
||||
});
|
||||
const normalizedItem = computed(() => normalizeGuideMapItem(props.item));
|
||||
|
||||
const handleSelect = () => {
|
||||
emit("didSelectItem", normalizedItem.value.raw);
|
||||
const locationOptions = getGuideMapNavigationOptions(normalizedItem.value.raw);
|
||||
if (!locationOptions) {
|
||||
uni.showToast({
|
||||
title: "暂无导航坐标",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.openLocation({
|
||||
...locationOptions,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: "打开导航失败",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const normalizeGuideMapItem = (item) => {
|
||||
const source = item && typeof item === "object" ? item : {};
|
||||
const resource = source.resource || {};
|
||||
const summary = resource.summary || {};
|
||||
const payload = resource.payload || {};
|
||||
const location = payload.location || {};
|
||||
|
||||
return {
|
||||
raw: source,
|
||||
id: resource.code || payload.poiCode || "",
|
||||
resourceCode: resource.code || "",
|
||||
resourceType: resource.type || "",
|
||||
title: summary.title || payload.name || "",
|
||||
subTitle: summary.subtitle || payload.description || "",
|
||||
coverUrl: summary.coverUrl || "",
|
||||
poiCode: payload.poiCode || "",
|
||||
name: payload.name || summary.title || "",
|
||||
categoryCode: payload.categoryCode || "",
|
||||
address: payload.address || "",
|
||||
description: payload.description || "",
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
};
|
||||
};
|
||||
|
||||
const getGuideMapNavigationOptions = (item) => {
|
||||
const normalizedItem = normalizeGuideMapItem(item);
|
||||
const hasEmptyCoordinate = [normalizedItem.latitude, normalizedItem.longitude]
|
||||
.some((value) => value === null || value === undefined || (typeof value === "string" && value.trim() === ""));
|
||||
|
||||
if (hasEmptyCoordinate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const latitude = Number(normalizedItem.latitude);
|
||||
const longitude = Number(normalizedItem.longitude);
|
||||
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
latitude,
|
||||
longitude,
|
||||
name: normalizedItem.name || normalizedItem.title,
|
||||
address: normalizedItem.address || normalizedItem.description,
|
||||
};
|
||||
};
|
||||
|
||||
export { getGuideMapNavigationOptions, normalizeGuideMapItem };
|
||||
@@ -5,10 +5,14 @@
|
||||
class="z-[12] flex w-full shrink-0 flex-row items-center bg-[#F0F8F3]"
|
||||
>
|
||||
<view
|
||||
class="ml-[12px] mr-[8px] flex h-[32px] w-[32px] shrink-0 items-center justify-center rounded-full border border-white/20 bg-[rgba(0,0,0,0.3)]"
|
||||
class="ml-[12px] mr-[8px] p-[4px] flex h-[36px] w-[36px] shrink-0 items-center justify-center rounded-full border border-white/70"
|
||||
@click="handleMainPanoramaClick"
|
||||
>
|
||||
<uni-icons type="arrow-left" size="18" color="#FFFFFF" />
|
||||
<image
|
||||
class="h-full w-full"
|
||||
src="https://one-feel-config-images-bucket.oss-cn-chengdu.aliyuncs.com/xiaoqi/back_home.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="discoveryTabs.length > 0" class="min-w-0 flex-1 overflow-hidden">
|
||||
<FindTabs v-model="activeIndex" :tabs="discoveryTabs" @change="handleTabChange" />
|
||||
@@ -23,13 +27,16 @@
|
||||
<CardSwiper v-if="discoveryCards.length > 0" :list="discoveryCards" @didSelectItem="handleCardClick" />
|
||||
<ExploreCards v-if="exploreCards.length > 0" :list="exploreCards" @didSelectItem="handleCardClick" />
|
||||
|
||||
<DiscoveryPhotoToolCard v-if="discoveryPhotoToolData" :item="discoveryPhotoToolData"
|
||||
@didSelectItem="handleToolCardClick" @didSelectOption="handleToolOptionClick" />
|
||||
<DiscoveryAudioToolCard v-if="discoveryAudioToolData" :item="discoveryAudioToolData"
|
||||
@didSelectItem="handleToolCardClick" />
|
||||
<DiscoveryGuideMapToolCard v-if="discoveryGuideMapToolData" :item="discoveryGuideMapToolData" />
|
||||
<DiscoveryGoodsCard v-if="discoveryGoodsData.length > 0" :list="discoveryGoodsData" />
|
||||
|
||||
<QuickQuestions v-if="discoveryQuickQuestions.length > 0" :list="discoveryQuickQuestions"
|
||||
@didSelectItem="handleQuickQuestionClick" />
|
||||
|
||||
<DiscoveryPhotoToolCard @didSelectItem="handleToolCardClick" @didSelectOption="handleToolOptionClick" />
|
||||
<DiscoveryAudioToolCard @didSelectItem="handleToolCardClick" />
|
||||
<DiscoveryGuideMapToolCard @didSelectItem="handleToolCardClick" />
|
||||
<DiscoveryGoodsCard />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -50,8 +57,7 @@ import DiscoveryAudioToolCard from "./components/DiscoveryAudioToolCard/index.vu
|
||||
import DiscoveryGuideMapToolCard from "./components/DiscoveryGuideMapToolCard/index.vue";
|
||||
import DiscoveryGoodsCard from "./components/DiscoveryGoodsCard/index.vue";
|
||||
import PanoramicHome from "./components/PanoramicHome/index.vue";
|
||||
|
||||
import { homeTabsData, getNearbyTags, homeTabContentData, homeQuickQuestionData } from "../../request/api/MainPageDataApi";
|
||||
import { homeTabsData, getNearbyTags, homeTabContentData, homeQuickQuestionData, resourceCenterData } from "../../request/api/MainPageDataApi";
|
||||
import { useAppStore, useLocationStore } from "@/store";
|
||||
import { JumpType } from "../../model/ChatModel";
|
||||
|
||||
@@ -65,6 +71,10 @@ const discoveryTabs = ref([]);
|
||||
const discoveryCards = ref([]);
|
||||
const exploreCards = ref([]);
|
||||
const discoveryQuickQuestions = ref([]);
|
||||
const discoveryPhotoToolData = ref(null);
|
||||
const discoveryAudioToolData = ref(null);
|
||||
const discoveryGuideMapToolData = ref(null);
|
||||
const discoveryGoodsData = ref([]);
|
||||
const emit = defineEmits(["content-scroll"]);
|
||||
|
||||
/// 是否是主景区引导页 默认是 true,点击主景区后为 false
|
||||
@@ -84,13 +94,17 @@ const handleMainPanoramaClick = () => {
|
||||
discoveryCards.value = [];
|
||||
exploreCards.value = [];
|
||||
discoveryQuickQuestions.value = [];
|
||||
discoveryPhotoToolData.value = null;
|
||||
discoveryAudioToolData.value = null;
|
||||
discoveryGuideMapToolData.value = null;
|
||||
discoveryGoodsData.value = [];
|
||||
};
|
||||
|
||||
|
||||
/// tabs 切换事件
|
||||
const handleTabChange = ({ tab, idx }) => {
|
||||
activeIndex.value = idx;
|
||||
queryDiscoveryData(tab.id);
|
||||
queryDiscoveryData(tab);
|
||||
}
|
||||
|
||||
/// 请求发现页tab数据
|
||||
@@ -123,24 +137,31 @@ const handlePanoramicHomeSelect = (item) => {
|
||||
|
||||
/// 根据id查询tab并设置activeIndex
|
||||
const findTabByIdWithActiveTabIndex = (tabsId) => {
|
||||
/// 查询是否有id参数
|
||||
const activeTabIndex = discoveryTabs.value.findIndex((tab) => tab.id === tabsId);
|
||||
/// 如果有则优先展示对应tab数据,没有则展示第一个tab数据
|
||||
if (activeTabIndex > -1) {
|
||||
activeIndex.value = activeTabIndex;
|
||||
queryDiscoveryData(tabsId);
|
||||
} else {
|
||||
if (discoveryTabs.value.length > 0) {
|
||||
activeIndex.value = 0;
|
||||
queryDiscoveryData(discoveryTabs.value[0].id);
|
||||
}
|
||||
}
|
||||
const resolvedTab = resolveDiscoveryTab(discoveryTabs.value, tabsId);
|
||||
if (!resolvedTab.tab) return;
|
||||
|
||||
activeIndex.value = resolvedTab.activeIndex;
|
||||
queryDiscoveryData(resolvedTab.tab);
|
||||
}
|
||||
|
||||
/// 找到对应的tab后,查询发现页数据
|
||||
const resolveDiscoveryTab = (tabs, tabId) => {
|
||||
if (!Array.isArray(tabs) || tabs.length === 0) {
|
||||
return { activeIndex: -1, tab: null };
|
||||
}
|
||||
|
||||
const matchedIndex = tabs.findIndex((tab) => tab.id === tabId);
|
||||
const activeIndex = matchedIndex > -1 ? matchedIndex : 0;
|
||||
return { activeIndex, tab: tabs[activeIndex] };
|
||||
};
|
||||
|
||||
/// 统一请求发现页数据
|
||||
const queryDiscoveryData = async (tabId) => {
|
||||
queryDiscoveryCards(tabId);
|
||||
queryQuickQuestionData(tabId);
|
||||
const queryDiscoveryData = async (tab) => {
|
||||
if (!tab?.id) return;
|
||||
|
||||
queryDiscoveryCards(tab.id);
|
||||
queryQuickQuestionData(tab.id);
|
||||
loadResourceCenterDataList(tab.resourceSetCode);
|
||||
};
|
||||
|
||||
/// 请求发现页卡片数据
|
||||
@@ -173,6 +194,20 @@ const queryQuickQuestionData = async (tabId) => {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按资源集编码读取当前生效快照
|
||||
const loadResourceCenterDataList = async (setCode) => {
|
||||
if (!setCode) return;
|
||||
const res = await resourceCenterData({ setCode });
|
||||
if (res.code === 0) {
|
||||
const discoveryResourceItems = res.data.items || [];
|
||||
// AUDIO POI COMMODITY COMPONENT
|
||||
discoveryPhotoToolData.value = discoveryResourceItems.find(item => item.resource?.type === 'COMPONENT') || null;
|
||||
discoveryAudioToolData.value = discoveryResourceItems.find(item => item.resource?.type === 'AUDIO') || null;
|
||||
discoveryGuideMapToolData.value = discoveryResourceItems.find(item => item.resource?.type === 'POI') || null;
|
||||
discoveryGoodsData.value = discoveryResourceItems.filter(item => item.resource?.type === 'COMMODITY') || [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一处理接口返回数据结构
|
||||
const configDataList = (data) => {
|
||||
return data.map((item) => ({
|
||||
|
||||
@@ -15,6 +15,11 @@ function homeTabContentData(args) {
|
||||
return request.post("/hotelBiz/mainScene/queryContentListWithCache", args);
|
||||
}
|
||||
|
||||
/// 按资源集编码读取当前生效快照
|
||||
function resourceCenterData(args) {
|
||||
return request.post("/hotelBiz/resourceCenter/client/sets", args);
|
||||
}
|
||||
|
||||
/// 获取距离用户最近的标签
|
||||
function getNearbyTags(args) {
|
||||
return request.get("/hotelBiz/mainScene/nearestTag", args);
|
||||
@@ -55,6 +60,7 @@ export {
|
||||
homeAllScenicData,
|
||||
homeTabsData,
|
||||
homeTabContentData,
|
||||
resourceCenterData,
|
||||
getNearbyTags,
|
||||
getLocalWeather,
|
||||
getTimeNoticeList,
|
||||
|
||||
Reference in new issue
Block a user