Selaa lähdekoodia

优化:二次封装下拉加载组件库

haiyang 1 viikko sitten
vanhempi
commit
89fdb30b04

+ 0 - 1
package.json

@@ -122,7 +122,6 @@
     "image-tools": "^1.4.0",
     "js-cookie": "^3.0.5",
     "lz-string": "^1.5.0",
-    "mescroll-uni": "^1.3.7",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
     "uview-plus": "^3.6.17",

+ 3 - 3
src/api/receiveCoupon.ts

@@ -56,11 +56,11 @@ export function getWxQcCode(params) {
     })
 }
 
-//标记分享成功接口
+// 标记分享成功接口
 export function markSuccess(shareId: string) {
     return http.Get('/couponCenter/APP/shareRecord/markSuccess', {
         params: {
-            shareId: shareId,
+            shareId,
         }
     })
-}
+}

+ 12 - 22
src/components/IncomeItem.vue

@@ -5,10 +5,20 @@
                 <image v-if="type === 0" :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
                 <image v-else :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
                 <view class="income-header-menu-text">
-                    <view class="income-header-menu-text-nickname">
+                    <view v-if="[5, 6].includes(data.commissionType)" class="income-header-menu-text-nickname">
+                        <template v-if="data.commissionType === 5">
+                            拉新奖励
+                        </template>
+                        <template v-else>
+                            分享奖励
+                        </template>
+                    </view>
+                    <view v-else class="income-header-menu-text-nickname">
                         {{ data.receiveUserName }}
                     </view>
-                    <view>{{ data.couponName }}</view>
+                    <view v-if="![5, 6].includes(data.commissionType)">
+                        {{ data.couponName }}
+                    </view>
                 </view>
             </view>
             <view class="income-header-menu-left">
@@ -36,26 +46,6 @@ const props = defineProps({
         default: 0,
     }
 })
-
-const dataType = {
-    type1: [1, 2, 3, 4],
-    type2: [5, 6],
-}
-
-const typeDataFormat = {
-    type1: {
-        receiveUserName: 'receiveUserName',
-        couponName: 'couponName',
-        changeAmount: 'changeAmount',
-        settleTime: 'settleTime',
-    },
-    type2: {
-        receiveUserName: 'receiveUserName',
-        couponName: 'couponName',
-        changeAmount: 'changeAmount',
-        settleTime: 'settleTime',
-    },
-}
 </script>
 
 <style lang="scss" scoped>

+ 488 - 0
src/components/mescroll.vue

