浏览代码

券中心改版

lishanshan 4 月之前
父节点
当前提交
18370d24d6
共有 59 个文件被更改,包括 7159 次插入34 次删除
  1. 4 4
      env/.env.development
  2. 12 0
      manifest.config.ts
  3. 2 2
      src/App.vue
  4. 1 1
      src/api/home.ts
  5. 5 0
      src/api/types/coupon.ts
  6. 2 2
      src/components/IncomeItem.vue
  7. 618 0
      src/components/couponList.vue
  8. 331 0
      src/components/exchangeCoupon.vue
  9. 371 0
      src/components/templateCoupon.vue
  10. 1 1
      src/hooks/useCouponShare.ts
  11. 1 1
      src/http/alova.ts
  12. 7 11
      src/pages-A/incomeShareInfo/index.vue
  13. 123 0
      src/pages-A/inviteUsageRules/index.vue
  14. 1431 0
      src/pages-A/shareCoupon/index.vue
  15. 1 1
      src/pages-A/shareFriend/index.vue
  16. 463 0
      src/pages/home/index.vue
  17. 3 1
      src/pages/income/income.vue
  18. 1 1
      src/router/config.ts
  19. 二进制
      src/static/fonts/字制区喜脉体.ttf
  20. 二进制
      src/static/images/gyy_logo.png
  21. 100 5
      src/store/area.ts
  22. 12 1
      src/store/token.ts
  23. 14 0
      src/style/index.scss
  24. 2 2
      src/tabbar/config.ts
  25. 201 0
      src/uni_modules/Sansnn-uQRCode/LICENSE.md
  26. 207 0
      src/uni_modules/Sansnn-uQRCode/README.md
  27. 12 0
      src/uni_modules/Sansnn-uQRCode/changelog.md
  28. 1 0
      src/uni_modules/Sansnn-uQRCode/common/cache.js
  29. 41 0
      src/uni_modules/Sansnn-uQRCode/common/queue.js
  30. 3 0
      src/uni_modules/Sansnn-uQRCode/common/types/cache.d.ts
  31. 4 0
      src/uni_modules/Sansnn-uQRCode/common/types/queue.d.ts
  32. 25 0
      src/uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue
  33. 25 0
      src/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue
  34. 241 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js
  35. 18 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleLinearGradient.js
  36. 8 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStylePattern.js
  37. 17 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleRadialGradient.js
  38. 666 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/RenderingContext.js
  39. 11 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ActiveInfo.js
  40. 21 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Buffer.js
  41. 21 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Framebuffer.js
  42. 298 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLenum.js
  43. 142 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLmethod.js
  44. 23 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLtype.js
  45. 21 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Program.js
  46. 21 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Renderbuffer.js
  47. 1191 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/RenderingContext.js
  48. 22 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Shader.js
  49. 11 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ShaderPrecisionFormat.js
  50. 22 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Texture.js
  51. 22 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/UniformLocation.js
  52. 3 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/classUtils.js
  53. 74 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js
  54. 96 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js
  55. 24 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js
  56. 39 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js
  57. 33 0
      src/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js
  58. 79 0
      src/uni_modules/Sansnn-uQRCode/package.json
  59. 11 1
      src/utils/systemInfo.ts

+ 4 - 4
env/.env.development

@@ -7,13 +7,13 @@ VITE_SHOW_SOURCEMAP = true
 
 
 # 后台请求地址
-VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'
-# VITE_SERVER_BASEURL = 'http://192.168.1.232:8082/jeecg-boot'
-# VITE_SERVER_BASEURL = 'https://life.baoxianzhanggui.com/locallive-pro-java'
+# VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'
+# VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'
+VITE_SERVER_BASEURL = 'https://life.baoxianzhanggui.com/locallive-pro-java'
 
 
 # 静态资源请求地址
 VITE_CDN_DOMAIN = 'https://life.baoxianzhanggui.com/pro-img/securities'
 
 # 微信小程序码跳转版本
-VITE_WEIXIN_MINIAPP_VERSION = 'trial'
+VITE_WEIXIN_MINIAPP_VERSION = 'trial'

+ 12 - 0
manifest.config.ts

