Parcourir la source

优化:重构收益页面

haiyang il y a 2 semaines
Parent
commit
741edb94c4

+ 1 - 1
package.json

@@ -121,7 +121,7 @@
     "dayjs": "1.11.10",
     "image-tools": "^1.4.0",
     "js-cookie": "^3.0.5",
-    "mescroll.js": "^1.4.2",
+    "mescroll-uni": "^1.3.7",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
     "uview-plus": "^3.6.17",

+ 6 - 6
pnpm-lock.yaml

@@ -84,9 +84,9 @@ importers:
       js-cookie:
         specifier: ^3.0.5
         version: 3.0.5
-      mescroll.js:
-        specifier: ^1.4.2
-        version: 1.4.2
+      mescroll-uni:
+        specifier: ^1.3.7
+        version: 1.3.7
       pinia:
         specifier: 2.0.36
         version: 2.0.36(typescript@5.8.3)(vue@3.4.21(typescript@5.8.3))
@@ -4634,8 +4634,8 @@ packages:
   merge@2.1.1:
     resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
 
-  mescroll.js@1.4.2:
-    resolution: {integrity: sha512-tZDucS9DXUrIfTGXTY2L7e4mGLIQ8uMqY2GOaQAGrCHQssUADOIM0kcQlRCA6U6ffFPxXV4D+IbhzOy3zVQ1wA==}
+  mescroll-uni@1.3.7:
+    resolution: {integrity: sha512-1pQMtGA+iVRKhfJZZNXdBx05NnthIk6zm3hRbumswSA54eaKOMgpUDb9AQ2+rRdXmS6kLkEYSbW/fkb7/IyoAg==}
 
   methods@1.1.2:
     resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
@@ -12269,7 +12269,7 @@ snapshots:
 
   merge@2.1.1: {}
 
-  mescroll.js@1.4.2: {}
+  mescroll-uni@1.3.7: {}
 
   methods@1.1.2: {}
 

+ 12 - 2
src/api/me.ts

@@ -1,7 +1,8 @@
 import type {
     couponIssuerApplyByAddResponse,
     CouponIssuerApplyByIdResponse,
-    ShareStateResponse
+    ShareStateResponse,
+    InviteListResponse
 } from '@/api/types/me'
 import { http } from '@/http/alova'
 
@@ -24,10 +25,19 @@ export function couponIssuerApplyByAdd(couponIssuerApplyByAddForm: CouponIssuerA
 
 // 更新用户信息
 export function updateUserInfo(userInfo) {
-    return http.Post('/couponCenter/couponCenterUser/updateInformation', userInfo)
+    return http.Post('/couponCenter/APP/couponCenterUser/updateInformation', userInfo)
 }
 
 // 获取我的页面中分享统计数据,包括今日分享数、累计分享数、累计团员数
 export function getShareState() {
     return http.Get<ShareStateResponse>('/couponCenter/APP/mine/getShareStats')
 }
+
+// 分页查询邀请关系,支持按类型和手机号搜索,不传inviteType默认查询全部
+export function getInviteList(params) {
+    return http.Get<InviteListResponse>('/couponCenter/APP/mine/queryInviteRelationPage', {
+        params: {
+            ...params,
+        }
+    })
+}

+ 7 - 0
src/api/types/me.ts

@@ -18,3 +18,10 @@ export interface ShareStateResponse {
     totalShareCount: number
     totalTeamMemberCount: number
 }
+
+export interface InviteListResponse {
+    pages: number
+    records: object[]
+    total: number
+    current: number
+}

+ 117 - 0
src/components/IncomeItem.vue

@@ -0,0 +1,117 @@
+<template>
+    <view class="invite-item">
+        <view class="income-header-menu-item">
+            <view class="income-header-menu-icon">
+                <image v-if="type === 0" :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
+                <image v-else :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                <view class="income-header-menu-text">
+                    <view class="income-header-menu-text-nickname">
+                        {{ data.receiveUserName }}
+                    </view>
+                    <view>{{ data.couponName }}</view>
+                </view>
+            </view>
+            <view class="income-header-menu-left">
+                <view class="income-header-menu-left-amount">
+                    ¥{{ data.changeAmount }}
+                </view>
+                <view class="income-header-menu-left-time">
+                    {{ data.settleTime }}
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup lang="ts">
+import { getImageUrl } from '@/utils/imageUtil'
+
+const props = defineProps({
+    data: {
+        type: Object,
+        default: () => ({}),
+    },
+    type: {
+        type: Number,
+        default: 0,
+    }
+})
+
+const dataType = {
+    type1: [1, 2, 3, 4],
+    type2: [5, 6],
+}
+
+const typeDataFormat = {
+    type1: {
+        receiveUserName: 'receiveUserName',
+        couponName: 'couponName',
+        changeAmount: 'changeAmount',
+        settleTime: 'settleTime',
+    },
+    type2: {
+        receiveUserName: 'receiveUserName',
+        couponName: 'couponName',
+        changeAmount: 'changeAmount',
+        settleTime: 'settleTime',
+    },
+}
+</script>
+
+<style lang="scss" scoped>
+.invite-item {
+    background: #ffffff;
+    border-radius: 10rpx;
+
+    .income-header-menu-item {
+        height: 88rpx;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        border-bottom: 1px solid #eeeeee;
+        padding: 32rpx 0 29rpx;
+        margin: 0 20rpx;
+
+        &:last-child {
+            border-bottom: none;
+        }
+
+        .income-header-menu-icon {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            gap: 19rpx;
+
+            image {
+                width: 60rpx;
+                height: 60rpx;
+            }
+
+            .income-header-menu-text {
+                font-weight: 400;
+                font-size: 24rpx;
+                color: #888888;
+
+                .income-header-menu-text-nickname {
+                    font-size: 28rpx;
+                    color: #222222;
+                    margin-bottom: 18rpx;
+                }
+            }
+        }
+
+        .income-header-menu-left {
+            font-weight: 400;
+            font-size: 24rpx;
+            color: #888888;
+
+            .income-header-menu-left-amount {
+                font-size: 28rpx;
+                color: #222222;
+                margin-bottom: 18rpx;
+                text-align: right;
+            }
+        }
+    }
+}
+</style>

+ 30 - 0
src/components/InviteItem.vue

@@ -0,0 +1,30 @@
+<template>
+    <view class="invite-item">
+        <up-cell-group :border="false">
+            <up-cell title="头像" center>
+                <template #value>
+                    <up-avatar :src="data.avatarUrl" font-size="64rpx" />
+                </template>
+            </up-cell>
+            <up-cell title="用户昵称" :value="data.nickname" />
+            <up-cell title="手机号" :value="data.phone" />
+            <up-cell title="创建时间" :value="data.createTime" />
+        </up-cell-group>
+    </view>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+    data: {
+        type: Object,
+        default: () => ({}),
+    }
+})
+</script>
+
+<style lang="scss" scoped>
+.invite-item {
+    background: #ffffff;
+    border-radius: 10rpx;
+}
+</style>

