This commit is contained in:
lcj
2026-07-13 11:15:59 +08:00
parent 45a74578fc
commit ee0825c1e6
7 changed files with 225 additions and 18 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
VITE_API_BASE_URL=http://localhost:48080 VITE_API_BASE_URL=https://dscp-uat.hn12301.net
VITE_APP_TITLE=湖南12301文旅专区 VITE_APP_TITLE=湖南12301文旅专区
VITE_USE_MOCK=false VITE_USE_MOCK=false
+165 -5
View File
@@ -35,7 +35,7 @@
<!-- 预约提醒横幅 --> <!-- 预约提醒横幅 -->
<view v-if="scenic.needReservation" class="notice-banner"> <view v-if="scenic.needReservation" class="notice-banner">
<view class="icon-wrap icon-alert" /> <view class="icon-wrap icon-alert" />
<text class="notice-text">需预约后出行,未预约无法入园</text> <rich-text class="notice-text" :nodes="scenic.needReservation" />
</view> </view>
<!-- 特色标签行 --> <!-- 特色标签行 -->
@@ -132,7 +132,26 @@
<rich-text class="notice-html" :nodes="scenic.bookingNoticeHtml" /> <rich-text class="notice-html" :nodes="scenic.bookingNoticeHtml" />
</view> </view>
<view v-if="activeTab === 'intro'" class="content-section"> <view v-if="activeTab === 'intro'" class="content-section">
<text class="intro-text">{{ scenic.introduction }}</text> <rich-text class="notice-html" :nodes="scenic.introduction" />
</view>
</view>
<!-- 店铺公告弹窗 -->
<view v-if="noticeVisible" class="notice-overlay" @click="noticeVisible = false">
<view class="notice-popup" @click.stop>
<view class="notice-popup-accent" />
<view class="notice-popup-header">
<view class="notice-popup-icon" />
<text class="notice-popup-title">{{ storeNotice.title }}</text>
</view>
<scroll-view class="notice-popup-body" scroll-y>
<rich-text :nodes="storeNotice.content" />
</scroll-view>
<view class="notice-popup-footer">
<view class="notice-popup-btn" @click="noticeVisible = false">
<text>我知道了</text>
</view>
</view>
</view> </view>
</view> </view>
@@ -143,14 +162,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { post, getToken } from '../../../api/request' import { post, getToken } from '../../../api/request'
interface NoticeItem { title: string; content: string } interface NoticeItem { title: string; content: string }
interface CardBenefit { type: string; cardName: string; skuName: string; remainingCount: number; freeCount: number; bookingDeadline: string; cardModelId: string; resourceId: string; goodsId: string } interface CardBenefit { type: string; cardName: string; skuName: string; remainingCount: number; freeCount: number; bookingDeadline: string; cardModelId: string; resourceId: string; goodsId: string }
interface ScenicDetail { interface ScenicDetail {
id: number; name: string; price: number; freeCount: number; level: string id: number; name: string; price: number; freeCount: number; level: string
hours: string; needReservation: boolean; bookingDeadline: string hours: string; needReservation: string; bookingDeadline: string
phone: string; address: string; tags: string[]; images: string[] phone: string; address: string; tags: string[]; images: string[]
introduction: string; bookingNotice: NoticeItem[]; bookingNoticeHtml: string; cards: CardBenefit[] introduction: string; bookingNotice: NoticeItem[]; bookingNoticeHtml: string; cards: CardBenefit[]
scenicId: number; shopId: number; mainPic: string; status: number scenicId: number; shopId: number; mainPic: string; status: number
@@ -160,6 +179,9 @@ const scenic = ref<ScenicDetail>({ tags: [], images: [], cards: [], bookingNotic
const activeTab = ref<'notice' | 'intro'>('notice') const activeTab = ref<'notice' | 'intro'>('notice')
const currentSlide = ref(0) const currentSlide = ref(0)
const loading = ref(true) const loading = ref(true)
const currentShopId = ref('')
const storeNotice = ref<{ title: string; content: string }>({ title: '', content: '' })
const noticeVisible = ref(false)
function onSwiperChange(e: any) { function onSwiperChange(e: any) {
currentSlide.value = e.detail.current currentSlide.value = e.detail.current
@@ -171,6 +193,7 @@ const statusLabel = computed(() => ['', '正常运营', '审核中', '暂停运
onLoad((options) => { onLoad((options) => {
const shopId = options?.shopId || options?.id || '' const shopId = options?.shopId || options?.id || ''
currentShopId.value = shopId
if (!shopId) { if (!shopId) {
uni.showToast({ title: '缺少景区参数', icon: 'none' }) uni.showToast({ title: '缺少景区参数', icon: 'none' })
uni.navigateBack() uni.navigateBack()
@@ -178,8 +201,26 @@ onLoad((options) => {
} }
uni.setNavigationBarTitle({ title: '景区详情' }) uni.setNavigationBarTitle({ title: '景区详情' })
fetchDetail(String(shopId)) fetchDetail(String(shopId))
fetchShopNotice(String(shopId))
}) })
async function fetchShopNotice(shopId: string) {
try {
const res = await post<any>('/mp-api/mpShopNotice/queryShopNoticePage', {
shopId,
pageNo: 1,
pageSize: 5,
})
const list = res?.list || res?.data?.list || []
if (list.length > 0) {
storeNotice.value = { title: list[0].title || '', content: list[0].content || '' }
noticeVisible.value = true
}
} catch {
// silently fail
}
}
async function fetchDetail(shopId: string) { async function fetchDetail(shopId: string) {
loading.value = true loading.value = true
try { try {
@@ -220,6 +261,18 @@ async function fetchDetail(shopId: string) {
} }
} }
onShareAppMessage(() => ({
title: scenic.value.name || '景区详情',
path: `/packages/content/pages/scenic-detail?shopId=${currentShopId.value}`,
imageUrl: scenic.value.images?.[0] || '',
}))
onShareTimeline(() => ({
title: scenic.value.name || '景区详情',
query: `shopId=${currentShopId.value}`,
imageUrl: scenic.value.images?.[0] || '',
}))
function onHoursClick() { function onHoursClick() {
uni.showToast({ title: '营业时间仅供参考', icon: 'none' }) uni.showToast({ title: '营业时间仅供参考', icon: 'none' })
} }
@@ -575,7 +628,7 @@ $icon-crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' w
.deadline-text { .deadline-text {
margin-left: auto; margin-left: auto;
font-size: $font-size-xs; font-size: $font-size-sm;
color: $uv-tips-color; color: $uv-tips-color;
} }
@@ -705,6 +758,113 @@ $icon-crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' w
display: block; display: block;
} }
/* ====== 店铺公告弹窗 ====== */
.notice-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(4rpx);
z-index: $z-modal;
display: flex;
align-items: center;
justify-content: center;
animation: overlayIn 250ms ease;
}
@keyframes overlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
.notice-popup {
width: 620rpx;
max-height: 70vh;
background: $color-card;
border-radius: $radius-xl;
box-shadow: $shadow-xl;
overflow: hidden;
display: flex;
flex-direction: column;
animation: popupIn 300ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes popupIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.notice-popup-accent {
height: 6rpx;
background: $gradient-accent;
flex-shrink: 0;
}
.notice-popup-header {
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-6 $spacing-5 0;
flex-shrink: 0;
}
.notice-popup-icon {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 24 24' fill='none' stroke='%233c9cff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-bottom: $spacing-2;
}
.notice-popup-title {
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $uv-main-color;
font-family: $font-family-display;
line-height: 1.4;
text-align: center;
}
.notice-popup-body {
padding: $spacing-3 $spacing-5 $spacing-3;
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 1.9;
max-height: 45vh;
overflow-y: auto;
}
.notice-popup-footer {
padding: 0 $spacing-5 $spacing-5;
flex-shrink: 0;
}
.notice-popup-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
background: $gradient-accent;
border-radius: $radius-full;
transition: opacity $transition-fast;
text {
font-size: $font-size-base;
color: #fff;
font-weight: $font-weight-medium;
}
&:active { opacity: 0.85; }
}
/* ====== 底部安全区 ====== */ /* ====== 底部安全区 ====== */
.safe-bottom { .safe-bottom {
height: calc(40rpx + env(safe-area-inset-bottom)); height: calc(40rpx + env(safe-area-inset-bottom));
+24 -5
View File
@@ -97,7 +97,7 @@
<view class="name-price-row"> <view class="name-price-row">
<text class="scenic-name">{{ scenic.name }}</text> <text class="scenic-name">{{ scenic.name }}</text>
<view class="price-group"> <view class="price-group">
<text class="price-symbol">¥</text> <text class="price-symbol">¥</text>
<text class="price-value">{{ scenic.price }}</text> <text class="price-value">{{ scenic.price }}</text>
</view> </view>
</view> </view>
@@ -122,7 +122,7 @@
<text>全年还剩{{ scenic.remainingCount }}次</text> <text>全年还剩{{ scenic.remainingCount }}次</text>
</view> </view>
<view v-else-if="scenic.freeCount > 0" class="remaining-tag"> <view v-else-if="scenic.freeCount > 0" class="remaining-tag">
<text>全年可用{{ scenic.freeCount }}次</text> <text>全年最多可用{{ scenic.freeCount }}次</text>
</view> </view>
<view class="benefit-tag" :class="scenic.needReservation ? 'warning' : 'success'"> <view class="benefit-tag" :class="scenic.needReservation ? 'warning' : 'success'">
<text>{{ scenic.needReservation ? '需预约' : '免预约' }}</text> <text>{{ scenic.needReservation ? '需预约' : '免预约' }}</text>
@@ -132,7 +132,7 @@
<!-- 温馨提示 --> <!-- 温馨提示 -->
<view v-if="scenic.tips" class="tips-bar"> <view v-if="scenic.tips" class="tips-bar">
<view class="tips-icon">!</view> <view class="tips-icon">!</view>
<text class="tips-text">{{ scenic.tips }}</text> <rich-text class="tips-text" :nodes="scenic.tips" />
</view> </view>
</view> </view>
</view> </view>
@@ -195,7 +195,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { post } from '../../../api/request' import { post } from '../../../api/request'
interface ScenicItem { interface ScenicItem {
@@ -592,7 +592,12 @@ function clearSearch() {
} }
function goBack() { function goBack() {
const pages = getCurrentPages()
if (pages.length <= 1) {
uni.switchTab({ url: '/pages/index/index' })
} else {
uni.navigateBack() uni.navigateBack()
}
} }
function onCardClick(scenic: ScenicItem) { function onCardClick(scenic: ScenicItem) {
@@ -600,6 +605,18 @@ function onCardClick(scenic: ScenicItem) {
url: `/packages/content/pages/scenic-detail?shopId=${scenic.shopId}`, url: `/packages/content/pages/scenic-detail?shopId=${scenic.shopId}`,
}) })
} }
onShareAppMessage(() => ({
title: navTitle.value,
path: '/packages/content/pages/scenic-list',
imageUrl: 'https://aliyunoss.hn12301.net/sd/aizaipingxiang.png',
}))
onShareTimeline(() => ({
title: navTitle.value,
query: '',
imageUrl: 'https://aliyunoss.hn12301.net/sd/aizaipingxiang.png',
}))
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -997,6 +1014,7 @@ function onCardClick(scenic: ScenicItem) {
font-size: $font-size-xl; font-size: $font-size-xl;
color: $uv-error; color: $uv-error;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
margin-left: 5rpx;
} }
/* 营业时间 + 营业状态 同行 */ /* 营业时间 + 营业状态 同行 */
@@ -1173,7 +1191,8 @@ function onCardClick(scenic: ScenicItem) {
.tips-text { .tips-text {
font-size: $font-size-sm; font-size: $font-size-sm;
color: $uv-content-color; color: $uv-content-color;
line-height: 1.5; line-height: 1.6;
display: block;
} }
/* ====== uni-popup 弹窗内容 ====== */ /* ====== uni-popup 弹窗内容 ====== */
+16 -1
View File
@@ -26,7 +26,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { post, getToken } from '../../../api/request' import { post, getToken } from '../../../api/request'
import { BASE_URL } from '../../../config' import { BASE_URL } from '../../../config'
import { useUserStore } from '../../../store/user' import { useUserStore } from '../../../store/user'
@@ -42,6 +42,7 @@ const buyPrice = ref('0')
const skuId = ref('') const skuId = ref('')
const loading = ref(true) const loading = ref(true)
const paying = ref(false) const paying = ref(false)
const shareTitle = ref('爱上萍乡旅游年卡')
onLoad((query) => { onLoad((query) => {
goodsId.value = (query?.goodsId as string) || '821926819630853' goodsId.value = (query?.goodsId as string) || '821926819630853'
@@ -55,6 +56,7 @@ function sanitizeHtml(html: string): string {
interface CardModelDetail { interface CardModelDetail {
cardModel: any cardModel: any
name: string
goods: { id: string; mainPic: string } goods: { id: string; mainPic: string }
goodsInfo: { introduction: string } goodsInfo: { introduction: string }
sku: Array<{ id: string; buyPrice: number }> sku: Array<{ id: string; buyPrice: number }>
@@ -65,6 +67,7 @@ onMounted(async () => {
const data = await post<CardModelDetail>(`${BASE_URL}/open-api/xgh/cardModelDetail?goodsId=${goodsId.value}`, {}) const data = await post<CardModelDetail>(`${BASE_URL}/open-api/xgh/cardModelDetail?goodsId=${goodsId.value}`, {})
cardModleId.value = data.cardModel?.id || '' cardModleId.value = data.cardModel?.id || ''
mainPic.value = data.goods?.mainPic || '' mainPic.value = data.goods?.mainPic || ''
shareTitle.value = data.name || data.cardModel?.name || '爱上萍乡旅游年卡'
introduction.value = sanitizeHtml(data.goodsInfo?.introduction || '') introduction.value = sanitizeHtml(data.goodsInfo?.introduction || '')
const sku = data.sku?.[0] const sku = data.sku?.[0]
skuId.value = sku?.id || '' skuId.value = sku?.id || ''
@@ -77,6 +80,18 @@ onMounted(async () => {
} }
}) })
onShareAppMessage(() => ({
title: shareTitle.value,
path: `/packages/user/pages/buy?goodsId=${goodsId.value}`,
imageUrl: mainPic.value,
}))
onShareTimeline(() => ({
title: shareTitle.value,
query: `goodsId=${goodsId.value}`,
imageUrl: mainPic.value,
}))
async function handleBuy() { async function handleBuy() {
if (!userStore.isLogin) { if (!userStore.isLogin) {
uni.navigateTo({ url: '/pages/login/login' }) uni.navigateTo({ url: '/pages/login/login' })
+14 -1
View File
@@ -56,7 +56,7 @@
<view class="promo" @click="goCardDetail()"> <view class="promo" @click="goCardDetail()">
<image <image
class="promo-bg" class="promo-bg"
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=400&fit=crop" src="https://aliyunoss.hn12301.net/sd/aishang_goods.jpg"
mode="aspectFill" mode="aspectFill"
/> />
<view class="promo-overlay" /> <view class="promo-overlay" />
@@ -136,6 +136,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import AnnouncementModal from '../../components/announcement-modal.vue' import AnnouncementModal from '../../components/announcement-modal.vue'
import { post } from '../../api/request' import { post } from '../../api/request'
import type { BannerItem } from '../../api/types' import type { BannerItem } from '../../api/types'
@@ -187,6 +188,18 @@ onMounted(async () => {
} }
}) })
onShareAppMessage(() => ({
title: '爱上萍乡旅游年卡',
path: '/pages/index/index',
imageUrl: 'https://aliyunoss.hn12301.net/sd/aizaipingxiang.png',
}))
onShareTimeline(() => ({
title: '爱上萍乡旅游年卡',
query: '',
imageUrl: 'https://aliyunoss.hn12301.net/sd/aizaipingxiang.png',
}))
function onBannerClick(banner: BannerItem) { function onBannerClick(banner: BannerItem) {
if (!banner.linkUrl) return if (!banner.linkUrl) return
if (banner.linkUrl.startsWith('http')) { if (banner.linkUrl.startsWith('http')) {
+3 -3
View File
@@ -19,7 +19,7 @@
> >
<view class="btn-primary__inner"> <view class="btn-primary__inner">
<view class="wx-icon" /> <view class="wx-icon" />
<text class="btn-primary__text">微信一键登录</text> <text class="btn-primary__text">一键登录</text>
</view> </view>
</button> </button>
<!-- 已同意条款:微信授权按钮 --> <!-- 已同意条款:微信授权按钮 -->
@@ -33,7 +33,7 @@
> >
<view v-if="!loading" class="btn-primary__inner"> <view v-if="!loading" class="btn-primary__inner">
<view class="wx-icon" /> <view class="wx-icon" />
<text class="btn-primary__text">微信一键登录</text> <text class="btn-primary__text">一键登录</text>
</view> </view>
<view v-else class="btn-primary__inner"> <view v-else class="btn-primary__inner">
<view class="loading-spinner" /> <view class="loading-spinner" />
@@ -102,7 +102,7 @@ function openPrivacy() {
uni.navigateTo({ url: '/pages/login/privacy' }) uni.navigateTo({ url: '/pages/login/privacy' })
} }
/** 微信一键登录(获取手机号后调后端 OAuth2 登录接口) */ /** 一键登录(获取手机号后调后端 OAuth2 登录接口) */
async function handleLogin(e: AnyObject) { async function handleLogin(e: AnyObject) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') { if (e.detail.errMsg !== 'getPhoneNumber:ok') {
return return
+1 -1
View File
@@ -334,7 +334,7 @@ function onFaceModalConfirm() {
const featureActions = [ const featureActions = [
{ label: '我要绑定', icon: 'https://aliyunoss.hntgov.net/pxmall/card.png' }, { label: '我要绑定', icon: 'https://aliyunoss.hntgov.net/pxmall/card.png' },
{ label: '我要预约', icon: 'https://aliyunoss.hntgov.net/pxmall/reserve.png' }, { label: '我要预约', icon: 'https://aliyunoss.hntgov.net/pxmall/reserve.png' },
{ label: '使用记录', icon: 'https://aliyunoss.hntgov.net/pxmall/record.png' }, { label: '预约/使用记录', icon: 'https://aliyunoss.hntgov.net/pxmall/record.png' },
{ label: '使用说明', icon: 'https://aliyunoss.hntgov.net/pxmall/scenic.png' }, { label: '使用说明', icon: 'https://aliyunoss.hntgov.net/pxmall/scenic.png' },
] ]