@@ -0,0 +1,488 @@
+<template>
+    <view class="mescroll-uni-warp">
+        <scroll-view :id="viewId" class="mescroll-uni" :class="{ 'mescroll-uni-fixed': isFixed }"
+            :style="{ 'height': scrollHeight, 'padding-top': padTop, 'padding-bottom': padBottom, 'top': fixedTop, 'bottom': fixedBottom }"
+            :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" :scroll-y="scrollable"
+            :enable-back-to-top="true" :throttle="false" @scroll="scroll">
+            <view class="mescroll-uni-content mescroll-render-touch" :change:prop="wxsBiz.propObserver" :prop="wxsProp"
+                @touchstart="wxsBiz.touchstartEvent" @touchmove="wxsBiz.touchmoveEvent" @touchend="wxsBiz.touchendEvent"
+                @touchcancel="wxsBiz.touchendEvent">
+                <!-- 状态栏 -->
+                <view v-if="topbar && statusBarHeight" class="mescroll-topbar"
+                    :style="{ height: `${statusBarHeight}px`, background: topbar }" />
+
+                <view class="mescroll-wxs-content" :style="{ transform: translateY, transition }"
+                    :change:prop="wxsBiz.callObserver" :prop="callProp">
+                    <!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现) -->
+                    <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+                    <view v-if="mescroll.optDown.use" class="mescroll-downwarp"
+                        :style="{ background: mescroll.optDown.bgColor, color: mescroll.optDown.textColor }">
+                        <view class="downwarp-content">
+                            <view class="downwarp-progress mescroll-wxs-progress"
+                                :class="{ 'mescroll-rotate': isDownLoading }"
+                                :style="{ 'border-color': mescroll.optDown.textColor, 'transform': downRotate }" />
+                            <view class="downwarp-tip">
+                                {{ downText }}
+                            </view>
+                        </view>
+                    </view>
+
+                    <!-- 列表内容 -->
+                    <slot />
+
+                    <!-- 空布局 -->
+                    <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick" />
+
+                    <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过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"
+                        :style="{ background: mescroll.optUp.bgColor, color: mescroll.optUp.textColor }">
+                        <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+                        <!-- <view v-show="upLoadType === 1">
+                            <view class="upwarp-progress mescroll-rotate"
+                                :style="{ 'border-color': mescroll.optUp.textColor }" />
+                            <view class="upwarp-tip">
+                                {{ mescroll.optUp.textLoading }}
+                            </view>
+                        </view> -->
+                        <!-- 无数据 -->
+                        <!-- <view v-if="upLoadType === 2" class="upwarp-nodata">
+                            {{ mescroll.optUp.textNoMore }}
+                        </view> -->
+                        <up-loadmore :status="upLoadType === 1 ? 'loading' : upLoadType === 2 ? 'nomore' : 'loadmore'"
+                            :loading-text="mescroll.optUp.textLoading" :nomore-text="mescroll.optUp.textNoMore" />
+                    </view>
+                </view>
+
+                <!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+                <!-- #ifdef H5 -->
+                <view v-if="bottombar && windowBottom > 0" class="mescroll-bottombar"
+                    :style="{ height: `${windowBottom}px` }" />
+                <!-- #endif -->
+
+                <!-- 适配iPhoneX -->
+                <view v-if="safearea" class="mescroll-safearea" />
+            </view>
+        </scroll-view>
+
+        <!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动) -->
+        <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick" />
+
+        <!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+        <!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+        <view :change:prop="renderBiz.propObserver" :prop="wxsProp" />
+        <!-- #endif -->
+    </view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../custom-components/custom-mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+import renderBiz from '@/custom-components/custom-mescroll-uni/wxs/renderjs.js';
+export default {
+    mixins:[renderBiz]
+}
+</script>
+<!-- #endif -->
+
+<script>
+// 引入mescroll-uni.js,处理核心逻辑
+import MeScroll from '@/custom-components/custom-mescroll-uni/mescroll-uni-main.js';
+// 引入全局配置
+import GlobalOption from '@/custom-components/custom-mescroll-uni/mescroll-uni-option.js';
+// 引入空布局组件
+import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue';
+// 引入国际化工具类
+import mescrollI18n from '@/custom-components/custom-mescroll-uni/mescroll-i18n.js';
+// 引入回到顶部组件
+import MescrollTop from '@/custom-components/custom-mescroll-uni/components/mescroll-top.vue';
+// 引入兼容wxs(含renderjs)写法的mixins
+import WxsMixin from '@/custom-components/custom-mescroll-uni/wxs/mixins.js';
+/**
+ * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+ * @property {Object} down 下拉刷新的参数配置
+ * @property {Object} up 上拉加载的参数配置
+ * @property {Object} i18n 国际化的参数配置
+ * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+ * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+ * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+ * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+ * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+ * @event {Function} init 初始化完成的回调
+ * @event {Function} down 下拉刷新的回调
+ * @event {Function} up 上拉加载的回调
+ * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+ * @event {Function} topclick 点击回到顶部的按钮回调
+ * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+ * @example <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+ */
+export default {
+    name: 'mescroll',
+    mixins: [WxsMixin],
+    components: {
+        MescrollEmpty,
+        MescrollTop
+    },
+    props: {
+        down: Object,
+        up: Object,
+        i18n: Object,
+        top: [String, Number],
+        topbar: [Boolean, String],
+        bottom: [String, Number],
+        safearea: Boolean,
+        fixed: {
+            type: Boolean,
+            default: true
+        },
+        height: [String, Number],
+        bottombar:{
+            type: Boolean,
+            default: true
+        },
+        disableScroll: Boolean
+    },
+    data() {
+        return {
+        mescroll: {optDown:{},optUp:{}}, // mescroll实例
+        viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+        downHight: 0, //下拉刷新: 容器高度
+        downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+            downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+            upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+            isShowEmpty: false, // 是否显示空布局
+            isShowToTop: false, // 是否显示回到顶部按钮
+            scrollTop: 0, // 滚动条的位置
+            scrollAnim: false, // 是否开启滚动动画
+            windowTop: 0, // 可使用窗口的顶部位置
+            windowBottom: 0, // 可使用窗口的底部位置
+            windowHeight: 0, // 可使用窗口的高度
+            statusBarHeight: 0 // 状态栏高度
+            }
+            },
+    computed: {
+    // 是否使用fixed定位 (当height有值,则不使用)
+    isFixed(){
+    return !this.height && this.fixed
+    },
+    // mescroll的高度
+    scrollHeight(){
+    if (this.isFixed) {
+    return "auto"
+    } else if(this.height){
+    return this.toPx(this.height) + 'px'
+    }else{
+    return "100%"
+    }
+    },
+    // 下拉布局往下偏移的距离 (px)
+    numTop() {
+    return this.toPx(this.top)
+    },
+    fixedTop() {
+    return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+    },
+    padTop() {
+    return !this.isFixed ? this.numTop + 'px' : 0
+    },
+    // 上拉布局往上偏移 (px)
+    numBottom() {
+    return this.toPx(this.bottom)
+    },
+    fixedBottom() {
+    return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+    },
+    padBottom() {
+    return !this.isFixed ? this.numBottom + 'px' : 0
+    },
+    // 是否为重置下拉的状态
+    isDownReset(){
+    return this.downLoadType===3 || this.downLoadType===4
+    },
+    // 过渡
+    transition() {
+    return this.isDownReset ? 'transform 300ms' : '';
+    },
+    translateY() {
+    return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+    },
+    // 列表是否可滑动
+    scrollable(){
+    if(this.disableScroll) return false
+    return this.downLoadType===0 || this.isDownReset
+    },
+    // 是否在加载中
+    isDownLoading(){
+    return this.downLoadType === 3
+    },
+    // 旋转的角度
+    downRotate(){
+    return 'rotate(' + 360 * this.downRate + 'deg)'
+    },
+    // 文本提示
+    downText(){
+    if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+    switch (this.downLoadType){
+    case 1: return this.mescroll.optDown.textInOffset;
+    case 2: return this.mescroll.optDown.textOutOffset;
+    case 3: return this.mescroll.optDown.textLoading;
+    case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess :
+    this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+    default: return this.mescroll.optDown.textInOffset;
+    }
+    }
+    },
+    methods: {
+    //number,rpx,upx,px,% --> px的数值
+    toPx(num){
+    if(typeof num === "string"){
+    if (num.indexOf('px') !== -1) {
+    if(num.indexOf('rpx') !== -1) { // "10rpx"
+    num = num.replace('rpx', '');
+    } else if(num.indexOf('upx') !== -1) { // "10upx"
+    num = num.replace('upx', '');
+    } else { // "10px"
+    return Number(num.replace('px', ''))
+    }
+    }else if (num.indexOf('%') !== -1){
+    // 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+    let rate = Number(num.replace("%","")) / 100
+    return this.windowHeight * rate
+    }
+    }
+    return num ? uni.upx2px(Number(num)) : 0
+    },
+    //注册列表滚动事件,用于下拉刷新和上拉加载
+    scroll(e) {
+    this.mescroll.scroll(e.detail, () => {
+    this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+    })
+    },
+    // 点击空布局的按钮回调
+    emptyClick() {
+    this.$emit('emptyclick', this.mescroll)
+    },
+    // 点击回到顶部的按钮回调
+    toTopClick() {
+    this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+    this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+    },
+    // 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+    setClientHeight() {
+    if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+    this.isExec = true; // 避免多次获取
+    this.$nextTick(() => { // 确保dom已渲染
+    this.getClientInfo(data=>{
+    this.isExec = false;
+    if (data) {
+    this.mescroll.setClientHeight(data.height);
+    } else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+    this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+    setTimeout(() => {
+    this.setClientHeight()
+    }, this.clientNum * 100)
+    }
+    })
+    })
+    }
+    },
+    // 获取滚动区域的信息
+    getClientInfo(success){
+    let query = uni.createSelectorQuery();
+    // #ifndef MP-ALIPAY || MP-DINGTALK
+    query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+    // #endif
+    let view = query.select('#' + this.viewId);
+    view.boundingClientRect(data => {
+    success(data)
+    }).exec();
+    }
+    },
+    // 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+    created() {
+    let vm = this;
+    let diyOption = {
+    // 下拉刷新的配置
+    down: {
+    inOffset() {
+    vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+    },
+    outOffset() {
+    vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+    },
+    onMoving(mescroll, rate, downHight) {
+    // 下拉过程中的回调,滑动过程一直在执行;
+    vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+    vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+        },
+        showLoading(mescroll, downHight) {
+        vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+        vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+        },
+        beforeEndDownScroll(mescroll){
+        vm.downLoadType = 4;
+        return mescroll.optDown.beforeEndDelay // 延时结束的时长
+        },
+        endDownScroll() {
+        vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+        vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+        vm.downResetTimer && clearTimeout(vm.downResetTimer)
+        vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+        if(vm.downLoadType===4) vm.downLoadType = 0
+        },300)
+        },
+        // 派发下拉刷新的回调
+        callback: function(mescroll) {
+        vm.$emit('down', mescroll)
+        }
+        },
+        // 上拉加载的配置
+        up: {
+        // 显示加载中的回调
+        showLoading() {
+        vm.upLoadType = 1;
+        },
+        // 显示无更多数据的回调
+        showNoMore() {
+        vm.upLoadType = 2;
+        },
+        // 隐藏上拉加载的回调
+        hideUpScroll(mescroll) {
+        vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+        },
+        // 空布局
+        empty: {
+        onShow(isShow) { // 显示隐藏的回调
+        vm.isShowEmpty = isShow;
+        }
+        },
+        // 回到顶部
+        toTop: {
+        onShow(isShow) { // 显示隐藏的回调
+        vm.isShowToTop = isShow;
+        }
+        },
+        // 派发上拉加载的回调
+        callback: function(mescroll) {
+        vm.$emit('up', mescroll);
+        // 更新容器的高度 (多mescroll的情况)
+        vm.setClientHeight()
+        }
+        }
+        }
+        let i18nType = mescrollI18n.getType() // 当前语言类型
+        let i18nOption = {type: i18nType} // 国际化配置
+        MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+        MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+        MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+        MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+        let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+        MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+        // 初始化MeScroll对象
+        vm.mescroll = new MeScroll(myOption);
+        vm.mescroll.viewId = vm.viewId; // 附带id
+        vm.mescroll.i18n = i18nOption; // 挂载语言包
+        // init回调mescroll对象
+        vm.$emit('init', vm.mescroll);
+        // 设置高度
+        const sys = uni.getSystemInfoSync();
+        if(sys.windowTop) vm.windowTop = sys.windowTop;
+        if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+        if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+        if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+        // 使down的bottomOffset生效
+        vm.mescroll.setBodyHeight(sys.windowHeight);
+        // 因为使用的是scrollview,这里需自定义scrollTo
+        vm.mescroll.resetScrollTo((y, t) => {
+        vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+        if(typeof y === 'string'){
+        // 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+        vm.getClientInfo(function(rect){
+        let mescrollTop = rect.top // mescroll到顶部的距离
+        let selector;
+        if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+        selector = '#'+y // 不带#和. 则默认为id选择器
+        }else{
+        selector = y
+        // #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+        if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+        selector = y.split('>>>')[1].trim()
+        }
+        // #endif
+        }
+        uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+        if (rect) {
+        let curY = vm.mescroll.getScrollTop()
+        let top = rect.top - mescrollTop
+        top += curY
+        if(!vm.isFixed) top -= vm.numTop
+        vm.scrollTop = curY;
+        vm.$nextTick(function() {
+        vm.scrollTop = top
+        })
+        } else{
+        console.error(selector + ' does not exist');
+        }
+        }).exec()
+        })
+        return;
+        }
+        let curY = vm.mescroll.getScrollTop()
+        if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+        vm.scrollTop = curY;
+        vm.$nextTick(function() {
+        vm.scrollTop = y
+        })
+        } else {
+        vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+        vm.scrollTop = step
+        }, t)
+        }
+        })
+        // 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+        if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+        vm.mescroll.optUp.toTop.safearea = vm.safearea;
+        }
+        // 全局配置监听
+        uni.$on("setMescrollGlobalOption", options=>{
+        if(!options) return;
+        let i18nType = options.i18n ? options.i18n.type : null
+        if(i18nType && vm.mescroll.i18n.type != i18nType){
+        vm.mescroll.i18n.type = i18nType
+        mescrollI18n.setType(i18nType)
+        MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+        }
+        if(options.down){
+        let down = MeScroll.extend({}, options.down)
+        vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+        }
+        if(options.up){
+        let up = MeScroll.extend({}, options.up)
+        vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+        }
+        })
+        },
+        mounted() {
+        // 设置容器的高度
+        this.setClientHeight()
+        },
+        destroyed() {
+        // 注销全局配置监听
+        uni.$off("setMescrollGlobalOption")
+        }
+        }
+</script>
+
+<style>
+@import '@/custom-components/custom-mescroll-uni/mescroll-uni.css';
+@import '@/custom-components/custom-mescroll-uni/components/mescroll-down.css';
+@import '@/custom-components/custom-mescroll-uni/components/mescroll-up.css';
+</style>