@@ -125,6 +125,14 @@ export default defineManifestConfig({
             es6: true,
             minified: true,
         },
+        permission: {
+            "Geolocation" : {
+                "desc" : "你的位置信息将用于地图显示"
+            },
+            "scope.userLocation" : {
+                "desc" : "需要获取位置以提供搜索建议"
+            }
+        },
         lazyCodeLoading: VITE_LAZY_CODE_LOADING,
         optimization: {
             subPackages: true,
@@ -135,6 +143,10 @@ export default defineManifestConfig({
         // styleIsolation: 'shared',
         usingComponents: true,
         // __usePrivacyCheck__: true,
+        requiredPrivateInfos: [
+          'getLocation',
+          'chooseLocation'
+        ]
     },
     'mp-alipay': {
         usingComponents: true,

+ 2 - 2
src/App.vue

@@ -42,7 +42,7 @@ onHide(() => {
 @import 'uview-plus/index.scss';
 
 @font-face {
-    font-family: "Alibaba PuHuiTi", "PingFang SC", sans-serif;
+    font-family: "PingFang SC", "Alibaba PuHuiTi", sans-serif;
     font-weight: normal;
     font-style: normal;
     font-display: swap;
@@ -53,6 +53,6 @@ page,
 body,
 view,
 text {
-    font-family: "Alibaba PuHuiTi", "PingFang SC", sans-serif;
+    font-family: "PingFang SC", "Alibaba PuHuiTi", sans-serif;
 }
 </style>

+ 1 - 1
src/api/home.ts

@@ -111,7 +111,7 @@ export function getCategoryList() {
         transform(data, headers) {
             const respondedData = data || []
             return [{
-                name: '全部',
+                name: '按摩',
                 value: '0',
             }, ...respondedData.map(item => ({
                 name: item.title,

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

@@ -16,4 +16,9 @@ export interface CouponSituation {
     quantityToBeUsed: number
     quantityForComplimentary: number
     expired: number
+    pullNewCountThreshold: number
+    pullNewAmount: number | string
+    pullNewAverageAmount: number | string
+    pushCouponCountThreshold: number | string
+    pushCouponAmount: number | string
 }

+ 2 - 2
src/components/IncomeItem.vue

@@ -2,8 +2,8 @@
     <view class="invite-item" @tap="handleItemClick">
         <view class="income-header-menu-item">
             <view class="income-header-menu-icon">
-                <image v-if="type === 0" :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
-                <image v-else :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                <image v-if="type === 0" :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                <image v-else :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
                 <view class="income-header-menu-text">
                     <view v-if="[5, 6].includes(data.commissionType)" class="income-header-menu-text-nickname">
                         <template v-if="data.commissionType === 5">

+ 618 - 0
src/components/couponList.vue

@@ -0,0 +1,618 @@
+<script lang="ts" setup>
+import { usePagination, useRequest, actionDelegationMiddleware } from 'alova/client'
+import { storeToRefs } from 'pinia'
+import { reactive, ref, watch } from 'vue'
+import { getCategoryList, getNewCouponList } from '@/api/home'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import ExchangeCoupon from '@/components/exchangeCoupon.vue'
+import TemplateCoupon from "@/components/templateCoupon.vue";
+import MescrollUni from '@/components/mescroll.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue'
+import { useAreaStore } from '@/store/area'
+import { getImageUrl } from '@/utils/imageUtil'
+
+import { safeAreaInsets } from '@/utils'
+import { l } from 'node_modules/vite/dist/node/types.d-aGj9QkWt'
+import { useTokenStore } from '@/store/token'
+import { toLoginPage } from '@/utils/toLoginPage'
+
+const tokenStore = useTokenStore()
+const { hasLogin } = storeToRefs(tokenStore)
+definePage({
+    style: {
+        navigationBarTitleText: '优惠券',
+        navigationStyle: 'custom',
+    },
+})
+
+const areaStore = useAreaStore()
+const { currentCity } = storeToRefs(areaStore)
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const activeIndex = ref('0')
+const categoryType = ref('0');
+const couponType = ref('2')
+const searchValue = ref('')
+const showLoading = ref(false)
+const isSearching = ref(false)
+let mescroll = null
+
+
+const { send, data, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getNewCouponList({
+    pageNo: page,
+    pageSize,
+    name: searchValue.value === '' ? '' : searchValue.value,
+    relatedId: categoryType.value,
+    type: couponType.value === '0' ? '' : couponType.value,
+    country: currentCity.value?.value === 'all' ? '' : currentCity.value?.value,
+}), {
+    initialPage: 1,
+    initialPageSize: 7,
+    append: true,
+    preloadNextPage: false,
+    preloadPreviousPage: false,
+    total: response => response?.total || 0,
+    data: response => response?.records || [],
+    immediate: false,
+    middleware: actionDelegationMiddleware('GetCouponList')
+})
+
+const { send: sendCategoryList, data: categoryList, loading: categoryLoading } = useRequest(getCategoryList, {
+    initialData: [
+        {
+        name: '按摩',
+        value: '0',
+        }
+    ],
+    immediate: false,
+})
+
+
+watch(() => data.value, (newVal) => {
+    if (newVal.length === 0 && !loading.value) {
+        mescroll.showEmpty()
+    }
+    else {
+        mescroll.removeEmpty()
+    }
+})
+
+// watch(() => searchValue.value, () => {
+//     // 判断当前是否是搜索状态
+//     if (searchValue.value !== '') {
+//         isSearching.value = true
+//     } else {
+//         isSearching.value = false
+//     }
+// })
+
+// ============ mescroll 配置 ============
+const downOption = reactive({
+    use: true,
+    auto: false,
+    bgColor: '#f8f8fa',
+    textColor: '#666',
+    textInOffset: '下拉刷新',
+    textOutOffset: '释放刷新',
+    textLoading: '刷新中...',
+    offset: 80,
+})
+
+const upOption  = computed(() => ({
+    use: hasLogin.value,
+    auto: false,
+    noMoreSize: 6,
+    isBounce: true,
+    page: {
+        num: page.value,
+        size: pageSize.value,
+    },
+    empty: {
+        use: hasLogin.value,
+        tip: '暂无优惠券',
+        // btnText: '刷新试试',
+        icon: '/static/images/mescroll-empty1.png',
+        fixed: false,
+    },
+    textNoMore: '--- 已经到底了 ---',
+    toTop: {
+        src: '/static/images/mescroll-totop.png',
+        offset: 1000,
+        bottom: 120,
+    },
+    // 开启虚拟列表优化(大数据量时)
+    virtual: {
+        use: false,
+        rowHeight: 300,
+    }
+}))
+
+function mescrollInit(ref) {
+    mescroll = ref
+}
+
+async function downCallback() {
+
+    await refresh()
+}
+
+async function upCallback() {
+    if (isLastPage.value) {
+        console.log(mescroll)
+        mescroll.endSuccess(0, false)
+        return
+    }
+
+    page.value++
+}
+
+onSuccess(() => {
+    let isNextPage = isLastPage.value
+    // 此处处理alova isLastPage默认值为true,首次请求出现的无法加载下一页问题
+    if (pageCount.value > page.value && isLastPage.value) {
+        isNextPage = false
+    }
+    // mescroll.setPageNum(page.value - 1)
+    mescroll.endSuccess(data.value.length, !isNextPage)
+})
+
+onError(() => {
+    console.log('请求失败')
+    mescroll.showEmpty()
+    mescroll.endErr()
+})
+
+onLoad(async () => {
+    await sendCategoryList()
+    await send()
+})
+
+onHide(() => {
+
+    areaStore.resetCurrentCity();
+})
+
+onUnload(() => {
+    areaStore.resetCurrentCity();
+})
+
+// #ifdef MP-WEIXIN
+async function login() {
+    const currentPage = getCurrentPages()[0]
+    const redirectUrl = `/${currentPage.route}`
+    toLoginPage({ queryString: `?redirect=${encodeURIComponent(redirectUrl)}` })
+}
+// #endif
+
+async function tabChange(item) {
+    // 切换tab时,重置上拉加载
+    // mescroll.resetUpScroll(true)
+    // mescroll.showDownScroll()
+    categoryType.value = item.value
+
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+
+async function search() {
+    // if (activeIndex.value !== '0') {
+    //     activeIndex.value = '0'
+    // }
+    // categoryType.value = ''
+    // if (couponType.value !== '0') {
+    //     couponType.value = '0'
+    // }
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    // 判断当前是否是搜索状态
+    if (searchValue.value !== '') {
+        isSearching.value = true
+    } else {
+        isSearching.value = false
+    }
+    await reload()
+    showLoading.value = false
+}
+
+async function clear() {
+    searchValue.value = ''
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    isSearching.value = false
+    showLoading.value = false
+}
+
+function searchChange(value) {
+    searchValue.value = value
+}
+
+function regionSelection() {
+    uni.navigateTo({
+        url: '/pages-A/regionSelection/index',
+    })
+}
+
+async function couponTypeChange(type) {
+    couponType.value = type
+    activeIndex.value = '0'
+    mescroll.endUpScroll(false)
+    mescroll.scrollTo(0, 0)
+    showLoading.value = true
+    await reload()
+    showLoading.value = false
+}
+
+</script>
+
+<template>
+    <view class="invite-container">
+        <view class="invite-content">
+            <!-- <view class="invite-content"
+            :style="{ marginTop: `calc(${topSafeAreaHeight}px + 88rpx)`, height: `calc(100vh - ${topSafeAreaHeight}px - 108rpx)`}"> -->
+            <!-- coupon-tabs -->
+            <view
+                class="coupon-types h-42rpx bg-#ffffff pr-10rpx pl-10rpx flex flex-row flex-nowrap justify-between items-center gap-34rpx">
+                <view :class="{ 'coupon_type_active': couponType === '2' }"
+                    @click="couponTypeChange('2')">
+                    折扣券
+                </view>
+                <view :class="{ 'coupon_type_active': couponType === '3' }"
+                    @click="couponTypeChange('3')">
+                    满减券
+                </view>
+                <view :class="{ 'coupon_type_active': couponType === '1' }"
+                    @click="couponTypeChange('1')">
+                    兑换券
+                </view>
+            </view>
+
+            <!-- search -->
+            <view class="invite-content-search bg-white">
+                <view class="content-search-prefix" @tap="regionSelection">
+                    <up-icon size="12" style="margin-top: 2rpx;" name="map" />
+                    <text>
+                        {{ currentCity.name }}
+                    </text>
+                </view>
+                <up-input style="height: 100%; padding-left: 24rpx; font-size: 24rpx;" v-model="searchValue" border="none"
+                    class="invite-content-search-input" placeholder="请输入券名称" :clearable="true" @confirm="search"
+                    @clear="clear" />
+                <view class="search-button">
+                    <up-button type="text" @tap="search">
+                        搜索
+                    </up-button>
+                </view>
+            </view>
+
+            <!-- category-tabs -->
+            <view class="tabs-container flex">
+            <!-- <view :class="[!isSearching ? 'expanded' : 'collapsed']" class="tabs-container flex"> -->
+                <up-tabs v-model:current="activeIndex" :scrollable="true" class="tabs-content" lineWidth="0" :list="categoryList" 
+                    :active-style="{ color: '#ffffff', backgroundColor: '#FF5365', borderRadius: '999rpx', padding: '0 16rpx', lineHeight: '44rpx', zIndex: 2 }"
+                    :inactive-style="{ backgroundColor: '#F5F5F5', borderRadius: '999rpx', padding: '0 16rpx', lineHeight: '44rpx' }"
+                    item-style="height: 44rpx; padding: 0 20rpx 0 0" style="width: 100%;" @change="tabChange" />
+                <view class="tabs-mask"/>  
+            </view>
+
+            <!-- coupon-list -->
+            <view class="invite-content-list">
+                <!-- :style="{ height: `calc(100vh - ${topSafeAreaHeight}px - 70rpx - 88rpx - 20rpx - 176rpx)` }"> -->
+                <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false" offset="70"
+                    @init="mescrollInit" @down="downCallback" @up="upCallback" @emptyclick="reload">
+                    <template v-if="isSearching">
+                        <view v-for="(item, index) in data" :key="index">
+                            <!-- <view v-if="Array.isArray(item.data)" class="text-34rpx text-#333333 border">{{ item.name }}
+                            </view> -->
+                            <view v-for="coupon in item.data" :key="item.templateId" class="invite-content-item">
+                                <!-- <invite-item :data="item" /> -->
+                                <template v-if="coupon.type === '1'">
+                                    <ExchangeCoupon :coupon="coupon" />
+                                </template>
+                                <template v-else>
+                                    <TemplateCoupon :coupon="coupon" />
+                                </template>
+                            </view>
+                        </view>
+                    </template>
+                    <template v-else>
+                        <view v-for="item in data" :key="item.templateId" class="invite-content-item">
+                            <!-- <invite-item :data="item" /> -->
+                            <!-- 优惠券类型:1-兑换券,2-兑换券,3-满减券 -->
+                            <template v-if="item.type == '1'">
+                                <ExchangeCoupon :coupon="item" />
+                            </template>
+                            <template v-else>
+                                <TemplateCoupon :coupon="item" />
+                            </template>
+                        </view>
+                    </template>
+                    <!-- 滚动区域内的loading遮罩 -->
+                    <view v-if="showLoading && loading" class="loading-mask">
+                        <view class="loading-spinner" />
+                        <text class="loading-text">加载中...</text>
+                    </view>
+                    <view v-if="!hasLogin" class="empty-container">
+                        <view class="custom-empty"
+                        :style="{
+                            backgroundImage: `url(${getImageUrl('@img/index/login_bg.png')})`,
+                            backgroundSize: 'cover',
+                            backgroundPosition: 'center'
+                        }"/>
+                        <view class="login-btn" @click="login">去登录</view>
+                    </view>
+                </mescroll-uni>
+            </view>
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.invite-container {
+    // height: 100vh;
+    padding: 0 20rpx 20rpx;
+    margin-bottom: 20rpx;
+    background: linear-gradient( 180deg, #FF8180 0%, rgba(255,127,92,0.49) 30%);
+    border-radius: 0 0 32rpx 32rpx;
+
+    .invite-content {
+        // max-height: 1130rpx;
+        display: flex;
+        flex-direction: column;
+        border-radius: 32rpx 32rpx 32rpx 32rpx;
+        background-color: #ffffff;
+        padding: 24rpx 20rpx 20rpx;
+
+       .coupon-types {
+            view {
+                width: 94rpx;
+                line-height: 42rpx;
+                text-align: center;
+                font-weight: 400;
+                font-size: 30rpx;
+                color: rgba(0,0,0,0.5);
+                position: relative;
+            }
+
+            .coupon_type_active {
+                font-weight: 600 !important;
+                font-size: 30rpx;
+                color: #333333;
+                z-index: 1;
+
+                // 添加伪元素作为下划线
+                &:after {
+                    content: '';
+                    position: absolute;
+                    bottom: 6rpx;
+                    left: 0;
+                    width: 100%;
+                    height: 10rpx;
+                    background-color: #FF5666;
+                    z-index: -1;
+                }
+            }
+        }
+
+        .invite-content-search {
+            padding: 0 8rpx 0 24rpx;
+            margin: 31rpx 0 24rpx;
+            display: flex;
+            flex-direction: row;
+            flex-wrap: nowrap;
+            align-items: center;
+            height: 64rpx;
+            background: #F0F0F0;
+            border-radius: 999rpx;
+
+            .content-search-prefix {
+                min-width: 127rpx;
+                // border-top-left-radius: 10rpx;
+                // border-bottom-left-radius: 10rpx;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                position: relative;
+
+                text {
+                    font-weight: 400;
+                    font-size: 26rpx;
+                    color: #333333;
+                    text-overflow: ellipsis;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    max-width: 200rpx;
+                    margin: 0 23rpx 0 1rpx;
+                }
+
+                &:after {
+                    content: '';
+                    position: absolute;
+                    right: 0;
+                    top: 50%;
+                    transform: translateY(-50%);
+                    width: 1rpx;
+                    height: 20rpx;
+                    background: #333333;
+                }
+            }
+
+            .invite-content-search-input {
+                height: 60rpx;
+                border-radius: 0;
+            }
+
+            .search-button {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100rpx;
+                height: 44rpx;
+                background: #FF5365;
+                border-radius: 999rpx 999rpx 999rpx 999rpx;
+                font-weight: 400;
+                font-size: 26rpx;
+                color: #FFFFFF;
+            }
+        }
+
+        .tabs-container {
+            height: 46rpx;
+            display: flex;
+            flex-direction: row;
+            justify-content: start;
+            align-items: center;
+            position: relative;
+            margin-bottom: 20rpx;
+            overflow-x: auto; /* 添加横向滚动 */
+            white-space: nowrap; /* 防止标签换行 */
+
+            .tabs-content {
+                line-height: 46rpx;
+                font-weight: 400;
+                font-size: 26rpx;
+                color: #333333;
+            }
+
+            .tabs-mask {
+                position: absolute; 
+                top: 0;
+                right: 0;
+                z-index: 1;
+                width: 159rpx;
+                height: 44rpx;
+                background:  linear-gradient(135deg,rgba(255, 85, 101, 0) 0%, #FFFFFF 100%, );
+                pointer-events: none;
+            }
+
+        }
+
+        /* 收起状态 */
+        .tabs-container.collapsed {
+            opacity: 0;
+            max-height: 0;
+        }
+
+        /* 展开状态 */
+        .tabs-container.expanded {
+            opacity: 1;
+            max-height: 48rpx;
+        }
+
+        .invite-content-list {
+            display: flex;
+            flex-direction: column;
+            position: relative;
+
+            #mescrollContainer {
+                height: 100%;
+            }
+
+            .invite-content-item {
+                margin-bottom: 20rpx;
+            }
+
+            // loading遮罩样式
+            .loading-mask {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(255, 255, 255, 0.9);
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                z-index: 999;
+            }
+
+            .loading-spinner {
+                width: 40rpx;
+                height: 40rpx;
+                border: 4rpx solid #e0e0e0;
+                border-top-color: #ed6b66;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
+            }
+
+            .loading-text {
+                margin-top: 16rpx;
+                font-size: 24rpx;
+                color: #666;
+            }
+
+            @keyframes spin {
+                to {
+                    transform: rotate(360deg);
+                }
+            }
+        }
+    }
+
+    .accordion-enter-active,
+    .accordion-leave-active {
+        transition: all 0.4s ease;
+        overflow: hidden;
+    }
+
+    .accordion-enter-from,
+    .accordion-leave-to {
+        max-height: 0;
+        opacity: 0;
+        transform: translateY(-10rpx);
+    }
+
+    .accordion-enter-to,
+    .accordion-leave-from {
+        max-height: 88rpx;
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+/* 自定义 up-empty 图标大小 */
+.empty-container {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 13rpx;
+    margin: 20rpx 0 35rpx;
+
+    .custom-empty {
+        width: 160rpx;
+        height: 140rpx;
+    } 
+
+    .login-btn {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 102rpx;
+        height: 44rpx;
+        background: #FF5365;
+        border-radius: 999rpx;
+        font-weight: 400;
+        font-size: 26rpx;
+        color: #FFFFFF;
+    }
+}
+
+
+::v-deep {
+    .uni-input-placeholder  {
+        font-weight: 400;
+        font-size: 24rpx !important;
+        color: #808080;
+    }
+    .mescroll-empty {
+        padding-top: 150rpx;
+    }
+}
+</style>

+ 331 - 0
src/components/exchangeCoupon.vue

@@ -0,0 +1,331 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import ShortLinkModal from '@/components/ShortLinkModal.vue'
+import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
+import { getImageUrl } from '@/utils/imageUtil'
+import { useCouponShare } from '../hooks/useCouponShare'
+import { getPlatformSource } from '@/utils/platformSource'
+
+const props = defineProps({
+    coupon: {
+        type: Object,
+        default: () => ({
+            ruleDiscountRate: '0',
+            ruleMinSpendAmount: '0',
+            relatedName: '',
+            ruleDiscountCapAmount: '0',
+            validityType: '1',
+        }),
+    },
+    scrollViewId: {
+        type: String,
+        default: '',
+    },
+    enableCustomShare: {
+        type: Boolean,
+        default: false,
+    }
+})
+const emit = defineEmits(['shareClick'])
+const coupon = computed(() => props.coupon)
+const couponTemplateId = computed(() => coupon.value.templateId || '')
+const deadline = computed(() => {
+    const type = props.coupon.validityType
+    if (type === '2') {
+        return `领取后${props.coupon.validDays}天过期`
+        // return `自领取之日起${props.coupon.validDays}日内使用`
+    }
+    else if (type === '1') {
+        return `${props.coupon.validEndTime}`
+        // return `限${props.coupon.validStartTime}到${props.coupon.validEndTime}日内使用`
+    }
+    return '长期有效'
+})
+const category = computed(() => {
+    const relatedType = coupon.value.relatedType
+    if (coupon.value?.relatedName && coupon.value?.relatedName !== 'null') {
+        let relatedName = coupon.value.relatedName
+        if (relatedType === '2') {
+            relatedName = getLastPartAfterSlash(coupon.value.relatedName)
+            return `仅限${relatedName}分类商品使用`
+        }
+        else {
+            return `仅限${relatedName}商品使用`
+        }
+    }
+    return '不限商品分类'
+})
+
+// 分享弹窗相关 - 使用v-model控制弹窗显示
+const showShareModal = ref(false)
+const couponShareLink = ref('')
+const isExpand = ref(false);
+
+const { shareCoupon, ...shareHooks } = useCouponShare()
+
+// 调用分享弹窗
+async function handleShareCoupon() {
+    // 确保有优惠券数据
+    if (coupon.value.templateId) {
+        // uni.showLoading({
+        //     title: '获取分享链接中...',
+        //     mask: true,
+        // })
+        try {
+            const shareLink = await shareCoupon(couponTemplateId.value)
+            if (shareLink) {
+                couponShareLink.value = shareLink
+                showShareModal.value = true
+                if (props.enableCustomShare) {
+                    emit('shareClick', couponShareLink.value, shareHooks)
+                }
+            }
+        }
+        finally {
+            // uni.hideLoading()
+        }
+    }
+}
+
+// 关闭分享弹窗
+function handleCloseShareModal() {
+    console.log('分享弹窗已关闭')
+}
+</script>
+
+<template>
+    <view class="discount-coupon">
+        <image class="coupon-bg" :src="getImageUrl('@img/index/exchange-coupon-bg.png')" mode="aspectFill" />
+        <view class="discount-coupon-content">
+            <view class="discount-coupon-info-container">
+                <view class="discount-coupon-info-content">
+                    <view class="discount-coupon-info-desc">
+                        <view class="desc-text">
+                            {{ coupon.name }}
+                        </view>
+                        <view class="desc-type" @tap="isExpand = !isExpand">
+                            有效期限:{{ deadline }}
+                        </view>
+                        <view class="desc-more" @tap="isExpand = !isExpand">
+                            {{isExpand ? '收起详情' : '展开详情 '}} 
+                            <up-icon v-if="isExpand" size="10" color="#FF4129" name="arrow-up" />
+                            <up-icon size="10" color="#FF4129" v-else name="arrow-down" />
+                        </view>
+                    </view>
+                </view>
+            </view>
+            <view class="discount-btn-container">
+                <view class="share-text">分享赚 ¥{{ coupon?.ruleReductionAmount }}</view>
+                <view class="btn-bg">
+                    <button class="discount-btn" @click="handleShareCoupon">去分享</button>
+                </view>
+            </view>
+        </view>
+        <!-- <up-transition :show="isExpand" mode="fade-down" duration="300"> -->
+        <view :class="['coupon-info', isExpand ? 'expanded' : 'collapsed']">
+            <view class="info-content">
+                <view class="info-item">
+                    <view class="item-title">券类型:</view>
+                    <view class="item-content">兑换券</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定地区:</view>
+                    <view class="item-content">
+                        {{ coupon?.locality && coupon?.locality !== 'null' ?
+                            `仅可在${coupon?.locality}地区的商户使用` : '不限地区' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商户:</view>
+                    <view class="item-content">
+                        {{ coupon?.storeName && coupon?.storeName !== 'null' ?
+                            `仅限${coupon?.storeName}商户使用` : '不限商户' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商品:</view>
+                    <view class="item-content">{{ category }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定平台:</view>
+                    <view class="item-content">仅限{{ getPlatformSource(coupon?.platformSource) }}平台使用</view>
+                </view>
+            </view>
+        </view>
+        <!-- </up-transition> -->
+        <template v-if="!enableCustomShare">
+            <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId" :share-hooks="shareHooks"
+                :share-link="couponShareLink" :scroll-view-id="scrollViewId" @close="handleCloseShareModal" />
+        </template>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.discount-coupon {
+    width: 100%;
+    position: relative;
+    font-weight: 400;
+
+    .coupon-bg {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 170rpx;
+        object-fit: cover;
+        z-index: 1;
+        border-radius: 10rpx;
+    }
+
+    .discount-coupon-content {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        height: 170rpx;
+
+        .discount-coupon-info-container {
+            width: 73.47%;
+            display: flex;
+            flex-direction: column;
+            z-index: 1;
+
+            .discount-coupon-info-content {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                padding-left: 30rpx;
+
+                .discount-coupon-info-desc {
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    gap: 10rpx;
+                    padding-left: 4rpx;
+
+                    .desc-text {   
+                        max-width: 290rpx;
+                        overflow: hidden;
+                        text-overflow: ellipsis;
+                        white-space: nowrap;
+                        flex-wrap: nowrap;
+                        font-weight: 600;
+                        font-size: 24rpx;
+                        color: #000000;
+                        line-height: 34rpx;
+                    }
+
+                    .desc-type {
+                        font-size: 20rpx;
+                        color: #808080;
+                        line-height: 28rpx;
+                        display: flex;
+                        flex-wrap: nowrap;
+                    }
+
+                    .desc-more {
+                        font-size: 20rpx;
+                        color: #FF4129;
+                        display: flex;
+                        flex-wrap: nowrap;
+                        gap: 4rpx;
+                    }
+                }
+            }
+        }
+
+        .discount-btn-container {
+            height: 100%;
+            width: 26.53%;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+
+            .share-text {
+                font-weight: 500;
+                font-size: 22rpx;
+                color: #FFFFFF;
+                z-index: 2;
+                margin-bottom: 7rpx;
+            }
+
+            .btn-bg {
+                z-index: 2;
+
+                .discount-btn {
+                    display: flex;
+                    height: 48rpx;
+                    padding: 7rpx 24rpx;
+                    line-height: 34rpx;
+                    background: #FFFFFF;
+                    border-radius: 999rpx 999rpx 999rpx 999rpx;
+                    font-weight: 600;
+                    font-size: 24rpx;
+                    color: #FF5365;
+                    letter-spacing: 1rpx;
+                }
+            }
+        }
+    }
+
+    .coupon-info {
+        margin-top: -4rpx;
+        padding: 16rpx 26rpx 12rpx;
+        background: rgba(255,140,58,0.15);
+        border-radius: 0 0 32rpx 32rpx;
+        overflow: hidden;
+        transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+        box-shadow: 1rpx 1rpx 10rpx #fff9f4;
+
+        .info-content {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-start;
+            gap: 5rpx;
+
+            .info-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: top;
+                justify-content: flex-start;
+                gap: 10rpx;
+                font-size: 18rpx;
+                color: #808080;
+
+                .item-title {
+                    width:80rpx;
+                }
+
+                .item-content {
+                    flex: 1;
+                }
+            }
+        }
+    }
+
+    /* 收起状态 */
+    .coupon-info.collapsed {
+        opacity: 0;
+        max-height: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+        border-top-width: 0;
+        border-bottom-width: 0;
+    }
+
+    /* 展开状态 */
+    .coupon-info.expanded {
+        opacity: 1;
+        max-height: 500rpx;
+        /* 设置一个足够大的值以容纳所有内容 */
+        padding-top: 20rpx;
+        padding-bottom: 20rpx;
+        border-top-width: 11rpx;
+        border-bottom-width: 11rpx;
+    }
+}
+</style>

+ 371 - 0
src/components/templateCoupon.vue

@@ -0,0 +1,371 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import ShortLinkModal from '@/components/ShortLinkModal.vue'
+import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
+import { getImageUrl } from '@/utils/imageUtil'
+import { useCouponShare } from '../hooks/useCouponShare'
+import { getPlatformSource } from '@/utils/platformSource'
+
+const props = defineProps({
+    coupon: {
+        type: Object,
+        default: () => ({
+            ruleDiscountRate: '0',
+            ruleMinSpendAmount: '0',
+            relatedName: '',
+            ruleDiscountCapAmount: '0',
+            validityType: '1',
+        }),
+    },
+    scrollViewId: {
+        type: String,
+        default: '',
+    },
+    enableCustomShare: {
+        type: Boolean,
+        default: false,
+    }
+})
+const emit = defineEmits(['shareClick'])
+const coupon = computed(() => props.coupon)
+const couponTemplateId = computed(() => coupon.value.templateId || '')
+const deadline = computed(() => {
+    const type = props.coupon.validityType
+    if (type === '2') {
+        return `领取后${props.coupon.validDays}天过期`
+        // return `自领取之日起${props.coupon.validDays}日内使用`
+    }
+    else if (type === '1') {
+        return `${props.coupon.validEndTime}`
+        // return `限${props.coupon.validStartTime}到${props.coupon.validEndTime}日内使用`
+    }
+    return '长期有效'
+})
+const category = computed(() => {
+    const relatedType = coupon.value.relatedType
+    if (coupon.value?.relatedName && coupon.value?.relatedName !== 'null') {
+        let relatedName = coupon.value.relatedName
+        if (relatedType === '2') {
+            relatedName = getLastPartAfterSlash(coupon.value.relatedName)
+            return `仅限${relatedName}分类商品使用`
+        }
+        else {
+            return `仅限${relatedName}商品使用`
+        }
+    }
+    return '不限商品分类'
+})
+
+// 分享弹窗相关 - 使用v-model控制弹窗显示
+const showShareModal = ref(false)
+const couponShareLink = ref('')
+const isExpand = ref(false);
+
+const { shareCoupon, ...shareHooks } = useCouponShare()
+
+// 调用分享弹窗
+async function handleShareCoupon() {
+    // 确保有优惠券数据
+    if (coupon.value.templateId) {
+        // uni.showLoading({
+        //     title: '获取分享链接中...',
+        //     mask: true,
+        // })
+        try {
+            const shareLink = await shareCoupon(couponTemplateId.value)
+            console.log(couponTemplateId.value, '分享链接========');
+            
+            if (shareLink) {
+                couponShareLink.value = shareLink
+                showShareModal.value = true
+                if (props.enableCustomShare) {
+                    emit('shareClick', couponShareLink.value, shareHooks)
+                }
+            }
+        }
+        finally {
+            // uni.hideLoading()
+        }
+    }
+}
+
+// 关闭分享弹窗
+function handleCloseShareModal() {
+    console.log('分享弹窗已关闭')
+}
+</script>
+
+<template>
+    <view class="discount-coupon">
+        <image class="coupon-bg" :src="getImageUrl('@img/index/new-coupon-bg.png')" mode="aspectFill" />
+        <view class="discount-coupon-content">
+            <view class="discount-coupon-info-container">
+                <view class="discount-coupon-info-content">
+                    <view class="discount-coupon-info-price">
+                        <text class="discount-coupon-info-number">
+                            {{ coupon.type === '2' ? coupon?.ruleDiscountRate : coupon?.ruleReductionAmount }}
+                        </text>
+                        <view class="discount-coupon-info-unit">
+                            {{ coupon.type === '2' ? '折' : '元' }}
+                        </view>
+                    </view>
+                    <view class="discount-coupon-info-desc">
+                        <view class="desc-text">
+                            {{ coupon.name }}
+                        </view>
+                        <view class="desc-type" @tap="isExpand = !isExpand">
+                            <text v-if="coupon.type === '2'">最高优惠{{ coupon.ruleDiscountCapAmount }}元,满{{ coupon.ruleMinSpendAmount }}元可用</text>
+                            <text v-else>满{{ coupon.ruleMinSpendAmount }}元可用</text>
+                        </view>
+                        <view class="desc-type" @tap="isExpand = !isExpand">
+                            有效期限:{{ deadline }}
+                        </view>
+                        <view class="desc-more" @tap="isExpand = !isExpand">
+                            {{isExpand ? '收起详情' : '展开详情 '}} 
+                            <up-icon v-if="isExpand" size="10" color="#FF4129" name="arrow-up" />
+                            <up-icon size="10" color="#FF4129" v-else name="arrow-down" />
+                        </view>
+                    </view>
+                </view>
+            </view>
+            <view class="discount-btn-container">
+                <view class="share-text">分享赚 ¥{{ coupon.pushCouponAverageAmount }}</view>
+                <view class="btn-bg">
+                    <button class="discount-btn" @click="handleShareCoupon">去分享</button>
+                </view>
+            </view>
+        </view>
+        <!-- <up-transition :show="isExpand" mode="fade-down" duration="300"> -->
+        <view :class="['coupon-info', isExpand ? 'expanded' : 'collapsed']">
+            <view class="info-content">
+                <view class="info-item">
+                    <view class="item-title">券类型:</view>
+                    <view class="item-content">{{ coupon.type === '2' ? '折扣券' : '满减券' }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定地区:</view>
+                    <view class="item-content">
+                        {{ coupon?.locality && coupon?.locality !== 'null' ?
+                            `仅可在${coupon?.locality}地区的商户使用` : '不限地区' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商户:</view>
+                    <view class="item-content">
+                        {{ coupon?.storeName && coupon?.storeName !== 'null' ?
+                            `仅限${coupon?.storeName}商户使用` : '不限商户' }}
+                    </view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定商品:</view>
+                    <view class="item-content">{{ category }}</view>
+                </view>
+                <view class="info-item">
+                    <view class="item-title">限定平台:</view>
+                    <view class="item-content">仅限{{ getPlatformSource(coupon?.platformSource) }}平台使用</view>
+                </view>
+            </view>
+        </view>
+        <!-- </up-transition> -->
+        <template v-if="!enableCustomShare">
+            <ShortLinkModal v-model:show-modal="showShareModal" :coupon-id="couponTemplateId" :share-hooks="shareHooks"
+                :share-link="couponShareLink" :scroll-view-id="scrollViewId" @close="handleCloseShareModal" />
+        </template>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.discount-coupon {
+    width: 100%;
+    // max-height: 800rpx;
+    position: relative;
+    font-weight: 400;
+
+    .coupon-bg {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 170rpx;
+        object-fit: cover;
+        z-index: 1;
+        border-radius: 10rpx;
+    }
+
+    .discount-coupon-content {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        height: 170rpx;
+
+        .discount-coupon-info-container {
+            width: 73.47%;
+            display: flex;
+            flex-direction: column;
+            z-index: 1;
+
+            .discount-coupon-info-content {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+
+                .discount-coupon-info-price {
+                    width: 125rpx;
+                    height: 100%;
+                    display: flex;
+                    flex-direction: row;
+                    flex-wrap: nowrap;
+                    align-items: center;
+                    justify-content: center;
+                    gap: 5rpx;
+                    font-family: D-DIN Exp-DINExp, D-DIN Exp-DINExp;
+
+                    .discount-coupon-info-number {
+                        font-weight: bold;
+                        font-size: 60rpx;
+                        color: #FF5365;
+                        letter-spacing: -4rpx;
+                    }
+
+                    .discount-coupon-info-unit {
+                        font-weight: 500;
+                        font-size: 26rpx;
+                        color: #FF5365;
+                        padding-top: 18rpx;
+                    }
+                }
+
+                .discount-coupon-info-desc {
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    gap: 4rpx;
+                    padding-left: 4rpx;
+
+                    .desc-text {   
+                        max-width: 290rpx;
+                        overflow: hidden;
+                        text-overflow: ellipsis;
+                        white-space: nowrap;
+                        flex-wrap: nowrap;
+                        font-weight: 600;
+                        font-size: 24rpx;
+                        color: #000000;
+                        line-height: 34rpx;
+                    }
+
+                    .desc-type {
+                        font-size: 20rpx;
+                        color: #808080;
+                        line-height: 28rpx;
+                        display: flex;
+                        flex-wrap: nowrap;
+                    }
+
+                    .desc-more {
+                        font-size: 20rpx;
+                        color: #FF4129;
+                        display: flex;
+                        flex-wrap: nowrap;
+                        gap: 4rpx;
+                    }
+                }
+            }
+        }
+
+        .discount-btn-container {
+            height: 100%;
+            width: 26.53%;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+
+            .share-text {
+                font-weight: 500;
+                font-size: 22rpx;
+                color: #FFFFFF;
+                z-index: 2;
+                margin-bottom: 7rpx;
+            }
+
+            .btn-bg {
+                z-index: 2;
+
+                .discount-btn {
+                    display: flex;
+                    height: 48rpx;
+                    padding: 7rpx 24rpx;
+                    line-height: 34rpx;
+                    background: #FFFFFF;
+                    border-radius: 999rpx 999rpx 999rpx 999rpx;
+                    font-weight: 600;
+                    font-size: 24rpx;
+                    color: #FF5365;
+                    letter-spacing: 1rpx;
+                }
+            }
+        }
+    }
+
+    .coupon-info {
+        margin-top: -4rpx;
+        padding: 16rpx 26rpx 12rpx;
+        background: rgba(255,140,58,0.15);
+        border-radius: 0 0 32rpx 32rpx;
+        overflow: hidden;
+        transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+        box-shadow: 1rpx 1rpx 10rpx #fff9f4;
+
+        .info-content {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-start;
+            gap: 5rpx;
+
+            .info-item {
+                display: flex;
+                flex-direction: row;
+                flex-wrap: nowrap;
+                align-items: top;
+                justify-content: flex-start;
+                gap: 10rpx;
+                font-size: 18rpx;
+                color: #808080;
+
+                .item-title {
+                    width:80rpx;
+                }
+
+                .item-content {
+                    flex: 1;
+                }
+            }
+        }
+    }
+
+    /* 收起状态 */
+    .coupon-info.collapsed {
+        opacity: 0;
+        max-height: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+        border-top-width: 0;
+        border-bottom-width: 0;
+    }
+
+    /* 展开状态 */
+    .coupon-info.expanded {
+        opacity: 1;
+        max-height: 500rpx;
+        /* 设置一个足够大的值以容纳所有内容 */
+        padding-top: 20rpx;
+        padding-bottom: 20rpx;
+        border-top-width: 11rpx;
+        border-bottom-width: 11rpx;
+    }
+}
+</style>

+ 1 - 1
src/hooks/useCouponShare.ts

@@ -70,7 +70,7 @@ export function useCouponShare() {
             //     return shareLink.value
             // }
             uni.navigateTo({
-                url: `/pages-A/sharePage/index?couponId=${couponId}`,
+                url: `/pages-A/shareCoupon/index?couponId=${couponId}`,
             })
             return null
         }

+ 1 - 1
src/http/alova.ts

@@ -141,7 +141,7 @@ const alovaInstance = createAlova({
                                     toLoginPage({ mode: 'reLaunch' })
                                 }
                                 else if (cancel) {
-                                    uni.reLaunch({ url: '/pages/home/home' })
+                                    uni.reLaunch({ url: '/pages/home/index' })
                                 }
                             },
                             fail: () => {

+ 7 - 11
src/pages-A/incomeShareInfo/index.vue

@@ -7,6 +7,7 @@ import { getImageUrl } from '@/utils/imageUtil'
 import MescrollUni from '@/components/mescroll.vue'
 import { safeAreaInsets } from '@/utils'
 import { getPlatformSource } from '@/utils/platformSource'
+import { log } from 'console'
 
 interface IncomeShareInfo {
     batchId: bigint
@@ -21,13 +22,6 @@ definePage({
     },
 })
 
-const props = defineProps({
-    type: {
-        type: Number,
-        default: 0,
-    },
-})
-
 const isShow = ref(false)
 const titleText = ref('分享')
 const currentInfo = ref<IncomeShareInfo>()
@@ -84,11 +78,13 @@ defineExpose({
     }
 })
 
+const status = ref('')
 onLoad((options) => {
-    const { batchId, commissionType } = options
+    const { batchId, commissionType, type } = options
     if (batchId && commissionType) {
         currentInfo.value = { batchId: batchId, commissionType: Number(commissionType) }
         getShareInfo({ batchId: batchId, commissionType: Number(commissionType) })
+        status.value = type
     }
 })
 
@@ -200,10 +196,10 @@ function handleBackClick() {
                             <template v-for="(info, indez) in item.data" :key="indez">
                                 <up-cell :title="info.name" :label="getPlatformSource(info.platformSource)">
                                     <template #icon>
-                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-if="type === 0"
-                                            :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
-                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-else
+                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-if="status == '0'"
                                             :src="getImageUrl('@img/income/djs.png')" mode="aspectFit" />
+                                        <image class="w-[60rpx] h-[60rpx] mr-[10rpx]" v-else
+                                            :src="getImageUrl('@img/income/hb.png')" mode="aspectFit" />
                                     </template>
                                     <template #value>
                                         <view

+ 123 - 0
src/pages-A/inviteUsageRules/index.vue

@@ -0,0 +1,123 @@
+<template>
+    <view class="invite-container">
+        <CustomNavigationBar :show-back="true" title="使用规则" />
+        <view class="invite-content" :style="{ paddingTop: `calc(${topSafeAreaHeight}px + 90rpx)` }">
+            <view class="invite-card" 
+            :style="{
+                backgroundImage: `url(${getImageUrl('@img/invite/invite_card_bg.png')})`,
+                backgroundSize: 'cover',
+                backgroundPosition: 'center'
+            }"/>
+            <view class="invite-desc">
+                <view 
+                class="invite-desc-step"
+                :style="{
+                backgroundImage: `url(${getImageUrl('@img/invite/step_bg.png')})`,
+                backgroundSize: 'cover',
+                backgroundPosition: 'center'
+                }">
+                </view>
+                <view class="invite-line"></view>
+                <view class="invite-desc-title">
+                    使用规则
+                </view>
+                <view class="invite-desc-content">
+                    <view>1、分享优惠券给好友,好友首次登录<text class="desc-active">【本地生活】</text>或<text class="desc-active">【广誉源】</text>,你即可赚取相对应佣金;</view>
+                    <view>2、邀请新用户满<text class="desc-active">{{inviteData?.number || 0}}人</text> ( 本地生活和广誉源渠道合计 ), 可获得<text class="desc-active">{{inviteData?.money || 0}}元</text>现金奖励;</view>
+                    <view>3、邀请人数达标后,继续拉新仍<text class="desc-active">可获得持续佣金</text>,邀 请越多,奖励越多,上不封顶。</view>
+                </view>
+                <view class="invite-logo" 
+                :style="{
+                    backgroundImage: `url(${getImageUrl('@img/invite/company_logo.png')})`,
+                    backgroundSize: 'cover',
+                    backgroundPosition: 'center'
+                }"/>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { safeAreaInsets } from '@/utils'
+import { getImageUrl } from '@/utils/imageUtil'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const inviteData = ref({})
+
+// 生命周期
+onLoad(async (options) => {
+    inviteData.value = options
+})
+
+</script>
+
+<style lang="scss" scoped>
+/* 页面容器 */
+.invite-container { 
+    display: flex;
+    flex-direction: column;
+    width: 100vw;
+    min-height: 100vh;
+    background: #F5F5F5;
+
+    .invite-content {
+        width: 100%;
+        
+        .invite-card {
+            height: 310rpx;
+        }
+    }
+
+    .invite-desc {
+        min-height: 1028rpx;
+        background: #FFFFFF;
+        border-radius: 32rpx 32rpx 32rpx 32rpx;
+        padding: 38rpx;
+        margin: -38rpx 32rpx 59rpx;
+        z-index: 2;
+        position: relative;
+
+        .invite-desc-step {
+            height: 126rpx;
+        }
+
+        .invite-line {
+            height: 2rpx;
+            background: #EAEAEA;
+            margin: 32rpx 0;
+        }
+
+        .invite-desc-title {
+            font-size: 30rpx;
+            font-weight: 600;
+            color: #333333;
+            line-height: 42rpx;
+            margin-bottom: 16rpx;
+        }
+
+        .invite-desc-content {
+            display: flex;
+            flex-direction: column;
+            gap: 25rpx;
+            font-weight: 400;
+            font-size: 26rpx;
+            color: #6E6E6E;
+            line-height: 35rpx;
+
+            .desc-active {
+                color: #FF5365;
+            }
+        }
+
+        .invite-logo {
+            position: absolute;
+            left: 50%;
+            transform: translateX(-50%);
+            bottom: 38rpx;
+            width: 284rpx;
+            height: 117rpx;
+        }
+    }
+}
+</style>

+ 1431 - 0
src/pages-A/shareCoupon/index.vue

@@ -0,0 +1,1431 @@
+<template>
+    <view class="share-container">
+        <!-- 顶部导航栏(模拟美团风格) -->
+        <!-- <up-status-bar /> -->
+        <CustomNavigationBar :show-back="true" title="优惠券分享" />
+
+        <!-- 分享图片预览区域 -->
+        <view class="preview-area" :style="{ paddingTop: `calc(${topSafeAreaHeight}px + 90rpx)`}">
+            <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>
+
+            <!-- 广誉源H5链接-二维码 -->
+            <view v-if="couponDetail.platformSource == '2'" class="qc-code-img">
+                <uqrcode ref="uqrcodeRef" :hide="true" canvas-id="qrcode" size="240" sizeUnit="rpx"
+                    :value="`https://city.baoxianzhanggui.com/fragrance/#/pages/login/wxLogin?shareType='COUPON'&shareContentId=${shareId}`"
+                    h5DownloadName="领取优惠券" :h5SaveIsDownload="true" logo="@/static/images/gyy_logo.png" :logoSize="60" :logoBorderRadius="8" logoBorderColor="#FFFFFF" :logoBorderWidth="2" />
+                    <!-- h5DownloadName="领取优惠券" :h5SaveIsDownload="true" /> -->
+
+                <!-- <image mode="scaleToFill" :src="qcCodeImg" /> -->
+            </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>
+
+        <!-- 加载提示 -->
+        <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 { storeToRefs } from 'pinia'
+import { nextTick, ref } from 'vue'
+import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
+import { getCouponDetail } from '@/api/home'
+import { getWxQcCode, markSuccess } from '@/api/receiveCoupon'
+import { useInviteCodeStore } from '@/store/inviteQCCode'
+import { parseUrlParams, safeAreaInsets, isPcWeixinMp } from '@/utils'
+import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
+import { getImageUrl } from '@/utils/imageUtil'
+import Uqrcode from '@/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue'
+
+definePage({
+    style: {
+        navigationBarTitleText: '',
+        navigationStyle: 'custom',
+    }
+    // enableShareAppMessage: true,
+    // enableShareTimeline: true,
+})
+
+// 响应式数据
+const uqrcodeRef = ref(null)
+
+const loading = ref(false)
+const shareImageUrl = ref('')
+const canvasWidth = ref(750)
+const canvasHeight = ref(1200)
+const topSafeAreaHeight = safeAreaInsets?.top || 0
+const inviteCodeStore = useInviteCodeStore()
+const { inviteCode } = storeToRefs(inviteCodeStore)
+
+// import { c } from 'node_modules/vite/dist/node/types.d-aGj9QkWt'
+
+// 系统信息
+const systemInfo = ref({})
+
+const shareId = ref('')
+const shareRecordId = 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 })
+    }
+
+    // 页面加载后生成分享图片
+    await nextTick()
+    await generateShareImage()
+})
+
+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: 290,
+    })
+    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)
+        shareRecordId.value = params[inviteCode.value.shareKey]
+        console.log('小程序码imageUrl:', imageUrl);
+        
+        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 gyyShareImage() { 
+    return new Promise((resole, reject) => {
+        uqrcodeRef.value.toTempFilePath({
+            success: ({ tempFilePath }) => {
+                resole(tempFilePath);
+            },
+            fail: (err) => {
+                reject(err);
+            }
+        });
+    })
+}
+
+
+// 生成分享图片-本地生活
+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 {
+            if (couponDetail.value.platformSource === '1') {
+                qrCodeImg = await getWxacodeBase64()
+            } else {
+                // 广誉源-h5链接-二维码
+                qrCodeImg = await gyyShareImage()
+            }
+        }
+        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. 加载并绘制远程背景图片
+            let backgroundImgName = couponDetail.value.platformSource === '1' ? 'local_share_poster' : 'gyy_share_poster'
+            const backgroundImgUrl = getImageUrl(`@img/share/${backgroundImgName}.png`)
+            const backgroundImg = await loadRemoteImage(canvas, backgroundImgUrl)
+            ctx.drawImage(backgroundImg, 0, 0, width, height)
+
+            // 2. 绘制标题
+            drawTitle(ctx, width, height, scale)
+
+            // 3. 绘制优惠券金额
+            drawCouponAmount(ctx, width, height, scale)
+
+            // 4. 绘制优惠券描述
+            drawCouponDescription(ctx, width, height, scale)
+
+            // 5. 绘制二维码区域
+            if (couponDetail.value.platformSource === '2') {
+                await drawGyyQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale)
+            } else {
+                await drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale)
+            }
+
+            // 6. 绘制底部信息
+            // 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)
+        }
+    })
+}
+
+// 加载远程图片的函数
+async function loadRemoteImage(canvas, imageUrl) {
+    return new Promise((resolve, reject) => {
+        const image = canvas.createImage()
+        const timeout = setTimeout(() => {
+            reject(new Error('图片加载超时'))
+        }, 10000) // 10秒超时
+
+        uni.getImageInfo({
+            src: imageUrl,
+            success: (res) => {
+                image.onload = () => {
+                    clearTimeout(timeout)
+                    const imageWidth = image.naturalWidth || image.width || res.width
+                    const imageHeight = image.naturalHeight || image.height || res.height
+
+                    if ((isPcWeixinMp() || image.complete) && imageWidth > 0 && imageHeight > 0) {
+                        resolve(image)
+                    }
+                    else {
+                        reject(new Error('图片加载事件触发但尺寸无效'))
+                    }
+                }
+
+                image.onerror = (err) => {
+                    clearTimeout(timeout)
+                    console.error('加载远程图片失败:', err)
+                    reject(err)
+                }
+
+                image.src = res.path
+
+            },
+            fail: (fail) => {
+                reject(new Error('获取图片信息失败'))
+            },
+        })
+
+    })
+}
+
+// 绘制标题
+function drawTitle(ctx, width, height, scale) {
+    const titleText = couponDetail.value.type === '4' ? '兑换券' : '优惠券'
+    const textY = couponDetail.value.type === '4' ? '180' : '155'
+    const fontSize = couponDetail.value.type === '4' ? '24' : '18'
+    ctx.fillStyle = '#FF5365'
+    ctx.font = `500 ${fontSize * scale}px PinfgFang SC`
+    ctx.textAlign = 'center'
+
+    ctx.fillText(titleText, width / 2, textY * scale)
+}
+
+// 绘制优惠券金额
+function drawCouponAmount(ctx, width, height, scale) {
+    const centerX = width / 2
+    const bottomY = 220 * scale
+    ctx.textBaseline = 'bottom' // 设置文本基线为底部对齐
+    if (couponDetail.value.type === '4') {
+        // 兑换券:指定商品
+    }
+    else if (couponDetail.value.type === '2') {
+        // 折扣券:数字 + 折
+        const numberFont = `bold ${48 * scale}px D-DIN Exp-DINExp`
+        const unitFont = `bold ${16 * scale}px PingFang SC`
+
+        ctx.fillStyle = '#FF5365'
+
+        // 计算数字宽度
+        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 + 6 * scale // 8px间距(减小间距)
+
+        // 计算起始X坐标(整体居中)
+        const startX = centerX - totalWidth / 2
+
+        // 绘制数字
+        ctx.font = numberFont
+        ctx.textAlign = 'left'
+        ctx.fillText(number, startX, bottomY)
+
+        // 绘制"折"字(与数字间距6px,垂直位置微调使视觉对齐)
+        ctx.font = unitFont
+        // 根据字体大小比例微调"折"字的y坐标,使其视觉上与数字底部对齐
+        const unitY = bottomY - (48 - 16) * scale * 0.3 // 微调垂直位置
+        ctx.fillText(unit, startX + numberWidth + 5 * scale, unitY)
+    }
+    else if (couponDetail.value.type === '3') {
+        // 满减券:数字 + 元
+        const numberFont = `bold ${48 * scale}px D-DIN Exp-DINExp`
+        const symbolFont = `500 ${16 * scale}px PingFang SC`
+
+        // 计算数字宽度
+        ctx.fillStyle = '#FF5365'
+        ctx.font = numberFont
+        const number = couponDetail.value.ruleReductionAmount
+        const numberWidth = ctx.measureText(number).width
+
+        // 计算"元"符号宽度
+        ctx.fillStyle = '#FF5365'
+        ctx.font = symbolFont
+        const symbol = '元'
+        const symbolWidth = ctx.measureText(symbol).width
+        console.log('symbolWidth:', ctx.measureText(symbol))
+        // const symbolHeight = ctx.measureText(symbol).height
+
+        // 计算总宽度
+        const totalWidth = symbolWidth + numberWidth + 10 * scale // 10px间距(减小间距)
+
+        // 计算起始X坐标(整体居中)
+        const startX = centerX - totalWidth / 2
+
+        // 绘制"元"符号(垂直位置微调使视觉对齐)
+        ctx.textAlign = 'left'
+        // 根据字体大小比例微调"元"符号的y坐标,使其视觉上比数字底部高2px
+        const symbolY = bottomY - (48 - 16) * scale * 0.3 // 微调垂直位置
+        ctx.fillText(symbol, startX + numberWidth + 5 * scale, symbolY)
+
+        // 绘制数字
+        ctx.fillStyle = '#FF5365'
+        ctx.font = numberFont
+        ctx.fillText(number, startX + 3 * scale, bottomY)
+    }
+
+    // 恢复默认文本对齐方式
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'alphabetic'
+}
+
+// 绘制优惠券描述
+function drawCouponDescription(ctx, width, height, scale) {
+    // 主标题
+    ctx.fillStyle = '#000000'
+    ctx.font = `400 ${14 * scale}px PingFang SC`
+    ctx.textAlign = 'center'
+
+    // 文本自动换行辅助函数
+    const drawWrappedText = (text, fontSize, yPosition, color, maxLines = 2) => {
+        ctx.font = `${fontSize * scale}px PingFang SC`
+        ctx.fillStyle = color
+        ctx.textAlign = 'center'
+
+        const maxWidth = width - 80 * scale
+        const lineHeight = fontSize * 1.5 * scale
+        let currentY = yPosition
+        let currentLine = ''
+        let lineCount = 0
+
+        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
+                lineCount++
+
+                // 超过最大行数则停止
+                if (lineCount >= maxLines) {
+                    // 最后一行添加省略号
+                    currentLine = `${currentLine}...`
+                    break
+                }
+            }
+            else {
+                currentLine = testLine
+            }
+        }
+
+        if (currentLine && lineCount < maxLines) {
+            ctx.fillText(currentLine, width / 2, currentY)
+        }
+
+        return currentY + lineHeight // 返回下一行的y坐标
+    }
+
+    const relatedType = couponDetail.value.relatedType
+    let descriptionText = ''
+    let platformSource = couponDetail.value.platformSource
+    let type = couponDetail.value.type
+    let relatedName = couponDetail.value.relatedName
+
+    if (type === '4') {
+        // 兑换券-指定商品
+        relatedName = getLastPartAfterSlash(couponDetail.value.relatedName)
+        descriptionText = `限${relatedName}${platformSource === '1' ? '分类商品' : '类'}使用`
+    } else {
+        if (relatedType === '1') {
+            descriptionText = `全部适用`
+        } else {
+            descriptionText = `限部分适用`
+        }
+    }
+
+    // 使用自动换行函数绘制
+    const fontSize = 14
+    const lineHeight = 20 * 1.5 * scale
+    const descY = couponDetail.value.type === '4' ? 275 : 255
+    const nextLineY = drawWrappedText(descriptionText, fontSize, descY * scale, '#000000')
+}
+
+// 绘制二维码区域
+async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
+    const qrSize = 120 * scale
+    const qrX = (width - qrSize) / 2
+    const qrY = couponDetail.value.type === '4' ? 310 * scale : 280 * scale
+
+    // 使用柔和的白色背景,增加卡片层次感
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 清除阴影
+    ctx.shadowColor = 'transparent'
+    ctx.shadowBlur = 0
+    ctx.shadowOffsetY = 0
+
+    // 1. 优化二维码边框:添加双层边框效果
+    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()
+
+    // 2. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
+    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')
+                    // #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秒超时
+                uni.getImageInfo({
+                    src: tempFilePath || imgSrc,
+                    success: (res) => {
+                        qrImage.onload = () => {
+                            // console.log('图片加载成功:', res)
+                            clearTimeout(timeout)
+                            // console.log('二维码图片加载成功', {
+                            //     complete: qrImage.complete,
+                            //     naturalWidth: qrImage.naturalWidth || qrCodeImg.width || res.width,
+                            //     naturalHeight: qrImage.naturalHeight || qrCodeImg.height || res.height,
+                            // }, qrImage)
+
+                            // 获取图片尺寸,兼容微信小程序环境
+                            const imageWidth = qrImage.naturalWidth || qrImage.width || res.width
+                            const imageHeight = qrImage.naturalHeight || qrImage.height || res.height
+                            // 再次检查图片是否真正加载完成
+                            if ((isPcWeixinMp() || qrImage.complete) && imageWidth > 0 && imageHeight > 0) {
+                                resolve()
+                            }
+                            else {
+                                reject(new Error('图片加载事件触发但尺寸无效'))
+                            }
+                        }
+                        qrImage.onerror = (err) => {
+                            clearTimeout(timeout)
+                            console.error('加载二维码图片失败:', err)
+                            reject(err)
+                        }
+
+                        // 优先使用临时文件路径,否则使用base64数据
+                        qrImage.src = res.path
+                    },
+                    fail: (fail) => {
+                        reject(new Error('获取图片信息失败'))
+                    },
+                })
+            })
+
+            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' : '临时文件路径')
+            }
+        }
+    }
+
+    // 3. 优化二维码提示文字:使用更现代的字体和颜色
+    // 有效期
+    ctx.fillStyle = '#000000'
+    ctx.font = `400 ${14 * scale}px PingFang SC`
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    const validityType = couponDetail.value.validityType
+    let validityText = ''
+
+    if (validityType === '2') {
+        validityText = `自领取之日起${couponDetail.value.validDays}日内使用`
+    } else if (validityType === '1') {
+        validityText = `限${couponDetail.value.validStartTime}到${couponDetail.value.validEndTime}内使用`
+    } else {
+        validityText = `长期有效`
+    }
+    const validityY = couponDetail.value.type === '4' ? 40 : 37
+    ctx.fillText(validityText, width / 2, qrY + qrSize + validityY * scale)
+
+    // 使用规则
+    ctx.fillStyle = '#FF5365'
+    ctx.font = `600 ${15 * scale}px PingFang SC`
+    let ruleText = ''
+    if (couponDetail.value.type === '2') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用,最高优惠${couponDetail.value.ruleDiscountCapAmount}元`
+    }
+    else if (couponDetail.value.type === '3') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用`
+    }
+
+    ctx.fillText(ruleText, width / 2, qrY + qrSize + 65 * scale)
+}
+
+// 绘制二维码区域(广誉源)
+async function drawGyyQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale) {
+    const qrSize = 120 * scale
+    const qrX = (width - qrSize) / 2
+    const qrY = couponDetail.value.type === '4' ? 310 * scale : 280 * scale
+
+    // 使用柔和的白色背景,增加卡片层次感
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 清除阴影
+    ctx.shadowColor = 'transparent'
+    ctx.shadowBlur = 0
+    ctx.shadowOffsetY = 0
+
+    // 1. 优化二维码边框:添加双层边框效果(直角正方形)
+    const borderWidth = 8 * scale  // 外层边框宽度
+    const innerBorderWidth = 4 * scale  // 内层边框宽度
+
+    // 外层渐变边框
+    ctx.save()
+    ctx.beginPath()
+    // 绘制外层正方形(比二维码大 borderWidth)
+    ctx.rect(qrX - borderWidth, qrY - borderWidth, qrSize + borderWidth * 2, qrSize + borderWidth * 2)
+
+    // 创建渐变效果
+    const gradient = ctx.createLinearGradient(qrX, qrY, qrX + qrSize, qrY + qrSize)
+    gradient.addColorStop(0, '#E8F5E9')
+    gradient.addColorStop(1, '#C8E6C9')
+
+    ctx.fillStyle = gradient
+    ctx.fill()
+    ctx.restore()
+
+    // 内层白色边框
+    ctx.beginPath()
+    // 绘制内层正方形(比二维码大 innerBorderWidth)
+    ctx.rect(qrX - innerBorderWidth, qrY - innerBorderWidth, qrSize + innerBorderWidth * 2, qrSize + innerBorderWidth * 2)
+    ctx.fillStyle = '#FFFFFF'
+    ctx.fill()
+
+    // 2. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
+    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秒超时
+                uni.getImageInfo({
+                    src: tempFilePath || imgSrc,
+                    success: (res) => {
+                        qrImage.onload = () => {
+                            // console.log('图片加载成功:', res)
+                            clearTimeout(timeout)
+                            // console.log('二维码图片加载成功', {
+                            //     complete: qrImage.complete,
+                            //     naturalWidth: qrImage.naturalWidth || qrCodeImg.width || res.width,
+                            //     naturalHeight: qrImage.naturalHeight || qrCodeImg.height || res.height,
+                            // }, qrImage)
+
+                            // 获取图片尺寸,兼容微信小程序环境
+                            const imageWidth = qrImage.naturalWidth || qrImage.width || res.width
+                            const imageHeight = qrImage.naturalHeight || qrImage.height || res.height
+                            // 再次检查图片是否真正加载完成
+                            if ((isPcWeixinMp() || qrImage.complete) && imageWidth > 0 && imageHeight > 0) {
+                                resolve()
+                            }
+                            else {
+                                reject(new Error('图片加载事件触发但尺寸无效'))
+                            }
+                        }
+                        qrImage.onerror = (err) => {
+                            clearTimeout(timeout)
+                            console.error('加载二维码图片失败:', err)
+                            reject(err)
+                        }
+
+                        // 优先使用临时文件路径,否则使用base64数据
+                        qrImage.src = res.path
+                    },
+                    fail: (fail) => {
+                        reject(new Error('获取图片信息失败'))
+                    },
+                })
+            })
+
+            const finalWidth = qrImage.naturalWidth || qrImage.width
+            const finalHeight = qrImage.naturalHeight || qrImage.height
+
+            // 最后再检查一次图片尺寸
+            if (finalWidth <= 0 || finalHeight <= 0) {
+                throw new Error('图片尺寸无效')
+            }
+
+            // 绘制图片(正方形,不需要裁剪)
+            ctx.save()
+            ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
+
+
+            // 图片绘制成功后,添加一个精致的内边框
+            ctx.restore()
+            ctx.beginPath()
+            ctx.rect(qrX, qrY, qrSize, qrSize)
+            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' : '临时文件路径')
+            }
+        }
+    }
+
+    // 3. 优化二维码提示文字:使用更现代的字体和颜色
+    // 有效期
+    ctx.fillStyle = '#000000'
+    ctx.font = `400 ${14 * scale}px PingFang SC`
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    const validityType = couponDetail.value.validityType
+    let validityText = ''
+
+    if (validityType === '2') {
+        validityText = `自领取之日起${couponDetail.value.validDays}日内使用`
+    } else if (validityType === '1') {
+        validityText = `限${couponDetail.value.validStartTime}到${couponDetail.value.validEndTime}内使用`
+    } else {
+        validityText = `长期有效`
+    }
+    const validityY = couponDetail.value.type === '4' ? 40 : 37
+    ctx.fillText(validityText, width / 2, qrY + qrSize + validityY * scale)
+
+    // 使用规则
+    ctx.fillStyle = '#FF5365'
+    ctx.font = `600 ${15 * scale}px PingFang SC`
+    let ruleText = ''
+    if (couponDetail.value.type === '2') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用,最高优惠${couponDetail.value.ruleDiscountCapAmount}元`
+    }
+    else if (couponDetail.value.type === '3') {
+        ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用`
+    }
+
+    ctx.fillText(ruleText, width / 2, qrY + qrSize + 65 * scale)
+}
+
+// 绘制底部信息
+function drawFooter(ctx, width, height, scale) {
+    // 计算二维码区域的位置
+    const qrSize = 200 * scale
+    const qrY = 435 * 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,
+        success: async (res) => {
+            console.log('分享成功', res)
+            await markSuccess(shareRecordId.value)
+        },
+        fail: (err) => {
+            console.log('分享失败', err)
+        },
+    })
+}
+
+// 保存图片到相册
+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',
+        })
+        await markSuccess(shareRecordId.value)
+    }
+    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;
+}
+
+/* 顶部导航栏 */
+.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 {
+    font-family: Inter, Inter;
+    font-weight: 400;
+    font-size: 32rpx;
+    color: #FFFFFF;
+    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;
+    position: relative;
+    min-height: 1627rpx;
+}
+
+/* 预览图片容器 */
+.preview-image-wrapper {
+    width: 100%;
+    // max-width: 600rpx;
+    position: relative;
+}
+
+/* 预览图片 */
+.qc-code-img {
+    position: absolute;
+    top: -9999px;
+    left: -9999px;
+    opacity: 0;
+}
+
+.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: -25rpx;
+    border-radius: 30rpx 30rpx 0 0;
+    box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
+    z-index: 2;
+}
+
+/* 操作说明 */
+.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>

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

