vending-machine/mobile/pages/index/index.vue
18631081161 d43406380c
Some checks reported errors
continuous-integration/drone/push Build encountered an error
逻辑优化
2026-04-13 13:43:06 +08:00

533 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="home-page">
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<text class="nav-title">首页</text>
</view>
</view>
<!-- 页面内容 -->
<view :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- Banner轮播图 -->
<view class="banner-wrap">
<swiper class="banner-swiper" autoplay circular indicator-dots indicator-color="rgba(255,255,255,0.4)"
indicator-active-color="#ffffff">
<swiper-item v-for="banner in banners" :key="banner.id" @click="onBannerClick(banner)">
<image class="banner-image" :src="resolveImageUrl(banner.imageUrl)" mode="aspectFill" />
</swiper-item>
</swiper>
</view>
<!-- 成为会员入口 -->
<view class="membership-entry" @click="onEntryClick({ key: 'membership' })">
<view class="membership-entry-left">
<view class="membership-icon-wrap">
<image class="membership-icon-img" src="/static/ic_vip.png" mode="aspectFit" />
</view>
<view class="membership-text">
<text class="membership-title">{{ t('home.membership') }}</text>
<text class="membership-sub">Unlock VIP Benefits</text>
</view>
</view>
<text class="membership-arrow"></text>
</view>
<!-- 功能入口(节日印花 + 会员二维码) -->
<view class="entry-row">
<view class="entry-card" @click="onEntryClick({ key: 'stamps' })">
<image class="entry-icon" src="/static/ic_stamp.png" mode="aspectFit" />
<text class="entry-label">{{ t('home.stamps') }}</text>
</view>
<view class="entry-card" @click="onEntryClick({ key: 'qrcode' })">
<image class="entry-icon" src="/static/ic_qrcode.png" mode="aspectFit" />
<text class="entry-label">{{ t('home.qrcode') }}</text>
</view>
</view>
<!-- 可兑换优惠券 -->
<view class="coupon-header">
<text class="coupon-title">{{ t('home.redeemableCoupons') || '可兑换优惠券' }}</text>
<view class="coupon-guide-link" @click="handleGuideEntry">
<image class="guide-icon-img" src="/static/ic_explain.png" mode="aspectFit" />
<text class="guide-text">{{ t('home.guide') }}</text>
</view>
</view>
<view class="coupon-list">
<view class="coupon-card" v-for="coupon in coupons" :key="coupon.id">
<image class="coupon-bg" src="/static/ic_coupon_bg.png" mode="aspectFill" />
<view class="coupon-inner">
<view class="coupon-info">
<text class="coupon-name">{{ coupon.name }}</text>
<view class="coupon-detail-row">
<text class="coupon-dot">●</text>
<text class="coupon-detail-label">{{ t('couponCard.expireLabel') }}</text>
<text class="coupon-detail-value highlight">{{ formatExpire(coupon.expireAt) }}</text>
</view>
<view class="coupon-detail-row">
<text class="coupon-dot">●</text>
<text class="coupon-detail-label">{{ t('couponCard.conditionLabel') }}</text>
<text class="coupon-detail-value highlight">{{ coupon.pointsCost ?? 0 }}{{ t('couponCard.pointsUnit') }}</text>
</view>
</view>
<view class="coupon-action">
<view class="redeem-btn" @click="onRedeemClick(coupon)">
<text class="redeem-btn-text">{{ t('couponCard.redeemBtn') }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<EmptyState v-if="coupons.length === 0" text="暂无可兑换优惠券" />
<!-- 使用说明弹窗 -->
<CouponGuidePopup :visible="showGuide" :content="guideContent" @close="showGuide = false" />
<!-- 兑换确认弹窗 -->
<RedeemPopup :visible="showRedeem" :coupon="selectedCoupon" @confirm="onRedeemConfirm"
@cancel="showRedeem = false" />
<!-- 会员二维码弹窗 -->
<QrcodePopup :visible="showQrcode" @close="showQrcode = false" />
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
useI18n
} from 'vue-i18n'
import {
getBanners,
getEntries,
getCouponGuide
} from '../../api/content.js'
import {
getRedeemableCoupons,
redeemCoupon
} from '../../api/coupon.js'
import {
useUserStore
} from '../../stores/user.js'
import {
navigateBanner
} from '../../utils/navigation.js'
import {
resolveImageUrl
} from '../../utils/image.js'
import RedeemPopup from '../../components/RedeemPopup.vue'
import CouponGuidePopup from '../../components/CouponGuidePopup.vue'
import QrcodePopup from '../../components/QrcodePopup.vue'
import EmptyState from '../../components/EmptyState.vue'
const {
t
} = useI18n()
const userStore = useUserStore()
// 状态栏高度
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
// 数据
const banners = ref([])
const entries = ref([])
const coupons = ref([])
// 弹窗状态
const showGuide = ref(false)
const guideContent = ref('')
const showRedeem = ref(false)
const selectedCoupon = ref(null)
const showQrcode = ref(false)
// Banner点击导航
function onBannerClick(banner) {
navigateBanner(banner)
}
// 功能入口点击
async function onEntryClick(entry) {
switch (entry.key) {
case 'membership':
uni.navigateTo({
url: '/pages/membership/membership'
})
break
case 'stamps':
if (!userStore.isLoggedIn) {
uni.navigateTo({
url: '/pages/login/login'
})
return
}
uni.navigateTo({
url: '/pages/stamps/stamps'
})
break
case 'qrcode':
await handleQrcodeEntry()
break
case 'guide':
await handleGuideEntry()
break
}
}
// 会员二维码入口:判断会员状态
async function handleQrcodeEntry() {
if (!userStore.isLoggedIn) {
uni.navigateTo({
url: '/pages/login/login'
})
return
}
try {
// 如果已有用户信息缓存,直接判断,不等网络请求
if (userStore.userInfo?.isMember) {
showQrcode.value = true
// 后台静默刷新用户信息
userStore.fetchUserInfo().catch(() => {})
return
}
// 没有缓存时才等待请求
await userStore.fetchUserInfo()
if (userStore.userInfo?.isMember) {
showQrcode.value = true
} else {
uni.navigateTo({
url: '/pages/membership/membership'
})
}
} catch (e) {}
}
// 使用说明入口
async function handleGuideEntry() {
try {
const res = await getCouponGuide()
guideContent.value = res.data?.content || res.data || ''
showGuide.value = true
} catch (e) {}
}
// 格式化到期时间
function formatExpire(dateStr) {
if (!dateStr) return t('couponCard.noLimit')
const d = new Date(dateStr)
return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`
}
// 点击兑换按钮
function onRedeemClick(coupon) {
selectedCoupon.value = coupon
showRedeem.value = true
}
// 确认兑换
async function onRedeemConfirm() {
if (!selectedCoupon.value) return
try {
await redeemCoupon(selectedCoupon.value.id)
showRedeem.value = false
selectedCoupon.value = null
uni.showToast({
title: t('home.redeemSuccess'),
icon: 'none'
})
await loadCoupons()
} catch (e) {
showRedeem.value = false
}
}
// 加载数据
async function loadBanners() {
try {
const res = await getBanners()
banners.value = res.data || []
} catch (e) {}
}
async function loadEntries() {
try {
const res = await getEntries()
entries.value = res.data || []
} catch (e) {}
}
async function loadCoupons() {
try {
const res = await getRedeemableCoupons()
coupons.value = res.data || []
} catch (e) {}
}
onMounted(() => {
loadBanners()
loadEntries()
loadCoupons()
})
</script>
<style scoped>
/* 自定义导航栏 */
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.home-page {
min-height: 100vh;
background-color: #f5f0e8;
}
/* Banner */
.banner-wrap {
padding: 24rpx 24rpx 0;
}
.banner-swiper {
width: 100%;
height: 340rpx;
border-radius: 20rpx;
overflow: hidden;
}
.banner-image {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
/* 成为会员入口 */
.membership-entry {
margin: 24rpx 24rpx 0;
background: linear-gradient(135deg, #4a5d4a, #3d4f3d);
border-radius: 20rpx;
padding: 30rpx 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.membership-entry-left {
display: flex;
align-items: center;
}
.membership-icon-wrap {
width: 64rpx;
height: 64rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.membership-icon-img {
width: 40rpx;
height: 40rpx;
}
.membership-text {
display: flex;
flex-direction: column;
}
.membership-title {
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
margin-bottom: 4rpx;
}
.membership-sub {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
}
.membership-arrow {
font-size: 40rpx;
color: rgba(255, 255, 255, 0.6);
}
/* 功能入口行 */
.entry-row {
display: flex;
padding: 24rpx 24rpx 0;
gap: 24rpx;
}
.entry-card {
flex: 1;
background-color: #ffffff;
border-radius: 20rpx;
padding: 36rpx 0;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.entry-icon {
width: 72rpx;
height: 72rpx;
margin-bottom: 16rpx;
}
.entry-label {
font-size: 26rpx;
color: #333;
}
/* 优惠券标题行 */
.coupon-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 24rpx 16rpx;
}
.coupon-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.coupon-guide-link {
display: flex;
align-items: center;
}
.guide-icon-img {
width: 28rpx;
height: 28rpx;
margin-right: 6rpx;
}
.guide-text {
font-size: 24rpx;
color: #999;
}
/* 优惠券列表 */
.coupon-list {
padding: 0 24rpx 24rpx;
}
.coupon-card {
position: relative;
margin-bottom: 20rpx;
border-radius: 20rpx;
overflow: hidden;
}
.coupon-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.coupon-inner {
position: relative;
z-index: 1;
padding: 32rpx;
display: flex;
flex-direction: column;
min-height: 240rpx;
}
.coupon-info {
flex: 1;
}
.coupon-name {
font-size: 34rpx;
color: #333;
font-weight: 600;
display: block;
margin-left: 28rpx;
margin-bottom: 16rpx;
}
.coupon-detail-row {
display: flex;
align-items: center;
margin-left: 28rpx;
margin-bottom: 8rpx;
}
.coupon-dot {
font-size: 14rpx;
color: #D9DED7;
margin-right: 10rpx;
}
.coupon-detail-label {
font-size: 24rpx;
color: #666;
}
.coupon-detail-value {
font-size: 24rpx;
color: #666;
}
.coupon-detail-value.highlight {
color: #c9a96e;
font-weight: 500;
}
/* 兑换按钮 */
.coupon-action {
display: flex;
justify-content: flex-end;
margin-top: 16rpx;
}
.redeem-btn {
background: linear-gradient(135deg, #d4a855, #c9a96e);
border-radius: 28rpx;
width: 172rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.redeem-btn-text {
font-size: 28rpx;
color: #ffffff;
white-space: nowrap;
}
</style>