Browse Source

优化:我的页面布局优化;优惠券分享界面优化;

haiyang 2 weeks ago
parent
commit
345b3fb4a1

+ 2 - 2
.gitignore

@@ -34,8 +34,8 @@ src/manifest.json
 src/pages.json
 
 # 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
-# pnpm-lock.yaml
-# package-lock.json
+pnpm-lock.yaml
+package-lock.json
 
 # TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
 # `git rm -r --cached .` 然后提交 commit 即可。

+ 1 - 0
package.json

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

+ 8 - 0
pnpm-lock.yaml

@@ -84,6 +84,9 @@ importers:
       js-cookie:
         specifier: ^3.0.5
         version: 3.0.5
+      mescroll.js:
+        specifier: ^1.4.2
+        version: 1.4.2
       pinia:
         specifier: 2.0.36
         version: 2.0.36(typescript@5.8.3)(vue@3.4.21(typescript@5.8.3))
@@ -4631,6 +4634,9 @@ packages:
   merge@2.1.1:
     resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
 
+  mescroll.js@1.4.2:
+    resolution: {integrity: sha512-tZDucS9DXUrIfTGXTY2L7e4mGLIQ8uMqY2GOaQAGrCHQssUADOIM0kcQlRCA6U6ffFPxXV4D+IbhzOy3zVQ1wA==}
+
   methods@1.1.2:
     resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
     engines: {node: '>= 0.6'}
@@ -12263,6 +12269,8 @@ snapshots:
 
   merge@2.1.1: {}
 
+  mescroll.js@1.4.2: {}
+
   methods@1.1.2: {}
 
   micromark-core-commonmark@2.0.3:

+ 1 - 0
src/App.ku.vue

@@ -43,6 +43,7 @@ defineExpose({
         <!-- <view class="hidden text-center">
             {{ helloKuRoot }},这里可以配置全局的东西
         </view> -->
+        <up-no-network />
 
         <KuRootView />
         <up-loading-page :loading="loadingStore.isLoading" z-index="1000" size="50" />

+ 5 - 0
src/api/me.ts

@@ -20,3 +20,8 @@ export interface CouponIssuerApplyByAddForm {
 export function couponIssuerApplyByAdd(couponIssuerApplyByAddForm: CouponIssuerApplyByAddForm) {
     return http.Post<couponIssuerApplyByAddResponse>('/couponCenter/APP/couponIssuerApply/add', couponIssuerApplyByAddForm)
 }
+
+// 更新用户信息
+export function updateUserInfo(userInfo) {
+    return http.Post('/couponCenter/couponCenterUser/updateInformation', userInfo)
+}

+ 37 - 0
src/api/receiveCoupon.ts

@@ -18,3 +18,40 @@ export function getCouponShareLink(params) {
         }
     })
 }
+
+// 生成微信小程序码
+export function getWxacode(token, id) {
+    return uni.request({
+        url: `https://api.weixin.qq.com/wxa/getwxacode?access_token=${token}`,
+        method: 'POST',
+        data: {
+            "path": `pages/home/home?couponCenter_shareId=${id}`,
+            "env_version": 'release',
+            "width": 280,
+        },
+        contentType: ContentTypeEnum.JSON,
+        responseType: 'arraybuffer',
+    })
+}
+
+export function getWxToken() {
+    return uni.request({
+        url: 'https://api.weixin.qq.com/cgi-bin/token',
+        method: 'GET',
+        data: {
+            "grant_type": 'client_credential',
+            "appid": 'wxab9634bf98628a03',
+            "secret": 'b0f8b0c5372fb825be9cf3d89c3fb28c',
+        },
+    })
+}
+
+export function getWxQcCode(params) {
+    return http.Post('/couponCenter/APP/shareRecord/createQRCode', params, {
+        responseType: 'arraybuffer',
+        cacheFor: null,
+        meta: {
+            returnResponse: true,
+        }
+    })
+}

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