@@ -208,7 +208,7 @@ async function drawCompleteShareImage(ctx, canvas, qrCodeImg) {
             const scale = width / 375 // 缩放比例
 
             // 1. 加载并绘制远程背景图片
-            const backgroundImgUrl = getImageUrl('@img/receiveCoupon/share-friend.png')
+            const backgroundImgUrl = getImageUrl('@img/share/share_coupon_bg.png')
             const backgroundImg = await loadRemoteImage(canvas, backgroundImgUrl)
             ctx.drawImage(backgroundImg, 0, 0, width, height)
 

+ 463 - 0
src/pages/home/index.vue

@@ -0,0 +1,463 @@
+<script lang="ts" setup>
+import { useRequest } from 'alova/client'
+import { storeToRefs } from 'pinia'
+import { ref, watch, computed } from 'vue'
+import { getAccountCount, getCouponDetail, getCouponSituation, getShareInfo } from '@/api/home'
+import CouponList from '@/components/couponList.vue'
+import { useShare } from '@/hooks/useShare'
+import { useNewCouponStore as useCouponStore } from '@/store/coupon'
+import { useTokenStore } from '@/store/token'
+import { useUserStore } from '@/store/user'
+import { getImageUrl } from '@/utils/imageUtil'
+import { toLoginPage } from '@/utils/toLoginPage'
+import { delayLoadingMiddleware } from '@/http/tools/delayLoadingMiddleware'
+
+defineOptions({
+    name: 'Home',
+})
+definePage({
+    // 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
+    type: 'home',
+    style: {
+        // 'custom' 表示开启自定义导航栏,默认 'default'
+        navigationStyle: 'custom',
+        navigationBarTitleText: '首页',
+    },
+})
+
+const tokenStore = useTokenStore()
+const { hasLogin } = storeToRefs(tokenStore)
+const userStore = useUserStore()
+
+const refreshing = ref(false)
+
+// 获取优惠券
+const couponStore = useCouponStore()
+const { loading, data } = storeToRefs(couponStore)
+const discountVoucherList = computed(() => data.value.discountVoucherList)
+const couponList = computed(() => data.value.couponList)
+
+
+// 获取首页收益
+const { send: getAccountCountRequest, data: accountCountData, loading: accountCountLoading } = useRequest(getAccountCount, {
+    immediate: false,
+    dependencies: [],
+    middleware: delayLoadingMiddleware(150, 200)
+})
+
+// 获取首页领券情况数据
+const { send: getCouponSituationRequest, data: couponSituationData } = useRequest(getCouponSituation, {
+    immediate: false,
+    dependencies: [],
+})
+
+// 创建分享hook实例
+const { getShareConfig, getTimelineShareConfig } = useShare()
+
+// #ifdef MP-WEIXIN
+// 分享给好友生命周期函数
+onShareAppMessage(() => {
+    const promise = new Promise((resolve, reject) => {
+        getShareConfig().then(resolve).catch(reject)
+    })
+    // const configPromise = await getShareConfig()
+    return {
+        promise,
+    }
+})
+
+// 分享到朋友圈生命周期函数
+onShareTimeline(() => {
+    const promise = new Promise((resolve, reject) => {
+        getTimelineShareConfig().then(resolve).catch(reject)
+    })
+    return {
+        promise,
+    }
+})
+
+// #endif
+
+onShow((options) => {
+    couponStore.getCouponListByType()
+    // 登录后查询收益数据
+    if (hasLogin.value) {
+        Promise.allSettled([getAccountCountRequest(), getCouponSituationRequest()])
+    }
+})
+
+// #ifdef MP-WEIXIN
+async function login() {
+    const currentPage = getCurrentPages()[0]
+    const redirectUrl = `/${currentPage.route}`
+    toLoginPage({ queryString: `?redirect=${encodeURIComponent(redirectUrl)}` })
+}
+// #endif
+
+// 顶部导航栏高度,设置banner位置
+const navigationBarHeight = ref(0)
+// #ifdef MP-WEIXIN
+function getNavigationBarHeight() {
+    uni.getSystemInfo({
+        success: (res) => {
+            const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+            // 顶部导航栏高度 = 状态栏高度 + 胶囊的高度
+            navigationBarHeight.value = res.statusBarHeight + menuButtonInfo.height + 12
+        },
+    })
+}
+getNavigationBarHeight()
+// #endif
+
+// 邀请新人
+function toInviteFriend() {
+    uni.navigateTo({
+        url: `/pages-A/inviteUsageRules/index?number=${couponSituationData.value?.pullNewCountThreshold}&money=${couponSituationData.value?.pullNewAmount}`,
+    })
+}
+function toSpendAndSaveCouponList() {
+    uni.navigateTo({
+        // url: '/pages-A/spendAndSaveCouponList/index?type=3',
+        // url: '/pages-A/regionSelection/index',
+        url: '/pages-A/newCouponList/index',
+    })
+}
+
+function toCouponRedemptionList(state) {
+    uni.navigateTo({
+        url: `/pages-A/couponRedemptionList/index?state=${state}`,
+    })
+}
+
+async function onRefresh() {
+    refreshing.value = true
+    await couponStore.getCouponListByType()
+    if (hasLogin.value) {
+        await userStore.fetchUserInfo()
+        await getAccountCountRequest()
+        await getCouponSituationRequest()
+    }
+    refreshing.value = false
+}
+</script>
+
+<template>
+    <view class="home-container">
+        <up-pull-refresh :refreshing="refreshing" @refresh="onRefresh">
+            <!-- 顶部区域 -->
+            <view class="home-header">
+                <view class="planet-font absolute z-3 text-xl c-black" :style="{ top: `${navigationBarHeight - 39}px` }">
+                    券中心
+                </view>
+                <view 
+                class="home-header-card" 
+                :style="{
+                    backgroundImage: `url(${getImageUrl('@img/index/income-card-bg.png')})`,
+                    backgroundSize: 'cover',
+                    backgroundPosition: 'center',
+                    marginTop: `${navigationBarHeight}px`
+                }">
+                    <view class="home-header-avatar-info">
+                        <view class="home-header-balance">
+                            我的收益(元)
+                        </view>
+                        <view class="home-header-balance-num">
+                            <view class="home-header-balance-num-amount">
+                                <template v-if="accountCountLoading">
+                                    <up-loading-icon size="65rpx" mode="semicircle" color="#fff" />
+                                </template>
+                                <template v-else>
+                                    {{ !hasLogin ? '********' : accountCountData?.balance || 0 }}
+                                </template>
+                            </view>
+                        </view>
+                    </view>
+                    <view class="home-header-tips">
+                        <view class="home-header-tips-item" @click="toCouponRedemptionList('2')">
+                            <view class="home-header-tips-item-des">
+                                已核销(张)
+                            </view>
+                            <view class="home-header-tips-item-num">
+                                {{ !hasLogin ? '*****' : couponSituationData?.usedQuantity || 0 }}
+                            </view>
+                        </view>
+                        <view class="home-header-tips-item ml-8" @click="toCouponRedemptionList('1')">
+                            <view class="home-header-tips-item-des">
+                                未核销(张)
+                            </view>
+                            <view class="home-header-tips-item-num">
+                                {{ !hasLogin ? '*****' :couponSituationData?.quantityToBeUsed || 0 }}
+                            </view>
+                        </view>
+                        <view class="home-header-tips-item ml-8">
+                            <view class="home-header-tips-item-des">
+                                已发放(张)
+                            </view>
+                            <view class="home-header-tips-item-num">
+                                {{ !hasLogin ? '*****' : couponSituationData?.quantityForComplimentary || 0 }}
+                            </view>
+                        </view>
+                    </view>
+                </view>
+            </view>
+            <!-- 邀请新人 -->
+            <view class="home-header-invite"
+            :style="{
+                backgroundImage: `url(${getImageUrl('@img/index/invite-card-bg.png')})`,
+                backgroundSize: 'cover',
+                backgroundPosition: 'center'
+            }">       
+                <view class="planet-font invite-bonus">本地生活 + 广誉源{{ !hasLogin ? '' : '共邀请' + (couponSituationData?.pullNewCountThreshold || 0) + '人' }}</view>
+                <view class="invite-desc">
+                    <view
+                    class="invite-desc-arrow"
+                    :style="{
+                        backgroundImage: `url(${getImageUrl('@img/index/right-arrow.png')})`,
+                        backgroundSize: 'cover',
+                        backgroundPosition: 'center'
+                    }"
+                    @click="toInviteFriend"
+                    />
+                </view>
+            </view>
+            <!-- 分享优惠券 -->
+            <view class="home-coupon-share">
+                <view class="share-card-bg"
+                :style="{
+                    backgroundImage: `url(${getImageUrl('@img/index/share-card-bg.png')})`,
+                    backgroundSize: 'cover',
+                    backgroundPosition: 'center'
+                }">
+                    <view v-if="!hasLogin" class="share-card-text">分享优惠券, 领现金佣金!</view>
+                    <view v-else class="share-card-text">分享优惠券{{couponSituationData?.pushCouponCountThreshold || '*'}}张, 领{{couponSituationData?.pushCouponAmount || '*'}}元现金佣金!</view>
+                </view>
+            </view>
+            <view class="home-coupon-list">
+                <view class="coupon-list">
+                    <u-sticky :offset-top="navigationBarHeight" :customNavHeight="44">
+                        <CouponList />
+                    </u-sticky>
+                </view>
+            </view>
+        </up-pull-refresh>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.home-container {
+    background-color: #ffffff;
+    line-height: 1;
+    position: relative;
+    min-height: calc(100vh - 100rpx - var(--safe-area-inset-bottom));
+    padding: 0 32rpx;
+
+    .home-header {
+        position: relative;
+        overflow: hidden;
+
+        .home-header-bg {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 686rpx;
+            height: 284rpx;
+            object-fit: cover;
+            transform: translate(-80px, 0);
+            z-index: 0;
+        }
+
+        .home-header-title {
+            font-weight: 400;
+            font-size: 40rpx;
+            color: #000;
+        }
+
+        .home-header-card {
+            height: 275rpx;
+            border-radius: 32rpx;
+            margin-bottom: 20rpx;
+            padding: 38rpx 32rpx 0;
+        }
+
+        .home-header-avatar-info {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            text-align: left;
+            position: relative;
+            z-index: 1;
+
+            .home-header-balance {
+                font-size: 24rpx;
+                font-weight: 400;
+                color: rgba(255,255,255,0.7);
+                margin-bottom: 20rpx;
+            }
+
+            .home-header-balance-num {
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+
+                .home-header-balance-num-amount {
+                    width: 100%;
+                    font-family: D-DIN Exp-DINExp, D-DIN Exp-DINExp;
+                    font-weight: bold;
+                    font-size: 60rpx;
+                    color: #ffffff;
+                    line-height: 60rpx;
+                }
+
+                .home-header-balance-num-btns {
+                    display: flex;
+                    gap: 15rpx;
+
+                    .home-header-balance-num-btn {
+                        padding: 20rpx 40rpx;
+                        border-radius: 33rpx;
+                        font-weight: 400;
+                        font-size: 26rpx;
+
+                        &.js {
+                            background: #da4c47;
+                            color: #ffffff;
+                        }
+
+                        &.tx {
+                            background: #bfbfbf;
+                            color: #747474;
+                        }
+                    }
+                }
+            }
+        }
+
+        .home-header-tips {
+            display: flex;
+            position: relative;
+            z-index: 1;
+            margin-top: 44rpx;
+
+            .home-header-tips-item {
+                flex: 1;
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                gap: 20rpx;
+                font-weight: 500;
+                font-size: 34rpx;
+                color: #ffffff;
+                line-height: 1;
+                position: relative;
+
+                &:not(:last-child):after {
+                    content: '';
+                    position: absolute;
+                    top: 15%;
+                    right: 0;
+                    transform: translateY(-50%);
+                    width: 2px;
+                    height: 24rpx;
+                    background: #ffffff;
+                }
+
+                .home-header-tips-item-num {
+                    text-align: left;
+                    font-family: D-DIN Exp, D-DIN Exp;
+                    font-weight: 400;
+                    font-size: 40rpx;
+                }
+
+                .home-header-tips-item-des {
+                    font-size: 24rpx;
+                    color: rgba(255,255,255,0.7);
+                }
+            }
+        }
+    }
+
+    // 邀请新人
+    .home-header-invite {
+        width: 100%;
+        height: 296rpx;
+        position: relative;
+        
+        .invite-bonus {
+            position: absolute;
+            top: 75rpx;
+            left: 40rpx;
+            font-weight: 600;
+            font-size: 24rpx;
+            color: #8A3E00;
+        }
+
+        .invite-desc {
+            font-weight: 500;
+            font-size: 22rpx;
+            color: #FFFFFF;
+            line-height: 27rpx;
+
+            .invite-desc-arrow {
+                width: 54rpx;
+                height: 54rpx;
+                position: absolute;
+                top: 24rpx;
+                right: 26rpx;
+            }
+        }
+    }
+
+    // 分享优惠券
+    .home-coupon-share {
+        position: relative;
+        top: -52rpx;
+
+        .share-card-bg {
+            width: 100%;
+            height: 155rpx;
+            position: relative;
+
+            .share-card-text {
+                font-weight: 500;
+                font-size: 30rpx;
+                color: #FFFFFF;
+                position: absolute;
+                left: 35rpx;
+                bottom: 20rpx;
+                line-height: 42rpx;
+            }
+        }
+    }
+
+    // 优惠券列表
+    .home-coupon-list {
+        margin-top: -52rpx;
+
+        .coupon-list {
+            min-height: 209rpx;
+            display: flex;
+            flex-direction: column;
+            gap: 20rpx;
+            position: relative;
+
+            .home-coupon-content-loading {
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                display: flex;
+                justify-content: center;
+                align-self: center;
+                z-index: 1;
+            }
+
+            .home-coupon-content-empty {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+            }
+        }
+    }
+}
+</style>

