Ver Fonte

Merge branch 'dev' of haiyang/couponCenter_mpapp into master

haiyang há 6 meses atrás
pai
commit
e0d1fe9bf9
39 ficheiros alterados com 2685 adições e 304 exclusões
  1. 5 3
      env/.env
  2. 5 4
      env/.env.development
  3. 8 4
      env/.env.production
  4. 14 10
      eslint.config.mjs
  5. 1 2
      manifest.config.ts
  6. 1 0
      package.json
  7. 10 4
      scripts/open-dev-tools.js
  8. 15 0
      src/App.vue
  9. 62 3
      src/api/home.ts
  10. 42 0
      src/api/income.ts
  11. 6 1
      src/api/receiveCoupon.ts
  12. 11 1
      src/components/CustomNavigationBar.vue
  13. 30 3
      src/components/IncomeItem.vue
  14. 216 0
      src/components/IncomeShareInfo.vue
  15. 377 0
      src/components/NewSpendAndSaveCoupon_large.vue
  16. 127 20
      src/components/discountCoupon.vue
  17. 239 116
      src/components/spendAndSaveCoupon_large.vue
  18. 5 3
      src/custom-components/custom-mescroll/mescroll-body.vue
  19. 7 2
      src/custom-components/custom-mescroll/mescroll-uni-main.js
  20. 52 0
      src/http/tools/delayLoadingMiddleware.ts
  21. 2 1
      src/pages-A/incomePage/index.vue
  22. 271 0
      src/pages-A/incomeShareInfo/index.vue
  23. 532 0
      src/pages-A/newCouponList/index.vue
  24. 312 0
      src/pages-A/regionSelection/index.vue
  25. 77 48
      src/pages-A/shareFriend/index.vue
  26. 53 40
      src/pages-A/sharePage/index.vue
  27. 15 14
      src/pages/home/home.vue
  28. 34 11
      src/pages/income/income.vue
  29. 14 7
      src/pages/my/my.vue
  30. BIN
      src/static/images/mescroll-empty1.png
  31. 48 0
      src/store/area.ts
  32. 26 3
      src/store/coupon.ts
  33. 1 1
      src/store/loading.ts
  34. 0 1
      src/store/token.ts
  35. 34 0
      src/utils/envField.ts
  36. 13 0
      src/utils/index.ts
  37. 16 0
      src/utils/platformSource.ts
  38. 1 1
      src/utils/systemInfo.ts
  39. 3 1
      uno.config.ts

+ 5 - 3
env/.env

@@ -9,8 +9,7 @@ VITE_WX_APPID = 'wx23776edbfe90d642'
 # 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
 VITE_APP_PUBLIC_BASE=/
 
-# 后台请求地址
-VITE_SERVER_BASEURL = 'https://life.baoxianzhanggui.com/locallive-pro-java'
+
 # 后台静态资源请求地址
 VITE_SERVER_RESOURCE_BASEURL ='/sys/common/static'
 # 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
@@ -31,5 +30,8 @@ VITE_AUTH_MODE = 'single'
 # 原生插件资源复制开关,控制是否启用 copy-native-resources 插件
 VITE_COPY_NATIVE_RES_ENABLE = false
 
-#微信开发者工具项目路径
+#windows系统下,是否开启微信开发者工具项目路径配置
+VITE_WECHAT_DEV_TOOL_PROJECT_PATH_ENABLE = false
+
+#windows系统下微信开发者工具项目路径
 VITE_WECHAT_DEV_TOOL_PROJECT_PATH = 'E:\\developmentKit\\WeChatDeveloperTools\\微信web开发者工具'

+ 5 - 4
env/.env.development

@@ -3,9 +3,7 @@ NODE_ENV = 'development'
 # 是否去除console 和 debugger
 VITE_DELETE_CONSOLE = false
 # 是否开启sourcemap
-VITE_SHOW_SOURCEMAP = false
-# 是否开启懒加载
-VITE_LAZY_CODE_LOADING = false
+VITE_SHOW_SOURCEMAP = true
 
 
 # 后台请求地址
@@ -15,4 +13,7 @@ VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'
 
 
 # 静态资源请求地址
-VITE_CDN_DOMAIN = 'https://life.baoxianzhanggui.com/pro-img/securities'
+VITE_CDN_DOMAIN = 'https://life.baoxianzhanggui.com/pro-img/securities'
+
+# 微信小程序码跳转版本
+VITE_WEIXIN_MINIAPP_VERSION = 'trial'

+ 8 - 4
env/.env.production

@@ -3,16 +3,20 @@ NODE_ENV = 'production'
 # 是否去除console 和 debugger
 VITE_DELETE_CONSOLE = true
 # 是否开启sourcemap
-VITE_SHOW_SOURCEMAP = false
+VITE_SHOW_SOURCEMAP = true
 # 是否开启懒加载
-VITE_LAZY_CODE_LOADING = true
+VITE_LAZY_CODE_LOADING = 'requiredComponents'
 
 # 后台请求地址
-# VITE_SERVER_BASEURL = 'https://prod.xxx.com'
+VITE_SERVER_BASEURL = 'https://life.baoxianzhanggui.com/locallive-pro-java'
 
 # 后台静态资源请求地址
 #VITE_SERVER_RESOURCE_BASEURL ='/sys/common/static'
 
 
 # 静态资源请求地址
-VITE_CDN_DOMAIN = 'https://life.baoxianzhanggui.com/pro-img/securities'
+VITE_CDN_DOMAIN = 'https://life.baoxianzhanggui.com/pro-img/securities'
+
+# 微信小程序码跳转版本
+# VITE_WEIXIN_MINIAPP_VERSION = 'release'
+VITE_WEIXIN_MINIAPP_VERSION = 'trial'

+ 14 - 10
eslint.config.mjs

@@ -5,7 +5,7 @@ export default uniHelper({
     vue: true,
     markdown: false,
     ignores: [
-    // 忽略uni_modules目录
+        // 忽略uni_modules目录
         '**/uni_modules/',
         // 忽略原生插件目录
         '**/nativeplugins/',
@@ -39,17 +39,21 @@ export default uniHelper({
         'vue/singleline-html-element-content-newline': ['error', { externalIgnores: ['text'] }],
         // vue SFC 调换顺序改这里
         'vue/block-order': ['error', { order: [['script', 'template'], 'style'] }],
-  },
+        'vue/first-attribute-linebreak': ['error', {
+            singleline: 'ignore', // 单行元素忽略此规则
+            multiline: 'always', // 多行元素强制在第一个属性前换行
+        }],
+    },
     formatters: {
-    /**
-     * Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
-     * By default uses Prettier
-     */
+        /**
+         * Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
+         * By default uses Prettier
+         */
         css: true,
         /**
-     * Format HTML files
-     * By default uses Prettier
-     */
+         * Format HTML files
+         * By default uses Prettier
+         */
         html: true,
-  }
+    }
 })

+ 1 - 2
manifest.config.ts

@@ -20,7 +20,6 @@ const {
     VITE_FALLBACK_LOCALE,
     VITE_LAZY_CODE_LOADING
 } = env
