瀏覽代碼

首页接口联调

haiyang 1 周之前
父節點
當前提交
f2ccc50183

+ 5 - 0
pages.config.ts

@@ -29,6 +29,11 @@ export default defineUniPages({
             postcss: true,
         },
         lazyCodeLoading: 'requiredComponents',
+        // 微信小程序全局分享配置
+        'window': {
+            'enableShareAppMessage': true,
+            'enableShareTimeline': true,
+        }
     },
     // tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
     'tabBar': tabBar as any,

+ 10 - 4
src/App.ku.vue

@@ -1,19 +1,25 @@
 <script setup lang="ts">
 import { ref } from 'vue'
 import FgTabbar from '@/tabbar/index.vue'
+import { useLoadingStore } from './store/loading'
 import { isPageTabbar, tabbarStore } from './tabbar/store'
 import { currRoute } from './utils'
 
 const isCurrentPageTabbar = ref(true)
+const loadingStore = useLoadingStore()
+
 onShow(() => {
     console.log('App.ku.vue onShow', currRoute())
     const { path } = currRoute()
     // “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
     // 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
     isCurrentPageTabbar.value = path === '/' || isPageTabbar(path)
-    if (isCurrentPageTabbar.value) {
-        tabbarStore.setAutoCurIdx(path)
-    }
+    setTimeout(() => {
+        if (isCurrentPageTabbar.value) {
+            tabbarStore.setAutoCurIdx(path)
+        }
+    }, 0)
+    loadingStore.hideLoading()
 })
 
 const helloKuRoot = ref('Hello AppKuVue')
@@ -33,7 +39,7 @@ defineExpose({
         </view> -->
 
         <KuRootView />
-
+        <up-loading-page :loading="loadingStore.isLoading" z-index="10000" size="50" />
         <FgTabbar v-if="isCurrentPageTabbar" />
     </view>
 </template>

+ 25 - 0
src/api/home.ts

@@ -13,10 +13,35 @@ export function getCouponList() {
     })
 }
 
+/**
+ *
+ * @returns 获取首页收益总数
+ */
 export function getAccountCount() {
     return http.Get<AccountCount>('/couponCenter/APP/couponIssuerAccount/queryByUserId')
 }
 
+/**
+ *
+ * @returns 获取首页优惠券领取情况
+ */
 export function getCouponSituation() {
     return http.Get<CouponSituation>('/couponCenter/APP/couponUserAsset/queryBySendUserId')
 }
+
+/**
+ * 根据优惠券类型获取优惠券数据
+ * @param params
+ * @returns
+ */
+export function getCouponByType(params) {
+    return http.Get('/couponCenter/APP/couponTemplate/getTemplatesPageByType', {
+        params,
+    })
+}
+
+export function getHomeCouponRedemptionList(params) {
+    return http.Post('/couponCenter/APP/couponUserAsset/detail', {
+        ...params,
+    })
+}

+ 1 - 1
src/api/types/coupon.ts