@@ -8,6 +8,7 @@ export interface AccountCount {
     status: number
     withdrawableAmount: number
     unsettledAmount: number
+    totalCommission: number
 }
 
 export interface CouponSituation {

+ 0 - 0
src/components/.gitkeep


+ 68 - 56
src/components/CustomNavigationBar.vue

@@ -1,24 +1,29 @@
 <script setup lang="ts">
 import { onMounted, ref } from 'vue'
+import { safeAreaInsets } from '@/utils'
 
 const props = withDefaults(defineProps<Props>(), {
     title: '页面标题',
     showBack: true,
+    backIconColor: '#000000',
     backgroundColor: '#ffffff',
-    textColor: '#ffffff',
+    textColor: '#333333',
+    height: '88rpx',
 })
 
 // Props
 interface Props {
-  title?: string
-  showBack?: boolean
-  backgroundColor?: string
-  textColor?: string
+    title?: string
+    showBack?: boolean
+    backIconColor?: string
+    backgroundColor?: string
+    textColor?: string
 }
 
 // 导航栏高度
 const navBarHeight = ref('44px')
 const navBarTop = ref('44px')
+const topSafeAreaHeight = safeAreaInsets?.top || 0
 
 // 返回按钮点击事件
 function handleBack() {
@@ -45,71 +50,78 @@ onMounted(() => {
 </script>
 
 <template>
-    <view
-        class="custom-navigation-bar"
-        :style="{
-            height: navBarHeight,
-            top: navBarTop,
-            backgroundColor: props.backgroundColor,
-            color: props.textColor,
-        }"
-    >
-        <!-- 返回按钮 -->
-        <view v-if="props.showBack" class="nav-back-btn" @click="handleBack">
-            <u-icon name="arrow-left" :color="props.textColor" size="18" class="back-icon" />
+    <view class="top-nav" :style="{
+        paddingTop: `${topSafeAreaHeight}px`,
+        backgroundColor: props.backgroundColor,
+    }">
+        <view class="custom-navbar">
+            <view v-if="props.showBack" class="back-btn" @tap="handleBack">
+                <up-icon name="arrow-left" size="36rpx" :color="props.textColor" />
+            </view>
+            <view class="nav-title" :style="{
+                color: props.textColor,
+            }">
+                {{ props.title }}
+            </view>
+            <view v-if="props.showBack" class="right-space" />
         </view>
-
-        <!-- 标题 -->
-        <view class="nav-title">
-            {{ props.title }}
-        </view>
-
-        <!-- 占位元素,保持标题居中 -->
-        <view v-if="props.showBack" class="nav-placeholder" />
     </view>
 </template>
 
 <style scoped>
-.custom-navigation-bar {
-  position: fixed;
-  left: 0;
-  right: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-sizing: border-box;
-  z-index: 999;
+/* 顶部导航栏 */
+.top-nav {
+    background: #ffffff;
+    box-shadow: 0rpx 8rpx 7rpx 0rpx rgba(233, 233, 233, 0.17);
+    position: fixed;
+    /* 固定定位 */
+    top: 0;
+    /* 顶部对齐 */
+    left: 0;
+    /* 左侧对齐 */
+    right: 0;
+    /* 右侧对齐 */
+    z-index: 999;
+    /* 确保在其他元素之上 */
+    padding-top: var(--safe-area-inset-top);
+    /* 适配安全区域 */
+}
+
+/* 自定义导航栏 */
+.custom-navbar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 20rpx 30rpx;
+    height: 88rpx;
+    box-sizing: border-box;
 }
 
-.nav-back-btn {
-  position: absolute;
-  left: 16px;
-  /* display: flex;
-  align-items: center;
-  justify-content: center; */
-  width: 44px;
-  height: 100%;
+/* 返回按钮 */
+.back-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
 }
 
 .back-icon {
-  font-size: 24px;
+    font-size: 36rpx;
+    color: #000000;
+    font-weight: bold;
 }
 
+/* 导航栏标题 */
 .nav-title {
-  height: 100%;
-  font-size: 18px;
-  font-weight: 500;
-  text-align: center;
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
+    font-size: 34rpx;
+    font-weight: bold;
+    flex: 1;
+    text-align: center;
 }
 
-.nav-placeholder {
-  height: 100%;
-  position: absolute;
-  right: 16px;
-  width: 44px;
+/* 右侧占位 */
+.right-space {
+    width: 60rpx;
 }
 </style>

+ 9 - 0
src/components/ShortLinkModal.vue

@@ -83,6 +83,12 @@ function handleClose() {
     emit('close')
     cleanShareLink()
 }
+
+function btnclick() {
+    uni.navigateTo({
+        url: shareLinkDisplay.value,
+    })
+}
 </script>
 
 <template>
@@ -97,6 +103,9 @@ function handleClose() {
                     复制
                 </button>
             </view>
+            <button @click="btnclick">
+                点击领取
+            </button>
             <view v-if="copySuccess" class="copy-success">
                 复制成功!
             </view>

+ 12 - 9
src/hooks/useCouponShare.ts

@@ -59,16 +59,19 @@ export function useCouponShare() {
             }
 
             // // 请求短链
-            const result = await getShareLink({
-                pathUrl: redirectPath.value,
-                templateId: couponId,
+            // const result = await getShareLink({
+            //     pathUrl: redirectPath.value,
+            //     templateId: couponId,
+            // })
+            // if (result) {
+            //     // 保存短链
+            //     console.log('分享短链:', result)
+            //     shareLink.value = result as string
+            //     return shareLink.value
+            // }
+            uni.navigateTo({
+                url: `/pages-A/sharePage/index?couponId=${couponId}`,
             })
-            if (result) {
-                // 保存短链
-                console.log('分享短链:', result)
-                shareLink.value = result as string
-                return shareLink.value
-            }
             return null
         }
         catch (error) {

+ 6 - 6
src/hooks/useShare.ts

@@ -41,9 +41,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 }
@@ -128,9 +128,9 @@ export function useShare(initOptions: ShareOptions = {}) {
     }
 
     /**
-                                                     * 获取朋友圈分享配置 - 可以直接在onShareTimeline中调用
-                                                     * @returns 完整的朋友圈分享配置
-                                                     */
+       * 获取朋友圈分享配置 - 可以直接在onShareTimeline中调用
+       * @returns 完整的朋友圈分享配置
+       */
     const getTimelineShareConfig = async (): Promise<WechatMiniprogram.Page.ICustomShareTimelineContent | {}> => {
         const config = await getShareConfig() as WechatMiniprogram.Page.ICustomShareContent
         return {

+ 5 - 0
src/http/alova.ts

@@ -100,6 +100,11 @@ const alovaInstance = createAlova({
                 return response
             }
 
+            // 特殊处理:返回原始响应
+            if (config.meta?.returnResponse) {
+                return response
+            }
+
             // 处理 HTTP 状态码错误
             if (statusCode !== 200) {
                 // 直接处理401错误,跳转到登录页面

+ 0 - 1
src/pages-A/bank/index.vue

@@ -44,7 +44,6 @@ const list = ref([{
 
 <style lang="scss" scoped>
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     min-height: 100vh;
     background-color: #f5f5f5;
     line-height: 1;

+ 0 - 1
src/pages-A/myBankList/index.vue

@@ -86,7 +86,6 @@ function bindBank() {
 
 <style lang="scss" scoped>
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     min-height: 100vh;
     background-color: #f5f5f5;
     line-height: 1;

+ 186 - 0
src/pages-A/settingPage/index.vue

@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import { storeToRefs } from 'pinia'
+import { computed, ref } from 'vue'
+import { updateUserInfo } from '@/api/me'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import { useUserStore } from '@/store'
+import { safeAreaInsets } from '@/utils'
+
+definePage({
+    style: {
+        navigationBarTitleText: '设置',
+        navigationStyle: 'custom',
+    },
+})
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+
+// 获取当前登录用户信息
+const userStore = useUserStore()
+const { userInfo } = storeToRefs(userStore)
+const nickNameVisible = ref(false)
+const nickName = ref(userInfo.value.nickname)
+
+async function changeNickName() {
+    uni.showLoading({
+        mask: true,
+    })
+    try {
+        const result = await updateUserInfo({ nickname: nickName.value })
+        uni.hideLoading()
+        if (result) {
+            userStore.setUserNickName(nickName.value)
+            nickNameVisible.value = false
+            uni.showToast({
+                title: '昵称更新成功',
+                icon: 'success',
+            })
+        }
+        else {
+            uni.showToast({
+                title: '昵称更新失败',
+                icon: 'error',
+            })
+        }
+    }
+    catch (error) {
+        uni.hideLoading()
+        uni.showToast({
+            title: '昵称更新失败',
+            icon: 'error',
+        })
+    }
+}
+
+function onChooseAvatar(e) {
+    const avatarUrl = e.detail.avatarUrl
+    const uploadUrl = `${import.meta.env.VITE_SERVER_BASEURL}/sys/common/upload`
+    uni.showLoading({
+        mask: true,
+    })
+    uni.uploadFile({
+        url: uploadUrl,
+        filePath: avatarUrl,
+        name: 'file',
+        formData: {
+            'biz': 'temp',
+        },
+        success: async (uploadRes) => {
+            const res = JSON.parse(uploadRes.data)
+            if (res.success) {
+                const newAvatarUrl = `${import.meta.env.VITE_SERVER_BASEURL}/${res.message}`
+                try {
+                    const result = await updateUserInfo({ avatarUrl: newAvatarUrl })
+                    uni.hideLoading()
+                    if (result) {
+                        userStore.setUserAvatar(newAvatarUrl)
+                        uni.showToast({
+                            title: '头像更新成功',
+                            icon: 'success',
+                        })
+                    }
+                    else {
+                        uni.showToast({
+                            title: '头像更新失败',
+                            icon: 'error',
+                        })
+                    }
+                }
+                catch (error) {
+                    uni.hideLoading()
+                    uni.showToast({
+                        title: '头像更新失败',
+                        icon: 'error',
+                    })
+                }
+            }
+            else {
+                uni.hideLoading()
+                uni.showToast({
+                    title: '头像更新失败',
+                    icon: 'error',
+                })
+            }
+        },
+        fail: (failRes) => {
+            console.log('上传头像失败:', failRes)
+        }
+    })
+}
+</script>
+
+<template>
+    <view class="setting-container">
+        <!-- 自定义导航栏 -->
+        <CustomNavigationBar :show-back="true" title="设置" />
+        <!-- 内容区域 -->
+        <view class="setting-content" :style="{ marginTop: `calc(${topSafeAreaHeight}px + 94rpx)` }">
+            <up-cell-group :border="false" :custom-style="{
+                'border-radius': '10rpx',
+            }">
+                <button class="cell-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
+                    <up-cell title="头像" :is-link="true" center>
+                        <template #value>
+                            <up-avatar :src="userInfo.avatar" font-size="64rpx" />
+                        </template>
+                    </up-cell>
+                </button>
+                <up-cell title="昵称" :is-link="true" :value="userInfo.nickname" @tap="nickNameVisible = true" />
+            </up-cell-group>
+        </view>
+
+        <!-- 昵称修改弹窗 -->
+        <up-modal :show="nickNameVisible" title="修改昵称" :show-confirm-button="false" :show-cancel-button="false">
+            <up-input v-model="nickName" placeholder="请输入新昵称" clearable />
+
+            <template #confirmButton>
+                <view class="flex items-center justify-around">
+                    <view class="h-[70rpx] w-[226rpx]">
+                        <up-button block shape="circle" @tap="nickNameVisible = false">
+                            取消
+                        </up-button>
+                    </view>
+                    <view class="h-[70rpx] w-[226rpx]">
+                        <up-button color="linear-gradient(90deg, #EE6B67 0%, #FF7D78 100%);" block shape="circle"
+                            type="primary" @tap="changeNickName">
+                            确定
+                        </up-button>
+                    </view>
+                </view>
+            </template>
+        </up-modal>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.setting-container {
+    background-color: #f8f8fa;
+    height: calc(100vh - 20rpx);
+    padding: 20rpx 24rpx 0;
+
+    .setting-content {
+        background-color: #fff;
+
+        .cell-button {
+            // 移除默认边框和背景
+            background: transparent;
+            border: none;
+            padding: 0;
+            margin: 0;
+            text-align: left;
+
+            // 确保点击区域覆盖整个cell
+            display: block;
+            width: 100%;
+
+            // 移除button的默认点击效果
+            &::after {
+                border: none;
+            }
+
+            // 保持cell的原有间距
+            margin-bottom: 1px;
+        }
+    }
+}
+</style>

+ 945 - 0
src/pages-A/shareFriend/index.vue

@@ -0,0 +1,945 @@
+<template>
+    <view class="share-container">
+        <!-- 顶部导航栏(模拟美团风格) -->
+        <!-- <up-status-bar /> -->
+        <CustomNavigationBar :show-back="true" title="设置" />
+
+        <view class="share-content" :style="{ paddingTop: `calc(${topSafeAreaHeight}px + 94rpx)` }">
+            <!-- 分享图片预览区域 -->
+            <view class="preview-area">
+                <canvas id="shareCanvas" type="2d" canvas-id="shareCanvas"
+                    :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }" class="canvas-hide" />
+
+                <!-- 预览图片容器 -->
+                <view v-if="shareImageUrl" class="preview-image-wrapper">
+                    <image :src="shareImageUrl" mode="widthFix" class="preview-image" @longpress="handleLongPressImage"
+                        @tap="handleImagePreview" />
+                    <!-- 图片操作提示 -->
+                    <view class="image-tips">
+                        <text class="tip-text">长按保存或分享给好友</text>
+                    </view>
+                </view>
+
+                <!-- 生成图片按钮(如果自动生成失败) -->
+                <view v-if="!shareImageUrl && !loading" class="generate-btn-container">
+                    <button class="generate-btn" @tap="generateShareImage">
+                        <text class="icon">🔄</text>
+                        <text>生成分享图片</text>
+                    </button>
+                </view>
+
+                <!-- 加载提示 -->
+                <view v-if="loading" class="loading-mask">
+                    <view class="loading-content">
+                        <view class="loading-spinner" />
+                        <text>生成分享图片中...</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- <view class="share-buttons-container">
+                <view class="button-item">
+                    <up-button block :disabled="!shareImageUrl" @tap="handleSaveImage">
+                        保存图片
+                    </up-button>
+                </view>
+                <view class="button-item">
+                    <up-button block :disabled="!shareImageUrl" @tap="handleShareFriend">
+                        分享好友
+                    </up-button>
+                </view>
+            </view> -->
+
+            <!-- 邀请规则区域 -->
+            <view class="invite-rule-area">
+                <!-- title -->
+                <view class="invite-rule-title">
+                    <view class="title-icon" />
+                    <text class="title-text">
+                        邀请规则
+                    </text>
+                </view>
+                <view class="invite-rule-content">
+                    <view>
+                        点击保存邀请海报,分享好友、微信群、朋友圈、系统将会绑定上下级关系,下级继续推广可赚取佣金。
+                    </view>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { onLoad } from '@dcloudio/uni-app'
+import { nextTick, ref } from 'vue'
+import { getWxQcCode } from '@/api/receiveCoupon'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import { safeAreaInsets } from '@/utils'
+import { getImageUrl } from '@/utils/imageUtil'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+    }
+    // enableShareAppMessage: true,
+    // enableShareTimeline: true,
+})
+
+// 响应式数据
+const loading = ref(false)
+const shareImageUrl = ref('')
+const canvasWidth = ref(750)
+const canvasHeight = ref(1200)
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+
+// 系统信息
+const systemInfo = ref({})
+
+// 生命周期
+onLoad(async (options) => {
+    // 获取系统信息
+    systemInfo.value = uni.getSystemInfoSync()
+
+    // 页面加载后生成分享图片
+    nextTick(() => {
+        setTimeout(() => {
+            generateShareImage()
+        }, 300)
+    })
+})
+
+function handleBack() {
+    uni.navigateBack()
+}
+
+// 获取canvas实例
+function getCanvasInstance() {
+    return new Promise((resolve, reject) => {
+        const query = uni.createSelectorQuery()
+        query.select('#shareCanvas')
+            .fields({ node: true, size: true })
+            .exec((res) => {
+                if (res[0]) {
+                    const canvas = res[0].node
+                    const ctx = canvas.getContext('2d')
+                    resolve({ canvas, ctx })
+                }
+                else {
+                    reject(new Error('Canvas not found'))
+                }
+            })
+    })
+}
+
+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)
+        return
+
+    loading.value = true
+
+    try {
+        // 计算尺寸
+        const dpr = systemInfo.value.pixelRatio || 1
+        const baseWidth = 375
+        const imageRatio = 649 / 1413
+        canvasWidth.value = baseWidth * dpr
+        canvasHeight.value = Math.round((baseWidth / imageRatio) * dpr)
+
+        await nextTick()
+
+        // 获取canvas实例
+        const { canvas, ctx } = await getCanvasInstance()
+
+        // 设置canvas尺寸
+        canvas.width = canvasWidth.value
+        canvas.height = canvasHeight.value
+
+        // 生成二维码
+        let qrCodeImg = null
+        try {
+            qrCodeImg = await getWxacodeBase64()
+        }
+        catch (error) {
+            console.error('获取小程序失败:', error)
+            // 在小程序中,如果获取二维码失败,直接使用null
+            qrCodeImg = null
+        }
+
+        // 绘制完整的分享图片
+        await drawCompleteShareImage(ctx, canvas, qrCodeImg)
+    }
+    catch (error) {
+        console.error('生成分享图片失败:', error)
+        uni.showToast({
+            title: '生成失败,请重试',
+            icon: 'none',
+        })
+    }
+    finally {
+        loading.value = false
+    }
+}
+
+// 绘制圆角矩形
+function drawRoundRect(ctx, x, y, width, height, radius) {
+    ctx.beginPath()
+    ctx.moveTo(x + radius, y)
+    ctx.lineTo(x + width - radius, y)
+    ctx.arcTo(x + width, y, x + width, y + radius, radius)
+    ctx.lineTo(x + width, y + height - radius)
+    ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
+    ctx.lineTo(x + radius, y + height)
+    ctx.arcTo(x, y + height, x, y + height - radius, radius)
+    ctx.lineTo(x, y + radius)
+    ctx.arcTo(x, y, x + radius, y, radius)
+    ctx.closePath()
+}
+
+// 绘制完整的分享图片
+async function drawCompleteShareImage(ctx, canvas, qrCodeImg) {
+    return new Promise(async (resolve, reject) => {
+        try {
+            const { width, height } = canvas
+            const scale = width / 375 // 缩放比例
+
+            // 1. 加载并绘制远程背景图片
+            const backgroundImgUrl = getImageUrl('@img/receiveCoupon/share-friend.png')
+            const backgroundImg = await loadRemoteImage(canvas, backgroundImgUrl)
+            ctx.drawImage(backgroundImg, 0, 0, width, height)
+
+            // 2. 绘制小标题
+            drawTitle(ctx, width, height, scale)
+
+            // 3. 绘制规则描述
+            drawRulesDescription(ctx, width, height, scale)
+
+            // 4. 绘制二维码区域
+            await drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale)
+
+            // 生成图片
+            uni.canvasToTempFilePath({
+                canvas,
+                quality: 0.9,
+                success: (res) => {
+                    shareImageUrl.value = res.tempFilePath
+                    resolve()
+                },
+                fail: (err) => {
+                    console.error('生成图片失败:', err)
+                    reject(err)
+                }
+            }, this)
+        }
+        catch (error) {
+            console.error('绘制图片失败:', error)
+            reject(error)
+        }
+    })
+}
+
+// 加载远程图片的函数
+async function loadRemoteImage(canvas, imageUrl) {
+    return new Promise((resolve, reject) => {
+        const image = canvas.createImage()
+        const timeout = setTimeout(() => {
+            reject(new Error('图片加载超时'))
+        }, 10000) // 10秒超时
+
+        image.onload = () => {
+            clearTimeout(timeout)
+            const imageWidth = image.naturalWidth || image.width
+            const imageHeight = image.naturalHeight || image.height
+
+            if (image.complete && imageWidth > 0 && imageHeight > 0) {
+                resolve(image)
+            }
+            else {
+                reject(new Error('图片加载事件触发但尺寸无效'))
+            }
+        }
+
+        image.onerror = (err) => {
+            clearTimeout(timeout)
+            console.error('加载远程图片失败:', err)
+            reject(err)
+        }
+
+        image.src = 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'
+    ctx.font = `bold ${14 * scale}px sans-serif`
+    ctx.textAlign = 'left'
+    const MaxWidth = width - 44 * scale
+    // 计算文本块的起始位置
+    const textX = (width - MaxWidth) / 2
+    ctx.fillText('一、使用规则', textX, 430 * scale)
+}
+
+// 绘制使用规则描述
+function drawRulesDescription(ctx, width, height, scale) {
+    // 文本自动换行辅助函数
+    const drawWrappedText = (text, fontSize, yPosition, color) => {
+        ctx.font = `${fontSize * scale}px sans-serif`
+        ctx.fillStyle = color
+        ctx.textAlign = 'left'
+
+        const maxWidth = width - 44 * scale
+        const textX = (width - maxWidth) / 2
+        const lineHeight = fontSize * 1.5 * scale
+        let currentY = yPosition
+        let currentLine = ''
+
+        for (let char of text) {
+            const testLine = currentLine + char
+            const metrics = ctx.measureText(testLine)
+
+            if (metrics.width > maxWidth && currentLine.length > 0) {
+                ctx.fillText(currentLine, textX, currentY)
+                currentLine = char
+                currentY += lineHeight
+            }
+            else {
+                currentLine = testLine
+            }
+        }
+
+        if (currentLine) {
+            ctx.fillText(currentLine, textX, currentY)
+        }
+
+        return currentY + lineHeight // 返回下一行的y坐标
+    }
+
+    // 文本起始位置
+    let currentY = 460 * scale
+
+    const rules = [
+        '1.发券分享好友,好友领券到“晋掌柜生活”下单消费,即可赚取佣金;',
+        '2.分享小程序或发券分享好友,即可与好友绑定上下级的关系,下级申请成为发券人,发券或推广即可赚取佣金;',
+        '3.每日发券达到一定数量,或邀请好友成为发券人,可领取佣金*元。'
+    ];
+    // 使用规则
+    rules.forEach((rule) => {
+        currentY = drawWrappedText(rule, 12, currentY, '#000000')
+        currentY += 11 * scale
+    })
+}
+
+// 绘制二维码区域
+async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
+    const qrSize = 125 * scale
+    const qrX = ((width - qrSize) / 3) - 130
+    const qrY = 660 * scale
+
+    // 使用柔和的白色背景,增加卡片层次感
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 清除阴影
+    ctx.shadowColor = 'transparent'
+    ctx.shadowBlur = 0
+    ctx.shadowOffsetY = 0
+
+    // 2. 优化二维码边框:添加双层边框效果
+    const qrRadius = qrSize / 2
+    const centerX = qrX + qrRadius
+    const centerY = qrY + qrRadius
+
+    // 3. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
+    if (qrCodeImg) {
+        try {
+            let imgSrc = qrCodeImg
+            let tempFilePath = ''
+
+            // 检查是否已经是临时文件路径
+            const isTempFile = typeof qrCodeImg === 'string' && !qrCodeImg.startsWith('data:image')
+
+            // 如果是临时文件路径,直接使用
+            if (isTempFile) {
+                tempFilePath = qrCodeImg
+            }
+            else {
+                // 处理base64图片数据
+                if (typeof qrCodeImg === 'string') {
+                    // 安全地尝试解码URI,避免URIError
+                    try {
+                        if (qrCodeImg.includes('%')) {
+                            imgSrc = decodeURIComponent(qrCodeImg)
+                        }
+                    }
+                    catch (uriError) {
+                        console.warn('URI解码失败,使用原始数据:', uriError)
+                    }
+
+                    // 确保base64数据有正确的前缀
+                    if (!imgSrc.startsWith('data:image')) {
+                        imgSrc = `data:image/png;base64,${imgSrc}`
+                    }
+                }
+
+                // 尝试使用临时文件方式
+                try {
+                    // #ifdef MP-WEIXIN
+                    const fs = uni.getFileSystemManager()
+                    const tempFileName = `temp_qr_${Date.now()}.png`
+                    tempFilePath = `${uni.env.USER_DATA_PATH}/${tempFileName}`
+
+                    // 提取base64数据部分
+                    let base64Data = imgSrc
+                    if (imgSrc.includes('base64,')) {
+                        base64Data = imgSrc.split('base64,')[1]
+                    }
+
+                    fs.writeFileSync(tempFilePath, base64Data, 'base64')
+                    console.log('Base64已转换为临时文件:', tempFilePath)
+                    // #endif
+                }
+                catch (fileError) {
+                    console.warn('转换base64为临时文件失败,尝试直接使用base64:', fileError)
+                    // 清除临时文件路径,确保使用base64
+                    tempFilePath = ''
+                }
+            }
+
+            const qrImage = canvas.createImage()
+
+            // 使用Promise确保图片加载完成
+            await new Promise((resolve, reject) => {
+                const timeout = setTimeout(() => {
+                    reject(new Error('图片加载超时'))
+                }, 10000) // 10秒超时
+
+                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
+            })
+
+            const finalWidth = qrImage.naturalWidth || qrImage.width
+            const finalHeight = qrImage.naturalHeight || qrImage.height
+
+            // 最后再检查一次图片尺寸
+            if (finalWidth <= 0 || finalHeight <= 0) {
+                throw new Error('图片尺寸无效')
+            }
+
+            // 绘制图片
+            ctx.save()
+            ctx.beginPath()
+            ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
+            ctx.clip()
+            ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
+
+            // 图片绘制成功后,添加一个精致的内边框
+            ctx.restore()
+            ctx.beginPath()
+            ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
+
+            console.log('二维码绘制成功')
+        }
+        catch (error) {
+            console.error('绘制二维码失败:', error)
+
+            // 增加更详细的错误日志
+            if (typeof qrCodeImg === 'string') {
+                console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
+            }
+        }
+    }
+
+    // 4. 优化二维码提示文字:使用更现代的字体和颜色
+    ctx.fillStyle = '#000000'
+    ctx.font = `${18 * scale}px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
+    ctx.fontWeight = '400'
+    ctx.textAlign = 'left'
+    ctx.textBaseline = 'middle'
+    ctx.fillText('立即邀请好友扫码', (width / 2) - 10, qrY + (qrSize / 2) - 17 * scale)
+    ctx.fillText('成为发券人', (width / 2) - 10, qrY + (qrSize / 2) + 17 * scale)
+}
+
+// 分享给好友
+function handleShareFriend() {
+    if (!shareImageUrl.value) {
+        uni.showToast({
+            title: '请先生成分享图片',
+            icon: 'none',
+        })
+        return
+    }
+
+    uni.showShareImageMenu({
+        path: shareImageUrl.value,
+        needShowEntrance: false,
+    })
+}
+
+// 保存图片到相册
+async function handleSaveImage() {
+    if (!shareImageUrl.value) {
+        uni.showToast({
+            title: '请先生成分享图片',
+            icon: 'none',
+        })
+        return
+    }
+
+    uni.showLoading({
+        title: '保存中...',
+        mask: true,
+    })
+
+    try {
+        await uni.saveImageToPhotosAlbum({
+            filePath: shareImageUrl.value,
+        })
+
+        uni.hideLoading()
+        uni.showToast({
+            title: '保存成功',
+            icon: 'success',
+        })
+    }
+    catch (error) {
+        uni.hideLoading()
+        console.error('保存失败:', error)
+
+        if (error.errMsg && error.errMsg.includes('auth deny')) {
+            uni.showModal({
+                title: '提示',
+                content: '需要您授权保存到相册',
+                confirmText: '去设置',
+                success: (res) => {
+                    if (res.confirm) {
+                        uni.openSetting()
+                    }
+                }
+            })
+        }
+        else {
+            uni.showToast({
+                title: '保存失败,请重试',
+                icon: 'none',
+            })
+        }
+    }
+}
+
+// 长按图片操作
+function handleLongPressImage() {
+    uni.showActionSheet({
+        itemList: ['保存到相册', '分享给好友'],
+        success: (res) => {
+            if (res.tapIndex === 0) {
+                handleSaveImage()
+            }
+            else if (res.tapIndex === 1) {
+                handleShareFriend()
+            }
+        }
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+/* 页面容器 */
+.share-container {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+    background: #f5f5f5;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+}
+
+/* 预览区域 */
+.preview-area {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    // background: #ffffff;
+    padding: 40rpx 20rpx;
+    position: relative;
+    min-height: 700rpx;
+}
+
+/* 预览图片容器 */
+.preview-image-wrapper {
+    width: 100%;
+    // max-width: 600rpx;
+    position: relative;
+}
+
+/* 预览图片 */
+.preview-image {
+    width: 100%;
+    // border-radius: 20rpx;
+    box-shadow: 15rpx 25rpx 50rpx rgba(0, 0, 0, 0.2);
+    transition:
+        transform 0.3s ease,
+        box-shadow 0.3s ease;
+}
+
+.preview-image:active {
+    transform: scale(1.02);
+    box-shadow: 20rpx 30rpx 60rpx rgba(0, 0, 0, 0.25);
+}
+
+/* 图片操作提示 */
+.image-tips {
+    display: flex;
+    justify-content: center;
+    margin-top: 20rpx;
+    padding: 0 20rpx;
+}
+
+.tip-text {
+    font-size: 24rpx;
+    color: #666666;
+}
+
+/* 生成图片按钮容器 */
+.generate-btn-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+}
+
+/* 生成图片按钮 */
+.generate-btn {
+    background: linear-gradient(90deg, #ee6b67 0%, #ff7d78 100%);
+    color: #ffffff;
+    border: none;
+    border-radius: 50rpx;
+    padding: 25rpx 60rpx;
+    font-size: 32rpx;
+    font-weight: bold;
+    box-shadow: 0 8rpx 25rpx rgba(255, 100, 71, 0.4);
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    gap: 10rpx;
+}
+
+.generate-btn:active {
+    transform: translateY(2rpx);
+    box-shadow: 0 5rpx 15rpx rgba(255, 100, 71, 0.3);
+}
+
+.share-buttons-container {
+    margin: 16rpx 20rpx;
+    padding: 20rpx;
+    background-color: #ffffff;
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+
+    .button-item {
+        width: 226rpx;
+
+        :deep(.u-button) {
+            border-radius: 35rpx;
+            background: linear-gradient(90deg, #ee6b67 0%, #ff7d78 100%);
+            color: #ffffff;
+        }
+    }
+}
+
+/* 操作区域 */
+.invite-rule-area {
+    margin: 26rpx 20rpx;
+    background-color: #ffffff;
+    padding: 20rpx 30rpx;
+    border-radius: 15rpx;
+    box-shadow: 0 8rpx 25rpx rgba(255, 100, 71, 0.1);
+    display: flex;
+    flex-direction: column;
+    gap: 20rpx;
+
+    .invite-rule-title {
+        display: flex;
+        flex-wrap: nowrap;
+        flex-direction: row;
+        align-items: center;
+        gap: 16rpx;
+
+        .title-icon {
+            width: 6rpx;
+            height: 33rpx;
+            background: #ed6b66;
+            border-radius: 3rpx;
+        }
+
+        .title-text {
+            font-weight: bolder;
+            font-size: 34rpx;
+            color: #333333;
+        }
+    }
+
+    .invite-rule-content {
+        color: #666666;
+        font-size: 28rpx;
+        line-height: 42rpx;
+    }
+}
+
+/* 分享按钮容器 */
+.share-buttons {
+    display: flex;
+    justify-content: space-around;
+    margin-bottom: 40rpx;
+}
+
+/* 分享按钮 */
+.share-btn {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: #ffffff;
+    border: 2rpx solid #ff6347;
+    border-radius: 20rpx;
+    padding: 30rpx 20rpx;
+    width: 200rpx;
+    transition: all 0.3s ease;
+}
+
+.share-btn:active {
+    background: #fff5f0;
+    transform: translateY(2rpx);
+}
+
+/* 分享好友按钮 */
+.share-friend {
+    border-color: #52c41a;
+    color: #52c41a;
+}
+
+.share-friend .btn-icon {
+    background: #e6f7ff;
+    color: #52c41a;
+}
+
+/* 保存图片按钮 */
+.share-save {
+    border-color: #ff8c00;
+    color: #ff8c00;
+}
+
+.share-save .btn-icon {
+    background: #fff5f0;
+    color: #ff8c00;
+}
+
+/* 分享朋友圈按钮 */
+.share-moment {
+    border-color: #ff6347;
+    color: #ff6347;
+}
+
+.share-moment .btn-icon {
+    background: #fff5f0;
+    color: #ff6347;
+}
+
+/* 按钮图标 */
+.btn-icon {
+    width: 80rpx;
+    height: 80rpx;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 15rpx;
+}
+
+.btn-icon .icon {
+    font-size: 40rpx;
+}
+
+/* 按钮文本 */
+.btn-text {
+    font-size: 28rpx;
+    font-weight: bold;
+    margin-bottom: 8rpx;
+}
+
+/* 按钮描述 */
+.btn-desc {
+    font-size: 22rpx;
+    opacity: 0.8;
+}
+
+/* 额外提示 */
+.extra-tips {
+    background: #fffbe6;
+    border: 2rpx solid #ffeaa7;
+    border-radius: 15rpx;
+    padding: 25rpx;
+}
+
+.tip-content {
+    font-size: 24rpx;
+    color: #856404;
+    line-height: 36rpx;
+}
+
+/* 加载遮罩 */
+.loading-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(255, 255, 255, 0.9);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 999;
+}
+
+/* 加载内容 */
+.loading-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 加载动画 */
+.loading-spinner {
+    width: 80rpx;
+    height: 80rpx;
+    border: 8rpx solid #f3f3f3;
+    border-top: 8rpx solid #ff6347;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 20rpx;
+}
+
+/* 加载文字 */
+.loading-content text {
+    font-size: 28rpx;
+    color: #666666;
+}
+
+/* 隐藏canvas */
+.canvas-hide {
+    position: absolute;
+    top: -9999px;
+    left: -9999px;
+    opacity: 0;
+}
+
+/* 动画 */
+@keyframes spin {
+    0% {
+        transform: rotate(0deg);
+    }
+
+    100% {
+        transform: rotate(360deg);
+    }
+}
+
+/* 响应式设计 */
+@media screen and (max-height: 1000rpx) {
+    .preview-area {
+        min-height: 600rpx;
+    }
+}
+</style>

+ 1122 - 0
src/pages-A/sharePage/index.vue

@@ -0,0 +1,1122 @@
+<template>
+    <view class="share-container">
+        <!-- 顶部导航栏(模拟美团风格) -->
+        <!-- <up-status-bar /> -->
+        <view class="top-nav" :style="{ paddingTop: `${topSafeAreaHeight}px` }">
+            <view class="custom-navbar">
+                <view class="back-btn" @tap="handleBack">
+                    <up-icon name="arrow-left" size="36rpx" color="#ffffff" />
+                </view>
+                <view class="nav-title">
+                    优惠券分享
+                </view>
+                <view class="right-space" />
+            </view>
+        </view>
+
+        <!-- 分享图片预览区域 -->
+        <view class="preview-area" :style="{ paddingTop: `calc(${topSafeAreaHeight}px + 112rpx)` }">
+            <canvas id="shareCanvas" type="2d" canvas-id="shareCanvas"
+                :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }" class="canvas-hide" />
+
+            <!-- 预览图片容器 -->
+            <view v-if="shareImageUrl" class="preview-image-wrapper">
+                <image :src="shareImageUrl" mode="widthFix" class="preview-image" @longpress="handleLongPressImage"
+                    @tap="handleImagePreview" />
+                <!-- 图片操作提示 -->
+                <view class="image-tips">
+                    <text class="tip-text">长按保存/分享</text>
+                </view>
+            </view>
+
+            <!-- 生成图片按钮(如果自动生成失败) -->
+            <view v-if="!shareImageUrl && !loading" class="generate-btn-container">
+                <button class="generate-btn" @tap="generateShareImage">
+                    <text class="icon">🔄</text>
+                    <text>生成分享图片</text>
+                </button>
+            </view>
+        </view>
+
+        <!-- 分享操作区域 -->
+        <view class="action-area">
+            <!-- 操作说明 -->
+            <view class="action-intro">
+                <text class="intro-title">分享方式</text>
+                <text class="intro-desc">选择以下方式分享给好友</text>
+            </view>
+
+            <view class="share-buttons">
+                <!-- 分享给好友 - 使用微信原生分享卡片 -->
+                <button class="share-btn share-friend" @tap="handleShareFriend">
+                    <view class="btn-icon">
+                        <text class="icon">👥</text>
+                    </view>
+                    <text class="btn-text">分享好友</text>
+                    <text class="btn-desc">发送图片给好友</text>
+                </button>
+
+                <!-- 保存到相册 -->
+                <button class="share-btn share-save" :disabled="!shareImageUrl" @tap="handleSaveImage">
+                    <view class="btn-icon">
+                        <text class="icon">💾</text>
+                    </view>
+                    <text class="btn-text">保存图片</text>
+                    <text class="btn-desc">保存到手机相册</text>
+                </button>
+            </view>
+
+            <!-- 额外提示 -->
+            <!-- <view class="extra-tips">
+                <text class="tip-content">💡 提示:分享卡片包含进入小程序的入口,方便好友快速领取优惠券</text>
+            </view> -->
+        </view>
+
+        <!-- 加载提示 -->
+        <view v-if="loading" class="loading-mask">
+            <view class="loading-content">
+                <view class="loading-spinner" />
+                <text>生成分享图片中...</text>
+            </view>
+        </view>
+    </view>
+</template>
+
+<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 { safeAreaInsets } from '@/utils'
+import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+    }
+    // enableShareAppMessage: true,
+    // enableShareTimeline: true,
+})
+
+// 响应式数据
+const loading = ref(false)
+const shareImageUrl = ref('')
+const canvasWidth = ref(750)
+const canvasHeight = ref(1200)
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+
+// 系统信息
+const systemInfo = ref({})
+
+const shareId = ref('')
+
+const { send, data: couponDetail } = useRequest(getCouponDetail, {
+    immediate: false,
+    initialData: {
+        name: '',
+        type: '2',
+        relatedName: '',
+        ruleMinSpendAmount: '',
+        ruleReductionAmount: '',
+        validityType: '1',
+        validDays: '',
+        validStartTime: '',
+        validEndTime: '',
+    }
+})
+
+// 生命周期
+onLoad(async (options) => {
+    // 获取系统信息
+    systemInfo.value = uni.getSystemInfoSync()
+    if (options.couponId) {
+        shareId.value = options.couponId
+        await send({ templateId: options.couponId })
+    }
+
+    // 页面加载后生成分享图片
+    nextTick(() => {
+        setTimeout(() => {
+            generateShareImage()
+        }, 300)
+    })
+})
+
+function handleBack() {
+    uni.navigateBack()
+}
+
+// 获取canvas实例
+function getCanvasInstance() {
+    return new Promise((resolve, reject) => {
+        const query = uni.createSelectorQuery()
+        query.select('#shareCanvas')
+            .fields({ node: true, size: true })
+            .exec((res) => {
+                if (res[0]) {
+                    const canvas = res[0].node
+                    const ctx = canvas.getContext('2d')
+                    resolve({ canvas, ctx })
+                }
+                else {
+                    reject(new Error('Canvas not found'))
+                }
+            })
+    })
+}
+
+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: 'COUPON',
+        sharePath: 'pages/home/home',
+        shareContentId: shareId.value,
+        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)
+        return
+
+    loading.value = true
+
+    try {
+        // 计算尺寸
+        const dpr = systemInfo.value.pixelRatio || 1
+        canvasWidth.value = 375 * dpr
+        canvasHeight.value = 750 * dpr
+
+        await nextTick()
+
+        // 获取canvas实例
+        const { canvas, ctx } = await getCanvasInstance()
+
+        // 设置canvas尺寸
+        canvas.width = canvasWidth.value
+        canvas.height = canvasHeight.value
+
+        // 生成二维码
+        let qrCodeImg = null
+        try {
+            qrCodeImg = await getWxacodeBase64()
+        }
+        catch (error) {
+            console.error('获取小程序失败:', error)
+            // 在小程序中,如果获取二维码失败,直接使用null
+            qrCodeImg = null
+        }
+
+        // 绘制完整的分享图片
+        await drawCompleteShareImage(ctx, canvas, qrCodeImg)
+    }
+    catch (error) {
+        console.error('生成分享图片失败:', error)
+        uni.showToast({
+            title: '生成失败,请重试',
+            icon: 'none',
+        })
+    }
+    finally {
+        loading.value = false
+    }
+}
+
+// 绘制圆角矩形
+function drawRoundRect(ctx, x, y, width, height, radius) {
+    ctx.beginPath()
+    ctx.moveTo(x + radius, y)
+    ctx.lineTo(x + width - radius, y)
+    ctx.arcTo(x + width, y, x + width, y + radius, radius)
+    ctx.lineTo(x + width, y + height - radius)
+    ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
+    ctx.lineTo(x + radius, y + height)
+    ctx.arcTo(x, y + height, x, y + height - radius, radius)
+    ctx.lineTo(x, y + radius)
+    ctx.arcTo(x, y, x + radius, y, radius)
+    ctx.closePath()
+}
+
+// 绘制完整的分享图片
+async function drawCompleteShareImage(ctx, canvas, qrCodeImg) {
+    return new Promise(async (resolve, reject) => {
+        try {
+            const { width, height } = canvas
+            const scale = width / 375 // 缩放比例
+
+            // 1. 绘制背景
+            ctx.fillStyle = '#ffffff'
+            ctx.fillRect(0, 0, width, height)
+
+            // 2. 绘制顶部装饰
+            drawTopDecoration(ctx, width, height, scale)
+
+            // 3. 绘制标题
+            drawTitle(ctx, width, height, scale)
+
+            // 4. 绘制优惠券金额
+            drawCouponAmount(ctx, width, height, scale)
+
+            // 5. 绘制优惠券描述
+            drawCouponDescription(ctx, width, height, scale)
+
+            // 6. 绘制二维码区域
+            await drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale)
+
+            // 7. 绘制底部信息
+            drawFooter(ctx, width, height, scale)
+
+            // 生成图片
+            uni.canvasToTempFilePath({
+                canvas,
+                quality: 0.9,
+                success: (res) => {
+                    shareImageUrl.value = res.tempFilePath
+                    resolve()
+                },
+                fail: (err) => {
+                    console.error('生成图片失败:', err)
+                    reject(err)
+                }
+            }, this)
+        }
+        catch (error) {
+            console.error('绘制图片失败:', error)
+            reject(error)
+        }
+    })
+}
+
+// 绘制顶部装饰
+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 = '#ffffff'
+    ctx.font = `bold ${30 * scale}px sans-serif`
+    ctx.textAlign = 'center'
+    ctx.fillText('优惠券', width / 2, 60 * scale)
+
+    // 添加副标题
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'
+    ctx.font = `${16 * scale}px sans-serif`
+    ctx.fillText('专享优惠,先到先得', width / 2, 90 * 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)
+
+        ctx.font = `bold ${36 * scale}px Arial`
+        ctx.fillText('折', width / 2 + 45 * scale, 205 * scale)
+    }
+    else if (couponDetail.value.type === '3') {
+        ctx.fillText(couponDetail.value.ruleReductionAmount, width / 2, 220 * scale)
+
+        // 添加装饰性货币符号
+        ctx.fillStyle = '#FF9500'
+        ctx.font = `bold ${36 * scale}px Arial`
+        ctx.textAlign = 'right'
+        ctx.fillText('¥', width / 2 - 80 * scale, 210 * scale)
+    }
+}
+
+// 绘制优惠券描述
+function drawCouponDescription(ctx, width, height, scale) {
+    // 主标题
+    ctx.fillStyle = '#333333'
+    ctx.font = `${28 * scale}px sans-serif`
+    ctx.textAlign = 'center'
+
+    const relatedType = couponDetail.value.relatedType
+    const relatedName = getLastPartAfterSlash(couponDetail.value.relatedName)
+    if (relatedType === '1') {
+        ctx.fillText(`限${relatedName}分类商品使用`, width / 2, 280 * scale)
+    }
+    else {
+        ctx.fillText(`限${relatedName}商品使用`, width / 2, 280 * scale)
+    }
+
+    // 添加分隔线
+    ctx.strokeStyle = '#E9ECEF'
+    ctx.lineWidth = 2 * scale
+    ctx.beginPath()
+    ctx.moveTo(40 * scale, 300 * scale)
+    ctx.lineTo(width - 40 * scale, 300 * scale)
+    ctx.stroke()
+
+    // 文本自动换行辅助函数
+    const drawWrappedText = (text, fontSize, yPosition, color) => {
+        ctx.font = `${fontSize * scale}px sans-serif`
+        ctx.fillStyle = color
+        ctx.textAlign = 'center'
+
+        const maxWidth = width - 80 * scale
+        const lineHeight = fontSize * 1.5 * scale
+        let currentY = yPosition
+        let currentLine = ''
+
+        for (let char of text) {
+            const testLine = currentLine + char
+            const metrics = ctx.measureText(testLine)
+
+            if (metrics.width > maxWidth && currentLine.length > 0) {
+                ctx.fillText(currentLine, width / 2, currentY)
+                currentLine = char
+                currentY += lineHeight
+            }
+            else {
+                currentLine = testLine
+            }
+        }
+
+        if (currentLine) {
+            ctx.fillText(currentLine, width / 2, currentY)
+        }
+
+        return currentY + lineHeight // 返回下一行的y坐标
+    }
+
+    // 有效期
+    // ctx.fillStyle = '#FF6B35'
+    // ctx.font = `${20 * scale}px sans-serif`
+    // ctx.textAlign = 'center'
+
+    // 有效期
+    let currentY = 340 * scale
+    const validityType = couponDetail.value.validityType
+    let validityText = ''
+
+    if (validityType === '1') {
+        validityText = `自领取之日起${couponDetail.value.validDays}日内使用`
+    }
+    else if (validityType === '2') {
+        validityText = `限${couponDetail.value.validStartTime}到${couponDetail.value.validEndTime}日内使用`
+    }
+    else {
+        validityText = `长期有效`
+    }
+
+    currentY = drawWrappedText(validityText, 20, currentY, '#FF6B35')
+
+    // 使用规则
+    let ruleText = ''
+    if (couponDetail.value.type === '2') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用,最高优惠${couponDetail.value.ruleDiscountCapAmount}元`
+    }
+    else if (couponDetail.value.type === '3') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用`
+    }
+
+    drawWrappedText(ruleText, 18, currentY, '#666666')
+}
+
+// 绘制二维码区域
+async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
+    const qrSize = 180 * scale
+    const qrX = (width - qrSize) / 2
+    const qrY = 450 * scale
+
+    // 1. 优化卡片样式:使用更柔和的阴影和渐变背景
+    ctx.shadowColor = 'rgba(0, 0, 0, 0.08)'
+    ctx.shadowBlur = 15 * scale
+    ctx.shadowOffsetY = 8 * scale
+
+    drawRoundRect(ctx, qrX - 30 * scale, qrY - 30 * scale, qrSize + 60 * scale, qrSize + 80 * scale, 20 * scale)
+
+    // 使用柔和的白色背景,增加卡片层次感
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 清除阴影
+    ctx.shadowColor = 'transparent'
+    ctx.shadowBlur = 0
+    ctx.shadowOffsetY = 0
+
+    // 2. 优化二维码边框:添加双层边框效果
+    const qrRadius = qrSize / 2
+    const centerX = qrX + qrRadius
+    const centerY = qrY + qrRadius
+
+    // 外层渐变边框
+    ctx.save()
+    ctx.beginPath()
+    ctx.arc(centerX, centerY, qrRadius + 8 * scale, 0, Math.PI * 2)
+
+    // 创建渐变效果
+    const gradient = ctx.createLinearGradient(centerX - qrRadius, centerY - qrRadius, centerX + qrRadius, centerY + qrRadius)
+    gradient.addColorStop(0, '#E8F5E9')
+    gradient.addColorStop(1, '#C8E6C9')
+
+    ctx.fillStyle = gradient
+    ctx.fill()
+    ctx.restore()
+
+    // 内层白色边框
+    ctx.beginPath()
+    ctx.arc(centerX, centerY, qrRadius + 4 * scale, 0, Math.PI * 2)
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 3. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
+    if (qrCodeImg) {
+        try {
+            let imgSrc = qrCodeImg
+            let tempFilePath = ''
+
+            // 检查是否已经是临时文件路径
+            const isTempFile = typeof qrCodeImg === 'string' && !qrCodeImg.startsWith('data:image')
+
+            // 如果是临时文件路径,直接使用
+            if (isTempFile) {
+                tempFilePath = qrCodeImg
+            }
+            else {
+                // 处理base64图片数据
+                if (typeof qrCodeImg === 'string') {
+                    // 安全地尝试解码URI,避免URIError
+                    try {
+                        if (qrCodeImg.includes('%')) {
+                            imgSrc = decodeURIComponent(qrCodeImg)
+                        }
+                    }
+                    catch (uriError) {
+                        console.warn('URI解码失败,使用原始数据:', uriError)
+                    }
+
+                    // 确保base64数据有正确的前缀
+                    if (!imgSrc.startsWith('data:image')) {
+                        imgSrc = `data:image/png;base64,${imgSrc}`
+                    }
+                }
+
+                // 尝试使用临时文件方式
+                try {
+                    // #ifdef MP-WEIXIN
+                    const fs = uni.getFileSystemManager()
+                    const tempFileName = `temp_qr_${Date.now()}.png`
+                    tempFilePath = `${uni.env.USER_DATA_PATH}/${tempFileName}`
+
+                    // 提取base64数据部分
+                    let base64Data = imgSrc
+                    if (imgSrc.includes('base64,')) {
+                        base64Data = imgSrc.split('base64,')[1]
+                    }
+
+                    fs.writeFileSync(tempFilePath, base64Data, 'base64')
+                    console.log('Base64已转换为临时文件:', tempFilePath)
+                    // #endif
+                }
+                catch (fileError) {
+                    console.warn('转换base64为临时文件失败,尝试直接使用base64:', fileError)
+                    // 清除临时文件路径,确保使用base64
+                    tempFilePath = ''
+                }
+            }
+
+            const qrImage = canvas.createImage()
+
+            // 使用Promise确保图片加载完成
+            await new Promise((resolve, reject) => {
+                const timeout = setTimeout(() => {
+                    reject(new Error('图片加载超时'))
+                }, 10000) // 10秒超时
+
+                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
+            })
+
+            const finalWidth = qrImage.naturalWidth || qrImage.width
+            const finalHeight = qrImage.naturalHeight || qrImage.height
+
+            // 最后再检查一次图片尺寸
+            if (finalWidth <= 0 || finalHeight <= 0) {
+                throw new Error('图片尺寸无效')
+            }
+
+            // 绘制图片
+            ctx.save()
+            ctx.beginPath()
+            ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
+            ctx.clip()
+            ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
+
+            // 图片绘制成功后,添加一个精致的内边框
+            ctx.restore()
+            ctx.beginPath()
+            ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
+            ctx.lineWidth = 2 * scale
+            ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)'
+            ctx.stroke()
+
+            console.log('二维码绘制成功')
+        }
+        catch (error) {
+            console.error('绘制二维码失败:', error)
+
+            // 增加更详细的错误日志
+            if (typeof qrCodeImg === 'string') {
+                console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
+            }
+        }
+    }
+
+    // 4. 优化二维码提示文字:使用更现代的字体和颜色
+    ctx.fillStyle = '#424242'
+    ctx.font = `${22 * scale}px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    ctx.fillText('长按识别小程序码', width / 2, qrY + qrSize + 31 * scale)
+
+    // 增加辅助文字,让提示更清晰
+    ctx.fillStyle = '#9E9E9E'
+    ctx.font = `${16 * scale}px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
+    ctx.fillText('领取专属优惠券', width / 2, qrY + qrSize + 65 * scale)
+}
+
+// 绘制底部信息
+function drawFooter(ctx, width, height, scale) {
+    // 计算二维码区域的位置
+    const qrSize = 200 * scale
+    const qrY = 440 * scale
+
+    // 底部信息显示在二维码下方,与二维码提示文字保持适当间距
+    const footerY = qrY + qrSize + 80 * scale // 在"长按识别二维码领取"文字下方40*scale处
+
+    ctx.fillStyle = '#999999'
+    ctx.font = `${16 * scale}px sans-serif`
+    ctx.textAlign = 'center'
+    ctx.fillText('分享自微信小程序', width / 2, footerY)
+}
+
+// 分享给好友
+function handleShareFriend() {
+    if (!shareImageUrl.value) {
+        uni.showToast({
+            title: '请先生成分享图片',
+            icon: 'none',
+        })
+        return
+    }
+
+    uni.showShareImageMenu({
+        path: shareImageUrl.value,
+        needShowEntrance: false,
+    })
+}
+
+// 保存图片到相册
+async function handleSaveImage() {
+    if (!shareImageUrl.value) {
+        uni.showToast({
+            title: '请先生成分享图片',
+            icon: 'none',
+        })
+        return
+    }
+
+    uni.showLoading({
+        title: '保存中...',
+        mask: true,
+    })
+
+    try {
+        await uni.saveImageToPhotosAlbum({
+            filePath: shareImageUrl.value,
+        })
+
+        uni.hideLoading()
+        uni.showToast({
+            title: '保存成功',
+            icon: 'success',
+        })
+    }
+    catch (error) {
+        uni.hideLoading()
+        console.error('保存失败:', error)
+
+        if (error.errMsg && error.errMsg.includes('auth deny')) {
+            uni.showModal({
+                title: '提示',
+                content: '需要您授权保存到相册',
+                confirmText: '去设置',
+                success: (res) => {
+                    if (res.confirm) {
+                        uni.openSetting()
+                    }
+                }
+            })
+        }
+        else {
+            uni.showToast({
+                title: '保存失败,请重试',
+                icon: 'none',
+            })
+        }
+    }
+}
+
+// 长按图片操作
+function handleLongPressImage() {
+    uni.showActionSheet({
+        itemList: ['保存到相册', '分享给好友'],
+        success: (res) => {
+            if (res.tapIndex === 0) {
+                handleSaveImage()
+            }
+            else if (res.tapIndex === 1) {
+                handleShareFriend()
+            }
+        }
+    })
+}
+
+// 微信分享配置
+// onShareAppMessage(() => {
+//     return {
+//         title: null,
+//         path: null,
+//         imageUrl: shareImageUrl.value,
+//     }
+// })
+
+// onShareTimeline(() => {
+//     return {
+//         title: null,
+//         query: null,
+//         imageUrl: shareImageUrl.value,
+//     }
+// })
+</script>
+
+<style lang="scss" scoped>
+/* 页面容器 */
+.share-container {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+    background: #f5f5f5;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+}
+
+/* 顶部导航栏 */
+.top-nav {
+    background: linear-gradient(-46deg, #ff7a75 0%, #ed6b66 98%);
+    box-shadow: 0 2rpx 10rpx rgba(255, 100, 71, 0.3);
+    position: fixed;
+    /* 固定定位 */
+    top: 0;
+    /* 顶部对齐 */
+    left: 0;
+    /* 左侧对齐 */
+    right: 0;
+    /* 右侧对齐 */
+    z-index: 999;
+    /* 确保在其他元素之上 */
+    padding-top: var(--safe-area-inset-top);
+    /* 适配安全区域 */
+}
+
+/* 自定义导航栏 */
+.custom-navbar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 20rpx 30rpx;
+    height: 88rpx;
+    box-sizing: border-box;
+}
+
+/* 返回按钮 */
+.back-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.back-icon {
+    font-size: 36rpx;
+    color: #ffffff;
+    font-weight: bold;
+}
+
+/* 导航栏标题 */
+.nav-title {
+    color: #ffffff;
+    font-size: 36rpx;
+    font-weight: bold;
+    flex: 1;
+    text-align: center;
+}
+
+/* 右侧占位 */
+.right-space {
+    width: 60rpx;
+}
+
+/* 安全区域 */
+.safe-top {
+    background: linear-gradient(-46deg, #ff7a75 0%, #ed6b66 98%);
+}
+
+/* 预览区域 */
+.preview-area {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: #ffffff;
+    padding: 40rpx 20rpx;
+    position: relative;
+    min-height: 700rpx;
+}
+
+/* 预览图片容器 */
+.preview-image-wrapper {
+    width: 100%;
+    max-width: 600rpx;
+    position: relative;
+}
+
+/* 预览图片 */
+.preview-image {
+    width: 100%;
+    border-radius: 20rpx;
+    box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
+    transition:
+        transform 0.3s ease,
+        box-shadow 0.3s ease;
+}
+
+.preview-image:active {
+    transform: scale(1.02);
+    box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.2);
+}
+
+/* 图片操作提示 */
+.image-tips {
+    display: flex;
+    justify-content: center;
+    margin-top: 20rpx;
+    padding: 0 20rpx;
+}
+
+.tip-text {
+    font-size: 24rpx;
+    color: #666666;
+}
+
+/* 生成图片按钮容器 */
+.generate-btn-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+}
+
+/* 生成图片按钮 */
+.generate-btn {
+    background: linear-gradient(-46deg, #ff7a75 0%, #ed6b66 98%);
+    color: #ffffff;
+    border: none;
+    border-radius: 50rpx;
+    padding: 25rpx 60rpx;
+    font-size: 32rpx;
+    font-weight: bold;
+    box-shadow: 0 8rpx 25rpx rgba(255, 100, 71, 0.4);
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    gap: 10rpx;
+}
+
+.generate-btn:active {
+    transform: translateY(2rpx);
+    box-shadow: 0 5rpx 15rpx rgba(255, 100, 71, 0.3);
+}
+
+/* 操作区域 */
+.action-area {
+    background: #ffffff;
+    padding: 40rpx 30rpx;
+    margin-top: 20rpx;
+    border-radius: 30rpx 30rpx 0 0;
+    box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
+}
+
+/* 操作说明 */
+.action-intro {
+    text-align: center;
+    margin-bottom: 40rpx;
+}
+
+.intro-title {
+    display: block;
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333333;
+    margin-bottom: 10rpx;
+}
+
+.intro-desc {
+    display: block;
+    font-size: 28rpx;
+    color: #666666;
+}
+
+/* 分享按钮容器 */
+.share-buttons {
+    display: flex;
+    justify-content: space-around;
+    margin-bottom: 40rpx;
+}
+
+/* 分享按钮 */
+.share-btn {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: #ffffff;
+    border: 2rpx solid #ff6347;
+    border-radius: 20rpx;
+    padding: 30rpx 20rpx;
+    width: 200rpx;
+    transition: all 0.3s ease;
+}
+
+.share-btn:active {
+    background: #fff5f0;
+    transform: translateY(2rpx);
+}
+
+/* 分享好友按钮 */
+.share-friend {
+    border-color: #52c41a;
+    color: #52c41a;
+}
+
+.share-friend .btn-icon {
+    background: #e6f7ff;
+    color: #52c41a;
+}
+
+/* 保存图片按钮 */
+.share-save {
+    border-color: #ff8c00;
+    color: #ff8c00;
+}
+
+.share-save .btn-icon {
+    background: #fff5f0;
+    color: #ff8c00;
+}
+
+/* 分享朋友圈按钮 */
+.share-moment {
+    border-color: #ff6347;
+    color: #ff6347;
+}
+
+.share-moment .btn-icon {
+    background: #fff5f0;
+    color: #ff6347;
+}
+
+/* 按钮图标 */
+.btn-icon {
+    width: 80rpx;
+    height: 80rpx;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 15rpx;
+}
+
+.btn-icon .icon {
+    font-size: 40rpx;
+}
+
+/* 按钮文本 */
+.btn-text {
+    font-size: 28rpx;
+    font-weight: bold;
+    margin-bottom: 8rpx;
+}
+
+/* 按钮描述 */
+.btn-desc {
+    font-size: 22rpx;
+    opacity: 0.8;
+}
+
+/* 额外提示 */
+.extra-tips {
+    background: #fffbe6;
+    border: 2rpx solid #ffeaa7;
+    border-radius: 15rpx;
+    padding: 25rpx;
+}
+
+.tip-content {
+    font-size: 24rpx;
+    color: #856404;
+    line-height: 36rpx;
+}
+
+/* 加载遮罩 */
+.loading-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(255, 255, 255, 0.9);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 999;
+}
+
+/* 加载内容 */
+.loading-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 加载动画 */
+.loading-spinner {
+    width: 80rpx;
+    height: 80rpx;
+    border: 8rpx solid #f3f3f3;
+    border-top: 8rpx solid #ff6347;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 20rpx;
+}
+
+/* 加载文字 */
+.loading-content text {
+    font-size: 28rpx;
+    color: #666666;
+}
+
+/* 隐藏canvas */
+.canvas-hide {
+    position: absolute;
+    top: -9999px;
+    left: -9999px;
+    opacity: 0;
+}
+
+/* 动画 */
+@keyframes spin {
+    0% {
+        transform: rotate(0deg);
+    }
+
+    100% {
+        transform: rotate(360deg);
+    }
+}
+
+/* 响应式设计 */
+@media screen and (max-height: 1000rpx) {
+    .preview-area {
+        min-height: 600rpx;
+    }
+
+    .action-area {
+        padding: 30rpx 20rpx;
+    }
+}
+</style>

+ 0 - 1
src/pages-A/unlockRewards/index.vue

@@ -146,7 +146,6 @@ function submitPay() {
 }
 
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     min-height: 100vh;
     background-color: #f5f5f5;
     line-height: 1;

+ 0 - 1
src/pages-A/withdraw/index.vue

@@ -80,7 +80,6 @@ function goPage(page: string) {
 }
 
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     min-height: calc(100vh - 20rpx);
     background-color: #f5f5f5;
     line-height: 1;

+ 0 - 1
src/pages-A/withdrawalDetails/index.vue

@@ -161,7 +161,6 @@ function openDatePicker() {
 
 <style lang="scss" scoped>
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     min-height: 100vh;
     background-color: #f5f5f5;
     line-height: 1;

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

@@ -209,6 +209,14 @@ function goPage(page: string) {
                         未结算金额
                     </view>
                 </view>
+                <view class="income-header-tips-item">
+                    <view class="income-header-tips-item-num">
+                        {{ accountCountData?.totalCommission || 0 }}
+                    </view>
+                    <view class="income-header-tips-item-des">
+                        累计收益
+                    </view>
+                </view>
             </view>
         </view>
         <!-- 公告 -->
@@ -294,7 +302,6 @@ function goPage(page: string) {
 
 <style lang="scss" scoped>
 .profile-container {
-    font-family: Alibaba PuHuiTi;
     background-color: #f5f5f5;
     line-height: 1;
     display: flex;
@@ -372,7 +379,7 @@ function goPage(page: string) {
                 line-height: 1;
                 position: relative;
 
-                &:first-child:after {
+                &:not(:last-child):after {
                     content: '';
                     position: absolute;
                     top: 50%;

+ 200 - 55
src/pages/my/my.vue

@@ -7,6 +7,7 @@ import { getCouponIssuerApplyById } from '@/api/me'
 import { LOGIN_PAGE } from '@/router/config'
 import { useUserStore } from '@/store'
 import { useTokenStore } from '@/store/token'
+import { changtime } from '@/utils'
 import { getImageUrl } from '@/utils/imageUtil'
 
 definePage({
@@ -21,7 +22,7 @@ const tokenStore = useTokenStore()
 // 使用storeToRefs解构userInfo
 const { userInfo } = storeToRefs(userStore)
 const { hasLogin } = storeToRefs(tokenStore)
-console.log('Login:', userInfo.value, hasLogin.value)
+const deadline = ref(Date.now())
 
 // 用户优惠券统计数据
 const { send: getCouponSituationRequest, data: couponSituationData } = useRequest(getCouponSituation, {
@@ -113,7 +114,7 @@ function getNavigationBarHeight() {
             const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
             console.log('顶部导航栏高度:', res.statusBarHeight, menuButtonInfo)
             // 顶部导航栏高度 = 状态栏高度 + 胶囊的高度
-            navigationBarHeight.value = res.statusBarHeight + menuButtonInfo.height + 12
+            navigationBarHeight.value = res.statusBarHeight + menuButtonInfo.height
         },
     })
 }
@@ -198,11 +199,13 @@ function menuClick(page) {
     <view class="profile-container">
         <!-- 顶部区域 -->
         <view class="me-header"
-            :style="{ background: `url(${getImageUrl('@img/me/me-bg.png')}) no-repeat center center`, backgroundSize: 'cover' }">
-            <view class="me-header-avatar-info" :style="{ paddingTop: `${navigationBarHeight}px` }">
-                <button class="me-header-avatar" @click="handleGetAvatar">
+            :style="{ background: `url(${getImageUrl('@img/me/me-bg.png')})`, backgroundSize: 'cover', backgroundRepeat: 'no-repeat', backgroundPosition: 'center center' }">
+            <!-- <view class="me-header-avatar-info" :style="{ paddingTop: `calc(${navigationBarHeight}px + 23rpx)` }"> -->
+            <view class="me-header-avatar-info" :style="{ paddingTop: `23rpx` }">
+                <!-- <button class="me-header-avatar" @click="handleGetAvatar">
                     <image :src="avatarDisplay" mode="aspectFill" />
-                </button>
+                </button> -->
+                <up-image :src="avatarDisplay" width="128rpx" height="128rpx" shape="circle" />
                 <view class="me-header-info">
                     <view class="me-header-name">
                         <!-- #ifdef MP-WEIXIN -->
@@ -216,28 +219,70 @@ function menuClick(page) {
                 </view>
             </view>
             <view class="me-header-tips">
-                <view class="me-header-tips-item">
-                    <view class="me-header-tips-item-num">
-                        {{ couponSituationData?.quantityForComplimentary || 0 }}
-                    </view>
-                    <view class="me-header-tips-item-des">
-                        已领取
+                <view class="me-header-tips-row header-data-time">
+                    <view class="desc-time">
+                        <up-icon name="info-circle" size="23rpx" color="#fff" />
+                        <view class="desc-text">
+                            数据截止到&nbsp;&nbsp;{{ changtime(deadline, 'YYYY-MM-DD HH:mm:ss') }}
+                        </view>
                     </view>
                 </view>
-                <view class="me-header-tips-item">
-                    <view class="me-header-tips-item-num">
-                        {{ couponSituationData?.usedQuantity || 0 }}
+                <view class="me-header-tips-row header-data-info">
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.quantityForComplimentary || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            今日邀请
+                        </view>
+                    </view>
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.usedQuantity || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            累计邀请
+                        </view>
                     </view>
-                    <view class="me-header-tips-item-des">
-                        已使用
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.expired || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            累计团员
+                        </view>
+                        <view class="me-header-tips-item-icon">
+                            <up-icon name="arrow-right" size="21rpx" color="#FFFFFF" />
+                        </view>
                     </view>
                 </view>
-                <view class="me-header-tips-item">
-                    <view class="me-header-tips-item-num">
-                        {{ couponSituationData?.expired || 0 }}
+                <view class="me-header-tips-row header-data-info">
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.quantityForComplimentary || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            今日发券
+                        </view>
+                    </view>
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.usedQuantity || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            今日领取
+                        </view>
                     </view>
-                    <view class="me-header-tips-item-des">
-                        已过期
+                    <view class="me-header-tips-item">
+                        <view class="me-header-tips-item-num">
+                            {{ couponSituationData?.expired || 0 }}
+                        </view>
+                        <view class="me-header-tips-item-des">
+                            今日使用
+                        </view>
+                        <view class="me-header-tips-item-icon">
+                            <up-icon name="arrow-right" size="21rpx" color="#FFFFFF" />
+                        </view>
                     </view>
                 </view>
             </view>
@@ -248,7 +293,51 @@ function menuClick(page) {
                 <view class="me-header-menu-icon">
                     <image :src="getImageUrl('@img/me/coupon-need.png')" mode="aspectFill" />
                     <view class="me-header-menu-text">
-                        发券人申请
+                        申请发券人
+                    </view>
+                </view>
+                <view class="me-header-menu-left">
+                    <up-icon name="arrow-right" color="#979797" size="12" />
+                </view>
+            </view>
+            <view class="me-header-menu-item" @click="menuClick('applyForm')">
+                <view class="me-header-menu-icon">
+                    <image :src="getImageUrl('@img/me/invite.png')" mode="aspectFill" />
+                    <view class="me-header-menu-text">
+                        我的邀请
+                    </view>
+                </view>
+                <view class="me-header-menu-left">
+                    <up-icon name="arrow-right" color="#979797" size="12" />
+                </view>
+            </view>
+            <view class="me-header-menu-item" @click="menuClick('applyForm')">
+                <view class="me-header-menu-icon">
+                    <image :src="getImageUrl('@img/me/inviter.png')" mode="aspectFill" />
+                    <view class="me-header-menu-text">
+                        我的邀请人
+                    </view>
+                </view>
+                <view class="me-header-menu-left">
+                    <up-icon name="arrow-right" color="#979797" size="12" />
+                </view>
+            </view>
+            <view class="me-header-menu-item" @click="menuClick('shareFriend')">
+                <view class="me-header-menu-icon">
+                    <image :src="getImageUrl('@img/me/share.png')" mode="aspectFill" />
+                    <view class="me-header-menu-text">
+                        邀请好友
+                    </view>
+                </view>
+                <view class="me-header-menu-left">
+                    <up-icon name="arrow-right" color="#979797" size="12" />
+                </view>
+            </view>
+            <view class="me-header-menu-item" @click="menuClick('settingPage')">
+                <view class="me-header-menu-icon">
+                    <image :src="getImageUrl('@img/me/setting.png')" mode="aspectFill" />
+                    <view class="me-header-menu-text">
+                        设置
                     </view>
                 </view>
                 <view class="me-header-menu-left">
@@ -321,10 +410,12 @@ function menuClick(page) {
 }
 
 .profile-container {
-    font-family: Alibaba PuHuiTi;
-
     .me-header {
-        height: 575rpx;
+        height: 746rpx;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
 
         .me-header-avatar-info {
             display: flex;
@@ -333,19 +424,6 @@ function menuClick(page) {
             align-items: center;
             gap: 24rpx;
 
-            .me-header-avatar {
-                width: 128rpx;
-                height: 128rpx;
-                border-radius: 50%;
-                overflow: hidden;
-
-                image {
-                    width: 100%;
-                    height: 100%;
-                    object-fit: cover;
-                }
-            }
-
             .me-header-info {
                 .me-header-name {
                     font-weight: 500;
@@ -356,29 +434,96 @@ function menuClick(page) {
         }
 
         .me-header-tips {
+            margin-top: 46rpx;
+            height: 317rpx;
+            width: 702rpx;
             display: flex;
-            margin-top: 49rpx;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            gap: 20rpx;
+            background: linear-gradient(90deg, #ff7671, #ff8e8a, #ff7671);
+            border-radius: 8rpx;
 
-            .me-header-tips-item {
-                flex: 1;
+            .me-header-tips-row {
                 display: flex;
-                flex-direction: column;
-                justify-content: center;
-                align-items: center;
-                gap: 15rpx;
-                font-weight: 500;
-                font-size: 34rpx;
-                color: #ffffff;
-                line-height: 1;
-
-                .me-header-tips-item-num {
-                    font-weight: bold;
+
+                .desc-time {
+                    width: 100%;
+                    display: flex;
+                    flex-direction: row;
+                    flex-wrap: nowrap;
+                    gap: 6rpx;
+                    justify-content: flex-end;
+                    padding-right: 21rpx;
+
+                    font-weight: 400;
+                    font-size: 20rpx;
+                    color: #ffffff;
+                    line-height: 20rpx;
                 }
 
-                .me-header-tips-item-des {
+                .me-header-tips-item {
+                    flex: 1;
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    align-items: center;
+                    gap: 15rpx;
+                    font-weight: 500;
+                    font-size: 34rpx;
+                    color: #ffffff;
+                    line-height: 1;
+
+                    .me-header-tips-item-num {
+                        font-weight: bold;
+                    }
+
+                    .me-header-tips-item-des {
+                        font-weight: 400;
+                        font-size: 24rpx;
+                        color: #ffffff;
+                    }
+                }
+            }
+
+            .header-data-time {
+                align-self: flex-end;
+            }
+
+            .me-header-tips-row.header-data-info {
+                align-self: center;
+                width: 642rpx;
+                height: 96rpx;
+                background: rgba($color: #851b17, $alpha: 0.1);
+                border-radius: 10rpx;
+
+                .me-header-tips-item {
                     font-weight: 400;
-                    font-size: 24rpx;
+                    font-size: 23rpx;
                     color: #ffffff;
+                    position: relative;
+
+                    &:not(:last-child):after {
+                        content: '';
+                        position: absolute;
+                        top: 50%;
+                        right: 0;
+                        transform: translateY(-50%);
+                        width: 2px;
+                        height: 40rpx;
+                        background: #ffffff;
+                    }
+
+                    .me-header-tips-item-icon {
+                        position: absolute;
+                        top: 50%;
+                        right: 21rpx;
+                        width: 21rpx;
+                        height: 21rpx;
+                        font-weight: bolder;
+                        transform: translateY(-50%);
+                    }
                 }
             }
         }

+ 13 - 2
src/store/user.ts

@@ -42,6 +42,16 @@ export const useUserStore = defineStore(
             uni.setStorageSync('user', updatedUser)
             console.log('本地存储用户头像已更新', updatedUser)
         }
+        const setUserNickName = (nickname: string) => {
+            userInfo.value.nickname = nickname
+            console.log('设置用户昵称', nickname)
+            console.log('userInfo', userInfo.value)
+            // 直接更新本地存储(无论本地是否已有用户信息)
+            const currentUser = uni.getStorageSync('user')
+            const updatedUser = currentUser ? { ...currentUser, nickname } : { ...userInfo.value }
+            uni.setStorageSync('user', updatedUser)
+            console.log('本地存储用户昵称已更新', updatedUser)
+        }
         // 删除用户信息
         const clearUserInfo = () => {
             userInfo.value = { ...userInfoState }
@@ -49,8 +59,8 @@ export const useUserStore = defineStore(
         }
 
         /**
-                                 * 获取用户信息
-                                 */
+                                     * 获取用户信息
+                                     */
         const fetchUserInfo = async () => {
             const res = JSON.parse(uni.getStorageSync('user'))
             return res
@@ -62,6 +72,7 @@ export const useUserStore = defineStore(
             fetchUserInfo,
             setUserInfo,
             setUserAvatar,
+            setUserNickName,
         }
     },
     {