Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebad453023 | ||
|
|
d567d1a2c4 | ||
|
|
0461ba2ea9 | ||
|
|
ec20e11c32 | ||
|
|
21659f61e3 | ||
|
|
6d477789b0 | ||
|
|
67ee9e2535 | ||
|
|
57537a601c | ||
|
|
563795422b | ||
|
|
b5983dec7c | ||
|
|
581734cedc | ||
|
|
7cfdd27bbc | ||
|
|
329547891e | ||
|
|
040c56aad1 | ||
|
|
0ae1b15007 | ||
|
|
fe1b3f5e02 | ||
|
|
7d7c235a71 | ||
|
|
0020c8bb47 | ||
|
|
cc1615162a | ||
|
|
b5206c18d1 | ||
|
|
22b1686c26 | ||
|
|
f4f4c05766 | ||
|
|
f7b08a0600 |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"zhinian": {
|
||||
"clientId": "6",
|
||||
"clientId": "8",
|
||||
"appId": "wx5e79df5996572539",
|
||||
"name": "念念",
|
||||
"theme": {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 呼叫热线弹窗布局调整设计
|
||||
|
||||
## 目标
|
||||
|
||||
调整呼叫热线弹窗的信息层级,使标题和关闭按钮稳定显示在弹窗顶部,并提高两个电话号码的可读性与可点击性。
|
||||
|
||||
## 设计
|
||||
|
||||
- 将标题和关闭按钮保留在独立头部区域,头部不参与正文内容排版,也不随正文压缩。
|
||||
- 正文继续展示原有提示文案,不改变文案含义。
|
||||
- 两个电话号码从行内文本中拆出,各自使用独立块级容器并各占一行。
|
||||
- 电话号码字号调整为 `20px`,保留现有蓝色样式和点击拨号行为。
|
||||
- 使用 Tailwind CSS 工具类完成布局调整,不新增 SCSS 布局样式。
|
||||
|
||||
## 数据与交互
|
||||
|
||||
电话号码常量、弹窗打开/关闭事件及 `uni.makePhoneCall` 调用保持不变。点击任一电话号码时,仍先关闭弹窗,再呼起对应号码的系统拨号界面。
|
||||
|
||||
## 验证
|
||||
|
||||
- 结构检查确认头部与正文是相邻且独立的区域。
|
||||
- 结构检查确认两个电话号码分别位于独立块级元素中。
|
||||
- 结构检查确认两个号码均使用更大的字号并保留独立点击事件。
|
||||
- 运行 `git diff --check` 检查格式问题。
|
||||
|
||||
## 范围
|
||||
|
||||
仅修改 `src/pages/ChatModule/CallMobile/index.vue` 的弹窗模板布局,不调整业务逻辑、文案内容或其他页面。
|
||||
@@ -0,0 +1,29 @@
|
||||
# ExploreCards 统一媒体渐变设计
|
||||
|
||||
## 目标
|
||||
|
||||
统一 ExploreCards 中图片和视频上方的渐变浮层。渐变不再随卡片 `tone` 使用不同主题色,而是固定为顶部透明、底部浅黑半透明,以保持不同媒体内容上的文字可读性和视觉一致性。
|
||||
|
||||
## 实现范围
|
||||
|
||||
- 在 `src/pages/Discovery/components/ExploreCards/styles/index.scss` 中保留一条公共 `.explore-card-gradient` 规则。
|
||||
- 公共渐变使用以下色阶:
|
||||
|
||||
```scss
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(5, 24, 24, 0) 0%,
|
||||
rgba(5, 24, 24, 0.18) 46%,
|
||||
rgba(5, 24, 24, 0.34) 100%
|
||||
)
|
||||
```
|
||||
|
||||
- 删除 `culture`、`adventure` 和 `nightlife` 对 `.explore-card-gradient` 的主题色覆盖。
|
||||
- 保留各 `tone` 的卡片底色,确保没有封面媒体时现有视觉不变。
|
||||
- 不调整图片或视频渲染、文字内容区、箭头和点击行为。
|
||||
|
||||
## 验证
|
||||
|
||||
- 静态检查样式中仅存在一条 `.explore-card-gradient` 背景定义。
|
||||
- 检查所有渐变色标均为 `rgba(5, 24, 24, ...)`,且顶部透明度为 `0`、底部透明度为 `0.34`。
|
||||
- 运行 `git diff --check`,确认没有空白符错误。
|
||||
@@ -95,7 +95,7 @@
|
||||
</view>
|
||||
<view class="mt-auto flex items-end justify-between pt-[6px]">
|
||||
<view>
|
||||
<text class="block text-[10px] text-[#9aa2ad]">已售 {{ product.sold || 0 }}</text>
|
||||
<text v-if="product.sold !== null && product.sold !== undefined" class="block text-[10px] text-[#9aa2ad]">已售 {{ product.sold }}</text>
|
||||
<text class="text-[12px] font-medium text-[#f26b32]">¥<text class="text-[20px] font-bold">{{ product.salePrice }}</text></text>
|
||||
</view>
|
||||
<view class="flex h-[28px] items-center justify-center rounded-full bg-[#f26b32] px-[13px]" @click="emit('buy', product.id)">
|
||||
@@ -106,7 +106,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="mt-[10px] rounded-[14px] bg-white px-[16px] py-[28px] text-center">
|
||||
<text class="text-[13px] text-[#9aa2ad]">暂无可售商品</text>
|
||||
<text class="text-[13px] text-[#9aa2ad]">{{ loading ? '加载中...' : '暂无可售商品' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -124,6 +124,7 @@ const props = defineProps({
|
||||
scenicSpot: { type: Object, required: true },
|
||||
groups: { type: Array, default: () => [] },
|
||||
products: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['back', 'open-travelers', 'open-orders', 'open-detail', 'buy'])
|
||||
@@ -134,6 +135,9 @@ const groupOffsets = ref([])
|
||||
const rightScrollTop = ref(0)
|
||||
const instance = getCurrentInstance()
|
||||
const productsById = computed(() => new Map(props.products.map((product) => [product.id, product])))
|
||||
const groupLayoutSignature = computed(() => props.groups
|
||||
.map((group) => `${group.id}:${(group.productIds || []).join(',')}`)
|
||||
.join('|'))
|
||||
|
||||
const productsForGroup = (group) => (group.productIds || [])
|
||||
.map((productId) => productsById.value.get(productId))
|
||||
@@ -196,7 +200,10 @@ const handleProductScroll = (event) => {
|
||||
}
|
||||
|
||||
onMounted(measureGroupOffsets)
|
||||
watch(() => props.groups.map((group) => group.id).join(','), measureGroupOffsets)
|
||||
watch(groupLayoutSignature, () => {
|
||||
groupOffsets.value = []
|
||||
measureGroupOffsets()
|
||||
}, { flush: 'post' })
|
||||
|
||||
const makePhoneCall = () => {
|
||||
if (!props.scenicSpot.phone) return
|
||||
|
||||
@@ -49,12 +49,9 @@ export const SCENIC_SPOT = {
|
||||
}
|
||||
|
||||
export const GROUPS = [
|
||||
{ id: 'ticket', name: '景点门票', subtitle: '累计销量 5000+', description: '一票通玩,核销后门票7日内有效', tag: '官方', productIds: ['adult', 'elderly', 'minor', 'free'], sortOrder: 1, enabled: true },
|
||||
{ id: 'vip', name: 'VIP速通', subtitle: '专属服务', description: '减少排队等待时间,按预约场次使用', tag: '推荐', productIds: ['vip-fast'], sortOrder: 2, enabled: true },
|
||||
{ id: 'bus', name: '直通车', subtitle: '往返直达', description: '贵阳市区集合,直达小七孔景区', tag: '省心', productIds: ['direct-bus'], sortOrder: 3, enabled: true },
|
||||
{ id: 'package', name: '门票+观光车', subtitle: '累计销量 1.5万+', description: '门票与观光车由第三方出票,园内项目使用自营权益码', tag: '热卖', productIds: ['classic'], sortOrder: 4, enabled: true },
|
||||
{ id: 'tour', name: '一日游', subtitle: '精选线路', description: '轻松游览小七孔代表性景观', tag: '品质', productIds: ['day-tour'], sortOrder: 5, enabled: true },
|
||||
{ id: 'play', name: '玩乐体验', subtitle: '园内自营项目', description: '工作人员扫描园内权益码后选择项目核销', tag: '自营', productIds: ['boat', 'guide'], sortOrder: 6, enabled: true },
|
||||
{ id: 'ticket', name: '门票', commodityTypeCode: '1', productIds: [], sortOrder: 1, enabled: true },
|
||||
{ id: 'maolan', name: '茂兰景区', commodityTypeCode: '67d07734e0af4caa918309f5a346a86e', productIds: [], sortOrder: 2, enabled: true },
|
||||
{ id: 'heritage-tribe', name: '世遗部落', commodityTypeCode: 'd5766c63028e46f1bd9aaa25d1dc7482', productIds: [], sortOrder: 3, enabled: true },
|
||||
]
|
||||
|
||||
const TIME_SLOTS = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00']
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<TicketHome
|
||||
:scenic-spot="SCENIC_SPOT"
|
||||
:groups="visibleGroups"
|
||||
:products="ticketStore.products"
|
||||
:products="products"
|
||||
:loading="loading"
|
||||
@back="goBack"
|
||||
@open-travelers="openTravelers"
|
||||
@open-orders="openOrders"
|
||||
@@ -14,18 +15,86 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { quickBookingList } from "@/request/api/GoodsApi";
|
||||
import TicketHome from "./components/TicketHome.vue";
|
||||
import { GROUPS, SCENIC_SPOT } from "./data.js";
|
||||
import { useTicketStore } from "./store.js";
|
||||
import { GROUPS, PLACEHOLDER_IMAGE, SCENIC_SPOT } from "./data.js";
|
||||
|
||||
const ticketStore = useTicketStore();
|
||||
const groups = ref(GROUPS.map((group) => ({ ...group, productIds: [] })));
|
||||
const products = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const visibleGroups = computed(() =>
|
||||
[...GROUPS]
|
||||
[...groups.value]
|
||||
.filter((group) => group.enabled)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
);
|
||||
|
||||
const normalizeStringList = (value) =>
|
||||
Array.isArray(value) ? value.filter(Boolean).map(String) : [];
|
||||
|
||||
const mapQuickBookingProduct = (item) => ({
|
||||
id: String(item.commodityId),
|
||||
commodityId: String(item.commodityId),
|
||||
name: item.commodityName || "未命名商品",
|
||||
shortTitle: item.commodityName || "未命名商品",
|
||||
salePrice: item.specificationPrice ?? item.commodityPrice ?? 0,
|
||||
sold: item.salesVolume ?? item.soldQuantity ?? item.saleCount ?? null,
|
||||
images: [item.commodityPhoto || PLACEHOLDER_IMAGE],
|
||||
highlights: normalizeStringList(item.commodityTradeRuleList),
|
||||
status: "on_sale",
|
||||
clientVisible: true,
|
||||
});
|
||||
|
||||
const loadQuickBookingProducts = async () => {
|
||||
loading.value = true;
|
||||
|
||||
const results = await Promise.all(
|
||||
GROUPS.map(async (group) => {
|
||||
try {
|
||||
const res = await quickBookingList({
|
||||
commodityTypeCode: group.commodityTypeCode,
|
||||
current: 1,
|
||||
size: 10,
|
||||
});
|
||||
const records = Array.isArray(res?.data?.records)
|
||||
? res.data.records.filter((item) => item?.commodityId)
|
||||
: [];
|
||||
|
||||
if (!Array.isArray(res?.data?.records)) {
|
||||
console.error(`查询${group.name}商品失败:`, res);
|
||||
}
|
||||
|
||||
return { groupId: group.id, records };
|
||||
} catch (error) {
|
||||
console.error(`查询${group.name}商品失败:`, error);
|
||||
return { groupId: group.id, records: [] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const recordsByGroupId = new Map(
|
||||
results.map(({ groupId, records }) => [groupId, records])
|
||||
);
|
||||
const productMap = new Map();
|
||||
|
||||
groups.value = GROUPS.map((group) => {
|
||||
const groupProducts = (recordsByGroupId.get(group.id) || []).map(
|
||||
mapQuickBookingProduct
|
||||
);
|
||||
groupProducts.forEach((product) => productMap.set(product.id, product));
|
||||
|
||||
return {
|
||||
...group,
|
||||
productIds: groupProducts.map((product) => product.id),
|
||||
};
|
||||
});
|
||||
products.value = [...productMap.values()];
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
onMounted(loadQuickBookingProducts);
|
||||
|
||||
const goBack = () => uni.navigateBack();
|
||||
const openTravelers = () =>
|
||||
uni.navigateTo({ url: "/pages-service/tickets/travelers" });
|
||||
@@ -33,10 +102,10 @@ const openOrders = () =>
|
||||
uni.navigateTo({ url: "/pages-service/tickets/orders" });
|
||||
const openProductDetail = (productId) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages-service/tickets/product-detail?productId=${productId}`,
|
||||
url: `/pages/goods/index?commodityId=${encodeURIComponent(productId)}`,
|
||||
});
|
||||
const startBooking = (productId) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages-service/tickets/booking?productId=${productId}`,
|
||||
url: `/pages/goods/index?commodityId=${encodeURIComponent(productId)}`,
|
||||
});
|
||||
</script>
|
||||
@@ -23,18 +23,13 @@ test('provides coherent default visitor ticket data', async () => {
|
||||
assert.deepEqual(SCENIC_SPOT, {
|
||||
id: 'libo-xiaoqikong', name: '荔波小七孔景区', level: '国家5A级景区', heritage: '世界自然遗产', address: '贵州省黔南布依族苗族自治州荔波县', phone: '0854-3619810', latitude: 25.2534, longitude: 107.7255, heroImage: SCENIC_HERO_IMAGE,
|
||||
})
|
||||
assert.deepEqual(GROUPS.map(({ id, name, sortOrder, enabled, productIds }) => ({ id, name, sortOrder, enabled, productIds })), [
|
||||
{ id: 'ticket', name: '景点门票', sortOrder: 1, enabled: true, productIds: ['adult', 'elderly', 'minor', 'free'] },
|
||||
{ id: 'vip', name: 'VIP速通', sortOrder: 2, enabled: true, productIds: ['vip-fast'] },
|
||||
{ id: 'bus', name: '直通车', sortOrder: 3, enabled: true, productIds: ['direct-bus'] },
|
||||
{ id: 'package', name: '门票+观光车', sortOrder: 4, enabled: true, productIds: ['classic'] },
|
||||
{ id: 'tour', name: '一日游', sortOrder: 5, enabled: true, productIds: ['day-tour'] },
|
||||
{ id: 'play', name: '玩乐体验', sortOrder: 6, enabled: true, productIds: ['boat', 'guide'] },
|
||||
assert.deepEqual(GROUPS.map(({ id, name, commodityTypeCode, sortOrder, enabled, productIds }) => ({ id, name, commodityTypeCode, sortOrder, enabled, productIds })), [
|
||||
{ id: 'ticket', name: '门票', commodityTypeCode: '1', sortOrder: 1, enabled: true, productIds: [] },
|
||||
{ id: 'maolan', name: '茂兰景区', commodityTypeCode: '67d07734e0af4caa918309f5a346a86e', sortOrder: 2, enabled: true, productIds: [] },
|
||||
{ id: 'heritage-tribe', name: '世遗部落', commodityTypeCode: 'd5766c63028e46f1bd9aaa25d1dc7482', sortOrder: 3, enabled: true, productIds: [] },
|
||||
])
|
||||
assert.deepEqual(PRODUCTS.map(({ id }) => id), ['adult', 'elderly', 'minor', 'free', 'vip-fast', 'direct-bus', 'classic', 'day-tour', 'boat', 'guide'])
|
||||
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
|
||||
assert.ok(GROUPS.every((group) => group.productIds.every((productId) => productsById.has(productId))))
|
||||
assert.deepEqual(GROUPS.flatMap((group) => group.productIds).sort(), PRODUCTS.map(({ id }) => id).sort())
|
||||
const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'clientVisible', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements']
|
||||
const reservationKeys = ['requiresDate', 'advanceDays', 'requiresTimeSlot', 'timeSlots', 'realNameRequired', 'idTypes', 'maxQuantity', 'contactPhoneRequired']
|
||||
const entitlementKeys = ['id', 'name', 'fulfillment', 'quantity', 'unit', 'refundAllocation']
|
||||
@@ -44,7 +39,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]), [
|
||||
|
||||
@@ -60,16 +60,23 @@ test("storefront synchronizes the left category from right-column scrolling", as
|
||||
assert.match(source, /groupOffsets/);
|
||||
assert.match(source, /scrollTop/);
|
||||
assert.match(source, /leftScrollIntoViewId/);
|
||||
assert.match(source, /groupLayoutSignature/);
|
||||
assert.match(source, /group\.productIds/);
|
||||
assert.match(source, /groupOffsets\.value = \[\]/);
|
||||
assert.match(source, /TopNavBar/);
|
||||
});
|
||||
|
||||
test("product detail is an independent prototype-aligned page", async () => {
|
||||
test("storefront products open the API-backed goods detail page", async () => {
|
||||
const indexPage = await readTicketFile("index.vue");
|
||||
const detailPage = await readTicketFile("product-detail.vue");
|
||||
const component = await readTicketFile("components/ProductDetail.vue");
|
||||
const data = await readTicketFile("data.js");
|
||||
|
||||
assert.match(indexPage, /uni\.navigateTo\(\{\s*url: `\/pages-service\/tickets\/product-detail\?productId=\$\{productId\}`/);
|
||||
assert.match(indexPage, /quickBookingList/);
|
||||
assert.match(indexPage, /commodityTypeCode: group\.commodityTypeCode/);
|
||||
assert.match(indexPage, /current: 1/);
|
||||
assert.match(indexPage, /size: 10/);
|
||||
assert.match(indexPage, /url: `\/pages\/goods\/index\?commodityId=\$\{encodeURIComponent\(productId\)\}`/);
|
||||
assert.doesNotMatch(indexPage, /detailProduct|<ProductDetail/);
|
||||
assert.match(detailPage, /onLoad\(\(options\)/);
|
||||
assert.match(detailPage, /options\?\.productId/);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</view>
|
||||
|
||||
<view class="discovery-panel" :class="{ 'is-collapsed': isDiscoveryPanelCollapsed }">
|
||||
<Discovery @content-scroll="handleDiscoveryContentScroll" />
|
||||
<Discovery :active="tabIndex === 0" @content-scroll="handleDiscoveryContentScroll" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="center" :safe-area="false">
|
||||
<view class="popup-content bg-white mx-[24px] px-[12px] py-16 rounded-[12px]">
|
||||
<view class="header flex items-center pb-[12px]">
|
||||
<view class="popup-content mx-[40px] flex max-h-[80vh] flex-col overflow-hidden rounded-[12px] bg-white">
|
||||
<view class="header flex shrink-0 items-center bg-white px-[12px] pb-[12px] pt-[16px]">
|
||||
<view class="title flex-1 text-center text-[17px] text-[#000000] font-[500] ml-[24px]">呼叫热线</view>
|
||||
<uni-icons type="close" size="24" color="#CACFD8" @click="close" />
|
||||
</view>
|
||||
|
||||
<view class="border-box pl-[12px] pr-[12px]">
|
||||
<view class="border-box min-h-0 flex-1 overflow-y-auto px-[24px] pb-[32px]">
|
||||
<view class="text-[16px] text-[#a3a3a3] leading-[22px]">
|
||||
如有任何疑问,欢迎随时致电景区咨询热线:
|
||||
<text class="text-[#2563eb]" @click="handleClick(mobleNumber1)">{{ mobleNumber1 }}</text> / <text
|
||||
class="text-[#2563eb]" @click="handleClick(mobleNumber2)">{{ mobleNumber2 }}</text>,
|
||||
我们将全程为您提供细致解答与暖心服务。
|
||||
如有任何疑问,欢迎随时致电景区咨询热线,我们将全程为您提供细致解答与暖心服务。
|
||||
</view>
|
||||
<view class="mt-[12px] flex flex-col gap-[8px]">
|
||||
<view class="phone-row text-[16px] text-[#a3a3a3] leading-[28px]" @click="handleClick(mobleNumber1)">热线1: <text class="text-[18px] text-[#2563eb]">{{ mobleNumber1 }}</text></view>
|
||||
<view class="phone-row text-[16px] text-[#a3a3a3] leading-[28px]" @click="handleClick(mobleNumber2)">热线2: <text class="text-[18px] text-[#2563eb]">{{ mobleNumber2 }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<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" />
|
||||
<view class="audio-play" @tap.stop="handlePlaybackToggle">
|
||||
<view v-if="isPlaying" class="flex items-center gap-[6px]">
|
||||
<view class="h-[20px] w-[5px] rounded-[2px] bg-white" />
|
||||
<view class="h-[20px] w-[5px] rounded-[2px] bg-white" />
|
||||
</view>
|
||||
<view v-else class="audio-play-triangle" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -14,58 +18,209 @@
|
||||
|
||||
<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 class="audio-progress-fill" :style="{ width: `${progressPercent}%` }" />
|
||||
</view>
|
||||
<text class="audio-time block w-[90px] truncate">
|
||||
{{ normalizedItem.currentTime }} / {{ normalizedItem.duration }}
|
||||
</text>
|
||||
<text class="audio-time block w-[90px] truncate">{{ playbackTimeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const DEFAULT_ITEM = {
|
||||
id: "audio-guide",
|
||||
title: "卧龙潭语音讲解",
|
||||
subTitle: "解说:响水洞瀑布",
|
||||
progress: 31,
|
||||
currentTime: "02:15",
|
||||
duration: "05:40",
|
||||
};
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
import { onPageHide } from "@dcloudio/uni-app";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
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 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,
|
||||
};
|
||||
});
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const currentTimeSeconds = ref(0);
|
||||
const audioDurationSeconds = ref(0);
|
||||
let audioContext = null;
|
||||
|
||||
const formatSeconds = (secondsValue) => {
|
||||
const value = Number(secondsValue);
|
||||
if (!Number.isFinite(value) || value < 0) return "";
|
||||
|
||||
const totalSeconds = Math.floor(value);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const metadataDurationSeconds = computed(() => {
|
||||
const durationMs = Number(normalizedItem.value.durationMs);
|
||||
return Number.isFinite(durationMs) && durationMs > 0 ? durationMs / 1000 : 0;
|
||||
});
|
||||
|
||||
const effectiveDurationSeconds = computed(
|
||||
() => audioDurationSeconds.value || metadataDurationSeconds.value,
|
||||
);
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!effectiveDurationSeconds.value) return 0;
|
||||
|
||||
const progress = (currentTimeSeconds.value / effectiveDurationSeconds.value) * 100;
|
||||
return Math.min(Math.max(progress, 0), 100);
|
||||
});
|
||||
|
||||
const playbackTimeText = computed(() => {
|
||||
if (!effectiveDurationSeconds.value && !currentTimeSeconds.value) return "";
|
||||
|
||||
const currentTimeText = formatSeconds(currentTimeSeconds.value);
|
||||
const durationText = formatSeconds(effectiveDurationSeconds.value);
|
||||
return durationText ? `${currentTimeText} / ${durationText}` : currentTimeText;
|
||||
});
|
||||
|
||||
const resetPlaybackState = () => {
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
audioDurationSeconds.value = 0;
|
||||
};
|
||||
|
||||
const destroyAudioContext = () => {
|
||||
if (!audioContext) return;
|
||||
|
||||
const context = audioContext;
|
||||
audioContext = null;
|
||||
context.pause();
|
||||
context.destroy();
|
||||
};
|
||||
|
||||
const stopAndResetPlayback = () => {
|
||||
destroyAudioContext();
|
||||
resetPlaybackState();
|
||||
};
|
||||
|
||||
const createAudioContext = () => {
|
||||
const url = normalizedItem.value.url;
|
||||
console.log("Creating audio context for URL:", url);
|
||||
if (!url) return null;
|
||||
|
||||
const context = uni.createInnerAudioContext();
|
||||
audioContext = context;
|
||||
context.autoplay = false;
|
||||
context.src = url;
|
||||
|
||||
context.onPlay(() => {
|
||||
if (audioContext === context) isPlaying.value = true;
|
||||
});
|
||||
|
||||
context.onPause(() => {
|
||||
if (audioContext === context) isPlaying.value = false;
|
||||
});
|
||||
|
||||
context.onCanplay(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
const duration = Number(context.duration);
|
||||
if (Number.isFinite(duration) && duration > 0) {
|
||||
audioDurationSeconds.value = duration;
|
||||
}
|
||||
});
|
||||
|
||||
context.onTimeUpdate(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
currentTimeSeconds.value = Number(context.currentTime) || 0;
|
||||
const duration = Number(context.duration);
|
||||
if (Number.isFinite(duration) && duration > 0) {
|
||||
audioDurationSeconds.value = duration;
|
||||
}
|
||||
});
|
||||
|
||||
context.onEnded(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
});
|
||||
|
||||
context.onStop(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
});
|
||||
|
||||
context.onError(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
stopAndResetPlayback();
|
||||
uni.showToast({
|
||||
title: "音频播放失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
const handlePlaybackToggle = () => {
|
||||
if (!normalizedItem.value.url) return;
|
||||
|
||||
const context = audioContext || createAudioContext();
|
||||
if (!context) return;
|
||||
|
||||
if (isPlaying.value) {
|
||||
isPlaying.value = false;
|
||||
context.pause();
|
||||
} else {
|
||||
isPlaying.value = true;
|
||||
context.play();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(active) => {
|
||||
if (!active) stopAndResetPlayback();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => normalizedItem.value.url,
|
||||
() => {
|
||||
stopAndResetPlayback();
|
||||
},
|
||||
);
|
||||
|
||||
onPageHide(() => {
|
||||
stopAndResetPlayback();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAndResetPlayback();
|
||||
});
|
||||
|
||||
const handleSelect = () => {
|
||||
emit("didSelectItem", normalizedItem.value.raw);
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
@@ -1,18 +1,7 @@
|
||||
<template>
|
||||
<view class="discovery-photo-tool-card" hover-class="is-pressed" hover-stay-time="80" @tap="handleSelect">
|
||||
<view class="discovery-photo-tool-card" hover-class="is-pressed" hover-stay-time="80" @tap="handleJump">
|
||||
<view class="photo-visual w-[88px] min-w-0">
|
||||
<view class="camera-illustration">
|
||||
<view class="camera-top" />
|
||||
<view class="camera-flash" />
|
||||
<view class="camera-body">
|
||||
<view class="camera-lens">
|
||||
<view class="camera-lens-inner" />
|
||||
</view>
|
||||
<view class="camera-dot is-large" />
|
||||
<view class="camera-dot is-small" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="photo-badge w-[calc(100%_-_18px)] truncate">{{ normalizedItem.badge }}</text>
|
||||
<image v-if="normalizedItem.coverUrl" class="block h-full w-full" :src="normalizedItem.coverUrl" mode="aspectFill" />
|
||||
</view>
|
||||
|
||||
<view class="photo-content min-w-0 flex-1">
|
||||
@@ -20,13 +9,12 @@
|
||||
<text class="photo-subtitle block w-full truncate">{{ normalizedItem.subTitle }}</text>
|
||||
|
||||
<view class="photo-options">
|
||||
<view v-for="(option, index) in normalizedItem.options" :key="getOptionKey(option, index)" class="photo-option min-w-0 flex-1"
|
||||
hover-class="is-option-pressed" hover-stay-time="80" @tap.stop="handleOptionSelect(option, index)">
|
||||
<view v-for="(photo, index) in normalizedItem.options" :key="getPhotoKey(photo, index)" class="photo-option min-w-0 flex-1"
|
||||
hover-class="is-option-pressed" hover-stay-time="80" @tap.stop="handleJump">
|
||||
<view class="photo-option-thumb">
|
||||
<image v-if="option.coverImage" class="photo-option-image" :src="option.coverImage" mode="aspectFill" />
|
||||
<view v-else class="photo-option-placeholder" :class="`is-${index}`" />
|
||||
<image v-if="photo?.photoUrl" class="photo-option-image" :src="photo.photoUrl" mode="aspectFill" />
|
||||
</view>
|
||||
<text class="photo-option-label block w-[76px] truncate">{{ option.label }}</text>
|
||||
<text class="photo-option-label block w-[76px] truncate">{{ photo?.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -35,20 +23,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const DEFAULT_OPTIONS = [
|
||||
{ id: "check-in", label: "景点打卡" },
|
||||
{ id: "companion", label: "小七陪伴" },
|
||||
{ id: "memory", label: "风格纪念" },
|
||||
];
|
||||
|
||||
const DEFAULT_ITEM = {
|
||||
id: "travel-photo",
|
||||
title: "在卧龙潭和小七合影",
|
||||
subTitle: "生成你在卧龙潭的专属纪念照",
|
||||
badge: "旅行纪念照",
|
||||
options: DEFAULT_OPTIONS,
|
||||
};
|
||||
import { navigateTo } from "@/router";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -57,37 +32,27 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["didSelectItem", "didSelectOption"]);
|
||||
|
||||
const normalizeOption = (option, index) => ({
|
||||
raw: option,
|
||||
id: option.id ?? option.tabContentId ?? index,
|
||||
label: option.label || option.title || DEFAULT_OPTIONS[index]?.label || "",
|
||||
coverImage: option.coverImage || option.image || option.url || "",
|
||||
});
|
||||
|
||||
const normalizedItem = computed(() => {
|
||||
const source = Object.keys(props.item).length > 0 ? props.item : DEFAULT_ITEM;
|
||||
const options = source.options || source.children || source.items || DEFAULT_OPTIONS;
|
||||
const resource = props.item?.resource;
|
||||
const summary = resource?.summary;
|
||||
const componentParams = resource?.payload?.componentParams;
|
||||
|
||||
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,
|
||||
badge: source.badge || source.tag || DEFAULT_ITEM.badge,
|
||||
options: options.slice(0, 3).map(normalizeOption),
|
||||
title: summary?.title,
|
||||
subTitle: summary?.subtitle,
|
||||
coverUrl: summary?.coverUrl,
|
||||
jumpUrl: componentParams?.jumpUrl,
|
||||
options: Array.isArray(componentParams?.photoList)
|
||||
? componentParams.photoList.slice(0, 3)
|
||||
: [],
|
||||
};
|
||||
});
|
||||
|
||||
const getOptionKey = (option, index) => option.id ?? option.label ?? index;
|
||||
const getPhotoKey = (photo, index) => photo?.photoUrl ?? photo?.title ?? index;
|
||||
|
||||
const handleSelect = () => {
|
||||
emit("didSelectItem", normalizedItem.value.raw);
|
||||
};
|
||||
|
||||
const handleOptionSelect = (option, index) => {
|
||||
emit("didSelectOption", option.raw, normalizedItem.value.raw, index);
|
||||
const handleJump = () => {
|
||||
if (!normalizedItem.value.jumpUrl) return;
|
||||
navigateTo(normalizedItem.value.jumpUrl);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -58,33 +58,21 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(180deg, rgba(43, 107, 122, 0) 42%, rgba(71, 179, 145, 0.72) 74%, #47b391 100%);
|
||||
background: linear-gradient(180deg, rgba(5, 24, 24, 0) 0%, rgba(5, 24, 24, 0.18) 66%, rgba(5, 24, 24, 0.64) 100%);
|
||||
}
|
||||
|
||||
.explore-card.is-culture {
|
||||
background: #b98563;
|
||||
}
|
||||
|
||||
.explore-card.is-culture .explore-card-gradient {
|
||||
background: linear-gradient(180deg, rgba(87, 57, 37, 0.08) 36%, rgba(150, 93, 55, 0.62) 72%, #b98563 100%);
|
||||
}
|
||||
|
||||
.explore-card.is-adventure {
|
||||
background: #5b895f;
|
||||
}
|
||||
|
||||
.explore-card.is-adventure .explore-card-gradient {
|
||||
background: linear-gradient(180deg, rgba(30, 58, 47, 0) 36%, rgba(47, 97, 54, 0.74) 74%, #5b895f 100%);
|
||||
}
|
||||
|
||||
.explore-card.is-nightlife {
|
||||
background: #438694;
|
||||
}
|
||||
|
||||
.explore-card.is-nightlife .explore-card-gradient {
|
||||
background: linear-gradient(180deg, rgba(12, 47, 78, 0.04) 34%, rgba(28, 92, 111, 0.72) 74%, #438694 100%);
|
||||
}
|
||||
|
||||
.explore-card-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -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,15 @@
|
||||
<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" />
|
||||
<DiscoveryAudioToolCard v-if="discoveryAudioToolData" :item="discoveryAudioToolData"
|
||||
:active="props.active" @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,21 +56,32 @@ 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";
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const locationStore = useLocationStore();
|
||||
/// 从个渠道获取如二维码
|
||||
const sceneId = appStore.sceneId || "";
|
||||
const currentTabsId = ref("");
|
||||
|
||||
const activeIndex = ref(0);
|
||||
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
|
||||
@@ -80,17 +97,29 @@ const handleContentScroll = (e) => {
|
||||
/// 点击主景区
|
||||
const handleMainPanoramaClick = () => {
|
||||
isMainPanoramaGuide.value = true;
|
||||
currentTabsId.value = "";
|
||||
discoveryTabs.value = [];
|
||||
clearDiscoveryData();
|
||||
};
|
||||
|
||||
/// 切换tab时,清空之前的发现页数据
|
||||
const clearDiscoveryData = () => {
|
||||
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时,清空之前的发现页数据
|
||||
clearDiscoveryData();
|
||||
}
|
||||
|
||||
/// 请求发现页tab数据
|
||||
@@ -123,24 +152,33 @@ 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?.id) return;
|
||||
|
||||
if (currentTabsId.value === resolvedTab.tab.id) return;
|
||||
currentTabsId.value = resolvedTab.tab.id;
|
||||
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 +211,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) => ({
|
||||
@@ -204,14 +256,6 @@ const handleToolCardClick = (item) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToolOptionClick = (option, parentItem) => {
|
||||
if (option?.jumpType !== undefined) {
|
||||
handleClick(option);
|
||||
} else {
|
||||
handleToolCardClick(parentItem);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = async (item) => {
|
||||
console.log(`执行点击事件: ${item.jumpType},参数:${JSON.stringify(item.jumpContent)}`);
|
||||
/// 商品
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-section">
|
||||
<text class="title">
|
||||
{{ goodsData.commodityName || "【成人票】云从朵花温泉门票" }}
|
||||
{{ goodsData.commodityName }}
|
||||
</text>
|
||||
<view class="tag-wrapper" v-if="goodsData.timeTag">
|
||||
<view class="time-tag">{{ goodsData.timeTag || "随时可退" }}</view>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<view class="flex min-h-0 flex-1 flex-col overflow-hidden bg-[#f5f5f5]">
|
||||
<view class="skeleton-block relative h-[300px] w-full shrink-0 overflow-hidden bg-[#e7ebef]" />
|
||||
|
||||
<view class="relative z-10 -mt-[30px] min-h-0 flex-1 rounded-t-[28px] bg-white px-[16px] pb-[24px] pt-[24px]">
|
||||
<view class="skeleton-block relative h-[22px] w-[72%] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
|
||||
<view class="mt-[12px] flex gap-[8px]">
|
||||
<view class="skeleton-block relative h-[20px] w-[64px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
|
||||
<view class="skeleton-block relative h-[20px] w-[76px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
|
||||
<view class="skeleton-block relative h-[20px] w-[56px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
|
||||
</view>
|
||||
<view class="mt-[18px] flex items-end">
|
||||
<view class="skeleton-block relative h-[28px] w-[110px] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
|
||||
<view class="skeleton-block relative ml-[12px] h-[16px] w-[70px] overflow-hidden rounded-[4px] bg-[#f0f2f4]" />
|
||||
</view>
|
||||
|
||||
<view class="mt-[24px] rounded-[12px] bg-[#f8f9fa] p-[14px]">
|
||||
<view class="flex items-center">
|
||||
<view class="skeleton-block relative h-[24px] w-[24px] overflow-hidden rounded-full bg-[#e5e9ed]" />
|
||||
<view class="ml-[10px] min-w-0 flex-1">
|
||||
<view class="skeleton-block relative h-[16px] w-[58%] overflow-hidden rounded-[4px] bg-[#e8ecef]" />
|
||||
<view class="skeleton-block relative mt-[8px] h-[12px] w-[86%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-[24px]">
|
||||
<view class="skeleton-block relative h-[20px] w-[88px] overflow-hidden rounded-[5px] bg-[#e8ecef]" />
|
||||
<view class="skeleton-block relative mt-[14px] h-[14px] w-full overflow-hidden rounded-[4px] bg-[#eef1f3]" />
|
||||
<view class="skeleton-block relative mt-[10px] h-[14px] w-[92%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
|
||||
<view class="skeleton-block relative mt-[10px] h-[14px] w-[76%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex shrink-0 items-center bg-white px-[12px] pb-[24px] pt-[12px]">
|
||||
<view class="skeleton-block relative h-[28px] w-[92px] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
|
||||
<view class="skeleton-block relative ml-auto h-[48px] w-[120px] overflow-hidden rounded-[10px] bg-[#e4e8ec]" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.skeleton-block {
|
||||
&::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.7) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
transform: translateX(-100%);
|
||||
animation: skeleton-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+122
-67
@@ -1,58 +1,73 @@
|
||||
<template>
|
||||
<view class="goods-container bg-[#f5f5f5]">
|
||||
<TopNavBar :title="navOpacity < 0.5 ? '' : '商品详情'" :background="`rgba(217, 238, 255, ${navOpacity})`"
|
||||
:titleColor="navOpacity < 0.5 ? '#ffffff' : '#000000'"
|
||||
:backIconColor="navOpacity < 0.5 ? '#ffffff' : '#000000'" />
|
||||
<TopNavBar :title="solidNavBar || navOpacity >= 0.5 ? '商品详情' : ''"
|
||||
:background="solidNavBar ? '#ffffff' : `rgba(217, 238, 255, ${navOpacity})`"
|
||||
:titleColor="solidNavBar || navOpacity >= 0.5 ? '#000000' : '#ffffff'"
|
||||
:backIconColor="solidNavBar || navOpacity >= 0.5 ? '#000000' : '#ffffff'" />
|
||||
|
||||
<!-- 滚动区域 -->
|
||||
<scroll-view class="content-wrapper" scroll-y @scroll="handleScroll">
|
||||
<ImageSwiper :border-radius="0" :height="300" :images="goodsData.commodityPhotoList" thumbnailBottom="42px" />
|
||||
<GoodsDetailSkeleton v-if="loadStatus === 'loading'" />
|
||||
|
||||
<view class="goods-content">
|
||||
<!-- 商品信息组件 -->
|
||||
<GoodInfo :goodsData="goodsData" />
|
||||
|
||||
<!-- 地址区域 -->
|
||||
<LocationCard :orderData="goodsData" />
|
||||
|
||||
<!-- 日期选择区域 -->
|
||||
<DateSelector v-if="goodsData.orderType == 0" @showCalendar="showCalendar" :checkInDate="selectedDate.startDate"
|
||||
:checkOutDate="selectedDate.endDate" :checkInDay="''" :checkOutDay="''" :nights="selectedDate.totalDays" />
|
||||
|
||||
<!-- 商品套餐组件 -->
|
||||
<GoodPackage
|
||||
v-if="goodsData.orderType != 0 && goodsData.commodityPackageConfig && goodsData.commodityPackageConfig.length > 0"
|
||||
:goodsData="goodsData" />
|
||||
|
||||
<!-- 商品设施组件 -->
|
||||
<GoodFacility v-if="goodsData.commodityEquipment && goodsData.commodityEquipment.length > 0"
|
||||
:goodsData="goodsData" />
|
||||
|
||||
<!-- 商品详情组件 -->
|
||||
<GoodDetail :goodsData="goodsData" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 立即抢购 -->
|
||||
<view class="footer border-top">
|
||||
<view class="amt text-[20px] font-bold text-[#ff3d60] line-height-28 flex items-center mr-[8px]">
|
||||
{{ calculatedTotalPrice }}
|
||||
</view>
|
||||
|
||||
<view class="btn rounded-[10px] flex items-center ml-auto pl-[8px]" @click="navigateToPay(goodsData)">
|
||||
<image class="icon" src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png" />
|
||||
<text class="text-[16px] font-[500] text-white">立即预定</text>
|
||||
<view v-else-if="loadStatus === 'error' || loadStatus === 'empty'"
|
||||
class="box-border flex min-h-0 flex-1 flex-col items-center justify-center px-[24px] pb-[80px] pt-[96px]">
|
||||
<CustomEmpty :statusText="loadStatus === 'error' ? '商品加载失败' : '暂无商品数据'" />
|
||||
<view v-if="loadStatus === 'error'"
|
||||
class="mt-[20px] flex h-[40px] items-center justify-center rounded-full bg-[#ff3d60] px-[28px]"
|
||||
@click="reloadGoods">
|
||||
<text class="text-[14px] font-[500] text-white">重新加载</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calender :visible="calendarVisible" mode="range" :range-require-price="true" :price-data="priceData"
|
||||
@close="handleCalendarClose" @range-select="handleDateSelect" />
|
||||
<template v-else>
|
||||
<!-- 滚动区域 -->
|
||||
<scroll-view class="content-wrapper" scroll-y @scroll="handleScroll">
|
||||
<ImageSwiper :border-radius="0" :height="300" :images="goodsData.commodityPhotoList" thumbnailBottom="42px" />
|
||||
|
||||
<view class="goods-content">
|
||||
<!-- 商品信息组件 -->
|
||||
<GoodInfo :goodsData="goodsData" />
|
||||
|
||||
<!-- 地址区域 -->
|
||||
<LocationCard :orderData="goodsData" />
|
||||
|
||||
<!-- 日期选择区域 -->
|
||||
<DateSelector v-if="goodsData.orderType == 0" @showCalendar="showCalendar" :checkInDate="selectedDate.startDate"
|
||||
:checkOutDate="selectedDate.endDate" :checkInDay="''" :checkOutDay="''" :nights="selectedDate.totalDays" />
|
||||
|
||||
<!-- 商品套餐组件 -->
|
||||
<GoodPackage
|
||||
v-if="goodsData.orderType != 0 && goodsData.commodityPackageConfig && goodsData.commodityPackageConfig.length > 0"
|
||||
:goodsData="goodsData" />
|
||||
|
||||
<!-- 商品设施组件 -->
|
||||
<GoodFacility v-if="goodsData.commodityEquipment && goodsData.commodityEquipment.length > 0"
|
||||
:goodsData="goodsData" />
|
||||
|
||||
<!-- 商品详情组件 -->
|
||||
<GoodDetail :goodsData="goodsData" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 立即抢购 -->
|
||||
<view class="footer border-top">
|
||||
<view class="amt text-[20px] font-bold text-[#ff3d60] line-height-28 flex items-center mr-[8px]">
|
||||
{{ calculatedTotalPrice }}
|
||||
</view>
|
||||
|
||||
<view class="btn rounded-[10px] flex items-center ml-auto pl-[8px]" @click="navigateToPay(goodsData)">
|
||||
<image class="icon" src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png" />
|
||||
<text class="text-[16px] font-[500] text-white">立即预定</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calender :visible="calendarVisible" mode="range" :range-require-price="true" :price-data="priceData"
|
||||
@close="handleCalendarClose" @range-select="handleDateSelect" />
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { goodsDetail, commodityDailyPriceList } from "@/request/api/GoodsApi";
|
||||
import { DateUtils } from "@/utils";
|
||||
@@ -65,12 +80,17 @@ import DateSelector from "./components/DateSelector/index.vue";
|
||||
import GoodDetail from "@/components/GoodDetail/index.vue";
|
||||
import GoodFacility from "./components/GoodFacility/index.vue";
|
||||
import GoodPackage from "./components/GoodPackage/index.vue";
|
||||
import GoodsDetailSkeleton from "./components/GoodsDetailSkeleton/index.vue";
|
||||
import CustomEmpty from "@/components/CustomEmpty/index.vue";
|
||||
import { useSelectedDateStore } from "@/store";
|
||||
|
||||
// 导航栏透明度 - 默认透明,随滚动变为不透明
|
||||
const navOpacity = ref(0);
|
||||
const calendarVisible = ref(false);
|
||||
const goodsData = ref({});
|
||||
const loadStatus = ref("loading");
|
||||
const currentCommodityId = ref("");
|
||||
const solidNavBar = computed(() => loadStatus.value !== "success");
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e) => {
|
||||
@@ -87,39 +107,67 @@ const selectedDate = ref({
|
||||
totalDays: 1,
|
||||
});
|
||||
|
||||
const priceData = ref([])
|
||||
const priceData = ref([]);
|
||||
|
||||
// 计算的总价格
|
||||
const calculatedTotalPrice = ref(0);
|
||||
|
||||
// 获取商品详情数据
|
||||
const goodsInfo = async (params) => {
|
||||
const res = await goodsDetail(params);
|
||||
loadStatus.value = "loading";
|
||||
goodsData.value = {};
|
||||
calculatedTotalPrice.value = 0;
|
||||
priceData.value = [];
|
||||
|
||||
goodsData.value = res.data;
|
||||
try {
|
||||
const res = await goodsDetail(params);
|
||||
|
||||
// 初始化计算价格
|
||||
calculatedTotalPrice.value = goodsData.value.specificationPrice || 0;
|
||||
if (res?.code !== 0) {
|
||||
console.error("查询商品详情失败:", res);
|
||||
loadStatus.value = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是酒店类型订单再获取获取商品日价格及库存
|
||||
if (goodsData.value.orderType == 0) {
|
||||
configGoodsData();
|
||||
getGoodsDailyPrice({
|
||||
commodityId: goodsData.value.commodityId,
|
||||
});
|
||||
}
|
||||
if (
|
||||
!res.data ||
|
||||
typeof res.data !== "object" ||
|
||||
Array.isArray(res.data) ||
|
||||
Object.keys(res.data).length === 0 ||
|
||||
!res.data.commodityId
|
||||
) {
|
||||
loadStatus.value = "empty";
|
||||
return;
|
||||
}
|
||||
|
||||
if (goodsData.value.commodityStatus !== "1") {
|
||||
uni.showModal({
|
||||
title: "温馨提示",
|
||||
content: "您选的商品暂不可预订,请重新选择。",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack({ delta: 1 });
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
goodsData.value = res.data;
|
||||
|
||||
// 初始化计算价格
|
||||
calculatedTotalPrice.value = goodsData.value.specificationPrice || 0;
|
||||
|
||||
// 判断是酒店类型订单再获取获取商品日价格及库存
|
||||
if (goodsData.value.orderType == 0) {
|
||||
configGoodsData();
|
||||
getGoodsDailyPrice({
|
||||
commodityId: goodsData.value.commodityId,
|
||||
});
|
||||
}
|
||||
|
||||
loadStatus.value = "success";
|
||||
|
||||
if (goodsData.value.commodityStatus !== "1") {
|
||||
uni.showModal({
|
||||
title: "温馨提示",
|
||||
content: "您选的商品暂不可预订,请重新选择。",
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
uni.navigateBack({ delta: 1 });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("查询商品详情失败:", error);
|
||||
loadStatus.value = "error";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,12 +187,19 @@ const getGoodsDailyPrice = async (params) => {
|
||||
|
||||
const selectedDateStore = useSelectedDateStore();
|
||||
onLoad(({ commodityId = "1950766939442774018" }) => {
|
||||
currentCommodityId.value = commodityId;
|
||||
|
||||
// 从store中获取选中的日期
|
||||
selectedDate.value = selectedDateStore.selectedDate;
|
||||
|
||||
goodsInfo({ commodityId });
|
||||
});
|
||||
|
||||
const reloadGoods = () => {
|
||||
if (!currentCommodityId.value) return;
|
||||
goodsInfo({ commodityId: currentCommodityId.value });
|
||||
};
|
||||
|
||||
// 显示日历弹窗
|
||||
const showCalendar = () => (calendarVisible.value = true);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+10
-10
@@ -13,16 +13,16 @@
|
||||
*/
|
||||
|
||||
/* 主题颜色(由 switch-client 命令自动更新) */
|
||||
$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;
|
||||
$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