@@ -1,6 +1,6 @@
 export interface CouponList {
     discountCoupon: []
-    equityVoucher: []
+    fullReduceCoupon: []
 }
 
 export interface AccountCount {

+ 12 - 8
src/components/discountCoupon.vue

@@ -59,7 +59,9 @@ const deadline = computed(() => {
 
 <style lang="scss" scoped>
 .discount-coupon {
-    width: 670rpx;
+    // width: 670rpx;
+    width: 100%;
+    max-width: 800rpx;
     height: 209rpx;
     position: relative;
     overflow: hidden;
@@ -70,15 +72,16 @@ const deadline = computed(() => {
         left: 0;
         width: 100%;
         height: 100%;
+        object-fit: cover;
         z-index: 0;
     }
 
     .discount-content {
         height: 95rpx;
         position: absolute;
-        top: 40%;
-        left: 40%;
-        transform: translate(-48%, -60%);
+        top: 35%;
+        left: 43%;
+        transform: translate(-50%, -50%);
         display: flex;
         flex-direction: row;
         justify-content: center;
@@ -126,6 +129,7 @@ const deadline = computed(() => {
                 font-weight: 400;
                 font-size: 22rpx;
                 color: #333333;
+                line-height: 30rpx;
             }
         }
     }
@@ -133,8 +137,8 @@ const deadline = computed(() => {
     .discount-desc-time {
         position: absolute;
         bottom: 8%;
-        left: 29%;
-        transform: translate(-50%, -50%);
+        left: 5%;
+        // transform: translate(0, -50%);
         font-weight: 400;
         font-size: 18rpx;
         color: #666666;
@@ -147,8 +151,8 @@ const deadline = computed(() => {
         font-size: 28rpx;
         color: #ffffff;
         top: 50%;
-        right: 3%;
-        transform: translate(-49%, -60%);
+        right: 7%;
+        transform: translateY(-60%);
         display: flex;
         flex-direction: column;
         justify-content: center;

+ 200 - 0
src/components/spendAndSaveCoupon_large.vue

@@ -0,0 +1,200 @@
+<script lang="ts" setup>
+import CouponImg from '@img/index/coupon3.png'
+
+const props = defineProps({
+    coupon: {
+        type: Object,
+        default: () => ({
+            ruleReductionAmount: '0',
+            ruleMinSpendAmount: '0',
+        }),
+    }
+})
+const coupon = computed(() => props.coupon)
+console.log(coupon.value)
+const deadline = computed(() => {
+    const type = props.coupon.validityType
+    if (type === '1') {
+        return `自领取之日起${props.coupon.validDays}日内使用`
+    }
+    else if (type === '2') {
+        return `${props.coupon.validStartTime}至${props.coupon.validEndTime}`
+    }
+    return '长期有效'
+})
+
+const category = computed(() => {
+    const relatedType = props.coupon.relatedType
+    if (relatedType === '1') {
+        return `限${props.coupon.relatedName}分类商品使用`
+    }
+    else {
+        return `限${props.coupon.relatedName}商品使用`
+    }
+})
+</script>
+
+<template>
+    <view class="discount-coupon">
+        <image class="coupon-bg" :src="CouponImg" mode="aspectFit" />
+        <view class="coupon-content">
+            <view class="coupon-amount">
+                <view class="coupon-amount-count">
+                    <view class="coupon-amount-count-icon">
+                        ¥
+                    </view>
+                    <view class="coupon-amount-count-number">
+                        {{ coupon?.ruleReductionAmount }}
+                    </view>
+                </view>
+                <view class="coupon-amount-text">
+                    满{{ coupon?.ruleMinSpendAmount }}元可用
+                </view>
+            </view>
+            <view class="coupon-info">
+                <view class="coupon-info-category">
+                    {{ category }}
+                </view>
+                <view class="coupon-info-time">
+                    <template v-if="coupon.validityType === '1'">
+                        <view>{{ deadline }}</view>
+                    </template>
+                    <template v-else-if="coupon.validityType === '2'">
+                        <view>使用日期</view>
+                        <view>{{ deadline }}</view>
+                    </template>
+                    <template v-else>
+                        <view>{{ deadline }}</view>
+                    </template>
+                </view>
+            </view>
+        </view>
+        <view class="discount-btn">
+            <view class="coupon-btn-container">
+                <view>立即</view>
+                <view>发券</view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.discount-coupon {
+    // width: 670rpx;
+    width: 100%;
+    max-width: 800rpx;
+    height: 200rpx;
+    position: relative;
+    overflow: hidden;
+
+    .coupon-bg {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        width: 100%;
+        height: 100%;
+        z-index: 0;
+        object-fit: cover;
+    }
+
+    .coupon-content {
+        position: absolute;
+        z-index: 1;
+        height: 161.5rpx;
+        width: calc(100% - 27.68%);
+        padding-top: 18rpx;
+        padding-left: 20rpx;
+        padding-bottom: 20rpx;
+        display: flex;
+        flex-direction: row;
+        gap: 3rpx;
+
+        .coupon-amount {
+            height: 100%;
+            width: 32%;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+
+            .coupon-amount-count {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: baseline;
+
+                .coupon-amount-count-icon {
+                    font-weight: 500;
+                    font-size: 27rpx;
+                    color: #f83003;
+                }
+
+                .coupon-amount-count-number {
+                    font-weight: 500;
+                    font-size: 70rpx;
+                    color: #f83003;
+                    clear: both;
+                }
+            }
+
+            .coupon-amount-text {
+                font-weight: 400;
+                font-size: 24rpx;
+                color: #f3513b;
+            }
+        }
+
+        .coupon-info {
+            height: 100%;
+            width: 68%;
+            display: flex;
+            flex-direction: column;
+            padding-left: 18rpx;
+
+            .coupon-info-category {
+                padding-top: 33rpx;
+                font-weight: 400;
+                font-size: 32rpx;
+                color: #333333;
+            }
+
+            .coupon-info-time {
+                padding-top: 14rpx;
+                display: flex;
+                flex-direction: column;
+                gap: 5rpx;
+                font-weight: 400;
+                font-size: 22rpx;
+                color: #888888;
+            }
+        }
+    }
+
+    .discount-btn {
+        position: absolute;
+        right: 0;
+        z-index: 1;
+        height: 100%;
+        width: 27.68%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .coupon-btn-container {
+            width: 115rpx;
+            height: 115rpx;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            gap: 10rpx;
+            transform: translateX(7rpx);
+
+            font-weight: 500;
+            font-size: 28rpx;
+            color: #ffffff;
+        }
+    }
+}
+</style>

+ 63 - 41
src/http/alova.ts

@@ -85,59 +85,81 @@ const alovaInstance = createAlova({
         }
     },
 
-    responded: (response, method) => {
-        const { config } = method
-        const { requestType } = config
-        const {
-            statusCode,
-            data: rawData,
-            errMsg,
-        } = response as UniNamespace.RequestSuccessCallbackResult
+    responded: {
+        onSuccess: (response, method) => {
+            const { config } = method
+            const { requestType } = config
+            const {
+                statusCode,
+                data: rawData,
+                errMsg,
+            } = response as UniNamespace.RequestSuccessCallbackResult
 
-        // 处理特殊请求类型(上传/下载)
-        if (requestType === 'upload' || requestType === 'download') {
-            return response
-        }
+            // 处理特殊请求类型(上传/下载)
+            if (requestType === 'upload' || requestType === 'download') {
+                return response
+            }
+
+            // 处理 HTTP 状态码错误
+            if (statusCode !== 200) {
+                // 直接处理401错误,跳转到登录页面
+                if (statusCode === 401) {
+                    uni.showToast({
+                        title: '登录已过期,请重新登录',
+                        icon: 'error',
+                        duration: 1500,
+                    })
+                    useTokenStore().cleanToken()
+                    setTimeout(() => {
+                        toLoginPage({ mode: 'reLaunch' })
+                    }, 1500)
+                    throw new Error('登录已过期')
+                }
 
-        // 处理 HTTP 状态码错误
-        if (statusCode !== 200) {
-            // 直接处理401错误,跳转到登录页面
-            if (statusCode === 401) {
+                const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
+                console.error('errorMessage===>', errorMessage)
                 uni.showToast({
-                    title: '登录已过期,请重新登录',
+                    title: errorMessage,
                     icon: 'error',
-                    duration: 1500,
                 })
-                useTokenStore().cleanToken()
-                setTimeout(() => {
-                    toLoginPage({ mode: 'reLaunch' })
-                }, 1500)
-                throw new Error('登录已过期')
+                throw new Error(`${errorMessage}:${errMsg}`)
+            }
+
+            // 处理业务逻辑错误
+            const { code, message, result: data } = rawData as IResponse
+            // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
+            if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+                if (config.meta?.toast !== false) {
+                    uni.showToast({
+                        title: message,
+                        icon: 'none',
+                    })
+                }
+                throw new Error(`请求错误[${code}]:${message}`)
+            }
+            // 处理成功响应,返回业务数据
+            return data
+        },
+        // 添加网络错误处理
+        onError: (error, method) => {
+            // 处理网络错误(断网、超时、DNS解析失败等)
+            console.error('网络错误:', error)
+
+            // 避免重复提示
+            if (error.message?.includes('HTTP请求错误') || error.message?.includes('请求错误[')) {
+                return
+            }
+
+            let errorMessage = '网络错误,请检查网络连接'
+            if (error.message?.includes('timeout')) {
+                errorMessage = '请求超时,请稍后重试'
             }
 
-            const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
-            console.error('errorMessage===>', errorMessage)
             uni.showToast({
                 title: errorMessage,
                 icon: 'error',
             })
-            throw new Error(`${errorMessage}:${errMsg}`)
-        }
-
-        // 处理业务逻辑错误
-        const { code, message, result: data } = rawData as IResponse
-        // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
-        if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
-            if (config.meta?.toast !== false) {
-                uni.showToast({
-                    title: message,
-                    icon: 'none',
-                })
-            }
-            throw new Error(`请求错误[${code}]:${message}`)
         }
-        // 处理成功响应,返回业务数据
-        return data
     },
 })
 

+ 4 - 0
src/main.ts

@@ -5,11 +5,15 @@ import { requestInterceptor } from './http/interceptor'
 import { routeInterceptor } from './router/interceptor'
 import store from './store'
 import { registerGlobalFilters } from './utils/directive'
+import { shareMixin } from './utils/shareMixin'
 import '@/style/index.scss'
 import 'virtual:uno.css'
 
 export function createApp() {
     const app = createSSRApp(App)
+    // 注册全局分享函数
+    app.mixin(shareMixin)
+
     app.use(store)
     app.use(uviewPlus)
     app.use(routeInterceptor)

+ 347 - 0
src/pages-A/couponRedemptionList/index.vue

@@ -0,0 +1,347 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { getHomeCouponRedemptionList } from '@/api/home'
+import { useScroll } from '@/hooks/useScroll'
+import { changtime, safeAreaInsets } from '@/utils'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+    },
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const currentState = ref('1')
+const currentCouponType = ref('3')
+const currentDate = ref(Date.now())
+const show = ref(false)
+const datetimePickerRef = ref(null)
+
+const {
+    list: data, // 响应式的数据列表
+    loading, // 是否加载中
+    finished, // 是否已全部加载
+    error, // 是否加载失败
+    refresh: onRefresh, // 下拉刷新方法
+    loadMore: onLoadMore, // 加载更多方法
+} = useScroll({
+    fetchData: async (page, pageSize) => {
+        const response = await getHomeCouponRedemptionList({
+            pageNo: page,
+            pageSize,
+            verificationStatus: currentState.value,
+            couponType: currentCouponType.value,
+            year: changtime(currentDate.value, 'YYYY'),
+            month: changtime(currentDate.value, 'MM'),
+        })
+        return response.records || []
+    },
+    pageSize: 7,
+})
+
+// 计算底部安全区高度
+const safeBottomHeight = computed(() => {
+    return safeAreaInsets?.bottom || 0
+})
+
+// 格式化日期显示:YYYY年MM月
+const formattedDate = computed(() => {
+    return changtime(currentDate.value, 'YYYY年MM月')
+})
+
+// 计算加载状态
+const status = computed(() => {
+    if (loading.value) {
+        return 'loading'
+    }
+    if (finished.value) {
+        return 'nomore'
+    }
+    if (error.value) {
+        return 'error'
+    }
+    return 'loadmore'
+})
+
+onLoad((options) => {
+    if (options.state) {
+        currentState.value = options.state
+    }
+})
+
+function redemptionChange(state) {
+    currentState.value = state
+    onRefresh()
+}
+
+function couponTypeChange(type) {
+    currentCouponType.value = type
+    onRefresh()
+}
+
+function datePickerConfirm() {
+    show.value = false
+    onRefresh()
+}
+</script>
+
+<template>
+    <view class="page-container">
+        <up-navbar title="优惠券" :auto-back="true" />
+        <up-status-bar />
+        <view class="redemption-type" :style="{ paddingTop: `${topSafeAreaHeight}px` }">
+            <view class="redemption-type-item" :class="{ active: currentState === '1' }" @click="redemptionChange('1')">
+                待核销
+            </view>
+            <up-line direction="col" color="#9A9B9B" length="40rpx" />
+            <view class="redemption-type-item" :class="{ active: currentState === '2' }" @click="redemptionChange('2')">
+                已核销
+            </view>
+        </view>
+        <view class="redemption-select">
+            <view class="redemption-select-item coupon-type">
+                <view class="coupon-type-item" :class="{ 'coupon-active': currentCouponType === '3' }"
+                    @click="couponTypeChange('3')">
+                    满减券
+                </view>
+                <view class="coupon-type-item" :class="{ 'coupon-active': currentCouponType === '2' }"
+                    @click="couponTypeChange('2')">
+                    折扣券
+                </view>
+            </view>
+            <view class="redemption-select-item">
+                <up-datetime-picker ref="datetimePickerRef" v-model="currentDate" :show="show" mode="year-month"
+                    @cancel="show = false" @confirm="datePickerConfirm" />
+                <view class="date-display" @click="show = true">
+                    {{ formattedDate }}
+                    <up-icon name="arrow-down" />
+                </view>
+            </view>
+        </view>
+        <view class="redemption-content">
+            <scroll-view scroll-y :refresher-enabled="true" :refresher-triggered="loading" @refresherrefresh="onRefresh"
+                @scrolltolower="onLoadMore">
+                <template v-for="(item, index) in data" :key="index">
+                    <view class="redemption-content-card">
+                        <template v-if="currentState === '1'">
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    优惠券名称
+                                </view>
+                                <view class="redemption-content-item-header-right">
+                                    {{ item?.couponName || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    用户名称
+                                </view>
+                                <view class="redemption-content-item-header-right">
+                                    {{ item?.receiveUserName || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    到期时间
+                                </view>
+                                <view class="redemption-content-item-right">
+                                    {{ item?.expireTime || '--' }}
+                                </view>
+                            </view>
+                        </template>
+                        <template v-if="currentState === '2'">
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    核销时间
+                                </view>
+                                <view class="redemption-content-item-header-right">
+                                    {{ item?.verificationTime || '--' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    优惠券名称
+                                </view>
+                                <view class="redemption-content-item-header-right">
+                                    {{ item?.couponName || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    核销用户
+                                </view>
+                                <view class="redemption-content-item-header-right">
+                                    {{ item?.receiveUserName || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    商品名称
+                                </view>
+                                <view class="redemption-content-item-right">
+                                    {{ item?.orderItemName || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    实付金额
+                                </view>
+                                <view class="redemption-content-item-right">
+                                    {{ item?.orderAmount || '' }}
+                                </view>
+                            </view>
+                            <view class="redemption-content-item">
+                                <view class="redemption-content-item-left">
+                                    预计获得佣金
+                                </view>
+                                <view class="redemption-content-item-right">
+                                    {{ item?.commission || '' }}
+                                </view>
+                            </view>
+                        </template>
+                    </view>
+                </template>
+                <template v-if="!data || data.length === 0">
+                    <up-empty mode="data" style="height: 100%" />
+                </template>
+                <!-- 加载状态提示 -->
+                <template v-else>
+                    <up-loadmore :status="status" />
+                </template>
+            </scroll-view>
+        </view>
+        <view class="safe-bottom" :style="{ height: `${safeBottomHeight}px` }" />
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+    height: 100vh;
+    background-color: #f8f8fa;
+    display: flex;
+    flex-direction: column; // 设置为flex布局,让子元素垂直排列
+
+    .redemption-type {
+        width: 100%;
+        height: 88rpx;
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+        background-color: #ffffff;
+        position: relative;
+    }
+
+    .redemption-type-item {
+        flex: 1;
+        text-align: center;
+        font-weight: 400;
+        font-size: 30rpx;
+        color: #888888;
+        line-height: 33rpx;
+        transition: color 0.3s ease; // 添加颜色过渡动画
+        position: relative;
+        z-index: 1; // 确保文字在指示器上方
+        padding: 20rpx 0; // 增加点击区域
+
+        &.active {
+            color: #333333;
+            font-weight: bolder;
+        }
+    }
+
+    .indicator {
+        position: absolute;
+        bottom: 0;
+        width: 30%;
+        height: 4rpx;
+        background-color: #333333;
+        transition: left 0.3s ease; // 添加左右移动的过渡动画
+        transform: translateX(-50%); // 居中对齐
+    }
+
+    .redemption-select {
+        height: 90rpx;
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        padding: 0 20rpx;
+
+        .coupon-type {
+            display: flex;
+            flex-direction: row;
+            flex-wrap: nowrap;
+            justify-content: center;
+            align-items: center;
+            gap: 19rpx;
+
+            .coupon-type-item {
+                width: 118rpx;
+                height: 45rpx;
+                padding: 11rpx 21rpx 10rpx 21rpx;
+                text-align: center;
+                background: #ffffff;
+                border-radius: 35rpx;
+                border: 1px solid #e5e5e5;
+
+                &.coupon-active {
+                    font-weight: 400;
+                    color: #ffffff;
+                    background: linear-gradient(180deg, #ff7a72 0%, #ff3500 100%);
+                }
+            }
+        }
+
+        .redemption-select-item {
+            .date-display {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                gap: 13rpx;
+            }
+        }
+    }
+
+    .redemption-content {
+        flex: 1; // 占据剩余空间
+        padding: 9px 20rpx;
+        overflow: hidden; // 隐藏超出部分
+
+        scroll-view {
+            height: 100%; // 充满整个容器
+        }
+
+        .redemption-content-card {
+            background-color: #ffffff;
+            border-radius: 10rpx;
+            padding: 39rpx 20rpx;
+            display: flex;
+            flex-direction: column;
+            flex-wrap: nowrap;
+            justify-content: space-around;
+            gap: 40rpx;
+
+            .redemption-content-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                justify-content: space-between;
+
+                .redemption-content-item-left {
+                    font-weight: 400;
+                    font-size: 26rpx;
+                    color: #666666;
+                }
+
+                .redemption-content-item-right {
+                    font-weight: 400;
+                    font-size: 26rpx;
+                    color: #333333;
+                }
+            }
+        }
+    }
+}
+</style>

+ 111 - 0
src/pages-A/discountcouponList/index.vue

@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { useScroll } from '@/hooks/useScroll'
+import { safeAreaInsets } from '@/utils'
+import { getCouponByType } from '../../api/home'
+import DiscountCoupon from '../../components/discountCoupon.vue'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+        // 禁用默认下拉刷新
+        enablePullDownRefresh: false,
+    },
+})
+
+const params = ref()
+
+// 使用内置的useScroll hooks
+const {
+    list: data, // 响应式的数据列表
+    loading, // 是否加载中
+    finished, // 是否已全部加载
+    error, // 是否加载失败
+    refresh: onRefresh, // 下拉刷新方法
+    loadMore: onLoadMore, // 加载更多方法
+} = useScroll({
+    fetchData: async (page, pageSize) => {
+        const response = await getCouponByType({
+            pageNo: page,
+            pageSize,
+            ...params.value,
+        })
+        return response.records || []
+    },
+    pageSize: 7,
+})
+
+// 计算底部安全区高度
+const safeBottomHeight = computed(() => {
+    return safeAreaInsets?.bottom || 0
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+
+// 页面加载时获取数据
+onLoad((options) => {
+    if (options.type) {
+        params.value = options
+        onRefresh()
+    }
+})
+</script>
+
+<template>
+    <view class="page-container">
+        <up-navbar title="折扣券" :auto-back="true" />
+        <up-status-bar />
+        <scroll-view :style="{ paddingTop: `${topSafeAreaHeight}px` }" class="discount-coupon-list" scroll-y
+            refresher-enabled :refresher-triggered="loading" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
+            <view class="list-content">
+                <view v-for="item in data" :key="item.templateId" class="discount-coupon-item">
+                    <DiscountCoupon :coupon="item" />
+                </view>
+
+                <!-- 加载状态提示 -->
+                <view v-if="loading && data.length > 0" class="loadmore-container">
+                    <up-loadmore status="loading" loading-text="努力加载中..." icon-size="18" />
+                </view>
+                <view v-else-if="finished && data.length > 0" class="loadmore-container">
+                    <up-loadmore status="nomore" nomore-text="我们是有底线的" icon-size="18" />
+                </view>
+            </view>
+        </scroll-view>
+
+        <!-- 安全区底部占位 -->
+        <view class="safe-bottom" :style="{ height: `${safeBottomHeight}px` }" />
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+}
+
+.discount-coupon-list {
+    flex: 1;
+    overflow-y: auto;
+
+    .list-content {
+        padding-top: 20rpx;
+        padding-left: 24rpx;
+        padding-right: 24rpx;
+        display: flex;
+        flex-direction: column;
+        gap: 20rpx;
+    }
+
+    .loadmore-container {
+        padding: 30rpx 0;
+        text-align: center;
+    }
+}
+
+.safe-bottom {
+    width: 100%;
+    background-color: #fff;
+}
+</style>

+ 111 - 0
src/pages-A/spendAndSaveCouponList/index.vue

@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { useScroll } from '@/hooks/useScroll'
+import { safeAreaInsets } from '@/utils'
+import { getCouponByType } from '../../api/home'
+import SpendAndSaveCoupon from '../../components/spendAndSaveCoupon_large.vue'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+        // 禁用默认下拉刷新
+        enablePullDownRefresh: false,
+    },
+})
+
+const params = ref()
+
+// 使用内置的useScroll hooks
+const {
+    list: data, // 响应式的数据列表
+    loading, // 是否加载中
+    finished, // 是否已全部加载
+    error, // 是否加载失败
+    refresh: onRefresh, // 下拉刷新方法
+    loadMore: onLoadMore, // 加载更多方法
+} = useScroll({
+    fetchData: async (page, pageSize) => {
+        const response = await getCouponByType({
+            pageNo: page,
+            pageSize,
+            ...params.value,
+        })
+        return response.records || []
+    },
+    pageSize: 7,
+})
+
+// 计算底部安全区高度
+const safeBottomHeight = computed(() => {
+    return safeAreaInsets?.bottom || 0
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+
+// 页面加载时获取数据
+onLoad((options) => {
+    if (options.type) {
+        params.value = options
+        onRefresh()
+    }
+})
+</script>
+
+<template>
+    <view class="page-container">
+        <up-navbar title="满减券" :auto-back="true" />
+        <up-status-bar />
+        <scroll-view :style="{ paddingTop: `${topSafeAreaHeight}px` }" class="discount-coupon-list" scroll-y
+            refresher-enabled :refresher-triggered="loading" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
+            <view class="list-content">
+                <view v-for="item in data" :key="item.templateId" class="discount-coupon-item">
+                    <SpendAndSaveCoupon :coupon="item" />
+                </view>
+
+                <!-- 加载状态提示 -->
+                <view v-if="loading && data.length > 0" class="loadmore-container">
+                    <up-loadmore status="loading" loading-text="努力加载中..." icon-size="18" />
+                </view>
+                <view v-else-if="finished && data.length > 0" class="loadmore-container">
+                    <up-loadmore status="nomore" nomore-text="我们是有底线的" icon-size="18" />
+                </view>
+            </view>
+        </scroll-view>
+
+        <!-- 安全区底部占位 -->
+        <view class="safe-bottom" :style="{ height: `${safeBottomHeight}px` }" />
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+}
+
+.discount-coupon-list {
+    flex: 1;
+    overflow-y: auto;
+
+    .list-content {
+        padding-top: 20rpx;
+        padding-left: 24rpx;
+        padding-right: 24rpx;
+        display: flex;
+        flex-direction: column;
+        gap: 20rpx;
+    }
+
+    .loadmore-container {
+        padding: 30rpx 0;
+        text-align: center;
+    }
+}
+
+.safe-bottom {
+    width: 100%;
+    background-color: #fff;
+}
+</style>

+ 41 - 6
src/pages/index/index.vue

@@ -54,6 +54,24 @@ onShow(() => {
     }
 })
 
+// // 页面分享给朋友
+// onShareAppMessage(() => {
+//     return {
+//         title: '券中心',
+//         path: '/pages/index/index',
+//         imageUrl: '/static/logo.svg',
+//     }
+// })
+
+// // 页面分享到朋友圈
+// onShareTimeline(() => {
+//     return {
+//         title: '优惠券管理系统',
+//         path: '/pages/index/index',
+//         imageUrl: '/static/logo.svg',
+//     }
+// })
+
 watch(() => couponSituationData, (newVal) => {
     console.log('首页领券情况数据:', newVal)
 })
@@ -81,6 +99,23 @@ function getNavigationBarHeight() {
 }
 getNavigationBarHeight()
 // #endif
+
+function toDiscountCouponList() {
+    uni.navigateTo({
+        url: '/pages-A/discountcouponList/index?type=2',
+    })
+}
+function toSpendAndSaveCouponList() {
+    uni.navigateTo({
+        url: '/pages-A/spendAndSaveCouponList/index?type=3',
+    })
+}
+
+function toCouponRedemptionList(state) {
+    uni.navigateTo({
+        url: `/pages-A/couponRedemptionList/index?state=${state}`,
+    })
+}
 </script>
 
 <template>
@@ -102,7 +137,7 @@ getNavigationBarHeight()
                 </view>
             </view>
             <view class="home-header-tips">
-                <view class="home-header-tips-item">
+                <view class="home-header-tips-item" @click="toCouponRedemptionList('2')">
                     <view class="home-header-tips-item-num">
                         {{ couponSituationData?.usedQuantity || 0 }}张
                     </view>
@@ -110,7 +145,7 @@ getNavigationBarHeight()
                         已核销
                     </view>
                 </view>
-                <view class="home-header-tips-item">
+                <view class="home-header-tips-item" @click="toCouponRedemptionList('1')">
                     <view class="home-header-tips-item-num">
                         {{ couponSituationData?.quantityToBeUsed || 0 }}张
                     </view>
@@ -147,13 +182,13 @@ getNavigationBarHeight()
                 </view>
             </view>
             <view class="home-header-coupon-content">
-                <template v-for="item in discountVoucherList" :key="item.id">
+                <template v-for="item in couponList" :key="item.id">
                     <spend-and-save-coupon :coupon="item" />
                 </template>
             </view>
             <view class="home-header-coupon-btn">
                 <up-button class="home-header-coupon-btn-text" text="查看更多优惠券"
-                    color="linear-gradient(0deg, #FFE8CE 0%, #FBB8A0 100%)" />
+                    color="linear-gradient(0deg, #FFE8CE 0%, #FBB8A0 100%)" @click="toSpendAndSaveCouponList" />
             </view>
         </view>
         <!-- 折扣券 -->
@@ -166,7 +201,7 @@ getNavigationBarHeight()
                 <view class="home-coupon-title-des">
                     分享折扣&nbsp;&nbsp;立享优惠
                 </view>
-                <view class="home-coupon-title-more">
+                <view class="home-coupon-title-more" @click="toDiscountCouponList">
                     <view class="home-coupon-title-more-text">
                         更多
                     </view>
@@ -174,7 +209,7 @@ getNavigationBarHeight()
                 </view>
             </view>
             <view class="home-coupon-content">
-                <template v-for="item in couponList" :key="item.id">
+                <template v-for="item in discountVoucherList" :key="item.id">
                     <discount-coupon :coupon="item" />
                 </template>
             </view>

+ 49 - 10
src/router/interceptor.ts

@@ -1,4 +1,5 @@
 import { isMp } from '@uni-helper/uni-env'
+import { useLoadingStore } from '@/store/loading'
 /**
  * by 菲鸽 on 2025-08-19
  * 路由拦截,通常也是登录拦截
@@ -51,6 +52,11 @@ export const navigateToInterceptor = {
         if (url === undefined) {
             return
         }
+
+        const loadingStore = useLoadingStore()
+        // 显示加载页
+        loadingStore.showLoading('页面跳转中...')
+
         let { path, query: _query } = parseUrlToObj(url)
 
         FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------')
@@ -77,12 +83,19 @@ export const navigateToInterceptor = {
         // 处理路由不存在的情况
         if (!isRouteExists(path)) {
             console.warn('路由不存在:', path)
-            uni.navigateTo({ url: NOT_FOUND_PAGE })
+            uni.navigateTo({
+                url: NOT_FOUND_PAGE,
+                complete: () => {
+                    loadingStore.hideLoading()
+                }
+            })
             return false // 明确表示阻止原路由继续执行
         }
 
         // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
-        tabbarStore.setAutoCurIdx(path)
+        setTimeout(() => {
+            tabbarStore.setAutoCurIdx(path)
+        }, 0)
 
         // 小程序里面使用平台自带的登录,则不走下面的逻辑
         if (isMp && !LOGIN_PAGE_ENABLE_IN_MP) {
@@ -100,12 +113,24 @@ export const navigateToInterceptor = {
             else {
                 console.log('已经登录,但是还在登录页', myQuery.redirect)
                 const url = myQuery.redirect || HOME_PAGE
-                if (isPageTabbar(url)) {
-                    uni.switchTab({ url })
-                }
-                else {
-                    uni.navigateTo({ url })
-                }
+                setTimeout(() => {
+                    if (isPageTabbar(url)) {
+                        uni.switchTab({
+                            url,
+                            complete: () => {
+                                loadingStore.hideLoading()
+                            }
+                        })
+                    }
+                    else {
+                        uni.navigateTo({
+                            url,
+                            complete: () => {
+                                loadingStore.hideLoading()
+                            }
+                        })
+                    }
+                }, 50)
                 return false // 明确表示阻止原路由继续执行
             }
         }
@@ -128,7 +153,14 @@ export const navigateToInterceptor = {
                     return true // 明确表示允许路由继续执行
                 }
                 FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
-                uni.navigateTo({ url: redirectUrl })
+                setTimeout(() => {
+                    uni.navigateTo({
+                        url: redirectUrl,
+                        complete: () => {
+                            loadingStore.hideLoading()
+                        }
+                    })
+                }, 50)
                 return false // 明确表示阻止原路由继续执行
             }
         }
@@ -139,7 +171,14 @@ export const navigateToInterceptor = {
             // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
             if (judgeIsExcludePath(path)) {
                 FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
-                uni.navigateTo({ url: redirectUrl })
+                setTimeout(() => {
+                    uni.navigateTo({
+                        url: redirectUrl,
+                        complete: () => {
+                            loadingStore.hideLoading()
+                        }
+                    })
+                }, 50)
                 return false // 修改为false,阻止原路由继续执行
             }
             return true // 明确表示允许路由继续执行

二進制
src/static/images/index/coupon3.png


+ 4 - 4
src/store/coupon.ts

@@ -3,14 +3,14 @@ import { getCouponList } from '@/api/home'
 
 export const useCouponStore = defineStore('coupon', {
     state: () => ({
-        discountVoucherList: [], // 满减
-        couponList: [], // 折扣
+        discountVoucherList: [], // 折扣
+        couponList: [], // 满减
     }),
     actions: {
         async getCouponListByType() {
             const res = await getCouponList()
-            this.discountVoucherList = res.discountCoupon || []
-            this.couponList = res.equityVoucher || []
+            this.discountVoucherList = (res.discountCoupon || []).slice(0, 3)
+            this.couponList = (res.fullReduceCoupon || []).slice(0, 3)
         }
     }
 })

+ 1 - 0
src/store/index.ts

@@ -15,6 +15,7 @@ setActivePinia(store)
 
 export default store
 
+export * from './loading'
 // 模块统一导出
 export * from './token'
 export * from './user'

+ 30 - 0
src/store/loading.ts

@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useLoadingStore = defineStore('loading', () => {
+    // 控制加载页显示状态
+    const isLoading = ref(false)
+    // 加载页文本
+    const loadingText = ref('正在加载...')
+
+    // 显示加载页
+    const showLoading = (text?: string) => {
+        if (text)
+            loadingText.value = text
+        isLoading.value = true
+    }
+
+    // 隐藏加载页
+    const hideLoading = (delay = 100) => {
+        setTimeout(() => {
+            isLoading.value = false
+        }, delay)
+    }
+
+    return {
+        isLoading,
+        loadingText,
+        showLoading,
+        hideLoading
+    }
+})

+ 22 - 2
src/tabbar/index.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 // i-carbon-code
 import type { CustomTabBarItem } from './types'
+import { useLoadingStore } from '@/store/loading'
 import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
 import { tabbarList, tabbarStore } from './store'
 
@@ -9,7 +10,9 @@ import { tabbarList, tabbarStore } from './store'
 defineOptions({
     virtualHost: true,
 })
+
 // #endif
+const loadingStore = useLoadingStore()
 
 /**
  * 中间的鼓包tabbarItem的点击事件
@@ -26,6 +29,10 @@ function handleClick(index: number) {
     if (index === tabbarStore.curIdx) {
         return
     }
+
+    // 显示加载页
+    loadingStore.showLoading('切换页面中...')
+
     if (tabbarList[index].isBulge) {
         handleClickBulge()
         return
@@ -33,10 +40,22 @@ function handleClick(index: number) {
     const url = tabbarList[index].pagePath
     tabbarStore.setCurIdx(index)
     if (tabbarCacheEnable) {
-        uni.switchTab({ url })
+        uni.switchTab({
+            url,
+            complete: () => {
+                // 切换完成后隐藏加载页
+                loadingStore.hideLoading()
+            }
+        })
     }
     else {
-        uni.navigateTo({ url })
+        uni.navigateTo({
+            url,
+            complete: () => {
+                // 切换完成后隐藏加载页
+                loadingStore.hideLoading()
+            }
+        })
     }
 }
 // #ifndef MP-WEIXIN || MP-ALIPAY
@@ -143,6 +162,7 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
     bottom: 0;
     left: 0;
     right: 0;
+    z-index: 999;
 
     border-top: 1px solid #eee;
     box-sizing: border-box;

+ 50 - 0
src/utils/shareMixin.ts

@@ -0,0 +1,50 @@
+import { useTokenStore } from '@/store/token'
+
+// 创建全局mixin
+export const shareMixin = {
+    onShareAppMessage() {
+        // 检查用户是否已登录
+        const tokenStore = useTokenStore()
+        if (!tokenStore.hasLogin) {
+            uni.showToast({
+                title: '请先登录后再分享',
+                icon: 'none',
+            })
+            return {
+                title: '',
+                path: '',
+                imageUrl: '',
+            }
+        }
+
+        console.log('全局分享给朋友拦截触发')
+        return {
+            title: '券中心',
+            path: '/pages/index/index',
+            imageUrl: '/static/logo.svg',
+        }
+    },
+
+    onShareTimeline() {
+        // 检查用户是否已登录
+        const tokenStore = useTokenStore()
+        if (!tokenStore.hasLogin) {
+            uni.showToast({
+                title: '请先登录后再分享',
+                icon: 'none',
+            })
+            return {
+                title: '',
+                path: '',
+                imageUrl: '',
+            }
+        }
+
+        console.log('全局分享到朋友圈拦截触发')
+        return {
+            title: '券中心',
+            path: '/pages/index/index',
+            imageUrl: '/static/logo.svg',
+        }
+    }
+}