+ 3 - 1
src/pages/income/income.vue

@@ -214,6 +214,8 @@ async function tabChange(index) {
     showLoading.value = true
     await reload()
     showLoading.value = false
+    console.log(activeIndex.value, '切换tab');
+    
 }
 
 async function pickerConfirm() {
@@ -273,7 +275,7 @@ function goPage(page: string) {
 
 function handleItemClick(item) {
     uni.navigateTo({
-        url: `/pages-A/incomeShareInfo/index?batchId=${item.batchId}&commissionType=${item.commissionType}`,
+        url: `/pages-A/incomeShareInfo/index?batchId=${item.batchId}&commissionType=${item.commissionType}&type=${activeIndex.value}`,
     })
     // incomeShareInfoRef.value.show(item)
 }

+ 1 - 1
src/router/config.ts

@@ -11,7 +11,7 @@ export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEE
 export const LOGIN_PAGE = '/pages-fg/login/login'
 export const REGISTER_PAGE = '/pages-fg/login/register'
 export const NOT_FOUND_PAGE = '/pages-fg/404/index'
-export const HOME_PAGE = '/pages/home/home'
+export const HOME_PAGE = '/pages/home/index'
 
 export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
 

二进制
src/static/fonts/字制区喜脉体.ttf


二进制
src/static/images/gyy_logo.png


+ 100 - 5
src/store/area.ts

@@ -1,13 +1,15 @@
+import { resolve } from 'path'
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
 
-const initCurrentCity = {
+let initCurrentCity = ref({
     name: '全部地区',
     value: 'all',
-}
+})
+
 
 export const useAreaStore = defineStore('area', () => {
-    const currentCity = ref(initCurrentCity)
+    const currentCity = ref(initCurrentCity.value)
     const lastSelectedCity = ref([])
 
     const addLastSelectedCity = (city) => {
@@ -27,14 +29,107 @@ export const useAreaStore = defineStore('area', () => {
     }
     const resetCurrentCity = () => {
         console.log('重置当前城市')
-        currentCity.value = initCurrentCity
+        currentCity.value = {
+            name: uni.getStorageSync('cityName') || initCurrentCity.value.name,
+            value: uni.getStorageSync('cityCode') || initCurrentCity.value.value,
+        } 
+    }
+
+    /**
+     * 获取当前地理位置
+     * @returns Promise 包含城市信息
+     */
+    const getLocationAndSetCity = (): Promise<{ name: string; value: string }> => {
+        return new Promise(async (resolve, reject) => {
+            // 1. 获取经纬度
+            uni.getLocation({
+                type: 'gcj02',
+                geocode: true,
+                success: async (res) => {
+                    console.log('获取地理位置成功:', res)
+                    
+                    // 保存定位信息
+                    uni.setStorageSync('positioning', { 
+                        latitude: res.latitude, 
+                        longitude: res.longitude 
+                    })
+                    
+                    // 2.天地图逆地理编码 (经纬度 -> 获取城市信息)
+                    await updateGeocode(res.latitude, res.longitude);    
+                    
+                    // 3. 首页默认当前城市
+                    const cityInfo = { 
+                        name: uni.getStorageSync('cityName'), 
+                        value: uni.getStorageSync('cityCode') 
+                    }
+                    initCurrentCity.value = cityInfo
+                    addLastSelectedCity(cityInfo)
+                },
+                fail: (err) => {
+                    console.error('获取地理位置失败:', err)
+                    // 获取失败时使用默认城市
+                    initCurrentCity.value = {
+                        name: '太原市',
+                        value: '140100',
+                    }
+                    reject(err)
+                },
+            })
+        })
+    }
+
+    /**
+     * 获取当前城市的逆地理编码信息
+     * @returns Promise 包含城市信息
+     */
+    const updateGeocode = async (latitude: number, longitude: number) => {
+        if (!latitude || !longitude) {
+            console.error('经纬度参数不能为空');
+            return;
+        }
+        try {
+            let postStr = { 'lon': longitude, 'lat': latitude };
+            const tiandituKey = '9bcf63358817bfb878e8236de8bf1423'
+            uni.request({
+                url: `https://api.tianditu.gov.cn/geocoder?postStr=${encodeURIComponent(JSON.stringify(postStr))}&type=geocode&tk=${tiandituKey}`,
+                success: (res: any) => {
+                    if (res.statusCode == 200 && res.data.status == 0) {
+                        let obj = res.data.result.addressComponent;
+                        uni.setStorageSync('cityName', obj.city);
+                        uni.setStorageSync('cityCode', obj.city_code.slice(3));
+                        uni.setStorageSync('addressName', obj.county);
+                        uni.setStorageSync('addressCode', obj.county_code.slice(3));
+                        // uni.setStorageSync('town', obj.town);
+                        // uni.setStorageSync('townCode', obj.town_code.slice(3));
+                        console.log('逆地理编码更新成功:', obj.city);
+                    }
+                }
+            });
+        } catch (error) {
+          console.error('获取城市信息失败:', error);
+        }
+    }
+
+    /**
+     * 尝试从本地存储恢复城市信息
+     */
+    const restoreCityFromStorage = () => {
+        const storedCity = uni.getStorageSync('cityName')
+        if (storedCity && storedCity !== '') {
+            currentCity.value = {
+                name: storedCity,
+                value: storedCity.replace('市', ''),
+            }
+        }
     }
 
     return {
         currentCity,
         lastSelectedCity,
         addLastSelectedCity,
-        resetCurrentCity
+        resetCurrentCity,
+        getLocationAndSetCity,
+        restoreCityFromStorage
     }
 }, {
     persist: {

+ 12 - 1
src/store/token.ts

@@ -16,6 +16,12 @@ import {
 import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
 import { isDoubleTokenMode } from '@/utils'
 import { useUserStore } from './user'
+import { useAreaStore } from '@/store/area'
+
+onLoad(() => {
+    const areaStore = useAreaStore()
+    areaStore.restoreCityFromStorage()
+})
 
 // 初始化状态
 const tokenInfoState = isDoubleTokenMode
@@ -165,6 +171,11 @@ export const useTokenStore = defineStore(
                     console.log('微信登录分享登录-params: ', JSON.parse(uni.getStorageSync('shareParams')))
                     await addInviteConversion(JSON.parse(uni.getStorageSync('shareParams')))
                 }
+
+                // 登录成功后获取地理位置
+                const areaStore = useAreaStore()
+                await areaStore.getLocationAndSetCity()
+
                 uni.showToast({
                     title: '登录成功',
                     icon: 'success',
@@ -175,7 +186,7 @@ export const useTokenStore = defineStore(
             catch (error) {
                 console.error('微信登录失败:', error)
                 uni.showToast({
-                    title: '登录失败啦,换个姿势试试',
+                    title: '登录失败',
                     icon: 'error',
                 })
                 throw error

+ 14 - 0
src/style/index.scss

@@ -1,6 +1,20 @@
 // 测试用的 iconfont,可生效
 // @import './iconfont.css';
 
+// 引入自定义字体
+@font-face {
+  font-family: 'PlanetFont';
+  src: url('../static/fonts/字制区喜脉体.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+  font-display: swap;
+}
+
+// 自定义字体类
+.planet-font {
+  font-family: 'PlanetFont', sans-serif;
+}
+
 .test {
   // 可以通过 @apply 多个样式封装整体样式
   @apply mt-4 ml-4;

+ 2 - 2
src/tabbar/config.ts

@@ -28,7 +28,7 @@ export const nativeTabbarList: NativeTabBarItem[] = [
     {
         iconPath: 'static/tabbar/home.png',
         selectedIconPath: 'static/tabbar/home_select.png',
-        pagePath: 'pages/home/home',
+        pagePath: 'pages/home/index',
         text: '首页',
     },
     {
@@ -50,7 +50,7 @@ export const nativeTabbarList: NativeTabBarItem[] = [
 export const customTabbarList: CustomTabBarItem[] = [
     {
         text: '首页',
-        pagePath: 'pages/home/home',
+        pagePath: 'pages/home/index',
         iconType: 'image',
         icon: '/static/tabbar/home.png',
         iconActive: '/static/tabbar/home_select.png',

+ 201 - 0
src/uni_modules/Sansnn-uQRCode/LICENSE.md

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

文件差异内容过多而无法显示
+ 207 - 0
src/uni_modules/Sansnn-uQRCode/README.md


+ 12 - 0
src/uni_modules/Sansnn-uQRCode/changelog.md

@@ -0,0 +1,12 @@
+## 4.0.6(2022-12-12)
+修复`getDrawModules`,第一次获取结果正常,后续获取`tile`模块不存在的问题;  
+修复安卓type:normal因Canvas API使用了小数或为0的参数导致生成异常的问题(注:安卓非2d Canvas部分API参数不支持携带小数,部分API参数必须大于0)。
+## 4.0.1(2022-11-28)
+优化组件loading属性的表现;  
+新增组件type选项normal,以便于在某些条件编译初始为type=2d时还可以选择使用非2d组件类型;  
+修复组件条件编译在其他编辑器语法提示报错;  
+修复原生对es5的支持。
+## 4.0.0(2022-11-21)
+v4版本源代码全面开放,开源地址:[https://github.com/Sansnn/uQRCode](https://github.com/Sansnn/uQRCode);  
+
+升级说明:v4为大版本更新,虽然已尽可能兼容上一代版本,但不可避免的还是存在一些细节差异,若更新后出现问题,请参考对照[v3 文档](https://uqrcode.cn/doc/v3),[v4 文档](https://uqrcode.cn/doc)进行修改。

+ 1 - 0
src/uni_modules/Sansnn-uQRCode/common/cache.js

@@ -0,0 +1 @@
+export const cacheImageList = [];

+ 41 - 0
src/uni_modules/Sansnn-uQRCode/common/queue.js

@@ -0,0 +1,41 @@
+function Queue() {
+  let waitingQueue = this.waitingQueue = [];
+  let isRunning = this.isRunning = false; // 记录是否有未完成的任务
+
+  function execute(task, resolve, reject) {
+    task()
+      .then((data) => {
+        resolve(data);
+      })
+      .catch((e) => {
+        reject(e);
+      })
+      .finally(() => {
+        // 等待任务队列中如果有任务,则触发它;否则设置isRunning = false,表示无任务状态
+        if (waitingQueue.length) {
+          const next = waitingQueue.shift();
+          execute(next.task, next.resolve, next.reject);
+        } else {
+          isRunning = false;
+        }
+      });
+  }
+  this.exec = function(task) {
+    return new Promise((resolve, reject) => {
+      if (isRunning) {
+        waitingQueue.push({
+          task,
+          resolve,
+          reject
+        });
+      } else {
+        isRunning = true;
+        execute(task, resolve, reject);
+      }
+    });
+  }
+}
+
+/* 队列实例,某些平台一起使用多个组件时需要通过队列逐一绘制,否则部分绘制方法异常,nvue端的iOS gcanvas尤其明显,在不通过队列绘制时会出现图片丢失的情况 */
+export const queueDraw = new Queue();
+export const queueLoadImage = new Queue();

+ 3 - 0
src/uni_modules/Sansnn-uQRCode/common/types/cache.d.ts

@@ -0,0 +1,3 @@
+declare module '*/common/cache' {
+  export const cacheImageList: Array;
+}

+ 4 - 0
src/uni_modules/Sansnn-uQRCode/common/types/queue.d.ts

@@ -0,0 +1,4 @@
+declare module '*/common/queue' {
+  export const queueDraw: any;
+  export const queueLoadImage: any;
+}

文件差异内容过多而无法显示
+ 25 - 0
src/uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue


文件差异内容过多而无法显示
+ 25 - 0
src/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue


+ 241 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js

@@ -0,0 +1,241 @@
+const isWeex = typeof WXEnvironment !== 'undefined';
+const isWeexIOS = isWeex && /ios/i.test(WXEnvironment.platform);
+const isWeexAndroid = isWeex && !isWeexIOS;
+
+import GLmethod from '../context-webgl/GLmethod';
+
+const GCanvasModule =
+    (typeof weex !== 'undefined' && weex.requireModule) ? (weex.requireModule('gcanvas')) :
+        (typeof __weex_require__ !== 'undefined') ? (__weex_require__('@weex-module/gcanvas')) : {};
+
+let isDebugging = false;
+
+let isComboDisabled = false;
+
+const logCommand = (function () {
+    const methodQuery = [];
+    Object.keys(GLmethod).forEach(key => {
+        methodQuery[GLmethod[key]] = key;
+    })
+    const queryMethod = (id) => {
+        return methodQuery[parseInt(id)] || 'NotFoundMethod';
+    }
+    const logCommand = (id, cmds) => {
+        const mId = cmds.split(',')[0];
+        const mName = queryMethod(mId);
+        console.log(`=== callNative - componentId:${id}; method: ${mName}; cmds: ${cmds}`);
+    }
+    return logCommand;
+})();
+
+function joinArray(arr, sep) {
+    let res = '';
+    for (let i = 0; i < arr.length; i++) {
+        if (i !== 0) {
+            res += sep;
+        }
+        res += arr[i];
+    }
+    return res;
+}
+
+const commandsCache = {}
+
+const GBridge = {
+
+    callEnable: (ref, configArray) => {
+
+        commandsCache[ref] = [];
+
+        return GCanvasModule.enable({
+            componentId: ref,
+            config: configArray
+        });
+    },
+
+    callEnableDebug: () => {
+        isDebugging = true;
+    },
+
+    callEnableDisableCombo: () => {
+        isComboDisabled = true;
+    },
+
+    callSetContextType: function (componentId, context_type) {
+        GCanvasModule.setContextType(context_type, componentId);
+    },
+
+    callReset: function(id){
+        GCanvasModule.resetComponent && canvasModule.resetComponent(componentId);
+    },
+
+    render: isWeexIOS ? function (componentId) {
+        return GCanvasModule.extendCallNative({
+            contextId: componentId,
+            type: 0x60000001
+        });
+    } : function (componentId) {
+        return callGCanvasLinkNative(componentId, 0x60000001, 'render');
+    },
+
+    render2d: isWeexIOS ? function (componentId, commands, callback) {
+
+        if (isDebugging) {
+            console.log('>>> >>> render2d ===');
+            console.log('>>> commands: ' + commands);
+        }
+		
+        GCanvasModule.render([commands, callback?true:false], componentId, callback);
+
+    } : function (componentId, commands,callback) {
+
+        if (isDebugging) {
+            console.log('>>> >>> render2d ===');
+            console.log('>>> commands: ' + commands);
+        }
+
+        callGCanvasLinkNative(componentId, 0x20000001, commands);
+		if(callback){
+		callback();
+		}
+    },
+
+    callExtendCallNative: isWeexIOS ? function (componentId, cmdArgs) {
+
+        throw 'should not be here anymore ' + cmdArgs;
+
+    } : function (componentId, cmdArgs) {
+
+        throw 'should not be here anymore ' + cmdArgs;
+
+    },
+
+
+    flushNative: isWeexIOS ? function (componentId) {
+
+        const cmdArgs = joinArray(commandsCache[componentId], ';');
+        commandsCache[componentId] = [];
+
+        if (isDebugging) {
+            console.log('>>> >>> flush native ===');
+            console.log('>>> commands: ' + cmdArgs);
+        }
+
+        const result = GCanvasModule.extendCallNative({
+            "contextId": componentId,
+            "type": 0x60000000,
+            "args": cmdArgs
+        });
+
+        const res = result && result.result;
+
+        if (isDebugging) {
+            console.log('>>> result: ' + res);
+        }
+
+        return res;
+
+    } : function (componentId) {
+
+        const cmdArgs = joinArray(commandsCache[componentId], ';');
+        commandsCache[componentId] = [];
+
+        if (isDebugging) {
+            console.log('>>> >>> flush native ===');
+            console.log('>>> commands: ' + cmdArgs);
+        }
+
+        const result = callGCanvasLinkNative(componentId, 0x60000000, cmdArgs);
+
+        if (isDebugging) {
+            console.log('>>> result: ' + result);
+        }
+
+        return result;
+    },
+
+    callNative: function (componentId, cmdArgs, cache) {
+
+        if (isDebugging) {
+            logCommand(componentId, cmdArgs);
+        }
+
+        commandsCache[componentId].push(cmdArgs);
+
+        if (!cache || isComboDisabled) {
+            return GBridge.flushNative(componentId);
+        } else {
+            return undefined;
+        }
+    },
+
+    texImage2D(componentId, ...args) {
+        if (isWeexIOS) {
+            if (args.length === 6) {
+                const [target, level, internalformat, format, type, image] = args;
+                GBridge.callNative(
+                    componentId,
+                    GLmethod.texImage2D + ',' + 6 + ',' + target + ',' + level + ',' + internalformat + ',' + format + ',' + type + ',' + image.src
+                )
+            } else if (args.length === 9) {
+                const [target, level, internalformat, width, height, border, format, type, image] = args;
+                GBridge.callNative(
+                    componentId,
+                    GLmethod.texImage2D + ',' + 9 + ',' + target + ',' + level + ',' + internalformat + ',' + width + ',' + height + ',' + border + ',' +
+                    + format + ',' + type + ',' + (image ? image.src : 0)
+                )
+            }
+        } else if (isWeexAndroid) {
+            if (args.length === 6) {
+                const [target, level, internalformat, format, type, image] = args;
+                GCanvasModule.texImage2D(componentId, target, level, internalformat, format, type, image.src);
+            } else if (args.length === 9) {
+                const [target, level, internalformat, width, height, border, format, type, image] = args;
+                GCanvasModule.texImage2D(componentId, target, level, internalformat, width, height, border, format, type, (image ? image.src : 0));
+            }
+        }
+    },
+
+    texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image) {
+        if (isWeexIOS) {
+            if (arguments.length === 8) {
+                GBridge.callNative(
+                    componentId,
+                    GLmethod.texSubImage2D + ',' + 6 + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset, + ',' + format + ',' + type + ',' + image.src
+                )
+            }
+        } else if (isWeexAndroid) {
+            GCanvasModule.texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image.src);
+        }
+    },
+
+    bindImageTexture(componentId, src, imageId) {
+        GCanvasModule.bindImageTexture([src, imageId], componentId);
+    },
+
+    perloadImage([url, id], callback) {
+        GCanvasModule.preLoadImage([url, id], function (image) {
+            image.url = url;
+            image.id = id;
+            callback(image);
+        });
+    },
+	
+	measureText(text, fontStyle, componentId) {
+	    return GCanvasModule.measureText([text, fontStyle], componentId);
+	},
+	
+	getImageData (componentId, x, y, w, h, callback) {
+		GCanvasModule.getImageData([x, y,w,h],componentId,callback);
+	},
+	
+	putImageData (componentId, data, x, y, w, h, callback) {
+		GCanvasModule.putImageData([x, y,w,h,data],componentId,callback);
+	},
+	
+	toTempFilePath(componentId, x, y, width, height, destWidth, destHeight, fileType, quality, callback){ 
+		GCanvasModule.toTempFilePath([x, y, width,height, destWidth, destHeight, fileType, quality], componentId, callback);
+	}
+}
+
+export default GBridge;

+ 18 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleLinearGradient.js

@@ -0,0 +1,18 @@
+class FillStyleLinearGradient {
+
+    constructor(x0, y0, x1, y1) {
+        this._start_pos = { _x: x0, _y: y0 };
+        this._end_pos = { _x: x1, _y: y1 };
+        this._stop_count = 0;
+        this._stops = [0, 0, 0, 0, 0];
+    }
+
+    addColorStop = function (pos, color) {
+        if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) {
+            this._stops[this._stop_count] = { _pos: pos, _color: color };
+            this._stop_count++;
+        }
+    }
+}
+
+export default FillStyleLinearGradient;

+ 8 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStylePattern.js

@@ -0,0 +1,8 @@
+class FillStylePattern {
+    constructor(img, pattern) {
+        this._style = pattern;
+        this._img = img;
+    }
+}
+
+export default FillStylePattern;

+ 17 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleRadialGradient.js

@@ -0,0 +1,17 @@
+class FillStyleRadialGradient {
+    constructor(x0, y0, r0, x1, y1, r1) {
+        this._start_pos = { _x: x0, _y: y0, _r: r0 };
+        this._end_pos = { _x: x1, _y: y1, _r: r1 };
+        this._stop_count = 0;
+        this._stops = [0, 0, 0, 0, 0];
+    }
+
+    addColorStop(pos, color) {
+        if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) {
+            this._stops[this._stop_count] = { _pos: pos, _color: color };
+            this._stop_count++;
+        }
+    }
+}
+
+export default FillStyleRadialGradient;

+ 666 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/RenderingContext.js

@@ -0,0 +1,666 @@
+import FillStylePattern from './FillStylePattern';
+import FillStyleLinearGradient from './FillStyleLinearGradient';
+import FillStyleRadialGradient from './FillStyleRadialGradient';
+import GImage from '../env/image.js';
+import {
+	ArrayBufferToBase64,
+	Base64ToUint8ClampedArray
+} from '../env/tool.js';
+
+export default class CanvasRenderingContext2D {
+
+	_drawCommands = '';
+
+	_globalAlpha = 1.0;
+
+	_fillStyle = 'rgb(0,0,0)';
+	_strokeStyle = 'rgb(0,0,0)';
+
+	_lineWidth = 1;
+	_lineCap = 'butt';
+	_lineJoin = 'miter';
+
+	_miterLimit = 10;
+
+	_globalCompositeOperation = 'source-over';
+
+	_textAlign = 'start';
+	_textBaseline = 'alphabetic';
+
+	_font = '10px sans-serif';
+
+	_savedGlobalAlpha = [];
+
+	timer = null;
+	componentId = null;
+
+	_notCommitDrawImageCache = [];
+	_needRedrawImageCache = [];
+	_redrawCommands = '';
+	_autoSaveContext = true;
+	// _imageMap = new GHashMap();
+	// _textureMap = new GHashMap();
+
+	constructor() {
+		this.className = 'CanvasRenderingContext2D';
+		//this.save()
+	}
+
+	setFillStyle(value) {
+		this.fillStyle = value;
+	}
+
+	set fillStyle(value) {
+		this._fillStyle = value;
+
+		if (typeof(value) == 'string') {
+			this._drawCommands = this._drawCommands.concat("F" + value + ";");
+		} else if (value instanceof FillStylePattern) {
+			const image = value._img;
+			if (!image.complete) {
+				image.onload = () => {
+					var index = this._needRedrawImageCache.indexOf(image);
+					if (index > -1) {
+						this._needRedrawImageCache.splice(index, 1);
+						CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+						this._redrawflush(true);
+					}
+				}
+				this._notCommitDrawImageCache.push(image);
+			} else {
+				CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+			}
+
+			//CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+			this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";");
+		} else if (value instanceof FillStyleLinearGradient) {
+			var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," +
+				value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," +
+				value._stop_count;
+			for (var i = 0; i < value._stop_count; ++i) {
+				command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
+			}
+			this._drawCommands = this._drawCommands.concat(command + ";");
+		} else if (value instanceof FillStyleRadialGradient) {
+			var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r
+				.toFixed(2) + "," +
+				value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," + value._end_pos._r.toFixed(2) + "," +
+				value._stop_count;
+			for (var i = 0; i < value._stop_count; ++i) {
+				command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
+			}
+			this._drawCommands = this._drawCommands.concat(command + ";");
+		}
+	}
+
+	get fillStyle() {
+		return this._fillStyle;
+	}
+
+	get globalAlpha() {
+		return this._globalAlpha;
+	}
+
+	setGlobalAlpha(value) {
+		this.globalAlpha = value;
+	}
+
+	set globalAlpha(value) {
+		this._globalAlpha = value;
+		this._drawCommands = this._drawCommands.concat("a" + value.toFixed(2) + ";");
+	}
+
+
+	get strokeStyle() {
+		return this._strokeStyle;
+	}
+
+	setStrokeStyle(value) {
+		this.strokeStyle = value;
+	}
+
+	set strokeStyle(value) {
+
+		this._strokeStyle = value;
+
+		if (typeof(value) == 'string') {
+			this._drawCommands = this._drawCommands.concat("S" + value + ";");
+		} else if (value instanceof FillStylePattern) {
+			CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+			this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";");
+		} else if (value instanceof FillStyleLinearGradient) {
+			var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," +
+				value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," +
+				value._stop_count;
+
+			for (var i = 0; i < value._stop_count; ++i) {
+				command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
+			}
+			this._drawCommands = this._drawCommands.concat(command + ";");
+		} else if (value instanceof FillStyleRadialGradient) {
+			var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r
+				.toFixed(2) + "," +
+				value._end_pos._x.toFixed(2) + "," + value._end_pos._y + ",".toFixed(2) + value._end_pos._r.toFixed(2) + "," +
+				value._stop_count;
+
+			for (var i = 0; i < value._stop_count; ++i) {
+				command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
+			}
+			this._drawCommands = this._drawCommands.concat(command + ";");
+		}
+	}
+
+	get lineWidth() {
+		return this._lineWidth;
+	}
+
+	setLineWidth(value) {
+		this.lineWidth = value;
+	}
+
+	set lineWidth(value) {
+		this._lineWidth = value;
+		this._drawCommands = this._drawCommands.concat("W" + value + ";");
+	}
+
+	get lineCap() {
+		return this._lineCap;
+	}
+
+	setLineCap(value) {
+		this.lineCap = value;
+	}
+
+	set lineCap(value) {
+		this._lineCap = value;
+		this._drawCommands = this._drawCommands.concat("C" + value + ";");
+	}
+
+	get lineJoin() {
+		return this._lineJoin;
+	}
+
+	setLineJoin(value) {
+		this.lineJoin = value
+	}
+
+	set lineJoin(value) {
+		this._lineJoin = value;
+		this._drawCommands = this._drawCommands.concat("J" + value + ";");
+	}
+
+	get miterLimit() {
+		return this._miterLimit;
+	}
+
+	setMiterLimit(value) {
+		this.miterLimit = value
+	}
+
+	set miterLimit(value) {
+		this._miterLimit = value;
+		this._drawCommands = this._drawCommands.concat("M" + value + ";");
+	}
+
+	get globalCompositeOperation() {
+		return this._globalCompositeOperation;
+	}
+
+	set globalCompositeOperation(value) {
+
+		this._globalCompositeOperation = value;
+		let mode = 0;
+		switch (value) {
+			case "source-over":
+				mode = 0;
+				break;
+			case "source-atop":
+				mode = 5;
+				break;
+			case "source-in":
+				mode = 0;
+				break;
+			case "source-out":
+				mode = 2;
+				break;
+			case "destination-over":
+				mode = 4;
+				break;
+			case "destination-atop":
+				mode = 4;
+				break;
+			case "destination-in":
+				mode = 4;
+				break;
+			case "destination-out":
+				mode = 3;
+				break;
+			case "lighter":
+				mode = 1;
+				break;
+			case "copy":
+				mode = 2;
+				break;
+			case "xor":
+				mode = 6;
+				break;
+			default:
+				mode = 0;
+		}
+
+		this._drawCommands = this._drawCommands.concat("B" + mode + ";");
+	}
+
+	get textAlign() {
+		return this._textAlign;
+	}
+
+	setTextAlign(value) {
+		this.textAlign = value
+	}
+
+	set textAlign(value) {
+
+		this._textAlign = value;
+		let Align = 0;
+		switch (value) {
+			case "start":
+				Align = 0;
+				break;
+			case "end":
+				Align = 1;
+				break;
+			case "left":
+				Align = 2;
+				break;
+			case "center":
+				Align = 3;
+				break;
+			case "right":
+				Align = 4;
+				break;
+			default:
+				Align = 0;
+		}
+
+		this._drawCommands = this._drawCommands.concat("A" + Align + ";");
+	}
+
+	get textBaseline() {
+		return this._textBaseline;
+	}
+
+	setTextBaseline(value) {
+		this.textBaseline = value
+	}
+
+	set textBaseline(value) {
+		this._textBaseline = value;
+		let baseline = 0;
+		switch (value) {
+			case "alphabetic":
+				baseline = 0;
+				break;
+			case "middle":
+				baseline = 1;
+				break;
+			case "top":
+				baseline = 2;
+				break;
+			case "hanging":
+				baseline = 3;
+				break;
+			case "bottom":
+				baseline = 4;
+				break;
+			case "ideographic":
+				baseline = 5;
+				break;
+			default:
+				baseline = 0;
+				break;
+		}
+
+		this._drawCommands = this._drawCommands.concat("E" + baseline + ";");
+	}
+
+	get font() {
+		return this._font;
+	}
+
+	setFontSize(size) {
+		var str = this._font;
+		var strs = str.trim().split(/\s+/);
+		for (var i = 0; i < strs.length; i++) {
+			var values = ["normal", "italic", "oblique", "normal", "small-caps", "normal", "bold",
+				"bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", "900",
+				"normal", "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
+				"semi-expanded", "expanded", "extra-expanded", "ultra-expanded"
+			];
+
+			if (-1 == values.indexOf(strs[i].trim())) {
+				if (typeof size === 'string') {
+					strs[i] = size;
+				} else if (typeof size === 'number') {
+					strs[i] = String(size) + 'px';
+				}
+				break;
+			}
+		}
+		this.font = strs.join(" ");
+	}
+
+	set font(value) {
+		this._font = value;
+		this._drawCommands = this._drawCommands.concat("j" + value + ";");
+	}
+
+	setTransform(a, b, c, d, tx, ty) {
+		this._drawCommands = this._drawCommands.concat("t" +
+			(a === 1 ? "1" : a.toFixed(2)) + "," +
+			(b === 0 ? "0" : b.toFixed(2)) + "," +
+			(c === 0 ? "0" : c.toFixed(2)) + "," +
+			(d === 1 ? "1" : d.toFixed(2)) + "," + tx.toFixed(2) + "," + ty.toFixed(2) + ";");
+	}
+
+	transform(a, b, c, d, tx, ty) {
+		this._drawCommands = this._drawCommands.concat("f" +
+			(a === 1 ? "1" : a.toFixed(2)) + "," +
+			(b === 0 ? "0" : b.toFixed(2)) + "," +
+			(c === 0 ? "0" : c.toFixed(2)) + "," +
+			(d === 1 ? "1" : d.toFixed(2)) + "," + tx + "," + ty + ";");
+	}
+
+	resetTransform() {
+		this._drawCommands = this._drawCommands.concat("m;");
+	}
+
+	scale(a, d) {
+		this._drawCommands = this._drawCommands.concat("k" + a.toFixed(2) + "," +
+			d.toFixed(2) + ";");
+	}
+
+	rotate(angle) {
+		this._drawCommands = this._drawCommands
+			.concat("r" + angle.toFixed(6) + ";");
+	}
+
+	translate(tx, ty) {
+		this._drawCommands = this._drawCommands.concat("l" + tx.toFixed(2) + "," + ty.toFixed(2) + ";");
+	}
+
+	save() {
+		this._savedGlobalAlpha.push(this._globalAlpha);
+		this._drawCommands = this._drawCommands.concat("v;");
+	}
+
+	restore() {
+		this._drawCommands = this._drawCommands.concat("e;");
+		this._globalAlpha = this._savedGlobalAlpha.pop();
+	}
+
+	createPattern(img, pattern) {
+		if (typeof img === 'string') {
+			var imgObj = new GImage();
+			imgObj.src = img;
+			img = imgObj;
+		}
+		return new FillStylePattern(img, pattern);
+	}
+
+	createLinearGradient(x0, y0, x1, y1) {
+		return new FillStyleLinearGradient(x0, y0, x1, y1);
+	}
+
+	createRadialGradient = function(x0, y0, r0, x1, y1, r1) {
+		return new FillStyleRadialGradient(x0, y0, r0, x1, y1, r1);
+	};
+
+	createCircularGradient = function(x0, y0, r0) {
+		return new FillStyleRadialGradient(x0, y0, 0, x0, y0, r0);
+	};
+
+	strokeRect(x, y, w, h) {
+		this._drawCommands = this._drawCommands.concat("s" + x + "," + y + "," + w + "," + h + ";");
+	}
+
+
+	clearRect(x, y, w, h) {
+		this._drawCommands = this._drawCommands.concat("c" + x + "," + y + "," + w +
+			"," + h + ";");
+	}
+
+	clip() {
+		this._drawCommands = this._drawCommands.concat("p;");
+	}
+
+	resetClip() {
+		this._drawCommands = this._drawCommands.concat("q;");
+	}
+
+	closePath() {
+		this._drawCommands = this._drawCommands.concat("o;");
+	}
+
+	moveTo(x, y) {
+		this._drawCommands = this._drawCommands.concat("g" + x.toFixed(2) + "," + y.toFixed(2) + ";");
+	}
+
+	lineTo(x, y) {
+		this._drawCommands = this._drawCommands.concat("i" + x.toFixed(2) + "," + y.toFixed(2) + ";");
+	}
+
+	quadraticCurveTo = function(cpx, cpy, x, y) {
+		this._drawCommands = this._drawCommands.concat("u" + cpx + "," + cpy + "," + x + "," + y + ";");
+	}
+
+	bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, ) {
+		this._drawCommands = this._drawCommands.concat(
+			"z" + cp1x.toFixed(2) + "," + cp1y.toFixed(2) + "," + cp2x.toFixed(2) + "," + cp2y.toFixed(2) + "," +
+			x.toFixed(2) + "," + y.toFixed(2) + ";");
+	}
+
+	arcTo(x1, y1, x2, y2, radius) {
+		this._drawCommands = this._drawCommands.concat("h" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + radius + ";");
+	}
+
+	beginPath() {
+		this._drawCommands = this._drawCommands.concat("b;");
+	}
+
+
+	fillRect(x, y, w, h) {
+		this._drawCommands = this._drawCommands.concat("n" + x + "," + y + "," + w +
+			"," + h + ";");
+	}
+
+	rect(x, y, w, h) {
+		this._drawCommands = this._drawCommands.concat("w" + x + "," + y + "," + w + "," + h + ";");
+	}
+
+	fill() {
+		this._drawCommands = this._drawCommands.concat("L;");
+	}
+
+	stroke(path) {
+		this._drawCommands = this._drawCommands.concat("x;");
+	}
+
+	arc(x, y, radius, startAngle, endAngle, anticlockwise) {
+
+		let ianticlockwise = 0;
+		if (anticlockwise) {
+			ianticlockwise = 1;
+		}
+
+		this._drawCommands = this._drawCommands.concat(
+			"y" + x.toFixed(2) + "," + y.toFixed(2) + "," +
+			radius.toFixed(2) + "," + startAngle + "," + endAngle + "," + ianticlockwise +
+			";"
+		);
+	}
+
+	fillText(text, x, y) {
+		let tmptext = text.replace(/!/g, "!!");
+		tmptext = tmptext.replace(/,/g, "!,");
+		tmptext = tmptext.replace(/;/g, "!;");
+		this._drawCommands = this._drawCommands.concat("T" + tmptext + "," + x + "," + y + ",0.0;");
+	}
+
+	strokeText = function(text, x, y) {
+		let tmptext = text.replace(/!/g, "!!");
+		tmptext = tmptext.replace(/,/g, "!,");
+		tmptext = tmptext.replace(/;/g, "!;");
+		this._drawCommands = this._drawCommands.concat("U" + tmptext + "," + x + "," + y + ",0.0;");
+	}
+
+	measureText(text) {
+		return CanvasRenderingContext2D.GBridge.measureText(text, this.font, this.componentId);
+	}
+
+	isPointInPath = function(x, y) {
+		throw new Error('GCanvas not supported yet');
+	}
+
+	drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
+		if (typeof image === 'string') {
+			var imgObj = new GImage();
+			imgObj.src = image;
+			image = imgObj;
+		}
+		if (image instanceof GImage) {
+			if (!image.complete) {
+				imgObj.onload = () => {
+					var index = this._needRedrawImageCache.indexOf(image);
+					if (index > -1) {
+						this._needRedrawImageCache.splice(index, 1);
+						CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+						this._redrawflush(true);
+					}
+				}
+				this._notCommitDrawImageCache.push(image);
+			} else {
+				CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
+			}
+			var srcArgs = [image, sx, sy, sw, sh, dx, dy, dw, dh];
+			var args = [];
+			for (var arg in srcArgs) {
+				if (typeof(srcArgs[arg]) != 'undefined') {
+					args.push(srcArgs[arg]);
+				}
+			}
+			this.__drawImage.apply(this, args);
+			//this.__drawImage(image,sx, sy, sw, sh, dx, dy, dw, dh);
+		}
+	}
+
+	__drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
+		const numArgs = arguments.length;
+
+		function drawImageCommands() {
+
+			if (numArgs === 3) {
+				const x = parseFloat(sx) || 0.0;
+				const y = parseFloat(sy) || 0.0;
+
+				return ("d" + image._id + ",0,0," +
+					image.width + "," + image.height + "," +
+					x + "," + y + "," + image.width + "," + image.height + ";");
+			} else if (numArgs === 5) {
+				const x = parseFloat(sx) || 0.0;
+				const y = parseFloat(sy) || 0.0;
+				const width = parseInt(sw) || image.width;
+				const height = parseInt(sh) || image.height;
+
+				return ("d" + image._id + ",0,0," +
+					image.width + "," + image.height + "," +
+					x + "," + y + "," + width + "," + height + ";");
+			} else if (numArgs === 9) {
+				sx = parseFloat(sx) || 0.0;
+				sy = parseFloat(sy) || 0.0;
+				sw = parseInt(sw) || image.width;
+				sh = parseInt(sh) || image.height;
+				dx = parseFloat(dx) || 0.0;
+				dy = parseFloat(dy) || 0.0;
+				dw = parseInt(dw) || image.width;
+				dh = parseInt(dh) || image.height;
+
+				return ("d" + image._id + "," +
+					sx + "," + sy + "," + sw + "," + sh + "," +
+					dx + "," + dy + "," + dw + "," + dh + ";");
+			}
+		}
+		this._drawCommands += drawImageCommands();
+	}
+
+	_flush(reserve, callback) {
+		const commands = this._drawCommands;
+		this._drawCommands = '';
+		CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback);
+		this._needRender = false;
+	}
+
+	_redrawflush(reserve, callback) {
+		const commands = this._redrawCommands;
+		CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback);
+		if (this._needRedrawImageCache.length == 0) {
+			this._redrawCommands = '';
+		}
+	}
+
+	draw(reserve, callback) {
+		if (!reserve) {
+			this._globalAlpha = this._savedGlobalAlpha.pop();
+			this._savedGlobalAlpha.push(this._globalAlpha);
+			this._redrawCommands = this._drawCommands;
+			this._needRedrawImageCache = this._notCommitDrawImageCache;
+			if (this._autoSaveContext) {
+				this._drawCommands = ("v;" + this._drawCommands);
+				this._autoSaveContext = false;
+			} else {
+				this._drawCommands = ("e;X;v;" + this._drawCommands);
+			}
+		} else {
+			this._needRedrawImageCache = this._needRedrawImageCache.concat(this._notCommitDrawImageCache);
+			this._redrawCommands += this._drawCommands;
+			if (this._autoSaveContext) {
+				this._drawCommands = ("v;" + this._drawCommands);
+				this._autoSaveContext = false;
+			}
+		}
+		this._notCommitDrawImageCache = [];
+		if (this._flush) {
+			this._flush(reserve, callback);
+		}
+	}
+
+	getImageData(x, y, w, h, callback) {
+		CanvasRenderingContext2D.GBridge.getImageData(this.componentId, x, y, w, h, function(res) {
+			res.data = Base64ToUint8ClampedArray(res.data);
+			if (typeof(callback) == 'function') {
+				callback(res);
+			}
+		});
+	}
+
+	putImageData(data, x, y, w, h, callback) {
+		if (data instanceof Uint8ClampedArray) {
+			data = ArrayBufferToBase64(data);
+			CanvasRenderingContext2D.GBridge.putImageData(this.componentId, data, x, y, w, h, function(res) {
+				if (typeof(callback) == 'function') {
+					callback(res);
+				}
+			});
+		}
+	}
+
+	toTempFilePath(x, y, width, height, destWidth, destHeight, fileType, quality, callback) {
+		CanvasRenderingContext2D.GBridge.toTempFilePath(this.componentId, x, y, width, height, destWidth, destHeight,
+			fileType, quality,
+			function(res) {
+				if (typeof(callback) == 'function') {
+					callback(res);
+				}
+			});
+	}
+}

+ 11 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ActiveInfo.js

@@ -0,0 +1,11 @@
+export default class WebGLActiveInfo {
+    className = 'WebGLActiveInfo';
+
+    constructor({
+        type, name, size
+    }) {
+        this.type = type;
+        this.name = name;
+        this.size = size;
+    }
+}

+ 21 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Buffer.js

@@ -0,0 +1,21 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLBuffer';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLBuffer {
+    className = name;
+
+    constructor(id) {
+        this.id = id;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 21 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Framebuffer.js

@@ -0,0 +1,21 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLFrameBuffer';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLFramebuffer {
+    className = name;
+
+    constructor(id) {
+        this.id = id;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 298 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLenum.js

@@ -0,0 +1,298 @@
+export default {
+    "DEPTH_BUFFER_BIT": 256,
+    "STENCIL_BUFFER_BIT": 1024,
+    "COLOR_BUFFER_BIT": 16384,
+    "POINTS": 0,
+    "LINES": 1,
+    "LINE_LOOP": 2,
+    "LINE_STRIP": 3,
+    "TRIANGLES": 4,
+    "TRIANGLE_STRIP": 5,
+    "TRIANGLE_FAN": 6,
+    "ZERO": 0,
+    "ONE": 1,
+    "SRC_COLOR": 768,
+    "ONE_MINUS_SRC_COLOR": 769,
+    "SRC_ALPHA": 770,
+    "ONE_MINUS_SRC_ALPHA": 771,
+    "DST_ALPHA": 772,
+    "ONE_MINUS_DST_ALPHA": 773,
+    "DST_COLOR": 774,
+    "ONE_MINUS_DST_COLOR": 775,
+    "SRC_ALPHA_SATURATE": 776,
+    "FUNC_ADD": 32774,
+    "BLEND_EQUATION": 32777,
+    "BLEND_EQUATION_RGB": 32777,
+    "BLEND_EQUATION_ALPHA": 34877,
+    "FUNC_SUBTRACT": 32778,
+    "FUNC_REVERSE_SUBTRACT": 32779,
+    "BLEND_DST_RGB": 32968,
+    "BLEND_SRC_RGB": 32969,
+    "BLEND_DST_ALPHA": 32970,
+    "BLEND_SRC_ALPHA": 32971,
+    "CONSTANT_COLOR": 32769,
+    "ONE_MINUS_CONSTANT_COLOR": 32770,
+    "CONSTANT_ALPHA": 32771,
+    "ONE_MINUS_CONSTANT_ALPHA": 32772,
+    "BLEND_COLOR": 32773,
+    "ARRAY_BUFFER": 34962,
+    "ELEMENT_ARRAY_BUFFER": 34963,
+    "ARRAY_BUFFER_BINDING": 34964,
+    "ELEMENT_ARRAY_BUFFER_BINDING": 34965,
+    "STREAM_DRAW": 35040,
+    "STATIC_DRAW": 35044,
+    "DYNAMIC_DRAW": 35048,
+    "BUFFER_SIZE": 34660,
+    "BUFFER_USAGE": 34661,
+    "CURRENT_VERTEX_ATTRIB": 34342,
+    "FRONT": 1028,
+    "BACK": 1029,
+    "FRONT_AND_BACK": 1032,
+    "TEXTURE_2D": 3553,
+    "CULL_FACE": 2884,
+    "BLEND": 3042,
+    "DITHER": 3024,
+    "STENCIL_TEST": 2960,
+    "DEPTH_TEST": 2929,
+    "SCISSOR_TEST": 3089,
+    "POLYGON_OFFSET_FILL": 32823,
+    "SAMPLE_ALPHA_TO_COVERAGE": 32926,
+    "SAMPLE_COVERAGE": 32928,
+    "NO_ERROR": 0,
+    "INVALID_ENUM": 1280,
+    "INVALID_VALUE": 1281,
+    "INVALID_OPERATION": 1282,
+    "OUT_OF_MEMORY": 1285,
+    "CW": 2304,
+    "CCW": 2305,
+    "LINE_WIDTH": 2849,
+    "ALIASED_POINT_SIZE_RANGE": 33901,
+    "ALIASED_LINE_WIDTH_RANGE": 33902,
+    "CULL_FACE_MODE": 2885,
+    "FRONT_FACE": 2886,
+    "DEPTH_RANGE": 2928,
+    "DEPTH_WRITEMASK": 2930,
+    "DEPTH_CLEAR_VALUE": 2931,
+    "DEPTH_FUNC": 2932,
+    "STENCIL_CLEAR_VALUE": 2961,
+    "STENCIL_FUNC": 2962,
+    "STENCIL_FAIL": 2964,
+    "STENCIL_PASS_DEPTH_FAIL": 2965,
+    "STENCIL_PASS_DEPTH_PASS": 2966,
+    "STENCIL_REF": 2967,
+    "STENCIL_VALUE_MASK": 2963,
+    "STENCIL_WRITEMASK": 2968,
+    "STENCIL_BACK_FUNC": 34816,
+    "STENCIL_BACK_FAIL": 34817,
+    "STENCIL_BACK_PASS_DEPTH_FAIL": 34818,
+    "STENCIL_BACK_PASS_DEPTH_PASS": 34819,
+    "STENCIL_BACK_REF": 36003,
+    "STENCIL_BACK_VALUE_MASK": 36004,
+    "STENCIL_BACK_WRITEMASK": 36005,
+    "VIEWPORT": 2978,
+    "SCISSOR_BOX": 3088,
+    "COLOR_CLEAR_VALUE": 3106,
+    "COLOR_WRITEMASK": 3107,
+    "UNPACK_ALIGNMENT": 3317,
+    "PACK_ALIGNMENT": 3333,
+    "MAX_TEXTURE_SIZE": 3379,
+    "MAX_VIEWPORT_DIMS": 3386,
+    "SUBPIXEL_BITS": 3408,
+    "RED_BITS": 3410,
+    "GREEN_BITS": 3411,
+    "BLUE_BITS": 3412,
+    "ALPHA_BITS": 3413,
+    "DEPTH_BITS": 3414,
+    "STENCIL_BITS": 3415,
+    "POLYGON_OFFSET_UNITS": 10752,
+    "POLYGON_OFFSET_FACTOR": 32824,
+    "TEXTURE_BINDING_2D": 32873,
+    "SAMPLE_BUFFERS": 32936,
+    "SAMPLES": 32937,
+    "SAMPLE_COVERAGE_VALUE": 32938,
+    "SAMPLE_COVERAGE_INVERT": 32939,
+    "COMPRESSED_TEXTURE_FORMATS": 34467,
+    "DONT_CARE": 4352,
+    "FASTEST": 4353,
+    "NICEST": 4354,
+    "GENERATE_MIPMAP_HINT": 33170,
+    "BYTE": 5120,
+    "UNSIGNED_BYTE": 5121,
+    "SHORT": 5122,
+    "UNSIGNED_SHORT": 5123,
+    "INT": 5124,
+    "UNSIGNED_INT": 5125,
+    "FLOAT": 5126,
+    "DEPTH_COMPONENT": 6402,
+    "ALPHA": 6406,
+    "RGB": 6407,
+    "RGBA": 6408,
+    "LUMINANCE": 6409,
+    "LUMINANCE_ALPHA": 6410,
+    "UNSIGNED_SHORT_4_4_4_4": 32819,
+    "UNSIGNED_SHORT_5_5_5_1": 32820,
+    "UNSIGNED_SHORT_5_6_5": 33635,
+    "FRAGMENT_SHADER": 35632,
+    "VERTEX_SHADER": 35633,
+    "MAX_VERTEX_ATTRIBS": 34921,
+    "MAX_VERTEX_UNIFORM_VECTORS": 36347,
+    "MAX_VARYING_VECTORS": 36348,
+    "MAX_COMBINED_TEXTURE_IMAGE_UNITS": 35661,
+    "MAX_VERTEX_TEXTURE_IMAGE_UNITS": 35660,
+    "MAX_TEXTURE_IMAGE_UNITS": 34930,
+    "MAX_FRAGMENT_UNIFORM_VECTORS": 36349,
+    "SHADER_TYPE": 35663,
+    "DELETE_STATUS": 35712,
+    "LINK_STATUS": 35714,
+    "VALIDATE_STATUS": 35715,
+    "ATTACHED_SHADERS": 35717,
+    "ACTIVE_UNIFORMS": 35718,
+    "ACTIVE_ATTRIBUTES": 35721,
+    "SHADING_LANGUAGE_VERSION": 35724,
+    "CURRENT_PROGRAM": 35725,
+    "NEVER": 512,
+    "LESS": 513,
+    "EQUAL": 514,
+    "LEQUAL": 515,
+    "GREATER": 516,
+    "NOTEQUAL": 517,
+    "GEQUAL": 518,
+    "ALWAYS": 519,
+    "KEEP": 7680,
+    "REPLACE": 7681,
+    "INCR": 7682,
+    "DECR": 7683,
+    "INVERT": 5386,
+    "INCR_WRAP": 34055,
+    "DECR_WRAP": 34056,
+    "VENDOR": 7936,
+    "RENDERER": 7937,
+    "VERSION": 7938,
+    "NEAREST": 9728,
+    "LINEAR": 9729,
+    "NEAREST_MIPMAP_NEAREST": 9984,
+    "LINEAR_MIPMAP_NEAREST": 9985,
+    "NEAREST_MIPMAP_LINEAR": 9986,
+    "LINEAR_MIPMAP_LINEAR": 9987,
+    "TEXTURE_MAG_FILTER": 10240,
+    "TEXTURE_MIN_FILTER": 10241,
+    "TEXTURE_WRAP_S": 10242,
+    "TEXTURE_WRAP_T": 10243,
+    "TEXTURE": 5890,
+    "TEXTURE_CUBE_MAP": 34067,
+    "TEXTURE_BINDING_CUBE_MAP": 34068,
+    "TEXTURE_CUBE_MAP_POSITIVE_X": 34069,
+    "TEXTURE_CUBE_MAP_NEGATIVE_X": 34070,
+    "TEXTURE_CUBE_MAP_POSITIVE_Y": 34071,
+    "TEXTURE_CUBE_MAP_NEGATIVE_Y": 34072,
+    "TEXTURE_CUBE_MAP_POSITIVE_Z": 34073,
+    "TEXTURE_CUBE_MAP_NEGATIVE_Z": 34074,
+    "MAX_CUBE_MAP_TEXTURE_SIZE": 34076,
+    "TEXTURE0": 33984,
+    "TEXTURE1": 33985,
+    "TEXTURE2": 33986,
+    "TEXTURE3": 33987,
+    "TEXTURE4": 33988,
+    "TEXTURE5": 33989,
+    "TEXTURE6": 33990,
+    "TEXTURE7": 33991,
+    "TEXTURE8": 33992,
+    "TEXTURE9": 33993,
+    "TEXTURE10": 33994,
+    "TEXTURE11": 33995,
+    "TEXTURE12": 33996,
+    "TEXTURE13": 33997,
+    "TEXTURE14": 33998,
+    "TEXTURE15": 33999,
+    "TEXTURE16": 34000,
+    "TEXTURE17": 34001,
+    "TEXTURE18": 34002,
+    "TEXTURE19": 34003,
+    "TEXTURE20": 34004,
+    "TEXTURE21": 34005,
+    "TEXTURE22": 34006,
+    "TEXTURE23": 34007,
+    "TEXTURE24": 34008,
+    "TEXTURE25": 34009,
+    "TEXTURE26": 34010,
+    "TEXTURE27": 34011,
+    "TEXTURE28": 34012,
+    "TEXTURE29": 34013,
+    "TEXTURE30": 34014,
+    "TEXTURE31": 34015,
+    "ACTIVE_TEXTURE": 34016,
+    "REPEAT": 10497,
+    "CLAMP_TO_EDGE": 33071,
+    "MIRRORED_REPEAT": 33648,
+    "FLOAT_VEC2": 35664,
+    "FLOAT_VEC3": 35665,
+    "FLOAT_VEC4": 35666,
+    "INT_VEC2": 35667,
+    "INT_VEC3": 35668,
+    "INT_VEC4": 35669,
+    "BOOL": 35670,
+    "BOOL_VEC2": 35671,
+    "BOOL_VEC3": 35672,
+    "BOOL_VEC4": 35673,
+    "FLOAT_MAT2": 35674,
+    "FLOAT_MAT3": 35675,
+    "FLOAT_MAT4": 35676,
+    "SAMPLER_2D": 35678,
+    "SAMPLER_CUBE": 35680,
+    "VERTEX_ATTRIB_ARRAY_ENABLED": 34338,
+    "VERTEX_ATTRIB_ARRAY_SIZE": 34339,
+    "VERTEX_ATTRIB_ARRAY_STRIDE": 34340,
+    "VERTEX_ATTRIB_ARRAY_TYPE": 34341,
+    "VERTEX_ATTRIB_ARRAY_NORMALIZED": 34922,
+    "VERTEX_ATTRIB_ARRAY_POINTER": 34373,
+    "VERTEX_ATTRIB_ARRAY_BUFFER_BINDING": 34975,
+    "IMPLEMENTATION_COLOR_READ_TYPE": 35738,
+    "IMPLEMENTATION_COLOR_READ_FORMAT": 35739,
+    "COMPILE_STATUS": 35713,
+    "LOW_FLOAT": 36336,
+    "MEDIUM_FLOAT": 36337,
+    "HIGH_FLOAT": 36338,
+    "LOW_INT": 36339,
+    "MEDIUM_INT": 36340,
+    "HIGH_INT": 36341,
+    "FRAMEBUFFER": 36160,
+    "RENDERBUFFER": 36161,
+    "RGBA4": 32854,
+    "RGB5_A1": 32855,
+    "RGB565": 36194,
+    "DEPTH_COMPONENT16": 33189,
+    "STENCIL_INDEX8": 36168,
+    "DEPTH_STENCIL": 34041,
+    "RENDERBUFFER_WIDTH": 36162,
+    "RENDERBUFFER_HEIGHT": 36163,
+    "RENDERBUFFER_INTERNAL_FORMAT": 36164,
+    "RENDERBUFFER_RED_SIZE": 36176,
+    "RENDERBUFFER_GREEN_SIZE": 36177,
+    "RENDERBUFFER_BLUE_SIZE": 36178,
+    "RENDERBUFFER_ALPHA_SIZE": 36179,
+    "RENDERBUFFER_DEPTH_SIZE": 36180,
+    "RENDERBUFFER_STENCIL_SIZE": 36181,
+    "FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE": 36048,
+    "FRAMEBUFFER_ATTACHMENT_OBJECT_NAME": 36049,
+    "FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL": 36050,
+    "FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE": 36051,
+    "COLOR_ATTACHMENT0": 36064,
+    "DEPTH_ATTACHMENT": 36096,
+    "STENCIL_ATTACHMENT": 36128,
+    "DEPTH_STENCIL_ATTACHMENT": 33306,
+    "NONE": 0,
+    "FRAMEBUFFER_COMPLETE": 36053,
+    "FRAMEBUFFER_INCOMPLETE_ATTACHMENT": 36054,
+    "FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT": 36055,
+    "FRAMEBUFFER_INCOMPLETE_DIMENSIONS": 36057,
+    "FRAMEBUFFER_UNSUPPORTED": 36061,
+    "FRAMEBUFFER_BINDING": 36006,
+    "RENDERBUFFER_BINDING": 36007,
+    "MAX_RENDERBUFFER_SIZE": 34024,
+    "INVALID_FRAMEBUFFER_OPERATION": 1286,
+    "UNPACK_FLIP_Y_WEBGL": 37440,
+    "UNPACK_PREMULTIPLY_ALPHA_WEBGL": 37441,
+    "CONTEXT_LOST_WEBGL": 37442,
+    "UNPACK_COLORSPACE_CONVERSION_WEBGL": 37443,
+    "BROWSER_DEFAULT_WEBGL": 37444
+};

+ 142 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLmethod.js

@@ -0,0 +1,142 @@
+let i = 1;
+
+const GLmethod = {};
+
+GLmethod.activeTexture = i++;         //1
+GLmethod.attachShader = i++;
+GLmethod.bindAttribLocation = i++;
+GLmethod.bindBuffer = i++;
+GLmethod.bindFramebuffer = i++;
+GLmethod.bindRenderbuffer = i++;
+GLmethod.bindTexture = i++;
+GLmethod.blendColor = i++;
+GLmethod.blendEquation = i++;
+GLmethod.blendEquationSeparate = i++; //10
+GLmethod.blendFunc = i++;
+GLmethod.blendFuncSeparate = i++;
+GLmethod.bufferData = i++;
+GLmethod.bufferSubData = i++;
+GLmethod.checkFramebufferStatus = i++;
+GLmethod.clear = i++;
+GLmethod.clearColor = i++;
+GLmethod.clearDepth = i++;
+GLmethod.clearStencil = i++;
+GLmethod.colorMask = i++;              //20
+GLmethod.compileShader = i++;
+GLmethod.compressedTexImage2D = i++;
+GLmethod.compressedTexSubImage2D = i++;
+GLmethod.copyTexImage2D = i++;
+GLmethod.copyTexSubImage2D = i++;
+GLmethod.createBuffer = i++;
+GLmethod.createFramebuffer = i++;
+GLmethod.createProgram = i++;
+GLmethod.createRenderbuffer = i++;
+GLmethod.createShader = i++;           //30
+GLmethod.createTexture = i++;
+GLmethod.cullFace = i++;
+GLmethod.deleteBuffer = i++;
+GLmethod.deleteFramebuffer = i++;
+GLmethod.deleteProgram = i++;
+GLmethod.deleteRenderbuffer = i++;
+GLmethod.deleteShader = i++;
+GLmethod.deleteTexture = i++;
+GLmethod.depthFunc = i++;
+GLmethod.depthMask = i++;              //40
+GLmethod.depthRange = i++;
+GLmethod.detachShader = i++;
+GLmethod.disable = i++;
+GLmethod.disableVertexAttribArray = i++;
+GLmethod.drawArrays = i++;
+GLmethod.drawArraysInstancedANGLE = i++;
+GLmethod.drawElements = i++;
+GLmethod.drawElementsInstancedANGLE = i++;
+GLmethod.enable = i++;
+GLmethod.enableVertexAttribArray = i++;    //50
+GLmethod.flush = i++;
+GLmethod.framebufferRenderbuffer = i++;
+GLmethod.framebufferTexture2D = i++;
+GLmethod.frontFace = i++;
+GLmethod.generateMipmap = i++;
+GLmethod.getActiveAttrib = i++;
+GLmethod.getActiveUniform = i++;
+GLmethod.getAttachedShaders = i++;
+GLmethod.getAttribLocation = i++;
+GLmethod.getBufferParameter = i++;         //60
+GLmethod.getContextAttributes = i++;
+GLmethod.getError = i++;
+GLmethod.getExtension = i++;
+GLmethod.getFramebufferAttachmentParameter = i++;
+GLmethod.getParameter = i++;
+GLmethod.getProgramInfoLog = i++;
+GLmethod.getProgramParameter = i++;
+GLmethod.getRenderbufferParameter = i++;
+GLmethod.getShaderInfoLog = i++;
+GLmethod.getShaderParameter = i++;         //70
+GLmethod.getShaderPrecisionFormat = i++;
+GLmethod.getShaderSource = i++;
+GLmethod.getSupportedExtensions = i++;
+GLmethod.getTexParameter = i++;
+GLmethod.getUniform = i++;
+GLmethod.getUniformLocation = i++;
+GLmethod.getVertexAttrib = i++;
+GLmethod.getVertexAttribOffset = i++;
+GLmethod.isBuffer = i++;
+GLmethod.isContextLost = i++;              //80
+GLmethod.isEnabled = i++;
+GLmethod.isFramebuffer = i++;
+GLmethod.isProgram = i++;
+GLmethod.isRenderbuffer = i++;
+GLmethod.isShader = i++;
+GLmethod.isTexture = i++;
+GLmethod.lineWidth = i++;
+GLmethod.linkProgram = i++;
+GLmethod.pixelStorei = i++;
+GLmethod.polygonOffset = i++;              //90
+GLmethod.readPixels = i++;
+GLmethod.renderbufferStorage = i++;
+GLmethod.sampleCoverage = i++;
+GLmethod.scissor = i++;
+GLmethod.shaderSource = i++;
+GLmethod.stencilFunc = i++;
+GLmethod.stencilFuncSeparate = i++;
+GLmethod.stencilMask = i++;
+GLmethod.stencilMaskSeparate = i++;
+GLmethod.stencilOp = i++;                  //100
+GLmethod.stencilOpSeparate = i++;
+GLmethod.texImage2D = i++;
+GLmethod.texParameterf = i++;
+GLmethod.texParameteri = i++;
+GLmethod.texSubImage2D = i++;
+GLmethod.uniform1f = i++;
+GLmethod.uniform1fv = i++;
+GLmethod.uniform1i = i++;
+GLmethod.uniform1iv = i++;
+GLmethod.uniform2f = i++;                  //110
+GLmethod.uniform2fv = i++;
+GLmethod.uniform2i = i++;
+GLmethod.uniform2iv = i++;
+GLmethod.uniform3f = i++;
+GLmethod.uniform3fv = i++;
+GLmethod.uniform3i = i++;
+GLmethod.uniform3iv = i++;
+GLmethod.uniform4f = i++;
+GLmethod.uniform4fv = i++;
+GLmethod.uniform4i = i++;                  //120
+GLmethod.uniform4iv = i++;
+GLmethod.uniformMatrix2fv = i++;
+GLmethod.uniformMatrix3fv = i++;
+GLmethod.uniformMatrix4fv = i++;
+GLmethod.useProgram = i++;
+GLmethod.validateProgram = i++;
+GLmethod.vertexAttrib1f = i++; //new
+GLmethod.vertexAttrib2f = i++; //new
+GLmethod.vertexAttrib3f = i++; //new
+GLmethod.vertexAttrib4f = i++; //new       //130
+GLmethod.vertexAttrib1fv = i++; //new
+GLmethod.vertexAttrib2fv = i++; //new
+GLmethod.vertexAttrib3fv = i++; //new
+GLmethod.vertexAttrib4fv = i++; //new
+GLmethod.vertexAttribPointer = i++;
+GLmethod.viewport = i++;
+
+export default GLmethod;

+ 23 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLtype.js

@@ -0,0 +1,23 @@
+const GLtype = {};
+
+[
+    "GLbitfield",    
+    "GLboolean",
+    "GLbyte",
+    "GLclampf",
+    "GLenum",
+    "GLfloat",
+    "GLint",
+    "GLintptr",
+    "GLsizei",
+    "GLsizeiptr",
+    "GLshort",
+    "GLubyte",
+    "GLuint",
+    "GLushort"
+].sort().map((typeName, i) => GLtype[typeName] = 1 >> (i + 1));
+
+export default GLtype;
+
+
+

+ 21 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Program.js

@@ -0,0 +1,21 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLProgram';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLProgram {
+    className = name;
+
+    constructor(id) {
+        this.id = id;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 21 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Renderbuffer.js

@@ -0,0 +1,21 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLRenderBuffer';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLRenderbuffer {
+    className = name;
+
+    constructor(id) {
+        this.id = id;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 1191 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/RenderingContext.js

@@ -0,0 +1,1191 @@
+import GLenum from './GLenum';
+import ActiveInfo from './ActiveInfo';
+import Buffer from './Buffer';
+import Framebuffer from './Framebuffer';
+import Renderbuffer from './Renderbuffer';
+import Texture from './Texture';
+import Program from './Program';
+import Shader from './Shader';
+import ShaderPrecisionFormat from './ShaderPrecisionFormat';
+import UniformLocation from './UniformLocation';
+import GLmethod from './GLmethod';
+
+const processArray = (array, checkArrayType = false) => {
+
+    function joinArray(arr, sep) {
+        let res = '';
+        for (let i = 0; i < arr.length; i++) {
+            if (i !== 0) {
+                res += sep;
+            }
+            res += arr[i];
+        }
+        return res;
+    }
+
+    let type = 'Float32Array';
+    if (checkArrayType) {
+        if (array instanceof Uint8Array) {
+            type = 'Uint8Array'
+        } else if (array instanceof Uint16Array) {
+            type = 'Uint16Array';
+        } else if (array instanceof Uint32Array) {
+            type = 'Uint32Array';
+        } else if (array instanceof Float32Array) {
+            type = 'Float32Array';
+        } else {
+            throw new Error('Check array type failed. Array type is ' + typeof array);
+        }
+    }
+
+    const ArrayTypes = {
+        Uint8Array: 1,
+        Uint16Array: 2,
+        Uint32Array: 4,
+        Float32Array: 14
+    };
+    return ArrayTypes[type] + ',' + btoa(joinArray(array, ','))
+}
+
+export default class WebGLRenderingContext {
+
+    // static GBridge = null;
+
+    className = 'WebGLRenderingContext';
+
+    constructor(canvas, type, attrs) {
+        this._canvas = canvas;
+        this._type = type;
+        this._version = 'WebGL 1.0';
+        this._attrs = attrs;
+        this._map = new Map();
+
+        Object.keys(GLenum)
+            .forEach(name => Object.defineProperty(this, name, {
+                value: GLenum[name]
+            }));
+    }
+
+    get canvas() {
+        return this._canvas;
+    }
+
+    activeTexture = function (textureUnit) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.activeTexture + ',' + textureUnit,
+            true
+        );
+    }
+
+    attachShader = function (progarm, shader) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.attachShader + ',' + progarm.id + ',' + shader.id,
+            true
+        );
+    }
+
+    bindAttribLocation = function (program, index, name) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bindAttribLocation + ',' + program.id + ',' + index + ',' + name,
+            true
+        )
+    }
+
+    bindBuffer = function (target, buffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bindBuffer + ',' + target + ',' + (buffer ? buffer.id : 0),
+            true
+        );
+    }
+
+    bindFramebuffer = function (target, framebuffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bindFramebuffer + ',' + target + ',' + (framebuffer ? framebuffer.id : 0),
+            true
+        )
+    }
+
+    bindRenderbuffer = function (target, renderBuffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bindRenderbuffer + ',' + target + ',' + (renderBuffer ? renderBuffer.id : 0),
+            true
+        )
+    }
+
+    bindTexture = function (target, texture) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bindTexture + ',' + target + ',' + (texture ? texture.id : 0),
+            true
+        )
+    }
+
+    blendColor = function (r, g, b, a) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.blendColor + ',' + target + ',' + r + ',' + g + ',' + b + ',' + a,
+            true
+        )
+    }
+
+    blendEquation = function (mode) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.blendEquation + ',' + mode,
+            true
+        )
+    }
+
+    blendEquationSeparate = function (modeRGB, modeAlpha) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.blendEquationSeparate + ',' + modeRGB + ',' + modeAlpha,
+            true
+        )
+    }
+
+
+    blendFunc = function (sfactor, dfactor) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.blendFunc + ',' + sfactor + ',' + dfactor,
+            true
+        );
+    }
+
+    blendFuncSeparate = function (srcRGB, dstRGB, srcAlpha, dstAlpha) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.blendFuncSeparate + ',' + srcRGB + ',' + dstRGB + ',' + srcAlpha + ',' + dstAlpha,
+            true
+        );
+    }
+
+    bufferData = function (target, data, usage) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bufferData + ',' + target + ',' + processArray(data, true) + ',' + usage,
+            true
+        )
+    }
+
+    bufferSubData = function (target, offset, data) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.bufferSubData + ',' + target + ',' + offset + ',' + processArray(data, true),
+            true
+        )
+    }
+
+    checkFramebufferStatus = function (target) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.checkFramebufferStatus + ',' + target
+        );
+        return Number(result);
+    }
+
+    clear = function (mask) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.clear + ',' + mask
+        );
+        this._canvas._needRender = true;
+    }
+
+    clearColor = function (r, g, b, a) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.clearColor + ',' + r + ',' + g + ',' + b,
+            true
+        )
+    }
+
+    clearDepth = function (depth) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.clearDepth + ',' + depth,
+            true
+        )
+    }
+
+    clearStencil = function (s) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.clearStencil + ',' + s
+        );
+    }
+
+    colorMask = function (r, g, b, a) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.colorMask + ',' + r + ',' + g + ',' + b + ',' + a
+        )
+    }
+
+    compileShader = function (shader) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.compileShader + ',' + shader.id,
+            true
+        )
+    }
+
+    compressedTexImage2D = function (target, level, internalformat, width, height, border, pixels) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.compressedTexImage2D + ',' + target + ',' + level + ',' + internalformat + ',' +
+            width + ',' + height + ',' + border + ',' + processArray(pixels),
+            true
+        )
+    }
+
+    compressedTexSubImage2D = function (target, level, xoffset, yoffset, width, height, format, pixels) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.compressedTexSubImage2D + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset + ',' +
+            width + ',' + height + ',' + format + ',' + processArray(pixels),
+            true
+        )
+    }
+
+
+    copyTexImage2D = function (target, level, internalformat, x, y, width, height, border) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.copyTexImage2D + ',' + target + ',' + level + ',' + internalformat + ',' + x + ',' + y + ',' +
+            width + ',' + height + ',' + border,
+            true
+        );
+    }
+
+    copyTexSubImage2D = function (target, level, xoffset, yoffset, x, y, width, height) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.copyTexSubImage2D + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset + ',' + x + ',' + y + ',' +
+            width + ',' + height
+        );
+    }
+
+    createBuffer = function () {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createBuffer + ''
+        );
+        const buffer = new Buffer(result);
+        this._map.set(buffer.uuid(), buffer);
+        return buffer;
+    }
+
+    createFramebuffer = function () {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createFramebuffer + ''
+        );
+        const framebuffer = new Framebuffer(result);
+        this._map.set(framebuffer.uuid(), framebuffer);
+        return framebuffer;
+    }
+
+
+    createProgram = function () {
+        const id = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createProgram + ''
+        );
+        const program = new Program(id);
+        this._map.set(program.uuid(), program);
+        return program;
+    }
+
+    createRenderbuffer = function () {
+        const id = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createRenderbuffer + ''
+        )
+        const renderBuffer = new Renderbuffer(id);
+        this._map.set(renderBuffer.uuid(), renderBuffer);
+        return renderBuffer;
+    }
+
+    createShader = function (type) {
+        const id = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createShader + ',' + type
+        )
+        const shader = new Shader(id, type);
+        this._map.set(shader.uuid(), shader);
+        return shader;
+    }
+
+    createTexture = function () {
+        const id = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.createTexture + ''
+        );
+        const texture = new Texture(id);
+        this._map.set(texture.uuid(), texture);
+        return texture;
+    }
+
+    cullFace = function (mode) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.cullFace + ',' + mode,
+            true
+        )
+    }
+
+
+    deleteBuffer = function (buffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteBuffer + ',' + buffer.id,
+            true
+        )
+    }
+
+    deleteFramebuffer = function (framebuffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteFramebuffer + ',' + framebuffer.id,
+            true
+        )
+    }
+
+    deleteProgram = function (program) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteProgram + ',' + program.id,
+            true
+        )
+    }
+
+    deleteRenderbuffer = function (renderbuffer) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteRenderbuffer + ',' + renderbuffer.id,
+            true
+        )
+    }
+
+    deleteShader = function (shader) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteShader + ',' + shader.id,
+            true
+        )
+    }
+
+    deleteTexture = function (texture) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.deleteTexture + ',' + texture.id,
+            true
+        )
+    }
+
+    depthFunc = function (func) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.depthFunc + ',' + func
+        )
+    }
+
+    depthMask = function (flag) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.depthMask + ',' + Number(flag),
+            true
+        )
+    }
+
+    depthRange = function (zNear, zFar) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.depthRange + ',' + zNear + ',' + zFar,
+            true
+        )
+    }
+
+    detachShader = function (program, shader) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.detachShader + ',' + program.id + ',' + shader.id,
+            true
+        )
+    }
+
+    disable = function (cap) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.disable + ',' + cap,
+            true
+        )
+    }
+
+    disableVertexAttribArray = function (index) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.disableVertexAttribArray + ',' + index,
+            true
+        );
+    }
+
+    drawArrays = function (mode, first, count) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.drawArrays + ',' + mode + ',' + first + ',' + count
+        )
+        this._canvas._needRender = true;
+    }
+
+    drawElements = function (mode, count, type, offset) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.drawElements + ',' + mode + ',' + count + ',' + type + ',' + offset + ';'
+        );
+        this._canvas._needRender = true;
+    }
+
+    enable = function (cap) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.enable + ',' + cap,
+            true
+        );
+    }
+
+    enableVertexAttribArray = function (index) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.enableVertexAttribArray + ',' + index,
+            true
+        )
+    }
+
+
+    flush = function () {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.flush + ''
+        )
+    }
+
+    framebufferRenderbuffer = function (target, attachment, textarget, texture, level) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.framebufferRenderbuffer + ',' + target + ',' + attachment + ',' + textarget + ',' + (texture ? texture.id : 0) + ',' + level,
+            true
+        )
+    }
+
+    framebufferTexture2D = function (target, attachment, textarget, texture, level) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.framebufferTexture2D + ',' + target + ',' + attachment + ',' + textarget + ',' + (texture ? texture.id : 0) + ',' + level,
+            true
+        )
+    }
+
+    frontFace = function (mode) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.frontFace + ',' + mode,
+            true
+        )
+    }
+
+    generateMipmap = function (target) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.generateMipmap + ',' + target,
+            true
+        )
+    }
+
+    getActiveAttrib = function (progarm, index) {
+        const resultString = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getActiveAttrib + ',' + progarm.id + ',' + index
+        )
+        const [type, size, name] = resultString.split(',');
+        return new ActiveInfo({
+            type: Number(type),
+            size: Number(size),
+            name
+        });
+    }
+
+    getActiveUniform = function (progarm, index) {
+        const resultString = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getActiveUniform + ',' + progarm.id + ',' + index
+        );
+        const [type, size, name] = resultString.split(',');
+        return new ActiveInfo({
+            type: Number(type),
+            size: Number(size),
+            name
+        })
+    }
+
+    getAttachedShaders = function (progarm) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getAttachedShaders + ',' + progarm.id
+        );
+        const [type, ...ids] = result;
+        return ids.map(id => this._map.get(Shader.uuid(id)));
+    }
+
+    getAttribLocation = function (progarm, name) {
+        return WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getAttribLocation + ',' + progarm.id + ',' + name
+        )
+    }
+
+    getBufferParameter = function (target, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getBufferParameter + ',' + target + ',' + pname
+        );
+        const [type, res] = getBufferParameter;
+        return res;
+    }
+
+    getError = function () {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getError + ''
+        )
+        return result;
+    }
+
+    getExtension = function (name) {
+        return null;
+    }
+
+    getFramebufferAttachmentParameter = function (target, attachment, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getFramebufferAttachmentParameter + ',' + target + ',' + attachment + ',' + pname
+        )
+        switch (pname) {
+            case GLenum.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME:
+                return this._map.get(Renderbuffer.uuid(result)) || this._map.get(Texture.uuid(result)) || null;
+            default:
+                return result;
+        }
+    }
+
+    getParameter = function (pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getParameter + ',' + pname
+        )
+        switch (pname) {
+            case GLenum.VERSION:
+                return this._version;
+            case GLenum.ARRAY_BUFFER_BINDING: // buffer
+            case GLenum.ELEMENT_ARRAY_BUFFER_BINDING: // buffer
+                return this._map.get(Buffer.uuid(result)) || null;
+            case GLenum.CURRENT_PROGRAM: // program
+                return this._map.get(Program.uuid(result)) || null;
+            case GLenum.FRAMEBUFFER_BINDING: // framebuffer
+                return this._map.get(Framebuffer.uuid(result)) || null;
+            case GLenum.RENDERBUFFER_BINDING: // renderbuffer
+                return this._map.get(Renderbuffer.uuid(result)) || null;
+            case GLenum.TEXTURE_BINDING_2D: // texture
+            case GLenum.TEXTURE_BINDING_CUBE_MAP: // texture
+                return this._map.get(Texture.uuid(result)) || null;
+            case GLenum.ALIASED_LINE_WIDTH_RANGE: // Float32Array
+            case GLenum.ALIASED_POINT_SIZE_RANGE: // Float32Array
+            case GLenum.BLEND_COLOR: // Float32Array
+            case GLenum.COLOR_CLEAR_VALUE: // Float32Array
+            case GLenum.DEPTH_RANGE: // Float32Array
+            case GLenum.MAX_VIEWPORT_DIMS: // Int32Array
+            case GLenum.SCISSOR_BOX: // Int32Array
+            case GLenum.VIEWPORT: // Int32Array            
+            case GLenum.COMPRESSED_TEXTURE_FORMATS: // Uint32Array
+            default:
+                const [type, ...res] = result.split(',');
+                if (res.length === 1) {
+                    return Number(res[0]);
+                } else {
+                    return res.map(Number);
+                }
+        }
+    }
+
+    getProgramInfoLog = function (progarm) {
+        return WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getProgramInfoLog + ',' + progarm.id
+        )
+    }
+
+    getProgramParameter = function (program, pname) {
+        const res = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getProgramParameter + ',' + program.id + ',' + pname
+        );
+
+        const [type, result] = res.split(',').map(i => parseInt(i));
+
+        if (type === 1) {
+            return Boolean(result);
+        } else if (type === 2) {
+            return result;
+        } else {
+            throw new Error('Unrecongized program paramater ' + res + ', type: ' + typeof res);
+        }
+    }
+
+
+    getRenderbufferParameter = function (target, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getRenderbufferParameter + ',' + target + ',' + pname
+        )
+        return result;
+    }
+
+
+    getShaderInfoLog = function (shader) {
+        return WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getShaderInfoLog + ',' + shader.id
+        );
+    }
+
+    getShaderParameter = function (shader, pname) {
+        return WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getShaderParameter + ',' + shader.id + ',' + pname
+        )
+    }
+
+    getShaderPrecisionFormat = function (shaderType, precisionType) {
+        const [rangeMin, rangeMax, precision] = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getShaderPrecisionFormat + ',' + shaderType + ',' + precisionType
+        );
+        const shaderPrecisionFormat = new ShaderPrecisionFormat({
+            rangeMin: Number(rangeMin),
+            rangeMax: Number(rangeMax),
+            precision: Number(precision)
+        });
+        return shaderPrecisionFormat;
+    }
+
+    getShaderSource = function (shader) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getShaderSource + ',' + shader.id
+        );
+        return result;
+    }
+
+    getSupportedExtensions = function () {
+        return Object.keys({});
+    }
+
+    getTexParameter = function (target, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getTexParameter + ',' + target + ',' + pname
+        )
+        return result;
+    }
+
+    getUniformLocation = function (program, name) {
+        const id = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getUniformLocation + ',' + program.id + ',' + name
+        );
+        if (id === -1) {
+            return null;
+        } else {
+            return new UniformLocation(Number(id));
+        }
+    }
+
+    getVertexAttrib = function (index, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getVertexAttrib + ',' + index + ',' + pname
+        );
+        switch (pname) {
+            case GLenum.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
+                return this._map.get(Buffer.uuid(result)) || null;
+            case GLenum.CURRENT_VERTEX_ATTRIB: // Float32Array
+            default:
+                return result;
+        }
+    }
+
+    getVertexAttribOffset = function (index, pname) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.getVertexAttribOffset + ',' + index + ',' + pname
+        )
+        return Number(result);
+    }
+
+    isBuffer = function (buffer) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isBuffer + ',' + buffer.id
+        )
+        return Boolean(result);
+    }
+
+    isContextLost = function () {
+        return false;
+    }
+
+    isEnabled = function (cap) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isEnabled + ',' + cap
+        )
+        return Boolean(result);
+    }
+
+    isFramebuffer = function (framebuffer) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isFramebuffer + ',' + framebuffer.id
+        )
+        return Boolean(result);
+    }
+
+    isProgram = function (program) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isProgram + ',' + program.id
+        )
+        return Boolean(result);
+    }
+
+    isRenderbuffer = function (renderBuffer) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isRenderbuffer + ',' + renderbuffer.id
+        )
+        return Boolean(result);
+    }
+
+    isShader = function (shader) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isShader + ',' + shader.id
+        )
+        return Boolean(result);
+    }
+
+    isTexture = function (texture) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.isTexture + ',' + texture.id
+        );
+        return Boolean(result);
+    }
+
+    lineWidth = function (width) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.lineWidth + ',' + width,
+            true
+        )
+    }
+
+    linkProgram = function (program) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.linkProgram + ',' + program.id,
+            true
+        );
+    }
+
+
+    pixelStorei = function (pname, param) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.pixelStorei + ',' + pname + ',' + Number(param)
+        )
+    }
+
+    polygonOffset = function (factor, units) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.polygonOffset + ',' + factor + ',' + units
+        )
+    }
+
+    readPixels = function (x, y, width, height, format, type, pixels) {
+        const result = WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.readPixels + ',' + x + ',' + y + ',' + width + ',' + height + ',' + format + ',' + type
+        )
+        return result;
+    }
+
+    renderbufferStorage = function (target, internalFormat, width, height) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.renderbufferStorage + ',' + target + ',' + internalFormat + ',' + width + ',' + height,
+            true
+        )
+    }
+
+    sampleCoverage = function (value, invert) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.sampleCoverage + ',' + value + ',' + Number(invert),
+            true
+        )
+    }
+
+    scissor = function (x, y, width, height) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.scissor + ',' + x + ',' + y + ',' + width + ',' + height,
+            true
+        )
+    }
+
+    shaderSource = function (shader, source) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.shaderSource + ',' + shader.id + ',' + source
+        )
+    }
+
+    stencilFunc = function (func, ref, mask) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilFunc + ',' + func + ',' + ref + ',' + mask,
+            true
+        )
+    }
+
+    stencilFuncSeparate = function (face, func, ref, mask) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilFuncSeparate + ',' + face + ',' + func + ',' + ref + ',' + mask,
+            true
+        )
+    }
+
+    stencilMask = function (mask) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilMask + ',' + mask,
+            true
+        )
+    }
+
+    stencilMaskSeparate = function (face, mask) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilMaskSeparate + ',' + face + ',' + mask,
+            true
+        )
+    }
+
+    stencilOp = function (fail, zfail, zpass) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilOp + ',' + fail + ',' + zfail + ',' + zpass
+        )
+    }
+
+    stencilOpSeparate = function (face, fail, zfail, zpass) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.stencilOp + ',' + face + ',' + fail + ',' + zfail + ',' + zpass,
+            true
+        )
+    }
+
+    texImage2D = function (...args) {
+        WebGLRenderingContext.GBridge.texImage2D(this._canvas.id, ...args);
+    }
+
+
+    texParameterf = function (target, pname, param) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.texParameterf + ',' + target + ',' + pname + ',' + param,
+            true
+        )
+    }
+
+    texParameteri = function (target, pname, param) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.texParameteri + ',' + target + ',' + pname + ',' + param
+        )
+    }
+
+    texSubImage2D = function (...args) {
+        WebGLRenderingContext.GBridge.texSubImage2D(this._canvas.id, ...args);
+    }
+
+    uniform1f = function (location, v0) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform1f + ',' + location.id + ',' + v0
+        )
+    }
+
+    uniform1fv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform1fv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform1i = function (location, v0) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform1i + ',' + location.id + ',' + v0,
+            // true
+        )
+    }
+
+    uniform1iv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform1iv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform2f = function (location, v0, v1) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform2f + ',' + location.id + ',' + v0 + ',' + v1,
+            true
+        )
+    }
+
+    uniform2fv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform2fv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform2i = function (location, v0, v1) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform2i + ',' + location.id + ',' + v0 + ',' + v1,
+            true
+        )
+    }
+
+    uniform2iv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform2iv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform3f = function (location, v0, v1, v2) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform3f + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2,
+            true
+        )
+    }
+
+    uniform3fv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform3fv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform3i = function (location, v0, v1, v2) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform3i + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2,
+            true
+        )
+    }
+
+    uniform3iv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform3iv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform4f = function (location, v0, v1, v2, v3) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform4f + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3,
+            true
+        )
+    }
+
+    uniform4fv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform4fv + ',' + location.id + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniform4i = function (location, v0, v1, v2, v3) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform4i + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3,
+            true
+        )
+    }
+
+    uniform4iv = function (location, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniform4iv + ',' + location.id + ',' + processArray(value, true),
+            true
+        )
+    }
+
+    uniformMatrix2fv = function (location, transpose, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniformMatrix2fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniformMatrix3fv = function (location, transpose, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniformMatrix3fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value),
+            true
+        )
+    }
+
+    uniformMatrix4fv = function (location, transpose, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.uniformMatrix4fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value),
+            true
+        );
+    }
+
+    useProgram = function (progarm) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.useProgram + ',' + progarm.id + '',
+            true
+        )
+    }
+
+
+    validateProgram = function (program) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.validateProgram + ',' + program.id,
+            true
+        )
+    }
+
+    vertexAttrib1f = function (index, v0) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib1f + ',' + index + ',' + v0,
+            true
+        )
+    }
+
+    vertexAttrib2f = function (index, v0, v1) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib2f + ',' + index + ',' + v0 + ',' + v1,
+            true
+        )
+    }
+
+    vertexAttrib3f = function (index, v0, v1, v2) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib3f + ',' + index + ',' + v0 + ',' + v1 + ',' + v2,
+            true
+        )
+    }
+
+    vertexAttrib4f = function (index, v0, v1, v2, v3) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib4f + ',' + index + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3,
+            true
+        )
+    }
+
+    vertexAttrib1fv = function (index, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib1fv + ',' + index + ',' + processArray(value),
+            true
+        )
+    }
+
+    vertexAttrib2fv = function (index, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib2fv + ',' + index + ',' + processArray(value),
+            true
+        )
+    }
+
+    vertexAttrib3fv = function (index, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib3fv + ',' + index + ',' + processArray(value),
+            true
+        )
+    }
+
+    vertexAttrib4fv = function (index, value) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttrib4fv + ',' + index + ',' + processArray(value),
+            true
+        )
+    }
+
+    vertexAttribPointer = function (index, size, type, normalized, stride, offset) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.vertexAttribPointer + ',' + index + ',' + size + ',' + type + ',' + Number(normalized) + ',' + stride + ',' + offset,
+            true
+        )
+    }
+
+    viewport = function (x, y, width, height) {
+        WebGLRenderingContext.GBridge.callNative(
+            this._canvas.id,
+            GLmethod.viewport + ',' + x + ',' + y + ',' + width + ',' + height,
+            true
+        )
+    }
+}