+ 1 - 0
src/custom-components/custom-mescroll-uni

@@ -0,0 +1 @@
+E:/workspace/coupon_manager_mp_app/node_modules/.pnpm/mescroll-uni@1.3.7/node_modules/mescroll-uni

+ 12 - 13
src/hooks/useShare.ts

@@ -4,7 +4,6 @@ import { useTokenStore } from '@/store/token'
 import { useUserStore } from '@/store/user'
 import { getImageUrl } from '@/utils/imageUtil'
 
-
 /**
  * 分享配置选项
  */
@@ -44,9 +43,9 @@ const defaultOptions: Required<ShareOptions> = {
  */
 export function useShare(initOptions: ShareOptions = {}) {
     /**
-               * 获取分享配置 - 可以直接在onShareAppMessage中调用
-               * @returns 完整的分享配置
-               */
+                         * 获取分享配置 - 可以直接在onShareAppMessage中调用
+                         * @returns 完整的分享配置
+                         */
     const getShareConfig = async (customOption?: ShareOptions, extraParams?): Promise<WechatMiniprogram.Page.ICustomShareContent | {}> => {
         // 合并默认配置和自定义配置
         const options = { ...defaultOptions, ...initOptions, ...customOption }
@@ -109,10 +108,10 @@ export function useShare(initOptions: ShareOptions = {}) {
                     title: options.title,
                     path: sharePath,
                     imageUrl,
-                    success: (res) => {
-                        console.log('分享成功', res)
-                        markSuccess(res)
-                    },
+                    // async success() {
+                    //     console.log('分享成功', res)
+                    //     await markSuccess(res)
+                    // },
                 }
             }
             else {
@@ -136,18 +135,18 @@ export function useShare(initOptions: ShareOptions = {}) {
     }
 
     /**
-               * 获取朋友圈分享配置 - 可以直接在onShareTimeline中调用
-               * @returns 完整的朋友圈分享配置
-               */
+                         * 获取朋友圈分享配置 - 可以直接在onShareTimeline中调用
+                         * @returns 完整的朋友圈分享配置
+                         */
     const getTimelineShareConfig = async (): Promise<WechatMiniprogram.Page.ICustomTimelineContent | {}> => {
         const config = await getShareConfig() as WechatMiniprogram.Page.ICustomShareContent
         return {
             title: config.title,
             path: config.path,
             imageUrl: config.imageUrl,
-            success: async(res) => {
+            success: async (res) => {
                 console.log('分享到朋友圈成功', res)
-                //-------这里掉大哥的方法--------------------------------
+                // -------这里掉大哥的方法--------------------------------
                 await markSuccess(res)
             },
         }

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

@@ -1,12 +1,12 @@
 <script lang="ts" setup>
 import { usePagination } from 'alova/client'
-import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
-import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
 import { reactive, ref, watch } from 'vue'
 import { getCouponIssuerAccountByPageMap } from '@/api/income'
 import { getInviteList } from '@/api/me'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import IncomeItem from '@/components/IncomeItem.vue'
+import MescrollUni from '@/components/mescroll.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
 
 import { changtime, safeAreaInsets } from '@/utils'
 

+ 2 - 2
src/pages-A/invitePage/index.vue

@@ -1,11 +1,11 @@
 <script lang="ts" setup>
 import { usePagination } from 'alova/client'
-import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
-import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
 import { reactive, ref, watch } from 'vue'
 import { getInviteList } from '@/api/me'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import InviteItem from '@/components/InviteItem.vue'
+import MescrollUni from '@/components/mescroll.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
 
 import { safeAreaInsets } from '@/utils'
 

+ 10 - 65
src/pages-A/shareFriend/index.vue

@@ -1,6 +1,5 @@
 <template>
     <view class="share-container">
-        <!-- 顶部导航栏(模拟美团风格) -->
         <!-- <up-status-bar /> -->
         <CustomNavigationBar :show-back="true" title="邀请好友" />
 
@@ -71,8 +70,9 @@
 
 <script setup>
 import { onLoad } from '@dcloudio/uni-app'
+import { storeToRefs } from 'pinia'
 import { nextTick, ref } from 'vue'
-import { getWxQcCode } from '@/api/receiveCoupon'
+import { markSuccess } from '@/api/receiveCoupon'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import { useInviteCodeStore } from '@/store/inviteQCCode'
 import { safeAreaInsets } from '@/utils'
@@ -97,6 +97,7 @@ const topSafeAreaHeight = safeAreaInsets?.top || 0
 // 系统信息
 const systemInfo = ref({})
 const inviteCodeStore = useInviteCodeStore()
+const { inviteCode } = storeToRefs(inviteCodeStore)
 
 // 生命周期
 onLoad(async (options) => {
@@ -134,41 +135,6 @@ function getCanvasInstance() {
     })
 }
 
-async function getWxacodeBase64() {
-    // const token = await getWxToken()
-    // console.log('token:', token, shareId.value)
-    // const res = await getWxacode(token.data.access_token, shareId.value)
-    const res = await getWxQcCode({
-        shareType: 'INVITE',
-        sharePath: 'pages/home/home',
-        width: 360,
-    })
-    console.log('res:', res)
-    const contentType = res.header['Content-Type'] || res.header['content-type']
-
-    if (contentType && contentType.includes('image')) {
-        // 返回的是图片
-        // 将arraybuffer转换为base64
-        const arrayBuffer = res.data
-        const base64 = uni.arrayBufferToBase64(arrayBuffer)
-        const imageUrl = `data:image/jpeg;base64,${base64}`
-        return imageUrl
-    }
-    else {
-        // 返回的是错误信息
-        try {
-            // 尝试将arraybuffer转换为字符串
-            const decoder = new TextDecoder('utf-8')
-            const errorText = decoder.decode(new Uint8Array(res.data))
-            const errorData = JSON.parse(errorText)
-            throw new Error(errorData.errmsg || '获取小程序码失败')
-        }
-        catch (e) {
-            throw new Error('获取小程序码失败,请检查access_token')
-        }
-    }
-}
-
 // 生成分享图片
 async function generateShareImage() {
     if (loading.value)
@@ -308,34 +274,6 @@ async function loadRemoteImage(canvas, imageUrl) {
     })
 }
 
-// 绘制顶部装饰
-function drawTopDecoration(ctx, width, height, scale) {
-    // 现代化渐变背景 - 美团橙色系
-    const gradient = ctx.createLinearGradient(0, 0, width, 0)
-    gradient.addColorStop(0, '#FF7A75')
-    gradient.addColorStop(0.98, '#ED6B66')
-
-    // 绘制带圆角的顶部卡片
-    drawRoundRect(ctx, 20 * scale, 10 * scale, width - 40 * scale, 160 * scale, 20 * scale)
-    ctx.fillStyle = gradient
-    ctx.fill()
-
-    // 添加阴影效果
-    ctx.shadowColor = 'rgba(255, 107, 53, 0.3)'
-    ctx.shadowBlur = 20 * scale
-    ctx.shadowOffsetY = 10 * scale
-
-    // 绘制主体卡片
-    drawRoundRect(ctx, 20 * scale, 130 * scale, width - 40 * scale, height - 150 * scale, 20 * scale)
-    ctx.fillStyle = '#FFFFFF'
-    ctx.fill()
-
-    // 清除阴影
-    ctx.shadowColor = 'transparent'
-    ctx.shadowBlur = 0
-    ctx.shadowOffsetY = 0
-}
-
 // 绘制使用规则标题
 function drawTitle(ctx, width, height, scale) {
     ctx.fillStyle = '#000000'
@@ -566,6 +504,13 @@ function handleShareFriend() {
     uni.showShareImageMenu({
         path: shareImageUrl.value,
         needShowEntrance: false,
+        success: async (res) => {
+            console.log('分享成功', res)
+            await markSuccess(inviteCode.value.shareData[inviteCode.value.shareKey])
+        },
+        fail: (err) => {
+            console.log('分享失败', err)
+        },
     })
 }
 

+ 84 - 15
src/pages-A/sharePage/index.vue

@@ -85,9 +85,11 @@
 <script setup>
 import { onLoad } from '@dcloudio/uni-app'
 import { useRequest } from 'alova/client'
-import { nextTick, onMounted, onUnmounted, ref } from 'vue'
-import { getCouponDetail, getShareInfo } from '@/api/home'
-import { getWxacode, getWxQcCode, getWxToken } from '@/api/receiveCoupon'
+import { storeToRefs } from 'pinia'
+import { nextTick, ref } from 'vue'
+import { getCouponDetail } from '@/api/home'
+import { getWxQcCode, markSuccess } from '@/api/receiveCoupon'
+import { useInviteCodeStore } from '@/store/inviteQCCode'
 import { safeAreaInsets } from '@/utils'
 import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
 
@@ -106,6 +108,8 @@ const shareImageUrl = ref('')
 const canvasWidth = ref(750)
 const canvasHeight = ref(1200)
 const topSafeAreaHeight = safeAreaInsets?.top || 0
+const inviteCodeStore = useInviteCodeStore()
+const { inviteCode } = storeToRefs(inviteCodeStore)
 
 // 系统信息
 const systemInfo = ref({})
@@ -359,24 +363,82 @@ function drawTitle(ctx, width, height, scale) {
 
 // 绘制优惠券金额
 function drawCouponAmount(ctx, width, height, scale) {
-    ctx.fillStyle = '#FF6B35'
-    ctx.font = `bold ${72 * scale}px Arial`
-    ctx.textAlign = 'center'
-    if (couponDetail.value.type === '2') {
-        ctx.fillText(couponDetail.value.ruleDiscountRate, width / 2, 220 * scale)
+    const centerX = width / 2
+    const bottomY = 220 * scale
+    ctx.textBaseline = 'bottom' // 设置文本基线为底部对齐
 
-        ctx.font = `bold ${36 * scale}px Arial`
-        ctx.fillText('折', width / 2 + 45 * scale, 205 * scale)
+    if (couponDetail.value.type === '2') {
+        // 折扣券:数字 + 折
+        const numberFont = `bold ${72 * scale}px Arial`
+        const unitFont = `bold ${36 * scale}px Arial`
+
+        ctx.fillStyle = '#FF6B35'
+
+        // 计算数字宽度
+        ctx.font = numberFont
+        const number = couponDetail.value.ruleDiscountRate
+        const numberWidth = ctx.measureText(number).width
+
+        // 计算"折"字宽度
+        ctx.font = unitFont
+        const unit = '折'
+        const unitWidth = ctx.measureText(unit).width
+
+        // 计算总宽度
+        const totalWidth = numberWidth + unitWidth + 8 * scale // 8px间距(减小间距)
+
+        // 计算起始X坐标(整体居中)
+        const startX = centerX - totalWidth / 2
+
+        // 绘制数字
+        ctx.font = numberFont
+        ctx.textAlign = 'left'
+        ctx.fillText(number, startX, bottomY)
+
+        // 绘制"折"字(与数字间距8px,垂直位置微调使视觉对齐)
+        ctx.font = unitFont
+        // 根据字体大小比例微调"折"字的y坐标,使其视觉上与数字底部对齐
+        const unitY = bottomY - (72 - 36) * scale * 0.15 // 微调垂直位置
+        ctx.fillText(unit, startX + numberWidth + 8 * scale, unitY)
     }
     else if (couponDetail.value.type === '3') {
-        ctx.fillText(couponDetail.value.ruleReductionAmount, width / 2, 220 * scale)
+        // 满减券:¥ + 数字
+        const numberFont = `bold ${72 * scale}px Arial`
+        const symbolFont = `bold ${36 * scale}px Arial`
 
-        // 添加装饰性货币符号
+        // 计算数字宽度
+        ctx.fillStyle = '#FF6B35'
+        ctx.font = numberFont
+        const number = couponDetail.value.ruleReductionAmount
+        const numberWidth = ctx.measureText(number).width
+
+        // 计算"¥"符号宽度
         ctx.fillStyle = '#FF9500'
-        ctx.font = `bold ${36 * scale}px Arial`
-        ctx.textAlign = 'right'
-        ctx.fillText('¥', width / 2 - 80 * scale, 210 * scale)
+        ctx.font = symbolFont
+        const symbol = '¥'
+        const symbolWidth = ctx.measureText(symbol).width
+
+        // 计算总宽度
+        const totalWidth = symbolWidth + numberWidth + 8 * scale // 8px间距(减小间距)
+
+        // 计算起始X坐标(整体居中)
+        const startX = centerX - totalWidth / 2
+
+        // 绘制"¥"符号(垂直位置微调使视觉对齐)
+        ctx.textAlign = 'left'
+        // 根据字体大小比例微调"¥"符号的y坐标,使其视觉上与数字底部对齐
+        const symbolY = bottomY - (72 - 36) * scale * 0.15 // 微调垂直位置
+        ctx.fillText(symbol, startX, symbolY)
+
+        // 绘制数字
+        ctx.fillStyle = '#FF6B35'
+        ctx.font = numberFont
+        ctx.fillText(number, startX + symbolWidth + 8 * scale, symbolY)
     }
+
+    // 恢复默认文本对齐方式
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'alphabetic'
 }
 
 // 绘制优惠券描述
@@ -686,6 +748,13 @@ function handleShareFriend() {
     uni.showShareImageMenu({
         path: shareImageUrl.value,
         needShowEntrance: false,
+        success: async (res) => {
+            console.log('分享成功', res)
+            await markSuccess(inviteCode.value.shareData[inviteCode.value.shareKey])
+        },
+        fail: (err) => {
+            console.log('分享失败', err)
+        },
     })
 }
 

+ 9 - 2
src/pages/home/home.vue

@@ -57,14 +57,21 @@ const { getShareConfig, getTimelineShareConfig } = useShare()
 
 // #ifdef MP-WEIXIN
 // 分享给好友生命周期函数
-onShareAppMessage(async (options) => {
-    return await getShareConfig()
+onShareAppMessage(() => {
+    const promise = new Promise((resolve, reject) => {
+        getShareConfig().then(resolve).catch(reject)
+    })
+    // const configPromise = await getShareConfig()
+    return {
+        promise,
+    }
 })
 
 // 分享到朋友圈生命周期函数
 onShareTimeline(async () => {
     return await getTimelineShareConfig()
 })
+
 // #endif
 
 onShow((options) => {

+ 2 - 2
src/pages/income/income.vue

@@ -1,12 +1,12 @@
 <script lang="ts" setup>
 import { usePagination, useRequest } from 'alova/client'
-import MescrollEmpty from 'mescroll-uni/components/mescroll-empty.vue'
-import MescrollUni from 'mescroll-uni/mescroll-uni.vue'
 import { storeToRefs } from 'pinia'
 import { computed, ref } 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 MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
 import { useShare } from '@/hooks/useShare'
 import { useUserStore } from '@/store'
 import { useTokenStore } from '@/store/token'

+ 7 - 1
src/store/inviteQCCode.ts

@@ -2,11 +2,14 @@ import lzString from 'lz-string'
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import { getWxQcCode } from '@/api/receiveCoupon'
+import { parseUrlParams } from '@/utils/index'
 
 export const useInviteCodeStore = defineStore('inviteCode', () => {
     const inviteCode = ref({
         compressedImage: '',
         imageType: '',
+        shareData: {},
+        shareKey: 'CCShareId',
     })
 
     const setInviteCode = (baseImg) => {
@@ -49,13 +52,16 @@ export const useInviteCodeStore = defineStore('inviteCode', () => {
         })
 
         const contentType = res.header['Content-Type'] || res.header['content-type']
-
+        const contentData = res.header['x-content-data'] || res.header['x-content-data']
         if (contentType && contentType.includes('image')) {
             // 返回的是图片
             // 将arraybuffer转换为base64
             const arrayBuffer = res.data
             const base64 = uni.arrayBufferToBase64(arrayBuffer)
             const imageUrl = `data:image/jpeg;base64,${base64}`
+            const params = parseUrlParams(contentData)
+            console.log(params, '获取到的参数')
+            inviteCode.value.shareData = params
             setInviteCode(imageUrl)
         }
         else {

+ 22 - 0
src/utils/index.ts

@@ -69,6 +69,28 @@ export function parseUrlToObj(url: string) {
     })
     return { path, query }
 }
+
+export function parseUrlParams(paramString) {
+    const params = {}
+    if (!paramString)
+        return params
+
+    // 移除可能的问号前缀
+    const cleanString = paramString.startsWith('?') ? paramString.slice(1) : paramString
+
+    // 分割参数对
+    const pairs = cleanString.split('&')
+
+    // 解析每对参数
+    pairs.forEach((pair) => {
+        const [key, value] = pair.split('=')
+        if (key) {
+            params[decodeURIComponent(key)] = decodeURIComponent(value || '')
+        }
+    })
+
+    return params
+}
 /**
  * 得到所有的需要登录的 pages,包括主包和分包的
  * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 excludeLoginPath, 与 route-block 配对使用