-// console.log('manifest.config.ts env:', env)
 
 export default defineManifestConfig({
     'name': VITE_APP_TITLE,
@@ -126,7 +125,7 @@ export default defineManifestConfig({
             es6: true,
             minified: true,
         },
-        lazyCodeLoading: 'requiredComponents',
+        lazyCodeLoading: VITE_LAZY_CODE_LOADING,
         optimization: {
             subPackages: true,
         },

+ 1 - 0
package.json

@@ -124,6 +124,7 @@
     "lz-string": "^1.5.0",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
+    "pinyin-pro": "^3.27.0",
     "uview-plus": "^3.6.17",
     "vue": "^3.4.21",
     "vue-i18n": "9.1.9",

+ 10 - 4
scripts/open-dev-tools.js

@@ -11,12 +11,13 @@ dotenv.config({ path: path.resolve(process.cwd(), 'env/.env') })
  */
 function _openDevTools() {
     const platform = process.platform // darwin, win32, linux
-    const { UNI_PLATFORM } = process.env //  mp-weixin, mp-alipay
+    const { UNI_PLATFORM, NODE_ENV } = process.env //  mp-weixin, mp-alipay
 
     const uniPlatformText = UNI_PLATFORM === 'mp-weixin' ? '微信小程序' : UNI_PLATFORM === 'mp-alipay' ? '支付宝小程序' : '小程序'
 
+    const outputDir = NODE_ENV === 'development' ? 'dev' : 'build'
     // 项目路径(构建输出目录)
-    const projectPath = path.resolve(process.cwd(), `dist/dev/${UNI_PLATFORM}`)
+    const projectPath = path.resolve(process.cwd(), `dist/${outputDir}/${UNI_PLATFORM}`)
 
     // 检查构建输出目录是否存在
     if (!fs.existsSync(projectPath)) {
@@ -41,12 +42,17 @@ function _openDevTools() {
     else if (platform === 'win32' || platform === 'win64') {
         // Windows
         if (UNI_PLATFORM === 'mp-weixin') {
+            let wechatDevToolPath = 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具'
             // 获取微信开发者工具路径,添加默认值或错误处理
-            const wechatDevToolPath = process.env.VITE_WECHAT_DEV_TOOL_PROJECT_PATH || 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具'
-            if (!wechatDevToolPath) {
+            if (process.env.VITE_WECHAT_DEV_TOOL_PROJECT_PATH_ENABLE === 'true') {
+                wechatDevToolPath = process.env.VITE_WECHAT_DEV_TOOL_PROJECT_PATH
+            }
+
+            if (process.env.VITE_WECHAT_DEV_TOOL_PROJECT_PATH_ENABLE === 'true' && !wechatDevToolPath) {
                 console.log('❌ 未配置微信开发者工具路径,请在 .env 文件中设置 VITE_WECHAT_DEV_TOOL_PROJECT_PATH')
                 return
             }
+            console.log("🚀 项目输出路径:", projectPath)
             const cliPath = path.join(wechatDevToolPath, 'cli.bat')
             command = `${cliPath} open --project "${projectPath}"`
         }

+ 15 - 0
src/App.vue

@@ -40,4 +40,19 @@ onHide(() => {
 
 <style lang="scss">
 @import 'uview-plus/index.scss';
+
+@font-face {
+    font-family: "Alibaba PuHuiTi", "PingFang SC", sans-serif;
+    font-weight: normal;
+    font-style: normal;
+    font-display: swap;
+}
+
+/* 应用到所有元素 */
+page,
+body,
+view,
+text {
+    font-family: "Alibaba PuHuiTi", "PingFang SC", sans-serif;
+}
 </style>

+ 62 - 3
src/api/home.ts

@@ -1,15 +1,25 @@
 import type { AccountCount, CouponList, CouponSituation } from '@/api/types/coupon'
 import { http } from '@/http/alova'
+import { ContentTypeEnum } from '@/http/tools/enum'
 
 /**
  *
  * @returns 获取首页优惠券数据
  */
-export function getCouponList() {
-    return http.Get<CouponList>('/couponCenter/APP/couponTemplate/queryByType', {
+export function getCouponList(limit: number = 3) {
+    return http.Get('/couponCenter/APP/couponTemplate/queryByType', {
         meta: {
             ignoreAuth: true,
         },
+        transform(data, headers) {
+            const respondedData = data as CouponList
+            const discountCoupon = respondedData?.discountCoupon || []
+            const couponList = respondedData?.fullReduceCoupon || []
+            return {
+                discountVoucherList: discountCoupon.slice(0, limit),
+                couponList: couponList.slice(0, limit),
+            }
+        },
     })
 }
 
@@ -18,7 +28,9 @@ export function getCouponList() {
  * @returns 获取首页收益总数
  */
 export function getAccountCount() {
-    return http.Get<AccountCount>('/couponCenter/APP/couponIssuerAccount/queryByUserId')
+    return http.Get<AccountCount>('/couponCenter/APP/couponIssuerAccount/queryByUserId', {
+        shareRequest: true,
+    })
 }
 
 /**
@@ -69,3 +81,50 @@ export function getIssuerDetail() {
 export function getIssuerStatus() {
     return http.Get('/couponCenter/APP/couponIssuerApply/queryStatusById')
 }
+
+export function getCityList() {
+    return http.Post('/customer/city/homelist', {}, {
+        transform: (data) => {
+            const cityData = data || [];
+
+            const letters = cityData.map(item => item.letter)
+            const cityList = cityData.map(item => {
+                if (Array.isArray(item.cityList)) {
+                    return item.cityList.map(city => ({
+                        name: city.areaCname,
+                        value: city.areaCode,
+                    }))
+                }
+                return []
+            })
+            return {
+                letters,
+                cityList
+            }
+        },
+        cacheFor: 1000 * 60 * 5,
+    })
+}
+
+export function getCategoryList() {
+    return http.Get('/couponCenter/APP/couponTemplate/queryCategoryListByCode', {
+        transform(data, headers) {
+            const respondedData = data || []
+            return [{
+                name: '全部',
+                value: '0',
+            }, ...respondedData.map(item => ({
+                name: item.title,
+                value: item.key,
+            }))]
+        },
+    })
+}
+
+export function getNewCouponList(params) {
+    return http.Post('/couponCenter/APP/couponTemplate/getTemplatesPageByCondition', params, {
+        headers: {
+            'Content-Type': ContentTypeEnum.FORM_URLENCODED,
+        },
+    })
+}

+ 42 - 0
src/api/income.ts

@@ -51,3 +51,45 @@ export function getUnlockAmountByUserId() {
 export function getAccountDetailTotalAmount(params: CouponIssuerAccountByPageMapForm) {
     return http.Post('/couponCenter/APP/couponIssuerAccountDetail/getAccountDetailTotalAmount', params)
 }
+
+// 收益明细-查询分享明细
+export function getBatchRelaPushDetailByBatchId(params) {
+    return http.Get('/couponCenter/APP/couponIssuerAccountDetail/getBatchRelaPushDetailByBatchId', {
+        params,
+        transform: (data, header) => {
+            return data.map(item => {
+                return {
+                    date: item.date,
+                    data: item['couponUserAssetList'].map(info => {
+                        return {
+                            ...info,
+                            name: info.receiveUserName,
+                            time: info.receiveTime,
+                        }
+                    })
+                }
+            })
+        }
+    })
+}
+
+// 收益明细-查询拉新明细
+export function getBatchRelaPullDetailByBatchId(params) {
+    return http.Get('/couponCenter/APP/couponIssuerAccountDetail/getBatchRelaPullDetailByBatchId', {
+        params,
+        transform: (data, header) => {
+            return data.map(item => {
+                return {
+                    date: item.date,
+                    data: item['CouponPullNewRecordList'].map(info => {
+                        return {
+                            ...info,
+                            name: info.nickName,
+                            time: info.createTime,
+                        }
+                    })
+                }
+            })
+        }
+    })
+}

+ 6 - 1
src/api/receiveCoupon.ts

@@ -1,5 +1,6 @@
 import { http } from '@/http/alova'
 import { ContentTypeEnum } from '@/http/tools/enum'
+import { getEnvField } from '@/utils/envField'
 
 export function getReceiveCouponDetail(couponId: string) {
     return http.Get('/couponCenter/APP/shareRecord/queryTemplateBySendUserId', {
@@ -15,12 +16,16 @@ export function getCouponShareLink(params) {
         params: {
             templateId: params.templateId,
             pathUrl: params.pathUrl,
+            version: import.meta.env.VITE_WEIXIN_MINIAPP_VERSION
         }
     })
 }
 
 export function getWxQcCode(params) {
-    return http.Post('/couponCenter/APP/shareRecord/createQRCode', params, {
+    return http.Post('/couponCenter/APP/shareRecord/createQRCode', {
+        ...params,
+        env_version: getEnvField(),
+    }, {
         responseType: 'arraybuffer',
         cacheFor: null,
         meta: {

+ 11 - 1
src/components/CustomNavigationBar.vue

@@ -10,6 +10,8 @@ const props = withDefaults(defineProps<Props>(), {
     textColor: '#333333',
     height: '88rpx',
     isShadow: true,
+    isFixed: true,
+    customBackClick: false
 })
 
 // Props
@@ -20,15 +22,22 @@ interface Props {
     backgroundColor?: string
     textColor?: string
     isShadow?: boolean
+    isFixed?: boolean
+    customBackClick?: boolean
 }
 
 // 导航栏高度
 const navBarHeight = ref('44px')
 const navBarTop = ref('44px')
 const topSafeAreaHeight = safeAreaInsets?.top || 0
+const emit = defineEmits(['backClick'])
 
 // 返回按钮点击事件
 function handleBack() {
+    if (props.customBackClick) {
+        emit('backClick')
+        return
+    }
     uni.navigateBack({
         delta: 1, // 返回层数,1表示返回上一页
     })
@@ -56,10 +65,11 @@ onMounted(() => {
         paddingTop: `${topSafeAreaHeight}px`,
         backgroundColor: props.backgroundColor,
         boxShadow: props.isShadow ? '0rpx 8rpx 7rpx 0rpx rgba(233, 233, 233, 0.17)' : 'none',
+        position: props.isFixed ? 'fixed' : 'static',
     }">
         <view class="custom-navbar">
             <view v-if="props.showBack" class="back-btn" @tap="handleBack">
-                <up-icon name="arrow-left" size="36rpx" :color="props.textColor" />
+                <up-icon name="arrow-left" size="36rpx" :color="props.backIconColor" />
             </view>
             <view class="nav-title" :style="{
                 color: props.textColor,

+ 30 - 3
src/components/IncomeItem.vue

@@ -1,5 +1,5 @@
 <template>
-    <view class="invite-item">
+    <view class="invite-item" @tap="handleItemClick">
         <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" />
@@ -18,12 +18,24 @@
                     </view>
                     <view v-if="![5, 6].includes(data.commissionType)">
                         {{ data.couponName }}
+                        <template v-if="data.status === 2">
+                            <text class="text-red ml-10rpx">(退款)</text>
+                        </template>
                     </view>
                 </view>
             </view>
             <view class="income-header-menu-left">
                 <view class="income-header-menu-left-amount">
-                    ¥{{ data.changeAmount }}
+                    <view class="center" v-if="![5, 6].includes(data.commissionType)">
+                        <template v-if="data.status === 2">
+                            <up-icon name="minus" size="12" color="#333333" />
+                        </template>
+                        <!-- <template v-else>
+                            <up-icon name="plus" size="12" color="#333333" />
+                        </template> -->
+                    </view>
+                    <up-text mode="price" :style="{ flex: '0' }" :text="data.changeAmount" align="right"
+                        color="#333333" />
                 </view>
                 <view class="income-header-menu-left-time">
                     {{ type === 1 ? data.settleTime : '预估' }}
@@ -44,8 +56,20 @@ const props = defineProps({
     type: {
         type: Number,
         default: 0,
-    }
+    },
+    itemClick: {
+        type: Function,
+        default: () => { },
+    },
 })
+
+const emit = defineEmits(['itemClick'])
+
+const handleItemClick = () => {
+    if ([5, 6].includes(props.data.commissionType)) {
+        emit('itemClick', props.data)
+    }
+}
 </script>
 
 <style lang="scss" scoped>
@@ -100,6 +124,9 @@ const props = defineProps({
                 color: #222222;
                 margin-bottom: 18rpx;
                 text-align: right;
+                display: flex;
+                flex-direction: row;
+                justify-content: flex-end;
             }
 
             .income-header-menu-left-time {

+ 216 - 0
src/components/IncomeShareInfo.vue

@@ -0,0 +1,216 @@
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue'
+import { useRequest } from 'alova/client'
+import { getBatchRelaPushDetailByBatchId, getBatchRelaPullDetailByBatchId } from '@/api/income'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import { getImageUrl } from '@/utils/imageUtil'
+import MescrollUni from '@/components/mescroll.vue'
+import { safeAreaInsets } from '@/utils'
+import { getPlatformSource } from '@/utils/platformSource'
+
+interface IncomeShareInfo {
+    batchId: bigint
+    commissionType: number
+    [key: string]: any
+}
+
+const props = defineProps({
+    type: {
+        type: Number,
+        default: 0,
+    },
+})
+
+const isShow = ref(false)
+const titleText = ref('分享')
+const currentInfo = ref<IncomeShareInfo>()
+let mescroll = null
+
+const { send, data, loading } = useRequest((...arg) => {
+    // 判断是分享还是拉新
+    if (currentInfo.value.commissionType === 6) {
+        return getBatchRelaPushDetailByBatchId(arg[0])
+    } else if (currentInfo.value.commissionType === 5) {
+        return getBatchRelaPullDetailByBatchId(arg[0])
+    }
+}, {
+    immediate: false,
+    initialData: [],
+})
+
+watch(() => data.value, (newVal) => {
+    if (newVal.length === 0 && !loading.value) {
+        // mescroll.showEmpty()
+    }
+    else {
+        // mescroll.removeEmpty()
+    }
+})
+
+const testData = computed(() => {
+    return [...data.value, ...data.value, ...data.value]
+})
+
+
+async function getShareInfo(info: IncomeShareInfo) {
+    currentInfo.value = info
+    // 判断是分享还是拉新
+    if (info.commissionType === 6) {
+        titleText.value = '分享'
+        await send({ batchId: info.batchId })
+    } else if (info.commissionType === 5) {
+        titleText.value = '拉新'
+        await send({ batchId: info.batchId })
+    }
+}
+
+defineExpose({
+    show(info: IncomeShareInfo, title?: string) {
+        if (title) {
+            titleText.value = title
+        }
+        isShow.value = true
+        getShareInfo(info)
+    },
+    hide() {
+        isShow.value = false
+    }
+})
+
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: false,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
+})
+
+const upOption = reactive({
+    use: true,
+    auto: false,
+    noMoreSize: 4,
+    page: {
+        num: 1,
+        size: 10,
+    },
+    empty: {
+        use: true,
+        tip: '暂无数据',
+        // btnText: '刷新试试',
+        icon: '/static/images/mescroll-empty1.png',
+        fixed: 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-empty1.png',
+    fixed: false,
+})
+
+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++
+}
+
+function handleTouchMove() {
+    return false
+}
+
+// 定义阻止滚轮事件的方法
+const preventWheel = (e: Event) => {
+    e.preventDefault();
+    return false;
+
+}
+
+function handleBackClick() {
+    isShow.value = false
+}
+</script>
+
+<template>
+    <up-popup catchtouchmove="true" min-height="100vh" @touchmove.stop.prevent="handleTouchMove"
+        @wheel.stop.prevent="preventWheel" :custom-style="{ width: '100vw' }" :show="isShow" mode="right">
+        <view class="h-100% w-100% bg-[#f5f5f5]">
+            <CustomNavigationBar @backClick="handleBackClick" :customBackClick="true" :is-fixed="false"
+                :title="titleText + '收益'" :show-back-button="true" background-color="#ed6b66" text-color="#ffffff"
+                back-icon-color="#ffffff" />
+            <view class="ml-[30rpx] mr-[30rpx] mt-[20rpx]"
+                :style="{ height: `calc(100% - 128rpx - ${safeAreaInsets.top}px)` }">
+                <!-- <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false" offset="70"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback"> -->
+                <scroll-view scroll-y :style="{ height: `calc(100vh - 128rpx - ${safeAreaInsets.top}px - 20rpx)` }">
+                    <template v-for="(item, index) in data" :key="index">
+                        <view class="bg-[#ffffff] rounded-[15rpx] shadow-[0_0_10rpx_rgba(0,0,0,0.1)] mb-[25rpx]">
+                            <view class="text-[28rpx] font-[bolder] text-[#333333] pl-15rpx py-15rpx flex items-center">
+                                <view class="w-[10rpx] h-[10rpx] bg-[#ed6b66] mr-[8rpx] rounded-full" />
+                                {{ item.date }}
+                                <!-- <u-title>{{ item.date }}</u-title> -->
+                            </view>
+                            <up-cell-group :border="false">
+                                <template v-for="(info, indez) in item.data" :key="indez">
+                                    <up-cell :title="info.name" :label="getPlatformSource(info.platformSource)">
+                                        <template #icon>
+                                            <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-if="type === 0"
+                                                :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
+                                            <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-else
+                                                :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                                        </template>
+                                        <template #value>
+                                            <view
+                                                class="flex flex-col flex-nowrap align-end justify-between text-[26rpx] text-[#444444]">
+                                                <view class="text-right mb-[10rpx]">
+                                                    {{ currentInfo.commissionType === 6 ? '分享' : '推荐新用户' }}
+                                                </view>
+                                                <view>{{ info.time }}</view>
+                                            </view>
+                                        </template>
+                                    </up-cell>
+                                </template>
+                            </up-cell-group>
+                        </view>
+                    </template>
+                    <up-empty v-if="data.length === 0" :icon="emptyOption.icon" :tip="emptyOption.tip"
+                        margin-top="40" />
+                </scroll-view>
+                <!-- </mescroll-uni> -->
+            </view>
+        </view>
+    </up-popup>
+</template>
+
+<style lang="scss" scoped></style>

+ 377 - 0
src/components/NewSpendAndSaveCoupon_large.vue

@@ -0,0 +1,377 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import ShortLinkModal from '@/components/ShortLinkModal.vue'
+import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
+import { getImageUrl } from '@/utils/imageUtil'
+import { useCouponShare } from '../hooks/useCouponShare'
+import { getPlatformSource } from '@/utils/platformSource'
+
+const props = defineProps({
+    coupon: {
+        type: Object,
+        default: () => ({
+            ruleDiscountRate: '0',
+            ruleMinSpendAmount: '0',
+            relatedName: '',
+            ruleDiscountCapAmount: '0',
+            validityType: '1',
+        }),
+    },
+    scrollViewId: {
+        type: String,
+        default: '',
+    },
+    enableCustomShare: {
+        type: Boolean,
+        default: false,
+    }
+})
+const emit = defineEmits(['shareClick'])
+const coupon = computed(() => props.coupon)
+const couponTemplateId = computed(() => coupon.value.templateId || '')
+const deadline = computed(() => {
+    const type = props.coupon.validityType
+    if (type === '2') {
+        return `自领取之日起${props.coupon.validDays}日内使用`
+    }
+    else if (type === '1') {
+        return `限${props.coupon.validStartTime}到${props.coupon.validEndTime}日内使用`
+    }
+    return '长期有效'
+})
+const category = computed(() => {
+    const relatedType = coupon.value.relatedType
+    if (coupon.value?.relatedName && coupon.value?.relatedName !== 'null') {
+        let relatedName = coupon.value.relatedName
+        if (relatedType === '2') {
+            relatedName = getLastPartAfterSlash(coupon.value.relatedName)
+            return `限${relatedName}分类商品使用`
+        }
+        else {
+            return `限${relatedName}商品使用`
+        }
+    }
+    return '不限商品分类'
+})
+
+// 分享弹窗相关 - 使用v-model控制弹窗显示
+const showShareModal = ref(false)
+const couponShareLink = ref('')
+const isExpand = ref(false);
+
+const { shareCoupon, ...shareHooks } = useCouponShare()
+
+// 调用分享弹窗
+async function handleShareCoupon() {
+    // 确保有优惠券数据
+    if (coupon.value.templateId) {
+        // uni.showLoading({
+        //     title: '获取分享链接中...',
+        //     mask: true,
+        // })
+        try {
+            const shareLink = await shareCoupon(couponTemplateId.value)
+            if (shareLink) {
+                couponShareLink.value = shareLink
+                showShareModal.value = true
+                if (props.enableCustomShare) {
+                    emit('shareClick', couponShareLink.value, shareHooks)
+                }
+            }
+        }
+        finally {
+            // uni.hideLoading()
+        }
+    }
+}
+
+// 关闭分享弹窗
+function handleCloseShareModal() {
+    console.log('分享弹窗已关闭')
+}
+</script>
+
+<template>
+    <view class="discount-coupon">
+        <image class="coupon-bg" :src="getImageUrl('@img/index/coupon4.png')" mode="aspectFill" />
+        <view class="discount-coupon-content">
+            <view class="discount-coupon-info-container">
+                <view class="discount-coupon-info-content">
+                    <view class="discount-coupon-info-price">
+                        <text class="discount-coupon-info-number">
+                            {{ coupon?.ruleReductionAmount }}
+                        </text>
+                        <view class="discount-coupon-info-unit">
+                            ¥
+                        </view>
+                    </view>
+                    <view class="discount-coupon-info-desc">
+                        <view class="desc-text">
+                            满{{ coupon.ruleMinSpendAmount }}元可用
+                        </view>
+                        <view class="desc-type" @tap="isExpand = !isExpand">
+                            {{ category }}
+                            <up-icon v-if="isExpand" name="arrow-up-fill" />
+                            <up-icon v-else name="arrow-down-fill" />
+                        </view>
+                    </view>
+                </view>
+                <view class="discount-coupon-info-time">
+                    {{ deadline }}
+                </view>
+            </view>
+            <view class="discount-btn-container">
+                <view class="btn-bg">
+                    <button class="discount-btn" @click="handleShareCoupon">
+                        <view>立即</view>
+                        <view>发券</view>
+                    </button>
+                </view>
+            </view>
+        </view>
+        <!-- <up-transition :show="isExpand" mode="fade-down" duration="300"> -->
+        <view :class="['coupon-info', isExpand ? 'expanded' : 'collapsed']">
+            <view class="info-content">
+                <view class="info-item">
+                    <view class="item-title">券类型:</view>
+                    <view class="item-content">{{ coupon.type === '2' ? '折扣券' : '满减券' }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定地区:</view>
+                    <view class="item-content">
+                        {{ coupon?.locality && coupon?.locality !== 'null' ?
+                            `仅可在${coupon?.locality}地区的商户使用` : '不限地区' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商户:</view>
+                    <view class="item-content">
+                        {{ coupon?.storeName && coupon?.storeName !== 'null' ?
+                            `仅限${coupon?.storeName}商户使用` : '不限商户' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商品:</view>
+                    <view class="item-content">{{ category }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定平台:</view>
+                    <view class="item-content">{{ getPlatformSource(coupon?.platformSource) }}</view>
+                </view>
+            </view>
+        </view>
+        <!-- </up-transition> -->
+        <template v-if="!enableCustomShare">
+            <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId" :share-hooks="shareHooks"
+                :share-link="couponShareLink" :scroll-view-id="scrollViewId" @close="handleCloseShareModal" />
+        </template>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.discount-coupon {
+    width: 100%;
+    max-width: 800rpx;
+    min-height: 209rpx;
+    position: relative;
+
+    .coupon-bg {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 209rpx;
+        object-fit: cover;
+        z-index: 1;
+    }
+
+    .discount-coupon-content {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        height: 209rpx;
+        // padding: 14rpx 0 17rpx 12rpx;
+
+        .discount-coupon-info-container {
+            width: 73.47%;
+            display: flex;
+            flex-direction: column;
+            padding: 14rpx 0 17rpx 12rpx;
+            z-index: 1;
+
+            .discount-coupon-info-content {
+                width: 100%;
+                height: 78.09%;
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                // justify-content: center;
+                gap: 20rpx;
+
+                .discount-coupon-info-price {
+                    width: 199rpx;
+                    display: flex;
+                    flex-direction: row;
+                    flex-wrap: nowrap;
+                    align-items: flex-end;
+                    padding-bottom: 22rpx;
+                    justify-content: center;
+                    padding-left: 10rpx;
+                    gap: 5rpx;
+
+                    .discount-coupon-info-number {
+                        height: 132rpx;
+                        font-weight: 500;
+                        font-size: 80rpx;
+                        color: #f8204b;
+                        line-height: 168rpx;
+                        letter-spacing: -8rpx;
+                    }
+
+                    .discount-coupon-info-unit {
+                        font-weight: 500;
+                        font-size: 27rpx;
+                        color: #f8204b;
+                        line-height: 56rpx;
+                    }
+                }
+
+                .discount-coupon-info-desc {
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    gap: 13rpx;
+
+                    .desc-text {
+                        font-weight: bold;
+                        font-size: 30rpx;
+                        color: #333333;
+                    }
+
+                    .desc-type {
+                        font-weight: 400;
+                        font-size: 22rpx;
+                        color: #333333;
+                        line-height: 30rpx;
+                        display: flex;
+                        flex-wrap: nowrap;
+
+
+                    }
+                }
+            }
+
+            .discount-coupon-info-time {
+                padding-top: 16rpx;
+                padding-bottom: 8rpx;
+                flex: 1;
+                width: 100%;
+                font-weight: 400;
+                font-size: 18rpx;
+                color: #666666;
+                line-height: 18rpx;
+                padding-left: 1rem;
+            }
+        }
+
+        .discount-btn-container {
+            height: 100%;
+            width: 26.53%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+
+            .btn-bg {
+                width: 125rpx;
+                height: 125rpx;
+                transform: translate(10%, 0);
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                z-index: 2;
+
+                .discount-btn {
+                    font-weight: bold;
+                    font-size: 28rpx;
+                    color: #ffffff;
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    gap: 10rpx;
+                    padding: 0;
+                    background-color: transparent;
+                    border: none;
+                    line-height: 28rpx;
+
+                    &::after {
+                        border: none;
+                        position: unset;
+                        height: inherit;
+                    }
+                }
+            }
+        }
+    }
+
+    .coupon-info {
+        padding: 20rpx 30rpx;
+        width: calc(100% - 82rpx);
+        border-left: 11rpx solid #ffd8d3;
+        border-right: 11rpx solid #ffd8d3;
+        border-bottom: 11rpx solid #ffd8d3;
+        background-color: #fefefe;
+        border-bottom-left-radius: 11rpx;
+        border-bottom-right-radius: 11rpx;
+        overflow: hidden;
+        transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+        box-shadow: 1rpx 1rpx 10rpx #fff9f4;
+
+        .info-content {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-start;
+            gap: 20rpx;
+
+            .info-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: center;
+                justify-content: flex-start;
+                gap: 10rpx;
+                color: #666;
+                font-weight: bold;
+                font-size: 26rpx;
+
+                .item-title {
+                    flex: 1;
+                    width: 160rpx;
+                    font-weight: bolder;
+                }
+            }
+        }
+    }
+
+    /* 收起状态 */
+    .coupon-info.collapsed {
+        opacity: 0;
+        max-height: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+        border-top-width: 0;
+        border-bottom-width: 0;
+    }
+
+    /* 展开状态 */
+    .coupon-info.expanded {
+        opacity: 1;
+        max-height: 500rpx;
+        /* 设置一个足够大的值以容纳所有内容 */
+        padding-top: 20rpx;
+        padding-bottom: 20rpx;
+        border-top-width: 11rpx;
+        border-bottom-width: 11rpx;
+    }
+}
+</style>

+ 127 - 20
src/components/discountCoupon.vue

@@ -4,6 +4,7 @@ import ShortLinkModal from '@/components/ShortLinkModal.vue'
 import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
 import { getImageUrl } from '@/utils/imageUtil'
 import { useCouponShare } from '../hooks/useCouponShare'
+import { getPlatformSource } from '@/utils/platformSource'
 
 const props = defineProps({
     coupon: {
@@ -40,18 +41,23 @@ const deadline = computed(() => {
 })
 const category = computed(() => {
     const relatedType = coupon.value.relatedType
-    const relatedName = getLastPartAfterSlash(coupon.value.relatedName)
-    if (relatedType === '2') {
-        return `限${relatedName}分类商品使用`
-    }
-    else {
-        return `限${relatedName}商品使用`
+    if (coupon.value?.relatedName && coupon.value?.relatedName !== 'null') {
+        let relatedName = coupon.value.relatedName
+        if (relatedType === '2') {
+            relatedName = getLastPartAfterSlash(coupon.value.relatedName)
+            return `限${relatedName}分类商品使用`
+        }
+        else {
+            return `限${relatedName}商品使用`
+        }
     }
+    return '不限商品分类'
 })
 
 // 分享弹窗相关 - 使用v-model控制弹窗显示
 const showShareModal = ref(false)
 const couponShareLink = ref('')
+const isExpand = ref(false);
 
 const { shareCoupon, ...shareHooks } = useCouponShare()
 
@@ -103,8 +109,10 @@ function handleCloseShareModal() {
                         <view class="desc-text">
                             满{{ coupon.ruleMinSpendAmount }}元可用
                         </view>
-                        <view class="desc-type">
+                        <view class="desc-type" @tap="isExpand = !isExpand">
                             {{ category }}
+                            <up-icon v-if="isExpand" name="arrow-up-fill" />
+                            <up-icon v-else name="arrow-down-fill" />
                         </view>
                     </view>
                 </view>
@@ -121,7 +129,38 @@ function handleCloseShareModal() {
                 </view>
             </view>
         </view>
-
+        <!-- <up-transition :show="isExpand" mode="fade-down" duration="300"> -->
+        <view :class="['coupon-info', isExpand ? 'expanded' : 'collapsed']">
+            <view class="info-content">
+                <view class="info-item">
+                    <view class="item-title">券类型:</view>
+                    <view class="item-content">{{ coupon.type === '2' ? '折扣券' : '满减券' }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定地区:</view>
+                    <view class="item-content">
+                        {{ coupon?.locality && coupon?.locality !== 'null' ?
+                            `仅可在${coupon?.locality}地区的商户使用` : '不限地区' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商户:</view>
+                    <view class="item-content">
+                        {{ coupon?.storeName && coupon?.storeName !== 'null' ?
+                            `仅限${coupon?.storeName}商户使用` : '不限商户' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商品:</view>
+                    <view class="item-content">{{ category }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定平台:</view>
+                    <view class="item-content">{{ getPlatformSource(coupon?.platformSource) }}</view>
+                </view>
+            </view>
+        </view>
+        <!-- </up-transition> -->
         <template v-if="!enableCustomShare">
             <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId" :share-hooks="shareHooks"
                 :share-link="couponShareLink" :scroll-view-id="scrollViewId" @close="handleCloseShareModal" />
@@ -133,7 +172,7 @@ function handleCloseShareModal() {
 .discount-coupon {
     width: 100%;
     max-width: 800rpx;
-    height: 209rpx;
+    min-height: 209rpx;
     position: relative;
 
     .coupon-bg {
@@ -141,9 +180,9 @@ function handleCloseShareModal() {
         top: 0;
         left: 0;
         width: 100%;
-        height: 100%;
+        height: 209rpx;
         object-fit: cover;
-        z-index: 0;
+        z-index: 1;
     }
 
     .discount-coupon-content {
@@ -151,7 +190,7 @@ function handleCloseShareModal() {
         display: flex;
         flex-direction: row;
         flex-wrap: nowrap;
-        height: inherit;
+        height: 209rpx;
         // padding: 14rpx 0 17rpx 12rpx;
 
         .discount-coupon-info-container {
@@ -168,7 +207,7 @@ function handleCloseShareModal() {
                 flex-direction: row;
                 flex-wrap: nowrap;
                 // justify-content: center;
-                gap: 30rpx;
+                gap: 20rpx;
 
                 .discount-coupon-info-price {
                     width: 199rpx;
@@ -177,19 +216,22 @@ function handleCloseShareModal() {
                     flex-wrap: nowrap;
                     align-items: flex-end;
                     padding-bottom: 22rpx;
-                    justify-content: flex-end;
+                    justify-content: center;
+                    padding-left: 10rpx;
+                    gap: 6rpx;
 
                     .discount-coupon-info-number {
                         height: 132rpx;
                         font-weight: 500;
-                        font-size: 132rpx;
+                        font-size: 80rpx;
                         color: #f8204b;
                         line-height: 168rpx;
+                        letter-spacing: -8rpx;
                     }
 
                     .discount-coupon-info-unit {
                         font-weight: 500;
-                        font-size: 56rpx;
+                        font-size: 36rpx;
                         color: #f8204b;
                         line-height: 56rpx;
                     }
@@ -212,6 +254,10 @@ function handleCloseShareModal() {
                         font-size: 22rpx;
                         color: #333333;
                         line-height: 30rpx;
+                        display: flex;
+                        flex-wrap: nowrap;
+
+
                     }
                 }
             }
@@ -237,15 +283,16 @@ function handleCloseShareModal() {
             align-items: center;
 
             .btn-bg {
-                width: 115rpx;
-                height: 115rpx;
-                transform: translate(10%, -4%);
+                width: 125rpx;
+                height: 125rpx;
+                transform: translate(10%, -8%);
                 display: flex;
                 justify-content: center;
                 align-items: center;
+                z-index: 2;
 
                 .discount-btn {
-                    font-weight: 500;
+                    font-weight: bold;
                     font-size: 28rpx;
                     color: #ffffff;
                     display: flex;
@@ -266,5 +313,65 @@ function handleCloseShareModal() {
             }
         }
     }
+
+    .coupon-info {
+        padding: 20rpx 30rpx;
+        width: calc(100% - 82rpx);
+        border-left: 11rpx solid #f0d4b9;
+        border-right: 11rpx solid #f0d4b9;
+        border-bottom: 11rpx solid #f0d4b9;
+        background-color: #fefefe;
+        border-bottom-left-radius: 11rpx;
+        border-bottom-right-radius: 11rpx;
+        overflow: hidden;
+        transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+        box-shadow: 1rpx 1rpx 10rpx #fff9f4;
+
+        .info-content {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-start;
+            gap: 20rpx;
+
+            .info-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: center;
+                justify-content: flex-start;
+                gap: 10rpx;
+                color: #666;
+                font-weight: bold;
+                font-size: 26rpx;
+
+                .item-title {
+                    flex: 1;
+                    width: 160rpx;
+                    font-weight: bolder;
+                }
+            }
+        }
+    }
+
+    /* 收起状态 */
+    .coupon-info.collapsed {
+        opacity: 0;
+        max-height: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+        border-top-width: 0;
+        border-bottom-width: 0;
+    }
+
+    /* 展开状态 */
+    .coupon-info.expanded {
+        opacity: 1;
+        max-height: 500rpx;
+        /* 设置一个足够大的值以容纳所有内容 */
+        padding-top: 20rpx;
+        padding-bottom: 20rpx;
+        border-top-width: 11rpx;
+        border-bottom-width: 11rpx;
+    }
 }
 </style>

+ 239 - 116
src/components/spendAndSaveCoupon_large.vue

@@ -4,6 +4,7 @@ import ShortLinkModal from '@/components/ShortLinkModal.vue'
 import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
 import { getImageUrl } from '@/utils/imageUtil'
 import { useCouponShare } from '../hooks/useCouponShare'
+import { getPlatformSource } from '@/utils/platformSource'
 
 const props = defineProps({
     coupon: {
@@ -38,17 +39,21 @@ const deadline = computed(() => {
 
 const category = computed(() => {
     const relatedType = props.coupon.relatedType
-    const relatedName = getLastPartAfterSlash(props.coupon.relatedName)
-    if (relatedType === '2') {
-        return `限${relatedName}分类商品使用`
-    }
-    else {
-        return `限${relatedName}商品使用`
+    if (coupon.value?.relatedName && coupon.value?.relatedName !== 'null') {
+        const relatedName = getLastPartAfterSlash(props.coupon.relatedName)
+        if (relatedType === '2') {
+            return `限${relatedName}分类商品使用`
+        }
+        else {
+            return `限${relatedName}商品使用`
+        }
     }
+    return '不限商品分类'
 })
 // 分享弹窗相关 - 使用v-model控制弹窗显示
 const showShareModal = ref(false)
 const couponShareLink = ref('')
+const isExpand = ref(false);
 
 const { shareCoupon, ...shareHooks } = useCouponShare()
 
@@ -85,49 +90,85 @@ function handleCloseShareModal() {
 <template>
     <view class="discount-coupon">
         <image class="coupon-bg" :src="getImageUrl('@img/index/coupon3.png')" mode="aspectFit" />
-        <view class="coupon-content">
-            <view class="coupon-amount">
-                <view class="coupon-amount-count">
-                    <view class="coupon-amount-count-icon">
-                        ¥
+        <view class="coupon-content-container">
+            <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-count-number">
-                        {{ coupon?.ruleReductionAmount }}
+                    <view class="coupon-amount-text">
+                        满{{ coupon?.ruleMinSpendAmount }}元可用
                     </view>
                 </view>
-                <view class="coupon-amount-text">
-                    满{{ coupon?.ruleMinSpendAmount }}元可用
+                <view class="coupon-info">
+                    <view class="coupon-info-category">
+                        {{ category }}
+                    </view>
+                    <view class="flex flex-row flex-nowrap text-[24rpx] text-[#666]">
+                        <view>使用规则:</view>
+                        <view class="text-blue" @tap="isExpand = !isExpand">{{ isExpand ? '收起' : '展开' }}</view>
+                    </view>
+                    <view class="coupon-info-time">
+                        <template v-if="coupon.validityType === '2'">
+                            <view>{{ deadline }}</view>
+                        </template>
+                        <template v-else-if="coupon.validityType === '1'">
+                            <!-- <view>使用日期</view> -->
+                            <view>{{ deadline }}</view>
+                        </template>
+                        <template v-else>
+                            <view>{{ deadline }}</view>
+                        </template>
+                    </view>
                 </view>
             </view>
-            <view class="coupon-info">
-                <view class="coupon-info-category">
-                    {{ category }}
+            <view class="discount-btn">
+                <button class="coupon-btn-container" @click="handleShareCoupon">
+                    <view>立即</view>
+                    <view>发券</view>
+                </button>
+                <template v-if="!enableCustomShare">
+                    <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId"
+                        :share-link="couponShareLink" :scroll-view-id="scrollViewId" :share-hooks="shareHooks"
+                        @close="handleCloseShareModal" />
+                </template>
+            </view>
+        </view>
+        <view :class="['coupon-express-info', isExpand ? 'expanded' : 'collapsed']">
+            <view class="info-content">
+                <view class="info-item">
+                    <view class="item-title">券类型:</view>
+                    <view class="item-content">{{ coupon.type === '2' ? '折扣券' : '满减券' }}</view>
                 </view>
-                <view class="coupon-info-time">
-                    <template v-if="coupon.validityType === '2'">
-                        <view>{{ deadline }}</view>
-                    </template>
-                    <template v-else-if="coupon.validityType === '1'">
-                        <!-- <view>使用日期</view> -->
-                        <view>{{ deadline }}</view>
-                    </template>
-                    <template v-else>
-                        <view>{{ deadline }}</view>
-                    </template>
+                <view class="info-item">
+                    <view class="item-title">限定地区:</view>
+                    <view class="item-content">
+                        {{ coupon?.locality && coupon?.locality !== 'null' ?
+                            `仅可在${coupon?.locality}地区的商户使用` : '不限地区' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商户:</view>
+                    <view class="item-content">
+                        {{ coupon?.storeName && coupon?.storeName !== 'null' ?
+                            `仅限${coupon?.storeName}商户使用` : '不限商户' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商品:</view>
+                    <view class="item-content">{{ category }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定平台:</view>
+                    <view class="item-content">{{ getPlatformSource(coupon?.platformSource) }}</view>
                 </view>
             </view>
         </view>
-        <view class="discount-btn">
-            <button class="coupon-btn-container" @click="handleShareCoupon">
-                <view>立即</view>
-                <view>发券</view>
-            </button>
-            <template v-if="!enableCustomShare">
-                <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId"
-                    :share-link="couponShareLink" :scroll-view-id="scrollViewId" :share-hooks="shareHooks"
-                    @close="handleCloseShareModal" />
-            </template>
-        </view>
     </view>
 </template>
 
@@ -136,7 +177,7 @@ function handleCloseShareModal() {
     // width: 670rpx;
     width: 100%;
     max-width: 800rpx;
-    height: 200rpx;
+    min-height: 200rpx;
     position: relative;
     overflow: hidden;
 
@@ -146,113 +187,195 @@ function handleCloseShareModal() {
         left: 0;
         right: 0;
         width: 100%;
-        height: 100%;
-        z-index: 0;
+        height: 200rpx;
+        z-index: 1;
         object-fit: cover;
     }
 
-    .coupon-content {
-        position: absolute;
-        height: 161.5rpx;
-        width: calc(100% - 27.68%);
-        padding-top: 18rpx;
-        padding-left: 20rpx;
-        padding-bottom: 20rpx;
+    .coupon-content-container {
+        // position: absolute;
+        width: 100%;
+        height: 200rpx;
         display: flex;
         flex-direction: row;
-        gap: 3rpx;
+        flex-wrap: nowrap;
 
-        .coupon-amount {
-            height: 100%;
-            width: 32%;
+        .coupon-content {
+            height: 161.5rpx;
+            width: calc(100% - 27.68%);
+            padding-top: 18rpx;
+            padding-left: 20rpx;
+            padding-bottom: 20rpx;
             display: flex;
-            flex-direction: column;
-            justify-content: center;
-            align-items: center;
+            flex-direction: row;
+            gap: 3rpx;
+            z-index: 2;
 
-            .coupon-amount-count {
+            .coupon-amount {
+                height: 100%;
+                width: 32%;
                 display: flex;
-                flex-direction: row;
-                flex-wrap: nowrap;
-                align-items: baseline;
+                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-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-count-number {
-                    font-weight: 500;
-                    font-size: 70rpx;
-                    color: #f83003;
-                    clear: both;
+                .coupon-amount-text {
+                    font-weight: 400;
+                    font-size: 24rpx;
+                    color: #f3513b;
                 }
             }
 
-            .coupon-amount-text {
-                font-weight: 400;
-                font-size: 24rpx;
-                color: #f3513b;
+            .coupon-info {
+                height: 100%;
+                width: 68%;
+                display: flex;
+                flex-direction: column;
+                justify-content: space-evenly;
+                padding-left: 18rpx;
+
+                .coupon-info-category {
+                    font-weight: bold;
+                    font-size: 28rpx;
+                    color: #333333;
+                }
+
+                .coupon-info-time {
+                    // padding-top: 14rpx;
+                    display: flex;
+                    flex-direction: column;
+                    gap: 5rpx;
+                    font-weight: 400;
+                    font-size: 22rpx;
+                    color: #888888;
+                }
             }
         }
 
-        .coupon-info {
+        .discount-btn {
+            z-index: 2;
             height: 100%;
-            width: 68%;
+            width: 27.68%;
             display: flex;
-            flex-direction: column;
-            justify-content: space-evenly;
-            padding-left: 18rpx;
+            justify-content: center;
+            align-items: center;
 
-            .coupon-info-category {
-                font-weight: 400;
-                font-size: 32rpx;
-                color: #333333;
-            }
+            .coupon-btn-container {
+                width: 115rpx;
+                height: 115rpx;
 
-            .coupon-info-time {
-                // padding-top: 14rpx;
-                display: flex;
-                flex-direction: column;
-                gap: 5rpx;
-                font-weight: 400;
-                font-size: 22rpx;
-                color: #888888;
+                transform: translateX(7rpx);
+                padding: 17rpx 20rpx 18rpx 20rpx;
+                background-color: transparent;
+                border: none;
+
+                font-weight: 500;
+                font-size: 28rpx;
+                color: #ffffff;
+                line-height: 41rpx;
+
+                &::after {
+                    border: none;
+                    position: unset;
+                    height: inherit;
+                }
             }
         }
     }
 
-    .discount-btn {
-        position: absolute;
-        right: 0;
-        z-index: 1;
-        height: 100%;
-        width: 27.68%;
-        display: flex;
-        justify-content: center;
-        align-items: center;
+    .coupon-express-info {
+        margin-top: -15rpx;
+        padding: 20rpx 30rpx;
+        padding-top: 35rpx;
+        width: calc(100% - 82rpx);
+        border-left: 11rpx solid #ff792f;
+        border-right: 11rpx solid #ff2c43;
+        // border-bottom: 11rpx solid transparent;
+        // border-image-slice: 1;
+        // border-image: linear-gradient(to right, #ff792f, #ff2c43);
+        position: relative;
+        background-color: #f9f9f9;
+        border-bottom-left-radius: 21rpx;
+        border-bottom-right-radius: 21rpx;
+        overflow: hidden;
+        transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
 
-        .coupon-btn-container {
-            width: 115rpx;
-            height: 115rpx;
+        &::after {
+            content: '';
+            position: absolute;
+            left: 0;
+            bottom: 0;
+            width: 100%;
+            height: 11rpx; // 底部边框宽度
+            background: linear-gradient(to right, #ff792f, #ff2c43);
+            z-index: 0;
+            // 处理圆角
+            border-bottom-left-radius: 21rpx;
+            border-bottom-right-radius: 21rpx;
+        }
 
-            transform: translateX(7rpx);
-            padding: 17rpx 20rpx 18rpx 20rpx;
-            background-color: transparent;
-            border: none;
+        .info-content {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-start;
+            gap: 20rpx;
 
-            font-weight: 500;
-            font-size: 28rpx;
-            color: #ffffff;
-            line-height: 41rpx;
+            .info-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: center;
+                justify-content: flex-start;
+                gap: 10rpx;
+                color: #666;
+                font-weight: bold;
+                font-size: 26rpx;
 
-            &::after {
-                border: none;
-                position: unset;
-                height: inherit;
+                .item-title {
+                    flex: 1;
+                    width: 160rpx;
+                    font-weight: bolder;
+                }
             }
         }
     }
+
+    /* 收起状态 */
+    .coupon-express-info.collapsed {
+        opacity: 0;
+        max-height: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+        border-top-width: 0;
+        border-bottom-width: 0;
+    }
+
+    /* 展开状态 */
+    .coupon-express-info.expanded {
+        opacity: 1;
+        max-height: 500rpx;
+        /* 设置一个足够大的值以容纳所有内容 */
+    }
+
 }
 </style>

+ 5 - 3
src/custom-components/custom-mescroll/mescroll-body.vue

@@ -30,7 +30,8 @@
 
             <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现) -->
             <!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
-            <view v-if="mescroll.optUp.use && !isDownLoading && upLoadType !== 3" class="mescroll-upwarp"
+            <view v-if="mescroll.optUp.use && !isDownLoading && !isShowEmpty && upLoadType !== 3"
+                class="mescroll-upwarp"
                 :style="{ background: mescroll.optUp.bgColor, color: mescroll.optUp.textColor }">
                 <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
                 <!-- <view v-show="upLoadType === 1">
@@ -45,7 +46,8 @@
                     {{ mescroll.optUp.textNoMore }}
                 </view> -->
                 <up-loadmore :status="upLoadType === 1 ? 'loading' : upLoadType === 2 ? 'nomore' : 'loadmore'"
-                    :loading-text="mescroll.optUp.textLoading" :nomore-text="mescroll.optUp.textNoMore" />
+                    :loading-text="mescroll.optUp.textLoading" :nomore-text="mescroll.optUp.textNoMore"
+                    :icon-color="mescroll.optUp.iconLoadingColor" :dashed="false" :line="true" />
             </view>
         </view>
 
@@ -400,7 +402,7 @@
 		destroyed() {
 			// 注销全局配置监听
 			uni.$off("setMescrollGlobalOption")
-		}
+		},
 	};
 </script>
 

+ 7 - 2
src/custom-components/custom-mescroll/mescroll-uni-main.js

@@ -833,6 +833,11 @@ MeScroll.prototype.preventDefault = function (e) {
     // 小程序不支持e.preventDefault, 已在wxs中禁止
     // app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
     // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
-    if (e && e.cancelable && !e.defaultPrevented)
-        e.preventDefault()
+    if (e && typeof e.cancelable !== 'undefined' && e.cancelable && !e.defaultPrevented) {
+        try {
+            e.preventDefault()
+        } catch (error) {
+            console.log('忽略取消不可取消的事件', error)
+        }
+    }
 }

+ 52 - 0
src/http/tools/delayLoadingMiddleware.ts

@@ -0,0 +1,52 @@
+/**
+ * 延迟loading中间件(支持延迟显示和延迟关闭)
+ * @param delayShowTimer 延迟显示时间,默认1000ms
+ * @param delayHideTimer 延迟关闭时间,默认300ms
+ * @returns 中间件函数
+ */
+export const delayLoadingMiddleware =
+    (delayShowTimer = 1000, delayHideTimer = 300) =>
+        async (ctx, next) => {
+            const { loading } = ctx.proxyStates;
+            // 自行控制loading
+            ctx.controlLoading();
+
+            let showTimer: NodeJS.Timeout | null = null;
+            let hideTimer: NodeJS.Timeout | null = null;
+
+            try {
+                if (delayShowTimer <= 0) {
+                    loading.v = true;
+                } else {
+                    // 延迟显示loading
+                    showTimer = setTimeout(() => {
+                        loading.v = true;
+                    }, delayShowTimer);
+                }
+
+                await next();
+
+                //  清除延迟显示定时器
+                if (showTimer) {
+                    clearTimeout(showTimer);
+                    showTimer = null;
+                }
+
+                // 延迟关闭loading(无论是否显示过loading)
+                hideTimer = setTimeout(() => {
+                    loading.v = false;
+                }, delayHideTimer);
+            } catch (error) {
+                // 发生错误时也需要正确处理定时器
+                if (showTimer) clearTimeout(showTimer);
+                if (hideTimer) clearTimeout(hideTimer);
+
+                // 延迟关闭loading
+                hideTimer = setTimeout(() => {
+                    loading.value = false;
+                }, delayHideTimer);
+
+                // 重新抛出错误,确保后续错误处理中间件能捕获到
+                throw error
+            }
+        }

+ 2 - 1
src/pages-A/incomePage/index.vue

@@ -16,6 +16,7 @@ definePage({
 })
 
 const topSafeAreaHeight = safeAreaInsets?.top || 0
+const bottomSafeAreaHeight = safeAreaInsets?.bottom || 0
 const activeIndex = ref(0)
 const showLoading = ref(false)
 const pickerDate = ref(Date.now())
@@ -182,7 +183,7 @@ async function pickerConfirm() {
                 </view>
             </view>
             <view class="income-content-list"
-                :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - 88rpx - 88rpx - 72rpx)` }">
+                :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - ${bottomSafeAreaHeight}px - 88rpx - 88rpx - 72rpx)` }">
                 <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false"
                     :is-show-empty="true" offset="70" @init="mescrollInit" @down="downCallback" @up="upCallback"
                     @emptyclick="reload">

+ 271 - 0
src/pages-A/incomeShareInfo/index.vue

@@ -0,0 +1,271 @@
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue'
+import { useRequest } from 'alova/client'
+import { getBatchRelaPushDetailByBatchId, getBatchRelaPullDetailByBatchId } from '@/api/income'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import { getImageUrl } from '@/utils/imageUtil'
+import MescrollUni from '@/components/mescroll.vue'
+import { safeAreaInsets } from '@/utils'
+import { getPlatformSource } from '@/utils/platformSource'
+
+interface IncomeShareInfo {
+    batchId: bigint
+    commissionType: number
+    [key: string]: any
+}
+
+definePage({
+    style: {
+        navigationBarTitleText: '收益详情',
+        navigationStyle: 'custom',
+    },
+})
+
+const props = defineProps({
+    type: {
+        type: Number,
+        default: 0,
+    },
+})
+
+const isShow = ref(false)
+const titleText = ref('分享')
+const currentInfo = ref<IncomeShareInfo>()
+let mescroll = null
+
+const { send, data, loading } = useRequest((...arg) => {
+    // 判断是分享还是拉新
+    if (currentInfo.value.commissionType === 6) {
+        return getBatchRelaPushDetailByBatchId(arg[0])
+    } else if (currentInfo.value.commissionType === 5) {
+        return getBatchRelaPullDetailByBatchId(arg[0])
+    }
+}, {
+    immediate: false,
+    initialData: [],
+})
+
+watch(() => data.value, (newVal) => {
+    if (newVal.length === 0 && !loading.value) {
+        // mescroll.showEmpty()
+    }
+    else {
+        // mescroll.removeEmpty()
+    }
+})
+
+const testData = computed(() => {
+    return [...data.value, ...data.value, ...data.value]
+})
+
+
+async function getShareInfo(info: IncomeShareInfo) {
+    currentInfo.value = info
+    // 判断是分享还是拉新
+    if (info.commissionType === 6) {
+        titleText.value = '分享'
+        await send({ batchId: info.batchId })
+    } else if (info.commissionType === 5) {
+        titleText.value = '拉新'
+        await send({ batchId: info.batchId })
+    }
+}
+
+defineExpose({
+    show(info: IncomeShareInfo, title?: string) {
+        if (title) {
+            titleText.value = title
+        }
+        isShow.value = true
+        getShareInfo(info)
+    },
+    hide() {
+        isShow.value = false
+    }
+})
+
+onLoad((options) => {
+    const { batchId, commissionType } = options
+    if (batchId && commissionType) {
+        currentInfo.value = { batchId: batchId, commissionType: Number(commissionType) }
+        getShareInfo({ batchId: batchId, commissionType: Number(commissionType) })
+    }
+})
+
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: false,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
+})
+
+const upOption = reactive({
+    use: true,
+    auto: false,
+    noMoreSize: 4,
+    page: {
+        num: 1,
+        size: 10,
+    },
+    empty: {
+        use: true,
+        tip: '暂无数据',
+        // btnText: '刷新试试',
+        icon: '/static/images/mescroll-empty1.png',
+        fixed: 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-empty1.png',
+    fixed: false,
+})
+
+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++
+}
+
+function handleTouchMove() {
+    return false
+}
+
+// 定义阻止滚轮事件的方法
+const preventWheel = (e: Event) => {
+    e.preventDefault();
+    return false;
+
+}
+
+function handleBackClick() {
+    isShow.value = false
+}
+</script>
+
+<template>
+    <!-- <up-popup catchtouchmove="true" min-height="100vh" @touchmove.stop.prevent="handleTouchMove"
+        @wheel.stop.prevent="preventWheel" :custom-style="{ width: '100vw' }" :show="isShow" mode="right"> -->
+    <view class="h-100vh w-100% bg-[#f5f5f5]">
+        <CustomNavigationBar @backClick="handleBackClick" :customBackClick="false" :is-fixed="false"
+            :title="titleText + '收益'" :show-back-button="true" background-color="#ed6b66" text-color="#ffffff"
+            back-icon-color="#ffffff" />
+        <view class="ml-[30rpx] mr-[30rpx] mt-[20rpx]"
+            :style="{ height: `calc(100% - 128rpx - ${safeAreaInsets.top}px)` }">
+            <!-- <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false" offset="70"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback"> -->
+            <scroll-view scroll-y :style="{ maxHeight: `calc(100vh - 128rpx - ${safeAreaInsets.top}px - 20rpx)` }">
+                <template v-for="(item, index) in data" :key="index">
+                    <view class="bg-[#ffffff] rounded-[15rpx] shadow-[0_0_10rpx_rgba(0,0,0,0.1)] mb-[25rpx]">
+                        <view class="text-[28rpx] font-[bolder] text-[#333333] pl-15rpx py-15rpx flex items-center">
+                            <view class="w-[10rpx] h-[10rpx] bg-[#ed6b66] mr-[8rpx] rounded-full" />
+                            {{ item.date }}
+                            <!-- <u-title>{{ item.date }}</u-title> -->
+                        </view>
+                        <up-cell-group :border="false">
+                            <template v-for="(info, indez) in item.data" :key="indez">
+                                <up-cell :title="info.name" :label="getPlatformSource(info.platformSource)">
+                                    <template #icon>
+                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-if="type === 0"
+                                            :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
+                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-else
+                                            :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                                    </template>
+                                    <template #value>
+                                        <view
+                                            class="flex flex-col flex-nowrap align-end justify-between text-[26rpx] text-[#444444]">
+                                            <view class="text-right mb-[10rpx]">
+                                                {{ currentInfo?.commissionType === 6 ? '分享' : '推荐新用户' }}
+                                            </view>
+                                            <view>{{ info.time }}</view>
+                                        </view>
+                                    </template>
+                                </up-cell>
+                            </template>
+                        </up-cell-group>
+                    </view>
+                </template>
+                <up-empty v-if="data.length === 0" :icon="emptyOption.icon" :tip="emptyOption.tip" margin-top="40" />
+                <!-- 滚动区域内的loading遮罩 -->
+                <view v-if="loading" class="loading-mask">
+                    <view class="loading-spinner" />
+                    <text class="loading-text">加载中...</text>
+                </view>
+            </scroll-view>
+            <!-- </mescroll-uni> -->
+        </view>
+    </view>
+    <!-- </up-popup> -->
+</template>
+
+<style lang="scss" scoped>
+// 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>

+ 532 - 0
src/pages-A/newCouponList/index.vue

@@ -0,0 +1,532 @@
+<script lang="ts" setup>
+import { usePagination, useRequest, actionDelegationMiddleware } from 'alova/client'
+import { storeToRefs } from 'pinia'
+import { reactive, ref, watch } from 'vue'
+import { getCategoryList, getNewCouponList } from '@/api/home'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import DiscountCoupon from '@/components/DiscountCoupon.vue'
+import SpendAndSaveCoupon from "@/components/NewSpendAndSaveCoupon_large.vue";
+import MescrollUni from '@/components/mescroll.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue'
+import { useAreaStore } from '@/store/area'
+
+import { safeAreaInsets } from '@/utils'
+
+definePage({
+    style: {
+        navigationBarTitleText: '优惠券',
+        navigationStyle: 'custom',
+    },
+})
+
+const areaStore = useAreaStore()
+const { currentCity } = storeToRefs(areaStore)
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const activeIndex = ref('0')
+const categoryType = ref('');
+const couponType = ref('0')
+const searchValue = ref('')
+const showLoading = ref(false)
+const isSearching = ref(false)
+let mescroll = null
+
+
+const { send, data, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getNewCouponList({
+    pageNo: page,
+    pageSize,
+    name: searchValue.value === '' ? '' : searchValue.value,
+    relatedId: categoryType.value,
+    type: couponType.value === '0' ? '' : couponType.value,
+    country: currentCity.value?.value === 'all' ? '' : currentCity.value?.value,
+}), {
+    initialPage: 1,
+    initialPageSize: 7,
+    append: true,
+    preloadNextPage: false,
+    preloadPreviousPage: false,
+    total: response => response?.total || 0,
+    data: response => response?.records || [],
+    immediate: false,
+    middleware: actionDelegationMiddleware('GetCouponList')
+})
+
+const { send: sendCategoryList, data: categoryList, loading: categoryLoading } = useRequest(getCategoryList, {
+    initialData: [{
+        name: '全部',
+        value: '0',
+    }],
+    immediate: false,
+})
+
+
+watch(() => data.value, (newVal) => {
+    if (newVal.length === 0 && !loading.value) {
+        mescroll.showEmpty()
+    }
+    else {
+        mescroll.removeEmpty()
+    }
+})
+
+// watch(() => searchValue.value, () => {
+//     // 判断当前是否是搜索状态
+//     if (searchValue.value !== '') {
+//         isSearching.value = true
+//     } else {
+//         isSearching.value = 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: 6,
+    isBounce: true,
+    page: {
+        num: page.value,
+        size: pageSize.value,
+    },
+    empty: {
+        use: true,
+        tip: '暂无优惠券',
+        // btnText: '刷新试试',
+        icon: '/static/images/mescroll-empty1.png',
+        fixed: false,
+    },
+    textNoMore: '--- 已经到底了 ---',
+    toTop: {
+        src: '/static/images/mescroll-totop.png',
+        offset: 1000,
+        bottom: 120,
+    },
+    // 开启虚拟列表优化(大数据量时)
+    virtual: {
+        use: false,
+        rowHeight: 300,
+    }
+})
+
+function mescrollInit(ref) {
+    mescroll = ref
+}
+
+async function downCallback() {
+
+    await refresh()
+}
+
+async function upCallback() {
+    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.showEmpty()
+    mescroll.endErr()
+})
+
+onLoad(async () => {
+    await sendCategoryList()
+    await send()
+})
+
+onHide(() => {
+
+    areaStore.resetCurrentCity();
+})
+
+onUnload(() => {
+    areaStore.resetCurrentCity();
+})
+
+async function tabChange(item) {
+    // 切换tab时,重置上拉加载
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    categoryType.value = item.index === 0 ? '' : item.value
+    if (couponType.value !== '0') {
+        couponType.value = '0'
+    }
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+
+async function search() {
+    if (activeIndex.value !== '0') {
+        activeIndex.value = '0'
+    }
+    categoryType.value = ''
+    if (couponType.value !== '0') {
+        couponType.value = '0'
+    }
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    // 判断当前是否是搜索状态
+    if (searchValue.value !== '') {
+        isSearching.value = true
+    } else {
+        isSearching.value = false
+    }
+    await reload()
+    showLoading.value = false
+}
+
+async function clear() {
+    searchValue.value = ''
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    isSearching.value = false
+    showLoading.value = false
+}
+
+function searchChange(value) {
+    searchValue.value = value
+}
+
+function regionSelection() {
+    uni.navigateTo({
+        url: '/pages-A/regionSelection/index',
+    })
+}
+
+async function couponTypeChange(type) {
+    couponType.value = type
+    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 bg-white">
+                <view class="content-search-prefix" @tap="regionSelection">
+                    <text>
+                        {{ currentCity.name }}
+                    </text>
+                    <up-icon style="margin-left: 9rpx;margin-right: 20rpx;" name="arrow-down-fill" />
+                </view>
+                <up-input style="height: 100%;" v-model="searchValue" border="none" prefix-icon="search"
+                    class="invite-content-search-input" placeholder="请输入关键字" :clearable="true" @confirm="search"
+                    @clear="clear" />
+                <view class="content-search-suffix">
+                    <view class="search-button">
+                        <up-button type="text" @tap="search">
+                            搜索
+                        </up-button>
+                    </view>
+                </view>
+            </view>
+            <view :class="[!isSearching ? 'expanded' : 'collapsed']" class="tabs-container flex mt-20rpx bg-white">
+                <up-tabs v-model:current="activeIndex" :scrollable="true" class="tabs-content" :list="categoryList"
+                    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="coupon-types h-70rpx bg-#ffffff pr-20rpx pl-20rpx flex flex-row flex-nowrap justify-start items-center gap-34rpx">
+                <view class="py-8rpx px-34rpx" :class="{ 'coupon_type_active': couponType === '0' }"
+                    @click="couponTypeChange('0')">
+                    全部
+                </view>
+                <view class="py-8rpx px-22rpx" :class="{ 'coupon_type_active': couponType === '2' }"
+                    @click="couponTypeChange('2')">
+                    折扣券
+                </view>
+                <view class="py-8rpx px-22rpx" :class="{ 'coupon_type_active': couponType === '3' }"
+                    @click="couponTypeChange('3')">
+                    满减券
+                </view>
+            </view>
+            <view class="invite-content-list"
+                :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - 70rpx - 88rpx - 20rpx - 176rpx)` }">
+                <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false" offset="70"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback" @emptyclick="reload">
+                    <template v-if="isSearching">
+                        <view v-for="(item, index) in data" :key="index" class="mt-27rpx">
+                            <view v-if="Array.isArray(item.data)" class="text-34rpx text-#333333 border">{{ item.name }}
+                            </view>
+                            <view v-for="coupon in item.data" :key="item.templateId" class="invite-content-item">
+                                <!-- <invite-item :data="item" /> -->
+                                <!-- 优惠券类型:2-折扣券,3-满减券 -->
+                                <template v-if="coupon.type === '2'">
+                                    <discount-coupon :coupon="coupon" />
+                                </template>
+                                <template v-else-if="coupon.type === '3'">
+                                    <spend-and-save-coupon :coupon="coupon" />
+                                </template>
+                            </view>
+                        </view>
+                    </template>
+                    <template v-else>
+                        <view v-for="item in data" :key="item.templateId" class="invite-content-item">
+                            <!-- <invite-item :data="item" /> -->
+                            <!-- 优惠券类型:2-折扣券,3-满减券 -->
+                            <template v-if="item.type === '2'">
+                                <discount-coupon :coupon="item" />
+                            </template>
+                            <template v-else-if="item.type === '3'">
+                                <spend-and-save-coupon :coupon="item" />
+                            </template>
+                        </view>
+                    </template>
+                    <!-- 滚动区域内的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;
+    background-color: #f8f8fa;
+
+    .invite-content {
+        display: flex;
+        flex-direction: column;
+
+        .invite-content-search {
+            padding: 10rpx 24rpx;
+            display: flex;
+            flex-direction: row;
+            flex-wrap: nowrap;
+
+            .content-search-prefix {
+                min-width: 127rpx;
+                background-color: #f6f8fc;
+                border-top-left-radius: 10rpx;
+                border-bottom-left-radius: 10rpx;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                position: relative;
+
+                text {
+                    font-weight: 400;
+                    font-size: 26rpx;
+                    color: #333333;
+                    text-overflow: ellipsis;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    padding-left: 30rpx;
+                    max-width: 110rpx;
+                }
+
+                &:after {
+                    content: '';
+                    position: absolute;
+                    right: 0;
+                    top: 50%;
+                    transform: translateY(-50%);
+                    width: 1rpx;
+                    height: 23rpx;
+                    background: #888888;
+                }
+            }
+
+            .invite-content-search-input {
+                height: 60rpx;
+                background-color: #f6f8fc;
+                padding-left: 20rpx !important;
+                padding-right: 20rpx !important;
+                border-radius: 0;
+            }
+
+            .content-search-suffix {
+                border-bottom-right-radius: 10rpx;
+                border-top-right-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: #888888;
+                    }
+                }
+            }
+        }
+
+        .tabs-container {
+            height: 88rpx;
+            display: flex;
+            flex-direction: row;
+            justify-content: space-around;
+            align-items: center;
+            box-shadow: 0rpx 1rpx 4rpx 0rpx rgba(213, 213, 213, 0.29);
+            overflow: hidden;
+            transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+
+            .tabs-content {
+                width: 100%;
+            }
+
+        }
+
+        /* 收起状态 */
+        .tabs-container.collapsed {
+            opacity: 0;
+            max-height: 0;
+        }
+
+        /* 展开状态 */
+        .tabs-container.expanded {
+            opacity: 1;
+            max-height: 90rpx;
+        }
+
+        .coupon-types {
+
+            view {
+                width: 115rpx;
+                height: 38rpx;
+                background-color: #f6f8fc;
+                border-radius: 5rpx;
+                text-align: center;
+                font-weight: 400;
+                font-size: 24rpx;
+                color: #888888;
+            }
+
+            .coupon_type_active {
+                color: #ed6b66;
+                font-weight: bold;
+                // background-color: #ed6b66;
+                // border: 1px solid #ed6b66;
+            }
+
+        }
+
+        .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.9);
+                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);
+                }
+            }
+        }
+    }
+
+    .accordion-enter-active,
+    .accordion-leave-active {
+        transition: all 0.4s ease;
+        overflow: hidden;
+    }
+
+    .accordion-enter-from,
+    .accordion-leave-to {
+        max-height: 0;
+        opacity: 0;
+        transform: translateY(-10rpx);
+    }
+
+    .accordion-enter-to,
+    .accordion-leave-from {
+        max-height: 88rpx;
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+</style>

+ 312 - 0
src/pages-A/regionSelection/index.vue

@@ -0,0 +1,312 @@
+<script lang="ts" setup>
+import { computed, ref, isRef, toRaw, watch, reactive } from 'vue'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue';
+import { useRequest, accessAction } from 'alova/client'
+import { getCityList } from '@/api/home'
+import { safeAreaInsets } from '@/utils'
+import { useAreaStore } from '@/store/area'
+import { storeToRefs } from 'pinia'
+import { pinyin } from 'pinyin-pro'
+
+definePage({
+    style: {
+        navigationBarTitleText: '地区选择',
+        navigationStyle: 'custom',
+    },
+})
+
+const safeAreaTop = safeAreaInsets?.top || 0
+const safeAreaInsetsBottom = safeAreaInsets?.bottom || 0
+
+const searchKeyword = ref('')
+const isSearch = ref(false);
+const areaStore = useAreaStore()
+const { lastSelectedCity } = storeToRefs(areaStore)
+const cityData = reactive({
+    letters: [],
+    cityList: [],
+})
+
+const { send, data, loading } = useRequest(getCityList, {
+    immediate: false,
+    initialData: {
+        letters: [],
+        cityList: [],
+    }
+})
+
+onLoad(async () => {
+    await send();
+    cityData.letters = [...data.value.letters]
+    cityData.cityList = [...data.value.cityList]
+})
+
+// watch(() => searchKeyword.value, (newValue) => {
+//     // 判断是否是搜索状态
+//     if (newValue.trim() === '') {
+//         isSearch.value = false
+//     } else {
+//         isSearch.value = true
+//     }
+// })
+
+
+const lastSelectedCityList = computed(() => {
+    return [...lastSelectedCity.value].reverse()
+})
+
+
+function handleTap(item) {
+    const temp = isRef(item) ? toRaw(item) : item
+    if (temp.value === 'all') {
+        areaStore.resetCurrentCity()
+    } else {
+        areaStore.currentCity = temp
+        areaStore.addLastSelectedCity(temp)
+    }
+    accessAction('GetCouponList', (delegatedActions) => {
+        delegatedActions.reload();
+    })
+    uni.navigateBack({
+        delta: 1,
+    })
+}
+
+
+// 搜索结果计算属性
+function searchResults() {
+    console.log('执行搜索', searchKeyword.value)
+    if (!searchKeyword.value.trim()) {
+        isSearch.value = false
+        return
+    }
+
+    const keyword = searchKeyword.value.trim().toLowerCase()
+    const filteredLetters: string[] = []
+    const filteredCityList: any[] = []
+
+    // 遍历所有字母分组
+    data.value.letters.forEach((letter: string, index: number) => {
+        const cityGroup = data.value.cityList[index]
+        if (!cityGroup) return
+
+        // 过滤当前字母分组下的城市
+        const filteredCities = cityGroup.filter((city: any) => {
+            if (!city?.name) return false
+
+            const cityName = city.name.toLowerCase()
+
+            // 1. 汉字匹配(全汉字、局部汉字模糊)
+            if (cityName.includes(keyword)) {
+                return true
+            }
+
+            // 2. 拼音匹配
+            // 获取城市名称的拼音(多种格式)
+            const fullPinyin = pinyin(city.name, { type: 'string' }).toLowerCase() // 全拼字符串
+            const firstLetterPinyin = pinyin(city.name, { type: 'first', style: 'lowercase' }) // 首字母
+            const pinyinArray = pinyin(city.name, { type: 'array' }) // 拼音数组
+
+            // 2.1 全拼音匹配和局部拼音模糊匹配
+            if (fullPinyin.includes(keyword)) {
+                return true
+            }
+
+            // 2.2 拼音首字母连续匹配
+            if (firstLetterPinyin.includes(keyword)) {
+                return true
+            }
+
+            // 2.3 拼音分段匹配(支持部分拼音匹配)
+            const pinyinSegment = pinyin(city.name, { type: 'array', tone: false }).join(' ')
+            if (pinyinSegment.toLowerCase().includes(keyword)) {
+                return true
+            }
+
+            // 3. 混合匹配(支持汉字和拼音混合输入)
+            // 例如:输入"北jing"可以匹配"北京"
+            const mixedMatch = keyword.split('').every(char => {
+                return cityName.includes(char) || fullPinyin.includes(char) || firstLetterPinyin.includes(char)
+            })
+
+            if (mixedMatch) {
+                return true
+            }
+
+            // 4. 字母直接匹配
+            if (letter.toLowerCase() === keyword) {
+                return true
+            }
+
+            return false
+        })
+
+        // 如果当前字母分组下有匹配的城市,添加到结果中
+        if (filteredCities.length > 0) {
+            filteredLetters.push(letter)
+            filteredCityList.push(filteredCities)
+        }
+    })
+
+    cityData.letters = filteredLetters.length > 0 ? [...filteredLetters] : ['']
+    cityData.cityList = [...filteredCityList]
+}
+
+// 搜索输入变化处理
+function handleSearch(value: string) {
+    isSearch.value = true
+    searchResults()
+}
+
+function handleClear() {
+    isSearch.value = false
+    // searchKeyword.value = ''
+}
+
+function handleBlur() {
+    if (!searchKeyword.value.trim()) {
+        isSearch.value = false
+    }
+}
+
+</script>
+
+<template>
+    <view class="region-selection-container">
+        <custom-navigation-bar :is-fixed="false" title="" background-color="#ed6b66" backIconColor="#ffffff" />
+        <view class="region-selection-search">
+            <up-search placeholder="搜索" :showAction="false" v-model="searchKeyword" @clear="handleClear"
+                @search="handleSearch" @blur="handleBlur" />
+        </view>
+        <transition name="fade">
+            <view v-if="!loading" class="region-selection-list"
+                :style="{ height: `calc(100vh - ${safeAreaTop}px - 88rpx - 56px)` }">
+                <up-index-list v-if="isSearch" active-color="#ed6b66" :safeBottomFix="true"
+                    :index-list="cityData.letters">
+                    <template #header>
+                        <view class="region-selection-header">
+                            <view class="region-selection-header-title">最近选择</view>
+                            <view class="region-selection-header-content">
+                                <view class="city-tag" @tap="handleTap({ name: '全部地区', value: 'all' })">全部地区</view>
+                                <view v-for="(city, index) in lastSelectedCityList" :key="index" class="city-tag"
+                                    @tap="handleTap(city)">
+                                    {{ city.name }}
+                                </view>
+                            </view>
+                        </view>
+                    </template>
+                    <template v-for="(item, index) in cityData.cityList" :key="index">
+                        <up-index-item>
+                            <up-index-anchor color="#666666" size="16" bg-color="#f8f8f8"
+                                :text="cityData.letters[index]" />
+                            <up-cell-group :border="false">
+                                <template v-for="(city, index) in item" :key="index">
+                                    <up-cell :clickable="true" style="padding: 0 20rpx;" :title="city.name"
+                                        :border="false" @tap="handleTap(city)" />
+                                </template>
+                            </up-cell-group>
+                        </up-index-item>
+                    </template>
+                </up-index-list>
+                <up-index-list v-else active-color="#ed6b66" :safeBottomFix="true" :index-list="data.letters">
+                    <template #header>
+                        <view class="region-selection-header">
+                            <view class="region-selection-header-title">最近选择</view>
+                            <view class="region-selection-header-content">
+                                <view class="city-tag" @tap="handleTap({ name: '全部地区', value: 'all' })">全部地区</view>
+                                <view v-for="(city, index) in lastSelectedCityList" :key="index" class="city-tag"
+                                    @tap="handleTap(city)">
+                                    {{ city.name }}
+                                </view>
+                            </view>
+                        </view>
+                    </template>
+                    <template v-for="(item, index) in data.cityList" :key="index">
+                        <up-index-item>
+                            <up-index-anchor color="#666666" size="16" bg-color="#f8f8f8" :text="data.letters[index]" />
+                            <up-cell-group :border="false">
+                                <template v-for="(city, index) in item" :key="index">
+                                    <up-cell :clickable="true" style="padding: 0 20rpx;" :title="city.name"
+                                        :border="false" @tap="handleTap(city)" />
+                                </template>
+                            </up-cell-group>
+                        </up-index-item>
+                    </template>
+                </up-index-list>
+
+            </view>
+            <view v-else class="region-selection-loading"
+                :style="{ height: `calc(100vh - ${safeAreaTop}px - 88rpx - 104rpx)` }">
+                <up-loading-icon mode="circle" size="28" color="#ed6b66" />
+            </view>
+        </transition>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.region-selection-container {
+
+    .region-selection-search {
+        background-color: #ffffff;
+        padding: 20rpx 30rpx;
+        width: calc(100vw - 60rpx);
+        height: 64rpx;
+        box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
+    }
+
+    .region-selection-header {
+        display: flex;
+        flex-direction: column;
+        gap: 20rpx;
+        padding: 20rpx 30rpx;
+
+        .region-selection-header-title {
+            font-size: 24rpx;
+            font-weight: bolder;
+        }
+
+        .region-selection-header-content {
+            display: flex;
+            flex-direction: row;
+            flex-wrap: wrap;
+            gap: 20rpx;
+            align-items: center;
+            justify-content: first baseline;
+
+            .city-tag {
+                background-color: #f8f8f8;
+                text-align: center;
+                padding: 20rpx 30rpx;
+                color: #333333;
+                transition: all 0.9s;
+
+                &:active {
+                    background-color: #e0e0e0;
+                }
+            }
+        }
+    }
+
+
+    .region-selection-list {
+
+        :deep(.u-index-list) {
+            height: inherit;
+            position: relative;
+        }
+
+        :deep(.u-index-list__letter) {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+
+    }
+
+    .region-selection-loading {
+        display: flex;
+        justify-content: center;
+        background-color: #f8f8f8;
+    }
+}
+</style>

+ 77 - 48
src/pages-A/shareFriend/index.vue

@@ -1,5 +1,5 @@
 <template>
-    <view class="share-container">
+    <view class="share-container pb-safe">
         <!-- <up-status-bar /> -->
         <CustomNavigationBar :show-back="true" title="邀请好友" />
 
@@ -65,6 +65,8 @@
                 </view>
             </view>
         </view>
+
+        <up-safe-bottom class="share-safe-bottom" />
     </view>
 </template>
 
@@ -75,7 +77,7 @@ import { nextTick, ref } from 'vue'
 import { markSuccess } from '@/api/receiveCoupon'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import { useInviteCodeStore } from '@/store/inviteQCCode'
-import { safeAreaInsets } from '@/utils'
+import { safeAreaInsets, isPcWeixinMp } from '@/utils'
 import { getImageUrl } from '@/utils/imageUtil'
 
 definePage({
@@ -248,26 +250,36 @@ async function loadRemoteImage(canvas, imageUrl) {
             reject(new Error('图片加载超时'))
         }, 10000) // 10秒超时
 
-        image.onload = () => {
-            clearTimeout(timeout)
-            const imageWidth = image.naturalWidth || image.width
-            const imageHeight = image.naturalHeight || image.height
+        uni.getImageInfo({
+            src: imageUrl,
+            success: (res) => {
+                image.onload = () => {
+                    clearTimeout(timeout)
+                    const imageWidth = image.naturalWidth || image.width || res.width
+                    const imageHeight = image.naturalHeight || image.height || res.height
 
-            if (image.complete && imageWidth > 0 && imageHeight > 0) {
-                resolve(image)
-            }
-            else {
-                reject(new Error('图片加载事件触发但尺寸无效'))
-            }
-        }
+                    if ((isPcWeixinMp() || image.complete) && imageWidth > 0 && imageHeight > 0) {
+                        resolve(image)
+                    }
+                    else {
+                        reject(new Error('图片加载事件触发但尺寸无效'))
+                    }
+                }
 
-        image.onerror = (err) => {
-            clearTimeout(timeout)
-            console.error('加载远程图片失败:', err)
-            reject(err)
-        }
+                image.onerror = (err) => {
+                    clearTimeout(timeout)
+                    console.error('加载远程图片失败:', err)
+                    reject(err)
+                }
+
+                image.src = res.path
+
+            },
+            fail: (fail) => {
+                reject(new Error('获取图片信息失败'))
+            },
+        })
 
-        image.src = imageUrl
     })
 }
 
@@ -335,7 +347,11 @@ function drawRulesDescription(ctx, width, height, scale) {
 // 绘制二维码区域
 async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
     const qrSize = 125 * scale
-    const qrX = ((width - qrSize) / 3) - 130
+    // const qrX = (width / 2) - qrSize - 100
+    let qrX = (width / 2) - qrSize - 80
+    if (isPcWeixinMp()) {
+        qrX = (width / 2) - qrSize - 35
+    }
     const qrY = 660 * scale
 
     // 使用柔和的白色背景,增加卡片层次感
@@ -415,35 +431,42 @@ async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
                 const timeout = setTimeout(() => {
                     reject(new Error('图片加载超时'))
                 }, 10000) // 10秒超时
+                uni.getImageInfo({
+                    src: tempFilePath || imgSrc,
+                    success: (res) => {
+                        qrImage.onload = () => {
+                            console.log('图片加载成功:', res)
+                            clearTimeout(timeout)
+                            console.log('二维码图片加载成功', {
+                                complete: qrImage.complete,
+                                naturalWidth: qrImage.naturalWidth || qrCodeImg.width || res.width,
+                                naturalHeight: qrImage.naturalHeight || qrCodeImg.height || res.height,
+                            }, qrImage)
+
+                            // 获取图片尺寸,兼容微信小程序环境
+                            const imageWidth = qrImage.naturalWidth || qrImage.width || res.width
+                            const imageHeight = qrImage.naturalHeight || qrImage.height || res.height
+                            // 再次检查图片是否真正加载完成
+                            if ((isPcWeixinMp() || qrImage.complete) && imageWidth > 0 && imageHeight > 0) {
+                                resolve()
+                            }
+                            else {
+                                reject(new Error('图片加载事件触发但尺寸无效'))
+                            }
+                        }
+                        qrImage.onerror = (err) => {
+                            clearTimeout(timeout)
+                            console.error('加载二维码图片失败:', err)
+                            reject(err)
+                        }
 
-                qrImage.onload = () => {
-                    clearTimeout(timeout)
-                    console.log('二维码图片加载成功', {
-                        complete: qrImage.complete,
-                        naturalWidth: qrImage.naturalWidth || qrCodeImg.width,
-                        naturalHeight: qrImage.naturalHeight || qrCodeImg.height,
-                    }, qrImage)
-
-                    // 获取图片尺寸,兼容微信小程序环境
-                    const imageWidth = qrImage.naturalWidth || qrImage.width
-                    const imageHeight = qrImage.naturalHeight || qrImage.height
-
-                    // 再次检查图片是否真正加载完成
-                    if (qrImage.complete && imageWidth > 0 && imageHeight > 0) {
-                        resolve()
-                    }
-                    else {
-                        reject(new Error('图片加载事件触发但尺寸无效'))
-                    }
-                }
-                qrImage.onerror = (err) => {
-                    clearTimeout(timeout)
-                    console.error('加载二维码图片失败:', err)
-                    reject(err)
-                }
-
-                // 优先使用临时文件路径,否则使用base64数据
-                qrImage.src = tempFilePath || imgSrc
+                        // 优先使用临时文件路径,否则使用base64数据
+                        qrImage.src = res.path
+                    },
+                    fail: (fail) => {
+                        reject(new Error('获取图片信息失败'))
+                    },
+                })
             })
 
             const finalWidth = qrImage.naturalWidth || qrImage.width
@@ -823,6 +846,12 @@ function handleLongPressImage() {
     line-height: 36rpx;
 }
 
+.share-safe-bottom {
+    background-color: #fff;
+    position: fixed;
+    bottom: 0;
+}
+
 /* 加载遮罩 */
 .loading-mask {
     position: fixed;

+ 53 - 40
src/pages-A/sharePage/index.vue

@@ -90,7 +90,7 @@ import { nextTick, ref } from 'vue'
 import { getCouponDetail } from '@/api/home'
 import { getWxQcCode, markSuccess } from '@/api/receiveCoupon'
 import { useInviteCodeStore } from '@/store/inviteQCCode'
-import { parseUrlParams, safeAreaInsets } from '@/utils'
+import { parseUrlParams, safeAreaInsets, isPcWeixinMp } from '@/utils'
 import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
 
 definePage({
@@ -188,7 +188,6 @@ async function getWxacodeBase64() {
         const base64 = uni.arrayBufferToBase64(arrayBuffer)
         const imageUrl = `data:image/jpeg;base64,${base64}`
         const params = parseUrlParams(contentData)
-        console.log(params, '获取到的参数')
         shareRecordId.value = params[inviteCode.value.shareKey]
         return imageUrl
     }
@@ -322,7 +321,6 @@ async function drawCompleteShareImage(ctx, canvas, qrCodeImg) {
 
 // 绘制顶部装饰
 function drawTopDecoration(ctx, width, height, scale) {
-    // 现代化渐变背景 - 美团橙色系
     const gradient = ctx.createLinearGradient(0, 0, width, 0)
     gradient.addColorStop(0, '#FF7A75')
     gradient.addColorStop(0.98, '#ED6B66')
@@ -492,18 +490,26 @@ function drawCouponDescription(ctx, width, height, scale) {
     }
 
     const relatedType = couponDetail.value.relatedType
-    const relatedName = getLastPartAfterSlash(couponDetail.value.relatedName)
     let descriptionText = ''
-
-    if (relatedType === '1') {
-        descriptionText = `限${relatedName}分类商品使用`
-    }
-    else {
-        descriptionText = `限${relatedName}商品使用`
+    if (couponDetail.value?.relatedName && couponDetail.value?.relatedName !== 'null') {
+        let relatedName = couponDetail.value.relatedName
+        if (relatedType === '2') {
+            relatedName = getLastPartAfterSlash(couponDetail.value.relatedName)
+            descriptionText = `限${relatedName}分类商品使用`
+        }
+        else {
+            descriptionText = `限${relatedName}商品使用`
+        }
+    } else if (couponDetail.value?.storeName && couponDetail.value?.storeName !== 'null') {
+        descriptionText = `仅限${couponDetail.value?.storeName}商户使用`
+    } else if (couponDetail.value?.locality && couponDetail.value?.locality !== 'null') {
+        descriptionText = `仅限${couponDetail.value?.locality}使用`
+    } else {
+        throw new Error('无法获取该优惠券内容')
     }
 
     // 使用自动换行函数绘制
-    const fontSize = 28
+    const fontSize = 20
     const lineHeight = fontSize * 1.5 * scale
     const nextLineY = drawWrappedText(descriptionText, fontSize, 260 * scale, '#333333')
 
@@ -654,41 +660,47 @@ async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
             }
 
             const qrImage = canvas.createImage()
-
             // 使用Promise确保图片加载完成
             await new Promise((resolve, reject) => {
                 const timeout = setTimeout(() => {
                     reject(new Error('图片加载超时'))
                 }, 10000) // 10秒超时
+                uni.getImageInfo({
+                    src: tempFilePath || imgSrc,
+                    success: (res) => {
+                        qrImage.onload = () => {
+                            console.log('图片加载成功:', res)
+                            clearTimeout(timeout)
+                            console.log('二维码图片加载成功', {
+                                complete: qrImage.complete,
+                                naturalWidth: qrImage.naturalWidth || qrCodeImg.width || res.width,
+                                naturalHeight: qrImage.naturalHeight || qrCodeImg.height || res.height,
+                            }, qrImage)
+
+                            // 获取图片尺寸,兼容微信小程序环境
+                            const imageWidth = qrImage.naturalWidth || qrImage.width || res.width
+                            const imageHeight = qrImage.naturalHeight || qrImage.height || res.height
+                            // 再次检查图片是否真正加载完成
+                            if ((isPcWeixinMp() || qrImage.complete) && imageWidth > 0 && imageHeight > 0) {
+                                resolve()
+                            }
+                            else {
+                                reject(new Error('图片加载事件触发但尺寸无效'))
+                            }
+                        }
+                        qrImage.onerror = (err) => {
+                            clearTimeout(timeout)
+                            console.error('加载二维码图片失败:', err)
+                            reject(err)
+                        }
 
-                qrImage.onload = () => {
-                    clearTimeout(timeout)
-                    console.log('二维码图片加载成功', {
-                        complete: qrImage.complete,
-                        naturalWidth: qrImage.naturalWidth || qrCodeImg.width,
-                        naturalHeight: qrImage.naturalHeight || qrCodeImg.height,
-                    }, qrImage)
-
-                    // 获取图片尺寸,兼容微信小程序环境
-                    const imageWidth = qrImage.naturalWidth || qrImage.width
-                    const imageHeight = qrImage.naturalHeight || qrImage.height
-
-                    // 再次检查图片是否真正加载完成
-                    if (qrImage.complete && imageWidth > 0 && imageHeight > 0) {
-                        resolve()
-                    }
-                    else {
-                        reject(new Error('图片加载事件触发但尺寸无效'))
-                    }
-                }
-                qrImage.onerror = (err) => {
-                    clearTimeout(timeout)
-                    console.error('加载二维码图片失败:', err)
-                    reject(err)
-                }
-
-                // 优先使用临时文件路径,否则使用base64数据
-                qrImage.src = tempFilePath || imgSrc
+                        // 优先使用临时文件路径,否则使用base64数据
+                        qrImage.src = res.path
+                    },
+                    fail: (fail) => {
+                        reject(new Error('获取图片信息失败'))
+                    },
+                })
             })
 
             const finalWidth = qrImage.naturalWidth || qrImage.width
@@ -706,6 +718,7 @@ async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
             ctx.clip()
             ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
 
+
             // 图片绘制成功后,添加一个精致的内边框
             ctx.restore()
             ctx.beginPath()

+ 15 - 14
src/pages/home/home.vue

@@ -1,16 +1,17 @@
 <script lang="ts" setup>
 import { useRequest } from 'alova/client'
 import { storeToRefs } from 'pinia'
-import { ref, watch } from 'vue'
+import { ref, watch, computed } from 'vue'
 import { getAccountCount, getCouponDetail, getCouponSituation, getShareInfo } from '@/api/home'
 import DiscountCoupon from '@/components/DiscountCoupon.vue'
 import SpendAndSaveCoupon from '@/components/SpendAndSaveCoupon.vue'
 import { useShare } from '@/hooks/useShare'
-import { useCouponStore } from '@/store/coupon'
+import { useNewCouponStore as useCouponStore } from '@/store/coupon'
 import { useTokenStore } from '@/store/token'
 import { useUserStore } from '@/store/user'
 import { getImageUrl } from '@/utils/imageUtil'
 import { toLoginPage } from '@/utils/toLoginPage'
+import { delayLoadingMiddleware } from '@/http/tools/delayLoadingMiddleware'
 
 defineOptions({
     name: 'Home',
@@ -31,19 +32,18 @@ const userStore = useUserStore()
 
 const refreshing = ref(false)
 
-// 分享配置
-const shareConfig = ref(null)
-const shareButtonRef = ref<HTMLElement | null>(null)
-const currentCouponId = ref<string>('')
-
 // 获取优惠券
 const couponStore = useCouponStore()
-const { couponList, discountVoucherList, loading } = storeToRefs(couponStore)
+const { loading, data } = storeToRefs(couponStore)
+const discountVoucherList = computed(() => data.value.discountVoucherList)
+const couponList = computed(() => data.value.couponList)
+
 
 // 获取首页收益
 const { send: getAccountCountRequest, data: accountCountData, loading: accountCountLoading } = useRequest(getAccountCount, {
     immediate: false,
     dependencies: [],
+    middleware: delayLoadingMiddleware(150, 200)
 })
 
 // 获取首页领券情况数据
@@ -81,7 +81,6 @@ onShareTimeline(() => {
 
 onShow((options) => {
     couponStore.getCouponListByType()
-    console.log(hasLogin.value)
     // 登录后查询收益数据
     if (hasLogin.value) {
         Promise.allSettled([getAccountCountRequest(), getCouponSituationRequest()])
@@ -103,7 +102,6 @@ function getNavigationBarHeight() {
     uni.getSystemInfo({
         success: (res) => {
             const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
-            console.log('顶部导航栏高度:', res.statusBarHeight, menuButtonInfo)
             // 顶部导航栏高度 = 状态栏高度 + 胶囊的高度
             navigationBarHeight.value = res.statusBarHeight + menuButtonInfo.height + 12
         },
@@ -114,12 +112,15 @@ getNavigationBarHeight()
 
 function toDiscountCouponList() {
     uni.navigateTo({
-        url: '/pages-A/discountcouponList/index?type=2',
+        // url: '/pages-A/discountcouponList/index?type=2',
+        url: '/pages-A/newCouponList/index',
     })
 }
 function toSpendAndSaveCouponList() {
     uni.navigateTo({
-        url: '/pages-A/spendAndSaveCouponList/index?type=3',
+        // url: '/pages-A/spendAndSaveCouponList/index?type=3',
+        // url: '/pages-A/regionSelection/index',
+        url: '/pages-A/newCouponList/index',
     })
 }
 
@@ -212,7 +213,7 @@ async function onRefresh() {
                     </view>
                 </view>
                 <view class="home-header-coupon-content">
-                    <template v-for="item in couponList" :key="item.id">
+                    <template v-for="(item, index) in couponList" :key="index">
                         <spend-and-save-coupon :coupon="item" />
                     </template>
                     <view v-if="loading" class="coupon-content-loading">
@@ -247,7 +248,7 @@ async function onRefresh() {
                     </view>
                 </view>
                 <view class="home-coupon-content">
-                    <template v-for="item in discountVoucherList" :key="item.id">
+                    <template v-for="(item, index) in discountVoucherList" :key="index">
                         <discount-coupon :coupon="item" />
                     </template>
                     <view v-if="loading" class="home-coupon-content-loading">

+ 34 - 11
src/pages/income/income.vue

@@ -6,13 +6,13 @@ import { computed, ref, watch } from 'vue'
 import { getAccountCount } from '@/api/home'
 import { getCouponIssuerAccountByPageMap } from '@/api/income'
 import IncomeItem from '@/components/IncomeItem.vue'
-import MescrollUni from '@/components/mescroll.vue'
 import MescrollBody from '@/custom-components/custom-mescroll/mescroll-body.vue'
 import { useShare } from '@/hooks/useShare'
 import { useUserStore } from '@/store'
 import { useTokenStore } from '@/store/token'
 import { changtime, menuButtonInfo, rpxToPx, safeAreaInsets, systemInfo } from '@/utils'
 import { getImageUrl } from '@/utils/imageUtil'
+import IncomeShareInfo from '@/components/IncomeShareInfo.vue'
 
 definePage({
     style: {
@@ -41,6 +41,7 @@ const pickerDate = ref(Date.now())
 const pickerShow = ref(false)
 const totalAmount = ref(0)
 const mescrollHeight = ref(0)
+const incomeShareInfoRef = ref(null)
 let mescroll = null
 
 // 新增:滚动距离和透明度
@@ -58,7 +59,7 @@ onPageScroll((e) => {
     scrollTop.value = e.scrollTop
 })
 
-const { send, data: incomeData, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getCouponIssuerAccountByPageMap({
+const { send: getIncomeData, data: incomeData, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getCouponIssuerAccountByPageMap({
     pageNo: page,
     pageSize,
     type: 1,
@@ -94,6 +95,12 @@ watch(() => headerOverlayOpacity.value, (newVal) => {
     }
 })
 
+watch(() => loading.value, (newVal) => {
+    if (newVal && !showLoading.value) {
+        mescroll.showUpScroll()
+    }
+})
+
 // ============ mescroll 配置 ============
 const downOption = reactive({
     use: true,
@@ -122,7 +129,8 @@ const upOption = reactive({
         icon: '/static/images/mescroll-empty.png',
         fixed: false,
     },
-    textNoMore: '--- 已经到底了 ---',
+    textNoMore: '已经到底了',
+    iconLoadingColor: '#ed6b66',
     toTop: {
         src: '/static/images/mescroll-totop.png',
         offset: 1000,
@@ -141,8 +149,6 @@ function mescrollInit(ref) {
 }
 
 async function downCallback() {
-    console.log('下拉刷新触发')
-
     // await refresh()
     await getAccountCountRequest()
     await refresh()
@@ -158,7 +164,7 @@ async function onRefresh() {
 }
 
 async function upCallback() {
-    console.log('上拉加载触发')
+    // console.log('上拉加载触发')
     if (isLastPage.value) {
         console.log(mescroll)
         mescroll.endSuccess(0, false)
@@ -169,7 +175,7 @@ async function upCallback() {
 }
 
 onReachBottom(() => {
-    console.log('上拉加载触发')
+    // console.log('上拉加载触发')
     if (isLastPage.value) {
         // console.log(mescroll)
         // mescroll.endSuccess(0, false)
@@ -193,6 +199,9 @@ onSuccess((response) => {
 onError(() => {
     console.log('请求失败')
     mescroll.endErr()
+    console.log(mescroll)
+    mescroll.hideUpScroll()
+    mescroll.showEmpty()
 })
 
 async function tabChange(index) {
@@ -219,18 +228,24 @@ async function pickerConfirm() {
 onShow(async () => {
     // 登录后查询收益数据
     if (hasLogin) {
-        await getAccountCountRequest()
-        await reload()
+        // await getAccountCountRequest()
+        // await reload()
         // 数据会在useScroll的onMounted中自动加载,这里不需要额外调用
     }
 })
 
-onLoad(() => {
+onLoad(async () => {
     // 计算mescroll最小高度
     const systemInfo = uni.getSystemInfoSync()
     const vh100 = systemInfo.screenHeight - systemInfo.safeAreaInsets.bottom
     const tabbarHeight = (systemInfo.screenWidth / 750) * 100
     mescrollHeight.value = vh100 - tabbarHeight
+
+    if (hasLogin) {
+        await getAccountCountRequest()
+        await getIncomeData()
+        // 数据会在useScroll的onMounted中自动加载,这里不需要额外调用
+    }
 })
 
 // 创建分享hook实例
@@ -255,6 +270,13 @@ function goPage(page: string) {
         url: `/pages-A/${page}/index`,
     })
 }
+
+function handleItemClick(item) {
+    uni.navigateTo({
+        url: `/pages-A/incomeShareInfo/index?batchId=${item.batchId}&commissionType=${item.commissionType}`,
+    })
+    // incomeShareInfoRef.value.show(item)
+}
 </script>
 
 <template>
@@ -346,7 +368,7 @@ function goPage(page: string) {
             </view>
             <view class="income-menu-list">
                 <view v-for="item in incomeData" :key="item.userId" class="income-content-item">
-                    <income-item :data="item" :type="activeIndex" />
+                    <income-item :data="item" :type="activeIndex" @itemClick="handleItemClick" />
                 </view>
                 <!-- 滚动区域内的loading遮罩 -->
                 <view v-if="showLoading && loading" class="loading-mask">
@@ -355,6 +377,7 @@ function goPage(page: string) {
             </view>
         </mescroll-body>
     </view>
+    <!-- <IncomeShareInfo ref="incomeShareInfoRef" :type="activeIndex" /> -->
 </template>
 
 <style lang="scss" scoped>

+ 14 - 7
src/pages/my/my.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import { useRequest } from 'alova/client'
 import { storeToRefs } from 'pinia'
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 import { getCouponSituation } from '@/api/home'
 import { getCouponIssuerApplyById, getShareState } from '@/api/me'
 import { useShare } from '@/hooks/useShare'
@@ -10,6 +10,7 @@ import { useInviterStore } from '@/store/inviter'
 import { useTokenStore } from '@/store/token'
 import { changtime, safeAreaInsets } from '@/utils'
 import { getImageUrl } from '@/utils/imageUtil'
+import { delayLoadingMiddleware } from '@/http/tools/delayLoadingMiddleware'
 
 definePage({
     style: {
@@ -29,22 +30,30 @@ const deadline = ref(Date.now())
 // 用户优惠券统计数据
 const { send: getCouponSituationRequest, data: couponSituationData, loading: couponSituationLoading } = useRequest(getCouponSituation, {
     immediate: false,
+    middleware: delayLoadingMiddleware(0, 500)
 })
 const { send: getShareStateRequest, data: shareStateData, loading: shareStateLoading } = useRequest(getShareState, {
     immediate: false,
+    middleware: delayLoadingMiddleware(0, 500)
 })
 
-onShow(async () => {
-    console.log('登录判断:', hasLogin.value)
+onLoad(async () => {
     // 登录后查询收益数据
     if (hasLogin.value) {
-        await userStore.fetchUserInfo()
         await getCouponSituationRequest({ queryType: 'time' })
         await getShareStateRequest()
         deadline.value = Date.now()
     }
 })
 
+onShow(async () => {
+    console.log('登录判断:', hasLogin.value)
+    // 进入页面后刷新用户信息
+    if (hasLogin.value) {
+        await userStore.fetchUserInfo()
+    }
+})
+
 function handleLogout() {
     uni.showModal({
         title: '提示',
@@ -121,7 +130,6 @@ async function menuClick(page, otherJs?: () => Promise<boolean>) {
 function isCouponIssuer(currentPage?: string) {
     // 发券人相关页面
     const validPages = ['myInviter', 'shareFriend', 'invitePage']
-    console.log('111111111111')
     // 判断跳转页面是否是发券人相关页面
     if (!validPages.includes(currentPage)) {
         return Promise.resolve(true)
@@ -214,8 +222,7 @@ onShareTimeline(async () => {
                 <view class="me-header-tips">
                     <view class="me-header-tips-row header-data-time">
                         <view class="desc-time">
-                            <up-loading-icon v-if="couponSituationLoading || shareStateLoading" size="23rpx"
-                                :loading="couponSituationLoading || shareStateLoading" color="#fff" />
+                            <up-loading-icon v-if="couponSituationLoading" size="23rpx" :loading="true" color="#fff" />
                             <up-icon v-else name="info-circle" size="23rpx" color="#fff" />
                             <view v-if="couponSituationLoading || shareStateLoading" class="desc-text">
                                 数据加载中...

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


+ 48 - 0
src/store/area.ts

@@ -0,0 +1,48 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+const initCurrentCity = {
+    name: '全部地区',
+    value: 'all',
+}
+
+export const useAreaStore = defineStore('area', () => {
+    const currentCity = ref(initCurrentCity)
+    const lastSelectedCity = ref([])
+
+    const addLastSelectedCity = (city) => {
+        // 判断新传入的城市是否已存在于数组中
+        const existingIndex = lastSelectedCity.value.findIndex(item => item.value === city.value)
+
+        if (existingIndex !== -1) {
+            // 如果存在,将其从当前位置删除
+            lastSelectedCity.value.splice(existingIndex, 1)
+        } else if (lastSelectedCity.value.length >= 7) {
+            // 如果不存在且数组长度已满,删除第一个元素
+            lastSelectedCity.value.shift()
+        }
+
+        // 将城市添加到数组末尾
+        lastSelectedCity.value.push(city)
+    }
+    const resetCurrentCity = () => {
+        console.log('重置当前城市')
+        currentCity.value = initCurrentCity
+    }
+
+    return {
+        currentCity,
+        lastSelectedCity,
+        addLastSelectedCity,
+        resetCurrentCity
+    }
+}, {
+    persist: {
+        key: 'area',
+        storage: {
+            getItem: uni.getStorageSync,
+            setItem: uni.setStorageSync,
+        },
+        paths: ['lastSelectedCity'],
+    },
+})

+ 26 - 3
src/store/coupon.ts

@@ -1,5 +1,7 @@
 import { defineStore } from 'pinia'
 import { getCouponList } from '@/api/home'
+import { useRequest } from 'alova/client'
+import { delayLoadingMiddleware } from '@/http/tools/delayLoadingMiddleware'
 
 export const useCouponStore = defineStore('coupon', {
     state: () => ({
@@ -11,10 +13,10 @@ export const useCouponStore = defineStore('coupon', {
         async getCouponListByType() {
             try {
                 this.loading = true
-                const res = await getCouponList()
+                const res = await getCouponList(3)
                 this.loading = false
-                this.discountVoucherList = (res.discountCoupon || []).slice(0, 3)
-                this.couponList = (res.fullReduceCoupon || []).slice(0, 3)
+                this.discountVoucherList = res.discountVoucherList
+                this.couponList = res.couponList
             }
             catch (error) {
                 this.loading = false
@@ -22,3 +24,24 @@ export const useCouponStore = defineStore('coupon', {
         }
     }
 })
+
+export const useNewCouponStore = defineStore('newCoupon', () => {
+    const { send, data, loading, } = useRequest(getCouponList, {
+        immediate: false,
+        initialData: {
+            discountVoucherList: [], // 折扣券
+            couponList: [], // 满减券
+        },
+        middleware: delayLoadingMiddleware(100, 150)
+    })
+
+    const getCouponListByType = async () => {
+        await send()
+    }
+
+    return {
+        data,
+        loading,
+        getCouponListByType,
+    }
+})

+ 1 - 1
src/store/loading.ts

@@ -15,7 +15,7 @@ export const useLoadingStore = defineStore('loading', () => {
     }
 
     // 隐藏加载页
-    const hideLoading = (delay = 100) => {
+    const hideLoading = (delay = 0) => {
         setTimeout(() => {
             isLoading.value = false
         }, delay)

+ 0 - 1
src/store/token.ts

@@ -272,7 +272,6 @@ export const useTokenStore = defineStore(
                                                                                                                                                                                      * 检查是否已登录且token有效
                                                                                                                                                                                      */
         const hasValidLogin = computed(() => {
-            console.log('hasValidLogin', hasLoginInfo.value && !isTokenExpired.value, hasLoginInfo.value, !isTokenExpired.value)
             return hasLoginInfo.value && !isTokenExpired.value
         })
 

+ 34 - 0
src/utils/envField.ts

@@ -0,0 +1,34 @@
+/**
+ * 根据当前小程序环境返回对应字段
+ * 开发版和体验版返回"体验版"
+ * 正式版返回"正式版"
+ */
+export function getEnvField(): string {
+    // 默认返回体验版
+    let envField = 'trial'
+
+    // 微信小程序端环境区分
+    try {
+        const accountInfo = uni.getAccountInfoSync()
+        const envVersion = accountInfo.miniProgram.envVersion
+
+        switch (envVersion) {
+            case 'develop':
+            case 'trial':
+                // 开发版和体验版返回"体验版"
+                envField = 'trial'
+                break
+            case 'release':
+                // 正式版返回"正式版"
+                envField = 'release'
+                break
+            default:
+                envField = 'trial'
+        }
+    } catch (error) {
+        console.error('获取小程序环境信息失败:', error)
+        envField = 'release'
+    }
+
+    return envField
+}

+ 13 - 0
src/utils/index.ts

@@ -6,6 +6,19 @@ import { menuButtonInfo, pxToRpx, rpxToPx, safeAreaInsets, systemInfo } from './
 
 export type PageInstance = Page.PageInstance<AnyObject, object> & { $page: Page.PageInstance<AnyObject, object> & { fullPath: string } }
 
+
+export const isPcWeixinMp = (): boolean => {
+    if (!isMpWeixin) return false
+
+    try {
+        // 电脑版微信小程序的userAgent包含Windows或Macintosh
+        const systemInfo = uni.getSystemInfoSync()
+        return /Windows|Macintosh/i.test(systemInfo.platform)
+    } catch (e) {
+        return false
+    }
+}
+
 export function getLastPage() {
     // getCurrentPages() 至少有1个元素,所以不再额外判断
     // const lastPage = getCurrentPages().at(-1)

+ 16 - 0
src/utils/platformSource.ts

@@ -0,0 +1,16 @@
+/**
+ * 券中心发券平台来源
+ */
+const platformSourceMap = {
+    '1': '晋掌柜生活',
+}
+
+/**
+ * 获取券中心发券平台来源
+ * @param platformSource 平台来源
+ * @returns 平台来源名称
+ */
+export function getPlatformSource(platformSource) {
+
+    return platformSourceMap[platformSource] || '未知'
+}

+ 1 - 1
src/utils/systemInfo.ts

@@ -23,7 +23,7 @@ systemInfo = uni.getSystemInfoSync()
 safeAreaInsets = systemInfo.safeAreaInsets
 // #endif
 
-console.log('systemInfo', systemInfo)
+// console.log('systemInfo', systemInfo)
 // 微信里面打印
 // pixelRatio: 3
 // safeArea: {top: 47, left: 0, right: 390, bottom: 810, width: 390, …}

+ 3 - 1
uno.config.ts

@@ -71,7 +71,7 @@ export default defineConfig({
         },
     ],
     // 动态图标需要在这里配置,或者写在vue页面中注释掉
-    safelist: ['i-carbon-home', 'i-carbon-user', 'i-carbon-chart-line'],
+    safelist: [],
     rules: [
         [
             'p-safe',
@@ -82,6 +82,8 @@ export default defineConfig({
         ],
         ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }],
         ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }],
+        ['mt-safe', { 'margin-top': 'env(safe-area-inset-top)' }],
+        ['mb-safe', { 'margin-bottom': 'env(safe-area-inset-bottom)' }],
     ],
     theme: {
         colors: {