+ 318 - 0
src/pages-A/incomePage/index.vue

@@ -0,0 +1,318 @@
+<script lang="ts" setup>
+import { usePagination } from 'alova/client'
+import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
+import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
+import { reactive, ref, watch } from 'vue'
+import { getCouponIssuerAccountByPageMap } from '@/api/income'
+import { getInviteList } from '@/api/me'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import IncomeItem from '@/components/IncomeItem.vue'
+
+import { changtime, safeAreaInsets } from '@/utils'
+
+definePage({
+    style: {
+        navigationBarTitleText: '收益',
+        navigationStyle: 'custom',
+    },
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const activeIndex = ref(0)
+const showLoading = ref(false)
+const pickerDate = ref(Date.now())
+const pickerShow = ref(false)
+const totalAmount = ref(0)
+let mescroll = null
+
+const inviteType = reactive([
+    { name: '已结算', key: 0 },
+    { name: '待结算', key: 1 },
+])
+
+const { send, data, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getCouponIssuerAccountByPageMap({
+    pageNo: page,
+    pageSize,
+    type: 1,
+    status: activeIndex.value,
+    year: changtime(pickerDate.value, 'YYYY'),
+    month: changtime(pickerDate.value, 'MM'),
+}), {
+    initialPage: 1,
+    initialPageSize: 10,
+    append: true,
+    preloadNextPage: false,
+    preloadPreviousPage: false,
+    total: response => response.total,
+    data: response => response.detailList,
+    immediate: false,
+})
+
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: true,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
+})
+
+const upOption = reactive({
+    use: true,
+    auto: false,
+    noMoreSize: 7,
+    isBounce: true,
+    page: {
+        num: page.value,
+        size: pageSize.value,
+    },
+    empty: {
+        use: false,
+    },
+    textNoMore: '--- 已经到底了 ---',
+    toTop: {
+        src: '/static/images/mescroll-totop.png',
+        offset: 1000,
+        bottom: 120,
+    },
+    // 开启虚拟列表优化(大数据量时)
+    virtual: {
+        use: false,
+        rowHeight: 300,
+    }
+})
+
+const emptyOption = reactive({
+    use: true,
+    tip: '暂无数据',
+    btnText: '刷新试试',
+    icon: '/static/images/mescroll-empty.png',
+    fixed: false,
+    top: '200rpx',
+})
+
+function mescrollInit(ref) {
+    mescroll = ref
+}
+
+async function downCallback() {
+    console.log('下拉刷新触发')
+
+    await refresh()
+}
+
+async function upCallback() {
+    console.log('上拉加载触发')
+    if (isLastPage.value) {
+        console.log(mescroll)
+        mescroll.endSuccess(0, false)
+        return
+    }
+
+    page.value++
+}
+
+onSuccess((response) => {
+    let isNextPage = isLastPage.value
+    // 此处处理alova isLastPage默认值为true,首次请求出现的无法加载下一页问题
+    if (pageCount.value > page.value && isLastPage.value) {
+        isNextPage = false
+    }
+    totalAmount.value = response.data?.totalAmount || 0
+    // mescroll.setPageNum(page.value - 1)
+    mescroll.endSuccess(data.value.length, !isNextPage)
+})
+
+onError(() => {
+    console.log('请求失败')
+    mescroll.endErr()
+})
+
+onShow(async () => {
+    await send()
+})
+
+async function tabChange(index) {
+    // 切换tab时,重置上拉加载
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    mescroll.hideUpScroll()
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+
+async function pickerConfirm() {
+    pickerShow.value = false
+    mescroll.scrollTo(0, 0)
+    console.log(mescroll)
+    mescroll.hideUpScroll()
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+</script>
+
+<template>
+    <view class="income-container">
+        <custom-navigation-bar :show-back="true" title="收益" />
+        <view class="income-content"
+            :style="{ marginTop: `calc(${topSafeAreaHeight}px + 88rpx)`, height: `calc(100vh - ${topSafeAreaHeight}px - 88rpx)` }">
+            <view class="tabs-container flex">
+                <up-tabs v-model:current="activeIndex" :scrollable="false" class="tabs-content" :list="inviteType"
+                    line-height="0" line-width="0" line-color="transparent"
+                    :active-style="{ fontWeight: 'bolder', fontSize: '30rpx', color: '#333333' }"
+                    :inactive-style="{ fontWeight: '400', fontSize: '30rpx', color: '#888888' }"
+                    item-style="height: 88rpx;" @change="tabChange" />
+            </view>
+            <view class="income-content-search">
+                <view class="income-menu-time-filter-text" @click="pickerShow = true">
+                    <text>{{ changtime(pickerDate) }}</text>
+                    <up-icon name="arrow-down" color="#666666" size="14" />
+                    <up-datetime-picker v-model="pickerDate" :show="pickerShow" mode="year-month" close-on-click-overlay
+                        @confirm="pickerConfirm" @cancel="pickerShow = false" />
+                </view>
+                <view class="income-menu-total-amount">
+                    合计:¥{{ totalAmount }}
+                </view>
+            </view>
+            <view class="income-content-list"
+                :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - 88rpx - 88rpx - 72rpx)` }">
+                <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback">
+                    <view v-for="item in data" :key="item.userId" class="income-content-item">
+                        <income-item :data="item" :type="activeIndex" />
+                    </view>
+                    <mescroll-empty v-if="data.length === 0 && !loading" icon="'none'" tip="暂无数据" :option="emptyOption"
+                        @emptyclick="reload" />
+                    <!-- 滚动区域内的loading遮罩 -->
+                    <view v-if="showLoading && loading" class="loading-mask">
+                        <view class="loading-spinner" />
+                        <text class="loading-text">加载中...</text>
+                    </view>
+                </mescroll-uni>
+            </view>
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.income-container {
+    // height: 100vh;
+
+    .income-content {
+        display: flex;
+        flex-direction: column;
+
+        .income-content-search {
+            padding: 27rpx 25rpx;
+            padding-bottom: 15rpx;
+            background-color: #f8f8fa;
+            height: 60rpx;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+
+            .income-menu-time-filter-text {
+                font-weight: 400;
+                font-size: 26rpx;
+                color: #000000;
+                display: flex;
+                align-items: center;
+                gap: 12rpx;
+            }
+
+            .income-menu-total-amount {
+                font-weight: 400;
+                font-size: 24rpx;
+                color: #333333;
+            }
+        }
+
+        .tabs-container {
+            height: 88rpx;
+            display: flex;
+            flex-direction: row;
+            justify-content: space-around;
+            align-items: center;
+
+            .tabs-content {
+                width: 100%;
+
+                :deep(.u-tabs__wrapper__nav__item) {
+                    &:first-child {
+                        position: relative;
+
+                        &:after {
+                            content: '';
+                            position: absolute;
+                            top: 50%;
+                            right: 0;
+                            transform: translateY(-50%);
+                            width: 2rpx;
+                            height: 40rpx;
+                            background-color: #9a9b9b;
+                        }
+                    }
+                }
+            }
+        }
+
+        .income-content-list {
+            background-color: #f8f8fa;
+            padding: 0 24rpx;
+            display: flex;
+            flex-direction: column;
+            position: relative;
+
+            #mescrollContainer {
+                height: 100%;
+            }
+
+            .income-content-item {
+                margin-top: 20rpx;
+            }
+
+            // loading遮罩样式
+            .loading-mask {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(255, 255, 255, 0.8);
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                z-index: 999;
+            }
+
+            .loading-spinner {
+                width: 40rpx;
+                height: 40rpx;
+                border: 4rpx solid #e0e0e0;
+                border-top-color: #ed6b66;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
+            }
+
+            .loading-text {
+                margin-top: 16rpx;
+                font-size: 24rpx;
+                color: #666;
+            }
+
+            @keyframes spin {
+                to {
+                    transform: rotate(360deg);
+                }
+            }
+        }
+    }
+}
+</style>

+ 302 - 0
src/pages-A/invitePage/index.vue

@@ -0,0 +1,302 @@
+<script lang="ts" setup>
+import { usePagination } from 'alova/client'
+import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
+import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
+import { reactive, ref, watch } from 'vue'
+import { getInviteList } from '@/api/me'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import InviteItem from '@/components/InviteItem.vue'
+
+import { safeAreaInsets } from '@/utils'
+
+definePage({
+    style: {
+        navigationBarTitleText: '邀请',
+        navigationStyle: 'custom',
+    },
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const activeIndex = ref(0)
+const searchValue = ref('')
+const showLoading = ref(false)
+let mescroll = null
+
+const inviteType = reactive([
+    { name: '全部', key: 0 },
+    { name: '直邀', key: 1 },
+    { name: '间邀', key: 2 },
+])
+
+const { send, data, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getInviteList({
+    pageNo: page,
+    pageSize,
+    inviteType: activeIndex.value,
+    phone: searchValue.value,
+}), {
+    initialPage: 1,
+    initialPageSize: 4,
+    append: true,
+    preloadNextPage: false,
+    preloadPreviousPage: false,
+    total: response => response.total,
+    data: response => response.records,
+    immediate: false,
+})
+
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: true,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
+})
+
+const upOption = reactive({
+    use: true,
+    auto: false,
+    noMoreSize: 4,
+    isBounce: true,
+    page: {
+        num: page.value,
+        size: pageSize.value,
+    },
+    empty: {
+        use: false,
+    },
+    textNoMore: '--- 已经到底了 ---',
+    toTop: {
+        src: '/static/images/mescroll-totop.png',
+        offset: 1000,
+        bottom: 120,
+    },
+    // 开启虚拟列表优化(大数据量时)
+    virtual: {
+        use: false,
+        rowHeight: 300,
+    }
+})
+
+const emptyOption = reactive({
+    use: true,
+    tip: '暂无数据',
+    btnText: '刷新试试',
+    icon: '/static/images/mescroll-empty.png',
+    fixed: false,
+    top: '200rpx',
+})
+
+function mescrollInit(ref) {
+    mescroll = ref
+}
+
+async function downCallback() {
+    console.log('下拉刷新触发')
+
+    await refresh()
+}
+
+async function upCallback() {
+    console.log('上拉加载触发')
+    if (isLastPage.value) {
+        console.log(mescroll)
+        mescroll.endSuccess(0, false)
+        return
+    }
+
+    page.value++
+}
+
+onSuccess(() => {
+    let isNextPage = isLastPage.value
+    // 此处处理alova isLastPage默认值为true,首次请求出现的无法加载下一页问题
+    if (pageCount.value > page.value && isLastPage.value) {
+        isNextPage = false
+    }
+    // mescroll.setPageNum(page.value - 1)
+    mescroll.endSuccess(data.value.length, !isNextPage)
+})
+
+onError(() => {
+    console.log('请求失败')
+    mescroll.endErr()
+})
+
+onShow(async () => {
+    await send()
+})
+
+async function tabChange(index) {
+    // 切换tab时,重置上拉加载
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+
+async function search() {
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+</script>
+
+<template>
+    <view class="invite-container">
+        <custom-navigation-bar :show-back="true" title="邀请" />
+        <view class="invite-content"
+            :style="{ marginTop: `calc(${topSafeAreaHeight}px + 88rpx)`, height: `calc(100vh - ${topSafeAreaHeight}px - 88rpx)` }">
+            <view class="invite-content-search">
+                <up-search v-model:value="searchValue" shape="square" class="invite-content-search-input"
+                    :show-action="false" placeholder="请输入手机号查询" clearabled @search="search" @clear="search">
+                    <template #inputRight>
+                        <view class="search-button">
+                            <up-button type="text" @tap="search">
+                                搜索
+                            </up-button>
+                        </view>
+                    </template>
+                </up-search>
+            </view>
+            <view class="tabs-container flex">
+                <up-tabs v-model:current="activeIndex" :scrollable="false" class="tabs-content" :list="inviteType"
+                    line-height="6rpx" line-width="67rpx" line-color="#ed6b66"
+                    :active-style="{ fontWeight: '500', fontSize: '30rpx', color: '#ed6b66' }"
+                    :inactive-style="{ fontWeight: '400', fontSize: '30rpx', color: '#333333' }"
+                    item-style="height: 88rpx;" @change="tabChange" />
+            </view>
+            <view class="invite-content-list"
+                :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - 88rpx - 176rpx)` }">
+                <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback">
+                    <view v-for="item in data" :key="item.userId" class="invite-content-item">
+                        <invite-item :data="item" />
+                    </view>
+                    <mescroll-empty v-if="data.length === 0 && !loading" icon="'none'" tip="暂无数据" :option="emptyOption"
+                        @emptyclick="reload" />
+                    <!-- 滚动区域内的loading遮罩 -->
+                    <view v-if="showLoading && loading" class="loading-mask">
+                        <view class="loading-spinner" />
+                        <text class="loading-text">加载中...</text>
+                    </view>
+                </mescroll-uni>
+            </view>
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.invite-container {
+    // height: 100vh;
+
+    .invite-content {
+        display: flex;
+        flex-direction: column;
+
+        .invite-content-search {
+            padding: 10rpx 24rpx;
+
+            .invite-content-search-input {
+                height: 60rpx;
+                border-radius: 10rpx;
+                background-color: #f6f8fc;
+
+                .search-button {
+                    width: 83rpx;
+                    font-weight: 400;
+                    font-size: 26rpx;
+                    color: #333333;
+                    padding-left: 13rpx;
+                    position: relative;
+
+                    &:after {
+                        content: '';
+                        position: absolute;
+                        left: 0;
+                        top: 50%;
+                        transform: translateY(-50%);
+                        width: 1rpx;
+                        height: 23rpx;
+                        background: #e0e0e0;
+                    }
+                }
+            }
+        }
+
+        .tabs-container {
+            height: 88rpx;
+            display: flex;
+            flex-direction: row;
+            justify-content: space-around;
+            align-items: center;
+
+            .tabs-content {
+                width: 100%;
+            }
+        }
+
+        .invite-content-list {
+            background-color: #f8f8fa;
+            padding: 0 24rpx;
+            display: flex;
+            flex-direction: column;
+            position: relative;
+
+            #mescrollContainer {
+                height: 100%;
+            }
+
+            .invite-content-item {
+                margin-top: 20rpx;
+            }
+
+            // loading遮罩样式
+            .loading-mask {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(255, 255, 255, 0.8);
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                z-index: 999;
+            }
+
+            .loading-spinner {
+                width: 40rpx;
+                height: 40rpx;
+                border: 4rpx solid #e0e0e0;
+                border-top-color: #ed6b66;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
+            }
+
+            .loading-text {
+                margin-top: 16rpx;
+                font-size: 24rpx;
+                color: #666;
+            }
+
+            @keyframes spin {
+                to {
+                    transform: rotate(360deg);
+                }
+            }
+        }
+    }
+}
+</style>