+ 22 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Shader.js

@@ -0,0 +1,22 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLShader';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLShader {
+    className = name;
+
+    constructor(id, type) {
+        this.id = id;
+        this.type = type;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 11 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ShaderPrecisionFormat.js

@@ -0,0 +1,11 @@
+export default class WebGLShaderPrecisionFormat {
+    className = 'WebGLShaderPrecisionFormat';
+
+    constructor({
+        rangeMin, rangeMax, precision
+    }) {
+        this.rangeMin = rangeMin;
+        this.rangeMax = rangeMax;
+        this.precision = precision;
+    }
+}

+ 22 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Texture.js

@@ -0,0 +1,22 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLTexture';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLTexture {
+    className = name;
+
+    constructor(id, type) {
+        this.id = id;
+        this.type = type;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 22 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/UniformLocation.js

@@ -0,0 +1,22 @@
+import {getTransferedObjectUUID} from './classUtils';
+
+const name = 'WebGLUniformLocation';
+
+function uuid(id) {
+    return getTransferedObjectUUID(name, id);
+}
+
+export default class WebGLUniformLocation {
+    className = name;
+
+    constructor(id, type) {
+        this.id = id;
+        this.type = type;
+    }
+
+    static uuid = uuid;
+
+    uuid() {
+        return uuid(this.id);
+    }
+}

+ 3 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/classUtils.js

@@ -0,0 +1,3 @@
+export function getTransferedObjectUUID(name, id) {
+    return `${name.toLowerCase()}-${id}`;
+}

+ 74 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js

@@ -0,0 +1,74 @@
+import GContext2D from '../context-2d/RenderingContext';
+import GContextWebGL from '../context-webgl/RenderingContext';
+
+export default class GCanvas {
+
+    // static GBridge = null;
+
+    id = null;
+
+    _needRender = true;
+
+    constructor(id, { disableAutoSwap }) {
+        this.id = id;
+
+        this._disableAutoSwap = disableAutoSwap;
+        if (disableAutoSwap) {
+            this._swapBuffers = () => {
+                GCanvas.GBridge.render(this.id);
+            }
+        }
+    }
+
+    getContext(type) {
+
+        let context = null;
+
+        if (type.match(/webgl/i)) {
+            context = new GContextWebGL(this);
+
+            context.componentId = this.id;
+
+            if (!this._disableAutoSwap) {
+                const render = () => {
+                    if (this._needRender) {
+                        GCanvas.GBridge.render(this.id);
+                        this._needRender = false;
+                    }
+                }
+                setInterval(render, 16);
+            }
+
+            GCanvas.GBridge.callSetContextType(this.id, 1); // 0 for 2d; 1 for webgl
+        } else if (type.match(/2d/i)) {
+            context = new GContext2D(this);
+
+            context.componentId = this.id;
+
+//             const render = ( callback ) => {
+// 
+//                 const commands = context._drawCommands;
+//                 context._drawCommands = '';
+// 
+//                 GCanvas.GBridge.render2d(this.id, commands, callback);
+//                 this._needRender = false;
+//             }
+// 			//draw方法触发
+// 			context._flush = render;
+//             //setInterval(render, 16);
+
+            GCanvas.GBridge.callSetContextType(this.id, 0);
+        } else {
+            throw new Error('not supported context ' + type);
+        }
+
+        return context;
+
+    }
+
+    reset() {
+        GCanvas.GBridge.callReset(this.id);
+    }
+
+
+}

+ 96 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js

@@ -0,0 +1,96 @@
+let incId = 1;
+
+const noop = function () { };
+
+class GImage {
+
+    static GBridge = null;
+
+    constructor() {
+        this._id = incId++;
+        this._width = 0;
+        this._height = 0;
+        this._src = undefined;
+        this._onload = noop;
+        this._onerror = noop;
+        this.complete = false;
+    }
+
+    get width() {
+        return this._width;
+    }
+    set width(v) {
+        this._width = v;
+    }
+
+    get height() {
+        return this._height;
+    }
+
+    set height(v) {
+        this._height = v;
+    }
+
+    get src() {
+        return this._src;
+    }
+
+    set src(v) {
+
+        if (v.startsWith('//')) {
+            v = 'http:' + v;
+        }
+
+        this._src = v;
+
+        GImage.GBridge.perloadImage([this._src, this._id], (data) => {
+            if (typeof data === 'string') {
+                data = JSON.parse(data);
+            }
+            if (data.error) {
+                var evt = { type: 'error', target: this };
+                this.onerror(evt);
+            } else {
+                this.complete = true;
+                this.width = typeof data.width === 'number' ? data.width : 0;
+                this.height = typeof data.height === 'number' ? data.height : 0;
+                var evt = { type: 'load', target: this };
+                this.onload(evt);
+            }
+        });
+    }
+
+    addEventListener(name, listener) {
+        if (name === 'load') {
+            this.onload = listener;
+        } else if (name === 'error') {
+            this.onerror = listener;
+        }
+    }
+
+    removeEventListener(name, listener) {
+        if (name === 'load') {
+            this.onload = noop;
+        } else if (name === 'error') {
+            this.onerror = noop;
+        }
+    }
+
+    get onload() {
+        return this._onload;
+    }
+
+    set onload(v) {
+        this._onload = v;
+    }
+
+    get onerror() {
+        return this._onerror;
+    }
+
+    set onerror(v) {
+        this._onerror = v;
+    }
+}
+
+export default GImage;

+ 24 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js

@@ -0,0 +1,24 @@
+
+export function ArrayBufferToBase64 (buffer) {
+    var binary = '';
+    var bytes = new Uint8ClampedArray(buffer);
+    for (var len = bytes.byteLength, i = 0; i < len; i++) {
+        binary += String.fromCharCode(bytes[i]);
+    }
+    return btoa(binary);
+}
+	
+export function Base64ToUint8ClampedArray(base64String) {
+	const padding = '='.repeat((4 - base64String.length % 4) % 4);
+	const base64 = (base64String + padding)
+		.replace(/\-/g, '+')
+		.replace(/_/g, '/');
+
+	const rawData = atob(base64);
+	const outputArray = new Uint8ClampedArray(rawData.length);
+
+	for (let i = 0; i < rawData.length; ++i) {
+		outputArray[i] = rawData.charCodeAt(i);
+	}
+	return outputArray;
+}

+ 39 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js

@@ -0,0 +1,39 @@
+import GCanvas from './env/canvas';
+import GImage from './env/image';
+
+import GWebGLRenderingContext from './context-webgl/RenderingContext';
+import GContext2D from './context-2d/RenderingContext';
+
+import GBridgeWeex from './bridge/bridge-weex';
+
+export let Image = GImage;
+
+export let WeexBridge = GBridgeWeex;
+
+export function enable(el, { bridge, debug, disableAutoSwap, disableComboCommands } = {}) {
+
+    const GBridge = GImage.GBridge = GCanvas.GBridge = GWebGLRenderingContext.GBridge = GContext2D.GBridge = bridge;
+
+    GBridge.callEnable(el.ref, [
+        0,      // renderMode: 0--RENDERMODE_WHEN_DIRTY, 1--RENDERMODE_CONTINUOUSLY
+        -1,     // hybridLayerType:  0--LAYER_TYPE_NONE 1--LAYER_TYPE_SOFTWARE 2--LAYER_TYPE_HARDWARE
+        false,  // supportScroll
+        false,  // newCanvasMode
+        1,      // compatible
+        'white',// clearColor
+        false   // sameLevel: newCanvasMode = true && true => GCanvasView and Webview is same level
+    ]);
+
+    if (debug === true) {
+        GBridge.callEnableDebug();
+    }
+    if (disableComboCommands) {
+        GBridge.callEnableDisableCombo();
+    }
+
+    var canvas = new GCanvas(el.ref, { disableAutoSwap });
+    canvas.width = el.style.width;
+    canvas.height = el.style.height;
+
+    return canvas;
+};

文件差异内容过多而无法显示
+ 33 - 0
src/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js


+ 79 - 0
src/uni_modules/Sansnn-uQRCode/package.json

@@ -0,0 +1,79 @@
+{
+  "id": "Sansnn-uQRCode",
+  "displayName": "uQRCode 全端二维码生成插件 支持nvue 支持nodejs服务端",
+  "version": "4.0.6",
+  "description": "uQRCode是一款基于Javascript环境开发的二维码生成插件,适用所有Javascript运行环境的前端应用和Node.js。",
+  "keywords": [
+    "二维码",
+    "uQRCode",
+    "qrcode",
+    "qr"
+],
+  "repository": "https://github.com/Sansnn/uQRCode",
+"engines": {
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/uqrcodejs",
+    "type": "sdk-js"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+          "vue2": "y",
+          "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 11 - 1
src/utils/systemInfo.ts

@@ -2,7 +2,17 @@
 // 获取屏幕边界到安全区域距离
 let systemInfo
 let safeAreaInsets
-const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+let menuButtonInfo
+
+// #ifdef MP-WEIXIN
+// 微信小程序平台使用该 API
+menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+// #endif
+
+// #ifndef MP-WEIXIN
+// 其他平台提供默认值
+menuButtonInfo = null
+// #endif
 
 // #ifdef MP-WEIXIN
 // 微信小程序使用新的API

部分文件因为文件数量过多而无法显示