+ 46 - 6
src/pages-A/settingPage/index.vue

@@ -1,6 +1,6 @@
 <script lang="ts" setup>
 import { storeToRefs } from 'pinia'
-import { computed, ref } from 'vue'
+import { computed, reactive, ref } from 'vue'
 import { updateUserInfo } from '@/api/me'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import { useUserStore } from '@/store'
@@ -19,17 +19,35 @@ const topSafeAreaHeight = safeAreaInsets?.top || 0
 const userStore = useUserStore()
 const { userInfo } = storeToRefs(userStore)
 const nickNameVisible = ref(false)
-const nickName = ref(userInfo.value.nickname)
+const formData = reactive({
+    nickName: userInfo.value.nickname,
+})
+const formRef = ref(null)
+
+const rules = reactive({
+    nickName: [
+        { required: true, message: '请输入昵称', trigger: ['blur', 'change'] },
+        {
+            pattern: /^[0-9a-z]*$/gi,
+            // 正则检验前先将值转为字符串
+            transform(value) {
+                return String(value)
+            },
+            message: '只能包含字母或数字',
+        },
+        { max: 12, message: '昵称长度不能超过12个字符', trigger: ['change'] },
+    ],
+})
 
 async function changeNickName() {
     uni.showLoading({
         mask: true,
     })
     try {
-        const result = await updateUserInfo({ nickname: nickName.value })
+        const result = await updateUserInfo(formData)
         uni.hideLoading()
         if (result) {
-            userStore.setUserNickName(nickName.value)
+            userStore.setUserNickName(formData.nickName)
             nickNameVisible.value = false
             uni.showToast({
                 title: '昵称更新成功',
@@ -52,6 +70,18 @@ async function changeNickName() {
     }
 }
 
+function submitNickName() {
+    console.log(formRef.value)
+    formRef.value.validate().then((valid) => {
+        if (valid) {
+            changeNickName()
+        }
+        else {
+            console.log('校验失败')
+        }
+    })
+}
+
 function onChooseAvatar(e) {
     const avatarUrl = e.detail.avatarUrl
     const uploadUrl = `${import.meta.env.VITE_SERVER_BASEURL}/sys/common/upload`
@@ -131,7 +161,11 @@ function onChooseAvatar(e) {
 
         <!-- 昵称修改弹窗 -->
         <up-modal :show="nickNameVisible" title="修改昵称" :show-confirm-button="false" :show-cancel-button="false">
-            <up-input v-model="nickName" placeholder="请输入新昵称" clearable />
+            <up-form ref="formRef" class="custom-form" style="width: 100%;" :model="formData" :rules="rules">
+                <up-form-item prop="nickName">
+                    <up-input v-model="formData.nickName" placeholder="请输入新昵称" clearable />
+                </up-form-item>
+            </up-form>
 
             <template #confirmButton>
                 <view class="flex items-center justify-around">
@@ -142,7 +176,7 @@ function onChooseAvatar(e) {
                     </view>
                     <view class="h-[70rpx] w-[226rpx]">
                         <up-button color="linear-gradient(90deg, #EE6B67 0%, #FF7D78 100%);" block shape="circle"
-                            type="primary" @tap="changeNickName">
+                            type="primary" @tap="submitNickName">
                             确定
                         </up-button>
                     </view>
@@ -182,5 +216,11 @@ function onChooseAvatar(e) {
             margin-bottom: 1px;
         }
     }
+
+    .custom-form {
+        :deep(.u-form-item__body__right__message) {
+            margin-left: 5px !important;
+        }
+    }
 }
 </style>

+ 1 - 1
src/pages-A/shareFriend/index.vue

@@ -2,7 +2,7 @@
     <view class="share-container">
         <!-- 顶部导航栏(模拟美团风格) -->
         <!-- <up-status-bar /> -->
-        <CustomNavigationBar :show-back="true" title="设置" />
+        <CustomNavigationBar :show-back="true" title="邀请好友" />
 
         <view class="share-content" :style="{ paddingTop: `calc(${topSafeAreaHeight}px + 94rpx)` }">
             <!-- 分享图片预览区域 -->

+ 222 - 220
src/pages/income/income.vue

@@ -1,15 +1,16 @@
 <script lang="ts" setup>
-import type { AccountDetailItem } from '@/api/income'
-import { useRequest } from 'alova/client'
+import { usePagination, useRequest } from 'alova/client'
+import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
+import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
 import { storeToRefs } from 'pinia'
-import { computed, ref, watch } from 'vue'
+import { computed, ref } from 'vue'
 import { getAccountCount } from '@/api/home'
-import { getAccountDetailTotalAmount, getCouponIssuerAccountByPageMap } from '@/api/income'
-import { useScroll } from '@/hooks/useScroll'
+import { getCouponIssuerAccountByPageMap } from '@/api/income'
+import IncomeItem from '@/components/IncomeItem.vue'
 import { useShare } from '@/hooks/useShare'
 import { useUserStore } from '@/store'
 import { useTokenStore } from '@/store/token'
-import { changtime, menuButtonInfo, safeAreaInsets, systemInfo } from '@/utils'
+import { changtime, menuButtonInfo, rpxToPx, safeAreaInsets, systemInfo } from '@/utils'
 import { getImageUrl } from '@/utils/imageUtil'
 
 definePage({
@@ -25,113 +26,164 @@ const tokenStore = useTokenStore()
 const { userInfo } = storeToRefs(userStore)
 const { hasLogin } = storeToRefs(tokenStore)
 
-const show = ref(false)
-const filterValue = ref(Date.now())
 // 发券人账户表-通过userId查询账户信息
 const { send: getAccountCountRequest, data: accountCountData } = useRequest(getAccountCount, {
     immediate: false,
 })
 
-const refreshing = ref(false)
-const status = ref(0) // 0-待结算, 1-已结算/成功
-const type = ref(1) // 明细类型: 1-佣金收入, 2-提现支出, 3-退款扣减, 4-系统调整
+const activeIndex = ref(0)
+const showLoading = ref(false)
+const pickerDate = ref(Date.now())
+const pickerShow = ref(false)
+const totalAmount = ref(0)
+let mescroll = null
+
+// 新增:滚动距离和透明度
+const scrollTop = ref(0)
+const headerOverlayOpacity = computed(() => {
+    // 当滚动距离超过150px时,完全显示纯色背景
+    const headerHeightForRPX = 526
+    const headerHeightForPx = rpxToPx(headerHeightForRPX)
+    const maxScroll = menuButtonInfo.height + safeAreaInsets.top + 64 + (headerHeightForPx - (menuButtonInfo.height + safeAreaInsets.top + 64)) / 2
+    return Math.min(scrollTop.value / maxScroll, 1)
+})
+
+// 新增:监听页面滚动
+onPageScroll((e) => {
+    scrollTop.value = e.scrollTop
+})
 
-const { send: getAccountDetailTotalAmountRequest, data: accountDetailTotalAmountData } = useRequest(() => getAccountDetailTotalAmount({ status: status.value, year: changtime(filterValue.value, 'YYYY'), month: changtime(filterValue.value, 'MM'), type: type.value }), {
+const { send, data: incomeData, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getCouponIssuerAccountByPageMap({
+    pageNo: page,
+    pageSize,
+    type: 1,
+    status: activeIndex.value,
+    year: changtime(pickerDate.value, 'YYYY'),
+    month: changtime(pickerDate.value, 'MM'),
+}), {
+    initialPage: 1,
+    initialPageSize: 10,
+    append: true,
+    preloadNextPage: false,
+    preloadPreviousPage: false,
+    total: response => response.total,
+    data: response => response.detailList,
     immediate: false,
 })
 
-// 获取账户明细分页数据(Map格式)---待结算
-const {
-    list: pendingSettlementData,
-    loading: pendingSettlementLoading,
-    finished: pendingSettlementFinished,
-    refresh: pendingSettlementRefresh,
-    loadMore: pendingSettlementLoadMore,
-} = useScroll<AccountDetailItem>({
-    fetchData: async (page, pageSize) => {
-        const response = await getCouponIssuerAccountByPageMap({
-            pageNo: page,
-            pageSize,
-            status: 0,
-            year: changtime(filterValue.value, 'YYYY'),
-            month: changtime(filterValue.value, 'MM'),
-            type: type.value,
-        })
-        return response.detailList || []
-    },
-    pageSize: 7,
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: false,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
 })
 
-// 获取账户明细分页数据(Map格式)---已结算
-const {
-    list: settledData,
-    loading: settledLoading,
-    finished: settledFinished,
-    refresh: settledRefresh,
-    loadMore: settledLoadMore,
-} = useScroll<AccountDetailItem>({
-    fetchData: async (page, pageSize) => {
-        const response = await getCouponIssuerAccountByPageMap({
-            pageNo: page,
-            pageSize,
-            status: 1,
-            year: changtime(filterValue.value, 'YYYY'),
-            month: changtime(filterValue.value, 'MM'),
-            type: type.value,
-        })
-        return response.detailList || []
+const upOption = reactive({
+    use: true,
+    auto: false,
+    noMoreSize: 7,
+    isBounce: true,
+    page: {
+        num: page.value,
+        size: pageSize.value,
+    },
+    empty: {
+        use: false,
     },
-    pageSize: 7,
+    textNoMore: '--- 已经到底了 ---',
+    toTop: {
+        src: '/static/images/mescroll-totop.png',
+        offset: 1000,
+        bottom: 120,
+    },
+    // 开启虚拟列表优化(大数据量时)
+    virtual: {
+        use: false,
+        rowHeight: 300,
+    }
 })
 
-async function confirm() {
-    // 函数实现
-    show.value = false
-    if (status.value === 0) {
-        await pendingSettlementRefresh()
-    }
-    else {
-        await settledRefresh()
-    }
-}
-function cancel() {
-    show.value = false
+const emptyOption = reactive({
+    use: true,
+    tip: '暂无数据',
+    btnText: '刷新试试',
+    icon: '/static/images/mescroll-empty.png',
+    fixed: false,
+    top: '200rpx',
+})
+
+function mescrollInit(ref) {
+    mescroll = ref
 }
-function close() {
-    show.value = false
+
+async function downCallback() {
+    console.log('下拉刷新触发')
+
+    // await refresh()
 }
 
-// 下拉刷新
-async function onRefresh() {
-    refreshing.value = true
-    // 重置列表并重新加载数据
-    if (status.value === 0) {
-        await pendingSettlementRefresh()
-    }
-    else {
-        await settledRefresh()
+async function upCallback() {
+    console.log('上拉加载触发')
+    if (isLastPage.value) {
+        console.log(mescroll)
+        mescroll.endSuccess(0, false)
+        return
     }
-    refreshing.value = false
+
+    page.value++
 }
 
-// 上拉加载更多
-function onLoadMore() {
-    console.log(status.value, '执行了')
-    if (status.value === 0) {
-        pendingSettlementLoadMore()
-    }
-    else {
-        settledLoadMore()
+onSuccess((response) => {
+    let isNextPage = isLastPage.value
+    // 此处处理alova isLastPage默认值为true,首次请求出现的无法加载下一页问题
+    if (pageCount.value > page.value && isLastPage.value) {
+        isNextPage = false
     }
+    totalAmount.value = response.data?.totalAmount || 0
+    // mescroll.setPageNum(page.value - 1)
+    mescroll.endSuccess(incomeData.value.length, !isNextPage)
+})
+
+onError(() => {
+    console.log('请求失败')
+    mescroll.endErr()
+})
+
+onShow(async () => {
+    await send()
+})
+
+async function tabChange(index) {
+    // 切换tab时,重置上拉加载
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    activeIndex.value = index
+    mescroll.hideUpScroll()
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
 }
 
-const list = ref<AccountDetailItem[]>([])
+async function pickerConfirm() {
+    pickerShow.value = false
+    mescroll.scrollTo(0, 0)
+    mescroll.hideUpScroll()
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
 
 onShow(async () => {
     // 登录后查询收益数据
     if (hasLogin) {
         await getAccountCountRequest()
-        await getAccountDetailTotalAmountRequest()
+        await send()
         // 数据会在useScroll的onMounted中自动加载,这里不需要额外调用
     }
 })
@@ -153,31 +205,6 @@ onShareTimeline(async () => {
 
 const isUnlock = computed(() => accountCountData && accountCountData?.status === 0)
 
-watch(() => accountCountData, (newVal) => {
-    console.log(newVal)
-})
-
-function showTimeFilter() {
-    show.value = true
-}
-const wjsList = ref<AccountDetailItem[]>([])
-const yjsList = ref<AccountDetailItem[]>([])
-const activeTab = ref('pending')
-async function changeTab(tab: string) {
-    if (activeTab.value === tab)
-        return
-    activeTab.value = tab
-    status.value = tab === 'pending' ? 0 : 1
-    await getAccountDetailTotalAmountRequest()
-    // 切换标签时刷新对应列表数据
-    if (tab === 'pending') {
-        await pendingSettlementRefresh()
-    }
-    else {
-        await settledRefresh()
-    }
-}
-
 function goPage(page: string) {
     uni.navigateTo({
         url: `/pages-A/${page}/index`,
@@ -187,7 +214,7 @@ function goPage(page: string) {
 
 <template>
     <view class="profile-container"
-        :style="{ height: `calc(100vh - ${safeAreaInsets.top}px - ${safeAreaInsets.bottom}px)` }">
+        :style="{ minHeight: `calc(100vh - ${safeAreaInsets.top}px - ${safeAreaInsets.bottom}px - 100rpx)` }">
         <!-- 顶部区域 -->
         <view class="income-header"
             :style="{ background: `url(${getImageUrl('@img/income/income-bg.png')})`, backgroundSize: '100% 110%', backgroundPosition: 'left bottom', backgroundRepeat: 'no-repeat' }">
@@ -245,72 +272,46 @@ function goPage(page: string) {
         </view>
         <!-- 菜单 -->
         <view class="income-header-menu" :style="{ marginTop: isUnlock ? '24rpx' : '-50rpx' }">
+            <view class="income-header-overlay"
+                :style="{ opacity: headerOverlayOpacity, height: `calc(${menuButtonInfo.bottom + 4}px + 50rpx)` }" />
             <!-- 结算筛选 -->
-            <view class="income-header-menu-filter">
-                <view class="income-header-menu-filter-item" :class="[activeTab === 'pending' ? 'active' : '']"
-                    @click="changeTab('pending')">
+            <view class="income-header-menu-filter" :style="{ top: `${menuButtonInfo.bottom + 4}px` }">
+                <view class="income-header-menu-filter-item" :class="[activeIndex === 0 ? 'active' : '']"
+                    @click="tabChange(0)">
                     待结算
                 </view>
-                <view class="income-header-menu-filter-item" :class="[activeTab === 'settled' ? 'active' : '']"
-                    @click="changeTab('settled')">
+                <view class="income-header-menu-filter-item" :class="[activeIndex === 1 ? 'active' : '']"
+                    @click="tabChange(1)">
                     已结算
                 </view>
             </view>
             <!-- 时间筛选 -->
-            <view class="income-menu-time-filter">
-                <view class="income-menu-time-filter-text" @click="showTimeFilter">
-                    <text>{{ changtime(filterValue) }}</text>
+            <view class="income-menu-time-filter" :style="{ top: `calc(${menuButtonInfo.bottom + 4}px + 90rpx)` }">
+                <view class="income-menu-time-filter-text" @click="pickerShow = true">
+                    <text>{{ changtime(pickerDate) }}</text>
                     <up-icon name="arrow-down" color="#666666" size="14" />
-                    <up-datetime-picker v-model="filterValue" :show="show" mode="year-month" close-on-click-overlay
-                        @confirm="confirm" @cancel="cancel" />
+                    <up-datetime-picker v-model="pickerDate" :show="pickerShow" mode="year-month" close-on-click-overlay
+                        @confirm="pickerConfirm" @cancel="pickerShow = false" />
                 </view>
                 <view>
-                    合计:¥{{ accountDetailTotalAmountData?.totalAmount || 0 }}
+                    合计:¥{{ totalAmount }}
                 </view>
             </view>
-            <!-- 优惠券列表 -->
-            <scroll-view class="income-header-menu-list" :scroll-y="true" :refresher-enabled="true"
-                :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
-                <template v-for="item in activeTab === 'pending' ? pendingSettlementData || [] : settledData || []"
-                    :key="item.id">
-                    <view class="income-header-menu-item">
-                        <view class="income-header-menu-icon">
-                            <image v-if="activeTab === 'settled'" :src="getImageUrl('@img/income/hb.png')"
-                                mode="aspectFit" />
-                            <image v-else :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
-                            <view class="income-header-menu-text">
-                                <view class="income-header-menu-text-nickname">
-                                    {{ item.receiveUserName }}
-                                </view>
-                                <view>{{ item.couponName }}</view>
-                            </view>
-                        </view>
-                        <view class="income-header-menu-left">
-                            <view class="income-header-menu-left-amount">
-                                ¥{{ item.changeAmount }}
-                            </view>
-                            <view class="income-header-menu-left-time">
-                                {{ item.settleTime }}
-                            </view>
-                        </view>
+            <view class="income-menu-list">
+                <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback">
+                    <view v-for="item in incomeData" :key="item.userId" class="income-content-item">
+                        <income-item :data="item" :type="activeIndex" />
                     </view>
-                </template>
-
-                <!-- 加载状态提示 -->
-                <view v-if="(activeTab === 'pending' ? pendingSettlementLoading : settledLoading)" class="loading-tip">
-                    加载中...
-                </view>
-                <view
-                    v-else-if="(activeTab === 'pending' ? pendingSettlementFinished : settledFinished) && (activeTab === 'pending' ? pendingSettlementData.length : settledData.length) > 0"
-                    class="finished-tip">
-                    没有更多数据了
-                </view>
-
-                <!-- 空状态 -->
-                <u-empty
-                    v-if="(activeTab === 'pending' ? pendingSettlementData.length === 0 : settledData.length === 0)"
-                    class="p-b-12 pt-12" mode="list" />
-            </scroll-view>
+                    <mescroll-empty v-if="incomeData.length === 0 && !loading" icon="'none'" tip="暂无数据"
+                        :option="emptyOption" @emptyclick="reload" />
+                    <!-- 滚动区域内的loading遮罩 -->
+                    <view v-if="showLoading && loading" class="loading-mask">
+                        <view class="loading-spinner" />
+                        <text class="loading-text">加载中...</text>
+                    </view>
+                </mescroll-uni>
+            </view>
         </view>
     </view>
 </template>
@@ -452,13 +453,24 @@ function goPage(page: string) {
         // background: #ffffff;
         border-radius: 10rpx 10rpx 0rpx 0rpx;
         margin-left: 24rpx;
-        margin-bottom: 24rpx;
         margin-right: 23rpx;
         flex: 1;
         display: flex;
         flex-direction: column;
         min-height: 0;
 
+        // 新增:吸顶区域上方的覆盖元素
+        .income-header-overlay {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            background-color: #ed6b66;
+            border-radius: 10rpx 10rpx 0rpx 0rpx;
+            z-index: 99;
+            pointer-events: none; // 确保不影响点击事件
+        }
+
         .income-header-menu-filter {
             display: flex;
             justify-content: space-between;
@@ -470,6 +482,9 @@ function goPage(page: string) {
             background: #ffffff;
             box-shadow: 0rpx 3rpx 7rpx 0rpx rgba(213, 213, 213, 0.29);
             border-radius: 10rpx 10rpx 0rpx 0rpx;
+            position: sticky;
+            top: 0;
+            z-index: 100;
 
             .income-header-menu-filter-item {
                 flex: 1;
@@ -507,6 +522,8 @@ function goPage(page: string) {
             padding: 30rpx 20rpx 14rpx;
             background: #ffffff;
             margin-top: 4rpx;
+            position: sticky;
+            z-index: 100;
 
             .income-menu-time-filter-text {
                 font-weight: 400;
@@ -518,70 +535,55 @@ function goPage(page: string) {
             }
         }
 
-        .income-header-menu-list {
+        .income-menu-list {
             background: #ffffff;
             flex: 1;
-            min-height: calc(30vh - 100rpx);
-            overflow-y: auto;
-            /* 确保内容超出时可滚动 */
-
-            .income-header-menu-item {
-                height: 88rpx;
-                display: flex;
-                justify-content: space-between;
-                align-items: center;
-                border-bottom: 1px solid #eeeeee;
-                padding: 32rpx 0 29rpx;
-                margin: 0 20rpx;
-
-                &:last-child {
-                    border-bottom: none;
-                }
-
-                .income-header-menu-icon {
-                    display: flex;
-                    justify-content: center;
-                    align-items: center;
-                    gap: 19rpx;
-
-                    image {
-                        width: 60rpx;
-                        height: 60rpx;
-                    }
+            display: flex;
+            flex-direction: column;
+            position: relative;
 
-                    .income-header-menu-text {
-                        font-weight: 400;
-                        font-size: 24rpx;
-                        color: #888888;
+            #mescrollContainer {
+                height: 100%;
+            }
 
-                        .income-header-menu-text-nickname {
-                            font-size: 28rpx;
-                            color: #222222;
-                            margin-bottom: 18rpx;
-                        }
-                    }
-                }
+            .income-content-item {
+                margin-top: 20rpx;
+            }
 
-                .income-header-menu-left {
-                    font-weight: 400;
-                    font-size: 24rpx;
-                    color: #888888;
+            // loading遮罩样式
+            .loading-mask {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(255, 255, 255, 0.8);
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                z-index: 999;
+            }
 
-                    .income-header-menu-left-amount {
-                        font-size: 28rpx;
-                        color: #222222;
-                        margin-bottom: 18rpx;
-                        text-align: right;
-                    }
-                }
+            .loading-spinner {
+                width: 40rpx;
+                height: 40rpx;
+                border: 4rpx solid #e0e0e0;
+                border-top-color: #ed6b66;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
             }
 
-            .loading-tip,
-            .finished-tip {
-                text-align: center;
-                padding: 20rpx 0;
-                color: #999;
+            .loading-text {
+                margin-top: 16rpx;
                 font-size: 24rpx;
+                color: #666;
+            }
+
+            @keyframes spin {
+                to {
+                    transform: rotate(360deg);
+                }
             }
         }
     }

+ 3 - 3
src/pages/my/my.vue

@@ -200,7 +200,7 @@ onShareTimeline(async () => {
                                 累计邀请
                             </view>
                         </view>
-                        <view class="me-header-tips-item">
+                        <view class="me-header-tips-item" @click="menuClick('invitePage')">
                             <view class="me-header-tips-item-num">
                                 {{ shareStateData?.totalTeamMemberCount || 0 }}
                             </view>
@@ -229,7 +229,7 @@ onShareTimeline(async () => {
                                 今日领取
                             </view>
                         </view>
-                        <view class="me-header-tips-item">
+                        <view class="me-header-tips-item" @click="menuClick('incomePage')">
                             <view class="me-header-tips-item-num">
                                 {{ couponSituationData?.usedQuantity || 0 }}
                             </view>
@@ -256,7 +256,7 @@ onShareTimeline(async () => {
                         <up-icon name="arrow-right" color="#979797" size="12" />
                     </view>
                 </view>
-                <view class="me-header-menu-item" @click="menuClick('applyForm')">
+                <view class="me-header-menu-item" @click="menuClick('invitePage')">
                     <view class="me-header-menu-icon">
                         <image :src="getImageUrl('@img/me/invite.png')" mode="aspectFill" />
                         <view class="me-header-menu-text">

BIN
src/static/images/mescroll-empty.png


BIN
src/static/images/mescroll-totop.png


+ 2 - 2
src/utils/index.ts

@@ -2,7 +2,7 @@ import type { PageMetaDatum, SubPackages } from '@uni-helper/vite-plugin-uni-pag
 import { isMpWeixin } from '@uni-helper/uni-env'
 import { pages, subPackages } from '@/pages.json'
 import { changtime } from './directive'
-import { menuButtonInfo, safeAreaInsets, systemInfo } from './systemInfo'
+import { menuButtonInfo, pxToRpx, rpxToPx, safeAreaInsets, systemInfo } from './systemInfo'
 
 export type PageInstance = Page.PageInstance<AnyObject, object> & { $page: Page.PageInstance<AnyObject, object> & { fullPath: string } }
 
@@ -161,4 +161,4 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
 export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
 
 // 导出时间格式化函数和系统信息
-export { changtime, menuButtonInfo, safeAreaInsets, systemInfo }
+export { changtime, menuButtonInfo, pxToRpx, rpxToPx, safeAreaInsets, systemInfo }

+ 23 - 0
src/utils/systemInfo.ts

@@ -36,4 +36,27 @@ console.log('systemInfo', systemInfo)
 // windowHeight: 753
 // windowTop: 0
 // windowWidth: 390
+
+/**
+ * 将 rpx 转换为 px
+ * @param rpx rpx值
+ * @returns px值
+ */
+export function rpxToPx(rpx: number): number {
+    // rpx基准是750rpx等于屏幕宽度
+    const screenWidth = systemInfo?.windowWidth || 375 // 默认375px作为 fallback
+    return (rpx * screenWidth) / 750
+}
+
+/**
+ * 将 px 转换为 rpx
+ * @param px px值
+ * @returns rpx值
+ */
+export function pxToRpx(px: number): number {
+    // rpx基准是750rpx等于屏幕宽度
+    const screenWidth = systemInfo?.windowWidth || 375 // 默认375px作为 fallback
+    return (px * 750) / screenWidth
+}
+
 export { menuButtonInfo, safeAreaInsets, systemInfo }