|
|
@@ -1,75 +1,72 @@
|
|
|
<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/index/index?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 class="share-container">
|
|
|
+ <!-- 顶部导航栏(模拟美团风格) -->
|
|
|
+ <!-- <up-status-bar /> -->
|
|
|
+ <CustomNavigationBar :show-back="true" title="优惠券分享" background-color="transparent" text-color="#ffffff"
|
|
|
+ :is-shadow="false" back-icon-color="#ffffff" />
|
|
|
+
|
|
|
+ <!-- 分享图片预览区域 -->
|
|
|
+ <view class="preview-area">
|
|
|
+ <canvas id="shareCanvas" type="2d" canvas-id="shareCanvas"
|
|
|
+ :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }" class="canvas-hide" />
|
|
|
+
|
|
|
+ <!-- 预览图片容器 -->
|
|
|
+ <view v-if="shareImageUrl" class="preview-image-wrapper">
|
|
|
+ <image :src="shareImageUrl" mode="scaleToFill" 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/index/index?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">
|
|
|
+ <button class="share-btn share-friend" open-type="share" :disabled="!shareImageUrl" @tap="handleShareFriend">
|
|
|
+ <u--image class='selected-icon' :src="getImageUrl('@img/share/share1.png')" mode="aspectFit" width="100rpx"
|
|
|
+ height="100rpx">
|
|
|
+ </u--image>
|
|
|
+ <text class="btn-text">微信好友</text>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button class="share-btn share-save" :disabled="!shareImageUrl" @tap="handleSaveImage">
|
|
|
+ <u--image class='selected-icon' :src="getImageUrl('@img/share/share2.png')" mode="aspectFit" width="100rpx"
|
|
|
+ height="100rpx">
|
|
|
+ </u--image>
|
|
|
+ <text class="btn-text">保存图片</text>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button class="share-btn share-link" :disabled="copyLinkLoading" @tap="handleCopyShareLink">
|
|
|
+ <u--image class='selected-icon' :src="getImageUrl('@img/share/share3.png')" mode="aspectFit" width="100rpx"
|
|
|
+ height="100rpx">
|
|
|
+ </u--image>
|
|
|
+ <text class="btn-text">分享链接</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载提示 -->
|
|
|
+ <view v-if="loading" class="loading-mask">
|
|
|
+ <view class="loading-content">
|
|
|
+ <view class="loading-spinner" />
|
|
|
+ <text>生成分享图片中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
@@ -79,7 +76,7 @@ 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 { getCouponShareLink, getWxQcCode, markSuccess } from '@/api/receiveCoupon'
|
|
|
import { useInviteCodeStore } from '@/store/inviteQCCode'
|
|
|
import { parseUrlParams, safeAreaInsets, isPcWeixinMp } from '@/utils'
|
|
|
import { getLastPartAfterSlash } from '@/utils/couponClassFormat'
|
|
|
@@ -87,12 +84,12 @@ 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,
|
|
|
+ style: {
|
|
|
+ navigationBarTitleText: '',
|
|
|
+ navigationStyle: 'custom',
|
|
|
+ }
|
|
|
+ // enableShareAppMessage: true,
|
|
|
+ // enableShareTimeline: true,
|
|
|
})
|
|
|
|
|
|
// 响应式数据
|
|
|
@@ -100,8 +97,12 @@ const uqrcodeRef = ref(null)
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const shareImageUrl = ref('')
|
|
|
+const shareLink = ref('')
|
|
|
+const copyLinkLoading = ref(false)
|
|
|
const canvasWidth = ref(750)
|
|
|
const canvasHeight = ref(1200)
|
|
|
+const posterDesignWidth = 750
|
|
|
+const posterDesignHeight = 1627
|
|
|
const topSafeAreaHeight = safeAreaInsets?.top || 0
|
|
|
const inviteCodeStore = useInviteCodeStore()
|
|
|
const { inviteCode } = storeToRefs(inviteCodeStore)
|
|
|
@@ -113,1317 +114,1456 @@ 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: '',
|
|
|
- }
|
|
|
+ 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()
|
|
|
+ // 获取系统信息
|
|
|
+ systemInfo.value = uni.getSystemInfoSync()
|
|
|
+ if (options.couponId) {
|
|
|
+ shareId.value = options.couponId
|
|
|
+ await send({ templateId: options.couponId })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载后生成分享图片
|
|
|
+ await nextTick()
|
|
|
+ await generateShareImage()
|
|
|
})
|
|
|
|
|
|
function handleBack() {
|
|
|
- uni.navigateBack()
|
|
|
+ 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'))
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
+ 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/index',
|
|
|
- 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
|
|
|
+ // 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/index',
|
|
|
+ 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 || '获取小程序码失败')
|
|
|
}
|
|
|
- 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')
|
|
|
- }
|
|
|
+ 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 gyyShareImage() {
|
|
|
+ return new Promise((resole, reject) => {
|
|
|
+ uqrcodeRef.value.toTempFilePath({
|
|
|
+ success: ({ tempFilePath }) => {
|
|
|
+ resole(tempFilePath);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
|
|
|
// 生成分享图片-本地生活
|
|
|
async function generateShareImage() {
|
|
|
- if (loading.value)
|
|
|
- return
|
|
|
+ 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)
|
|
|
+ loading.value = true
|
|
|
|
|
|
- await nextTick()
|
|
|
+ try {
|
|
|
+ // 计算尺寸
|
|
|
+ const dpr = systemInfo.value.pixelRatio || 1
|
|
|
+ const baseWidth = systemInfo.value.windowWidth || 375
|
|
|
+ const baseHeight = systemInfo.value.windowHeight || Math.round(baseWidth / (posterDesignWidth / posterDesignHeight))
|
|
|
+ canvasWidth.value = baseWidth * dpr
|
|
|
+ canvasHeight.value = baseHeight * dpr
|
|
|
|
|
|
- // 获取canvas实例
|
|
|
- const { canvas, ctx } = await getCanvasInstance()
|
|
|
+ await nextTick()
|
|
|
|
|
|
- // 设置canvas尺寸
|
|
|
- canvas.width = canvasWidth.value
|
|
|
- canvas.height = canvasHeight.value
|
|
|
+ // 获取canvas实例
|
|
|
+ const { canvas, ctx } = await getCanvasInstance()
|
|
|
|
|
|
- // 生成二维码
|
|
|
- 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
|
|
|
- }
|
|
|
+ // 设置canvas尺寸
|
|
|
+ canvas.width = canvasWidth.value
|
|
|
+ canvas.height = canvasHeight.value
|
|
|
|
|
|
- // 绘制完整的分享图片
|
|
|
- await drawCompleteShareImage(ctx, canvas, qrCodeImg)
|
|
|
+ // 生成二维码
|
|
|
+ let qrCodeImg = null
|
|
|
+ try {
|
|
|
+ if (couponDetail.value.platformSource === '1') {
|
|
|
+ qrCodeImg = await getWxacodeBase64()
|
|
|
+ } else {
|
|
|
+ // 广誉源-h5链接-二维码
|
|
|
+ qrCodeImg = await gyyShareImage()
|
|
|
+ }
|
|
|
}
|
|
|
catch (error) {
|
|
|
- console.error('生成分享图片失败:', error)
|
|
|
- uni.showToast({
|
|
|
- title: '生成失败,请重试',
|
|
|
- icon: 'none',
|
|
|
- })
|
|
|
- }
|
|
|
- finally {
|
|
|
- loading.value = false
|
|
|
+ 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()
|
|
|
+ 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 // 缩放比例
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
+ try {
|
|
|
+ const { width, height } = canvas
|
|
|
+ const scale = width / posterDesignWidth // Figma 750宽稿横向缩放
|
|
|
+ const yScale = height / posterDesignHeight // Figma 1627高稿纵向缩放
|
|
|
+
|
|
|
+ // 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. 绘制优惠券顶部文案
|
|
|
+ drawPosterTopContent(ctx, width, scale, yScale)
|
|
|
+
|
|
|
+ // 3. 绘制二维码区域
|
|
|
+ if (couponDetail.value.platformSource === '2') {
|
|
|
+ await drawGyyQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale, yScale)
|
|
|
+ } else {
|
|
|
+ await drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale, yScale)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 绘制二维码下方及底部文案
|
|
|
+ await drawPosterBottomContent(ctx, canvas, width, scale, yScale)
|
|
|
+
|
|
|
+ // 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)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- // 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)
|
|
|
+// 加载远程图片的函数
|
|
|
+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('图片加载事件触发但尺寸无效'))
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 2. 绘制标题
|
|
|
- drawTitle(ctx, width, height, scale)
|
|
|
+ image.onerror = (err) => {
|
|
|
+ clearTimeout(timeout)
|
|
|
+ console.error('加载远程图片失败:', err)
|
|
|
+ reject(err)
|
|
|
+ }
|
|
|
|
|
|
- // 3. 绘制优惠券金额
|
|
|
- drawCouponAmount(ctx, width, height, scale)
|
|
|
+ image.src = res.path
|
|
|
|
|
|
- // 4. 绘制优惠券描述
|
|
|
- drawCouponDescription(ctx, width, height, scale)
|
|
|
+ },
|
|
|
+ fail: (fail) => {
|
|
|
+ reject(new Error('获取图片信息失败'))
|
|
|
+ },
|
|
|
+ })
|
|
|
|
|
|
- // 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)
|
|
|
- }
|
|
|
- })
|
|
|
+function normalizeText(value) {
|
|
|
+ if (value === undefined || value === null)
|
|
|
+ return ''
|
|
|
+
|
|
|
+ const text = String(value).trim()
|
|
|
+ if (!text || text === 'null' || text === 'undefined')
|
|
|
+ return ''
|
|
|
+
|
|
|
+ return text
|
|
|
}
|
|
|
|
|
|
-// 加载远程图片的函数
|
|
|
-async function loadRemoteImage(canvas, imageUrl) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const image = canvas.createImage()
|
|
|
- const timeout = setTimeout(() => {
|
|
|
- reject(new Error('图片加载超时'))
|
|
|
- }, 10000) // 10秒超时
|
|
|
+function getFirstValidField(keys) {
|
|
|
+ for (const key of keys) {
|
|
|
+ const value = normalizeText(couponDetail.value?.[key])
|
|
|
+ if (value)
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+}
|
|
|
|
|
|
- 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 drawCenterEllipsisText(ctx, text, x, y, maxWidth, fontSize, scale, color = '#333333', weight = 400) {
|
|
|
+ let displayText = normalizeText(text)
|
|
|
+ if (!displayText)
|
|
|
+ return
|
|
|
|
|
|
- })
|
|
|
+ ctx.save()
|
|
|
+ ctx.font = `${weight} ${fontSize * scale}px PingFang SC`
|
|
|
+ ctx.fillStyle = color
|
|
|
+ ctx.textAlign = 'center'
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
+
|
|
|
+ if (ctx.measureText(displayText).width > maxWidth) {
|
|
|
+ while (displayText.length > 1 && ctx.measureText(`${displayText}...`).width > maxWidth) {
|
|
|
+ displayText = displayText.slice(0, -1)
|
|
|
+ }
|
|
|
+ displayText = `${displayText}...`
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillText(displayText, x, y)
|
|
|
+ ctx.restore()
|
|
|
}
|
|
|
|
|
|
-// 绘制标题
|
|
|
-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'
|
|
|
+function getPosterTitleText() {
|
|
|
+ const name = normalizeText(couponDetail.value.name)
|
|
|
+ if (name)
|
|
|
+ return name
|
|
|
+
|
|
|
+ if (couponDetail.value.type === '2')
|
|
|
+ return '折扣券'
|
|
|
+ if (couponDetail.value.type === '3')
|
|
|
+ return '满减券'
|
|
|
+ if (couponDetail.value.type === '4')
|
|
|
+ return '兑换券'
|
|
|
+ return '优惠券'
|
|
|
+}
|
|
|
|
|
|
- ctx.fillText(titleText, width / 2, textY * scale)
|
|
|
+function getPosterRuleText() {
|
|
|
+ const minSpendAmount = normalizeText(couponDetail.value.ruleMinSpendAmount)
|
|
|
+ const discountRate = normalizeText(couponDetail.value.ruleDiscountRate)
|
|
|
+ const discountCapAmount = normalizeText(couponDetail.value.ruleDiscountCapAmount)
|
|
|
+ const reductionAmount = normalizeText(couponDetail.value.ruleReductionAmount)
|
|
|
+
|
|
|
+ if (couponDetail.value.type === '2') {
|
|
|
+ const prefix = minSpendAmount && discountRate ? `满${minSpendAmount}元打${discountRate}折` : ''
|
|
|
+ const suffix = discountCapAmount ? `最高优惠${discountCapAmount}元` : ''
|
|
|
+ return [prefix, suffix].filter(Boolean).join(',')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (couponDetail.value.type === '3') {
|
|
|
+ if (minSpendAmount && reductionAmount)
|
|
|
+ return `满${minSpendAmount}元减${reductionAmount}元`
|
|
|
+ if (minSpendAmount)
|
|
|
+ return `满${minSpendAmount}元可用`
|
|
|
+ }
|
|
|
+
|
|
|
+ return ''
|
|
|
}
|
|
|
|
|
|
-// 绘制优惠券金额
|
|
|
-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)
|
|
|
- }
|
|
|
+function getGoodsText() {
|
|
|
+ const relatedName = normalizeText(couponDetail.value.relatedName)
|
|
|
+ if (!relatedName)
|
|
|
+ return '适用商品:全部适用'
|
|
|
|
|
|
- // 恢复默认文本对齐方式
|
|
|
- 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 (couponDetail.value.relatedType === '2')
|
|
|
+ return `适用商品:${getLastPartAfterSlash(relatedName)}`
|
|
|
|
|
|
- if (currentLine && lineCount < maxLines) {
|
|
|
- ctx.fillText(currentLine, width / 2, currentY)
|
|
|
- }
|
|
|
+ return `适用商品:${relatedName}`
|
|
|
+}
|
|
|
|
|
|
- return currentY + lineHeight // 返回下一行的y坐标
|
|
|
- }
|
|
|
+function getMerchantText() {
|
|
|
+ const storeName = normalizeText(couponDetail.value.storeName) || '不限商家'
|
|
|
+ const merchantCount = getMerchantCountValue()
|
|
|
+ return `适用商家:${storeName}${merchantCount ? ` ${merchantCount}+` : ''}`
|
|
|
+}
|
|
|
|
|
|
- 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 = `限部分适用`
|
|
|
- }
|
|
|
- }
|
|
|
+function getMerchantCountValue() {
|
|
|
+ return getFirstValidField([
|
|
|
+ 'merchantCount',
|
|
|
+ 'merchantNum',
|
|
|
+ 'storeCount',
|
|
|
+ 'storeNum',
|
|
|
+ 'shopCount',
|
|
|
+ 'shopNum',
|
|
|
+ 'applicableStoreCount',
|
|
|
+ 'applicableStoreNum',
|
|
|
+ 'applicableMerchantCount',
|
|
|
+ 'applicableMerchantNum',
|
|
|
+ ])
|
|
|
+}
|
|
|
|
|
|
- // 使用自动换行函数绘制
|
|
|
- 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')
|
|
|
+function getMerchantCountText() {
|
|
|
+ const merchantCount = getMerchantCountValue()
|
|
|
+ return merchantCount ? `${merchantCount}+` : ''
|
|
|
}
|
|
|
|
|
|
-// 绘制二维码区域
|
|
|
-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)
|
|
|
+function getMerchantAvatarList() {
|
|
|
+ const avatarSourceKeys = [
|
|
|
+ 'merchantAvatarList',
|
|
|
+ 'merchantLogoList',
|
|
|
+ 'merchantImageList',
|
|
|
+ 'merchantList',
|
|
|
+ 'storeAvatarList',
|
|
|
+ 'storeLogoList',
|
|
|
+ 'storeImageList',
|
|
|
+ 'storeLogoUrlList',
|
|
|
+ 'storeList',
|
|
|
+ 'storeInfoList',
|
|
|
+ 'shopAvatarList',
|
|
|
+ 'shopLogoList',
|
|
|
+ 'shopList',
|
|
|
+ 'applicableStoreLogoList',
|
|
|
+ 'applicableStoreList',
|
|
|
+ ]
|
|
|
+ const avatarFieldKeys = [
|
|
|
+ 'avatar',
|
|
|
+ 'avatarUrl',
|
|
|
+ 'headImg',
|
|
|
+ 'headImgUrl',
|
|
|
+ 'logo',
|
|
|
+ 'logoUrl',
|
|
|
+ 'image',
|
|
|
+ 'imageUrl',
|
|
|
+ 'pic',
|
|
|
+ 'picUrl',
|
|
|
+ 'storeLogo',
|
|
|
+ 'storeLogoUrl',
|
|
|
+ 'storeImage',
|
|
|
+ 'storeImageUrl',
|
|
|
+ 'shopLogo',
|
|
|
+ 'shopLogoUrl',
|
|
|
+ ]
|
|
|
+
|
|
|
+ for (const key of avatarSourceKeys) {
|
|
|
+ const source = couponDetail.value?.[key]
|
|
|
+ let list = []
|
|
|
+
|
|
|
+ if (Array.isArray(source)) {
|
|
|
+ list = source
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ const sourceText = normalizeText(source)
|
|
|
+ if (sourceText) {
|
|
|
+ list = sourceText.split(',').map(item => item.trim()).filter(Boolean)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 创建渐变效果
|
|
|
- const gradient = ctx.createLinearGradient(centerX - qrRadius, centerY - qrRadius, centerX + qrRadius, centerY + qrRadius)
|
|
|
- gradient.addColorStop(0, '#E8F5E9')
|
|
|
- gradient.addColorStop(1, '#C8E6C9')
|
|
|
+ const avatars = list
|
|
|
+ .map((item) => {
|
|
|
+ if (typeof item === 'string')
|
|
|
+ return normalizeImageUrl(item)
|
|
|
|
|
|
- ctx.fillStyle = gradient
|
|
|
- ctx.fill()
|
|
|
- ctx.restore()
|
|
|
+ for (const fieldKey of avatarFieldKeys) {
|
|
|
+ const imageUrl = normalizeImageUrl(item?.[fieldKey])
|
|
|
+ if (imageUrl)
|
|
|
+ return imageUrl
|
|
|
+ }
|
|
|
|
|
|
- // 内层白色边框
|
|
|
- ctx.beginPath()
|
|
|
- ctx.arc(centerX, centerY, qrRadius + 4 * scale, 0, Math.PI * 2)
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
- ctx.fill()
|
|
|
+ return ''
|
|
|
+ })
|
|
|
+ .filter(Boolean)
|
|
|
|
|
|
- // 2. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
|
|
|
- if (qrCodeImg) {
|
|
|
- try {
|
|
|
- let imgSrc = qrCodeImg
|
|
|
- let tempFilePath = ''
|
|
|
+ if (avatars.length)
|
|
|
+ return avatars
|
|
|
+ }
|
|
|
|
|
|
- // 检查是否已经是临时文件路径
|
|
|
- const isTempFile = typeof qrCodeImg === 'string' && !qrCodeImg.startsWith('data:image')
|
|
|
+ return []
|
|
|
+}
|
|
|
|
|
|
- // 如果是临时文件路径,直接使用
|
|
|
- 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 = ''
|
|
|
- }
|
|
|
- }
|
|
|
+function normalizeImageUrl(value) {
|
|
|
+ const imageUrl = normalizeText(value)
|
|
|
+ if (!imageUrl)
|
|
|
+ return ''
|
|
|
|
|
|
- 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('图片尺寸无效')
|
|
|
- }
|
|
|
+ if (/^(https?:|data:image|wxfile:|file:|\/)/.test(imageUrl))
|
|
|
+ return imageUrl
|
|
|
|
|
|
- // 绘制图片
|
|
|
- ctx.save()
|
|
|
- ctx.beginPath()
|
|
|
- ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
|
|
|
- ctx.clip()
|
|
|
- ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
|
|
|
+ return `${import.meta.env.VITE_SERVER_BASEURL}/${imageUrl.replace(/^\/+/, '')}`
|
|
|
+}
|
|
|
|
|
|
+function getAreaText() {
|
|
|
+ const locality = normalizeText(couponDetail.value.locality)
|
|
|
+ return `适用地区:${locality || '不限地区使用不限地区使用不限地区使用不限地区使用不限地区使用不限地区使用'}`
|
|
|
+}
|
|
|
|
|
|
- // 图片绘制成功后,添加一个精致的内边框
|
|
|
- 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()
|
|
|
+function getReceiveLimitText() {
|
|
|
+ const limit = getFirstValidField(['dailyLimit', 'limitQuantity', 'perDayLimit', 'receiveLimit'])
|
|
|
+ return `每日限领${limit || 0}张`
|
|
|
+}
|
|
|
|
|
|
- console.log('二维码绘制成功')
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- console.error('绘制二维码失败:', error)
|
|
|
+function getValidityText() {
|
|
|
+ const validityType = couponDetail.value.validityType
|
|
|
|
|
|
- // 增加更详细的错误日志
|
|
|
- if (typeof qrCodeImg === 'string') {
|
|
|
- console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ if (validityType === '1') {
|
|
|
+ const endTime = normalizeText(couponDetail.value.validEndTime)
|
|
|
+ return endTime ? `有效期至${endTime}` : ''
|
|
|
+ }
|
|
|
|
|
|
- // 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}元可用`
|
|
|
- }
|
|
|
+ if (validityType === '2') {
|
|
|
+ const validDays = normalizeText(couponDetail.value.validDays)
|
|
|
+ return validDays ? `领取后${validDays}日内有效` : ''
|
|
|
+ }
|
|
|
|
|
|
- ctx.fillText(ruleText, width / 2, qrY + qrSize + 65 * scale)
|
|
|
+ return '长期有效'
|
|
|
}
|
|
|
|
|
|
-// 绘制二维码区域(广誉源)
|
|
|
-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
|
|
|
+function drawAmountText(ctx, width, scale, yScale) {
|
|
|
+ const centerX = width / 2
|
|
|
+ const amount = couponDetail.value.type === '2'
|
|
|
+ ? normalizeText(couponDetail.value.ruleDiscountRate)
|
|
|
+ : normalizeText(couponDetail.value.ruleReductionAmount)
|
|
|
+ const unit = couponDetail.value.type === '2' ? '折' : '元'
|
|
|
+
|
|
|
+ if (!amount)
|
|
|
+ return
|
|
|
+
|
|
|
+ const amountFont = `700 ${88 * scale}px D-DIN Exp-DINExp, PingFang SC`
|
|
|
+ const unitFont = `400 ${26 * scale}px PingFang SC`
|
|
|
+ const { amountBaselineY } = getPosterLayout(scale, yScale)
|
|
|
+ const baselineY = amountBaselineY + 20
|
|
|
+ const gap = 3 * scale
|
|
|
+
|
|
|
+ ctx.save()
|
|
|
+ ctx.textAlign = 'left'
|
|
|
+ ctx.fillStyle = '#FF5365'
|
|
|
+ ctx.textBaseline = 'bottom'
|
|
|
+
|
|
|
+ ctx.font = amountFont
|
|
|
+ const amountWidth = ctx.measureText(amount).width
|
|
|
+ ctx.font = unitFont
|
|
|
+ const unitWidth = ctx.measureText(unit).width
|
|
|
+ const startX = centerX - (amountWidth + unitWidth + gap) / 2
|
|
|
+
|
|
|
+ ctx.font = amountFont
|
|
|
+ ctx.fillText(amount, startX, baselineY)
|
|
|
+ ctx.font = unitFont
|
|
|
+ ctx.fillText(unit, startX + amountWidth + gap, baselineY - 14 * scale)
|
|
|
+ ctx.restore()
|
|
|
+}
|
|
|
|
|
|
- // 使用柔和的白色背景,增加卡片层次感
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
- ctx.fill()
|
|
|
+function drawPosterTopContent(ctx, width, scale, yScale) {
|
|
|
+ const centerX = width / 2
|
|
|
+ const { titleY, ruleY } = getPosterLayout(scale, yScale)
|
|
|
|
|
|
- // 清除阴影
|
|
|
- ctx.shadowColor = 'transparent'
|
|
|
- ctx.shadowBlur = 0
|
|
|
- ctx.shadowOffsetY = 0
|
|
|
+ drawCenterEllipsisText(ctx, getPosterTitleText(), centerX, titleY, 460 * scale, 30, scale, '#FF5365', 500)
|
|
|
+ drawAmountText(ctx, width, scale, yScale)
|
|
|
+ drawCenterEllipsisText(ctx, getPosterRuleText(), centerX, ruleY, 333 * scale, 26, scale, '#FF5365')
|
|
|
+}
|
|
|
|
|
|
- // 1. 优化二维码边框:添加双层边框效果(直角正方形)
|
|
|
- const borderWidth = 8 * scale // 外层边框宽度
|
|
|
- const innerBorderWidth = 4 * scale // 内层边框宽度
|
|
|
+async function drawPosterBottomContent(ctx, canvas, width, scale, yScale) {
|
|
|
+ const centerX = width / 2
|
|
|
+ const { infoStartY, footerStartY } = getPosterLayout(scale, yScale)
|
|
|
+ const infoMaxWidth = 520 * scale
|
|
|
+ const footerMaxWidth = 440 * scale
|
|
|
+
|
|
|
+ drawCenterEllipsisText(ctx, getGoodsText(), centerX, infoStartY, infoMaxWidth, 20, scale, '#333333')
|
|
|
+ await drawMerchantLine(ctx, canvas, centerX, infoStartY + 50 * scale, infoMaxWidth, scale)
|
|
|
+ drawCenterEllipsisText(ctx, getAreaText(), centerX, infoStartY + 100 * scale, infoMaxWidth, 20, scale, '#333333')
|
|
|
+ drawCenterEllipsisText(ctx, getReceiveLimitText(), centerX, footerStartY, footerMaxWidth, 24, scale, '#FFFFFF')
|
|
|
+ drawCenterEllipsisText(ctx, getValidityText(), centerX, footerStartY + 46 * scale, footerMaxWidth, 24, scale, '#FFFFFF')
|
|
|
+}
|
|
|
+//头像绘制
|
|
|
+async function drawMerchantLine(ctx, canvas, centerX, y, maxWidth, scale) {
|
|
|
+ const avatars = getMerchantAvatarList().slice(0, 5)
|
|
|
+ const countText = getMerchantCountText()
|
|
|
+
|
|
|
+ const label = '适用商家:'
|
|
|
+ const avatarSize = 36 * scale
|
|
|
+ const overlap = 12 * scale
|
|
|
+ const avatarGroupWidth = avatars.length
|
|
|
+ ? avatarSize + Math.max(avatars.length - 1, 0) * (avatarSize - overlap)
|
|
|
+ : 0
|
|
|
+ const gap = 8 * scale
|
|
|
+
|
|
|
+ ctx.save()
|
|
|
+ ctx.font = `400 ${20 * scale}px PingFang SC`
|
|
|
+ ctx.fillStyle = '#333333'
|
|
|
+ ctx.textAlign = 'left'
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
+
|
|
|
+ const labelWidth = ctx.measureText(label).width
|
|
|
+ const countWidth = countText ? ctx.measureText(countText).width : 0
|
|
|
+ const avatarGap = avatars.length ? 4 * scale : 0
|
|
|
+ const countGap = countText && (avatars.length || labelWidth) ? gap : 0
|
|
|
+ const totalWidth = labelWidth + avatarGap + avatarGroupWidth + (countText ? countGap + countWidth : 0)
|
|
|
+
|
|
|
+ // if (totalWidth > maxWidth || (!avatars.length && !countText)) {
|
|
|
+ // ctx.restore()
|
|
|
+ // drawCenterEllipsisText(ctx, getMerchantText(), centerX, y, maxWidth, 20, scale, '#333333')
|
|
|
+ // return
|
|
|
+ // }
|
|
|
+
|
|
|
+ let currentX = centerX - totalWidth / 2
|
|
|
+
|
|
|
+ ctx.fillText(label, currentX, y)
|
|
|
+ currentX += labelWidth + avatarGap
|
|
|
+ ctx.restore()
|
|
|
+
|
|
|
+ for (let index = 0; index < avatars.length; index++) {
|
|
|
+ const avatarX = currentX + index * (avatarSize - overlap)
|
|
|
+ await drawCircleImage(ctx, canvas, avatars[index], avatarX, y - avatarSize / 2, avatarSize, scale)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (countText) {
|
|
|
+ ctx.save()
|
|
|
+ ctx.font = `400 ${20 * scale}px PingFang SC`
|
|
|
+ ctx.fillStyle = '#333333'
|
|
|
+ ctx.textAlign = 'left'
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
+ ctx.fillText(countText, currentX + avatarGroupWidth + countGap, y)
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // 外层渐变边框
|
|
|
+async function drawCircleImage(ctx, canvas, imageUrl, x, y, size, scale) {
|
|
|
+ try {
|
|
|
+ const image = await loadRemoteImage(canvas, imageUrl)
|
|
|
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.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2)
|
|
|
+ ctx.clip()
|
|
|
+ ctx.drawImage(image, x, y, size, size)
|
|
|
ctx.restore()
|
|
|
|
|
|
- // 内层白色边框
|
|
|
+ ctx.save()
|
|
|
ctx.beginPath()
|
|
|
- // 绘制内层正方形(比二维码大 innerBorderWidth)
|
|
|
- ctx.rect(qrX - innerBorderWidth, qrY - innerBorderWidth, qrSize + innerBorderWidth * 2, qrSize + innerBorderWidth * 2)
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
- ctx.fill()
|
|
|
+ ctx.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2)
|
|
|
+ ctx.lineWidth = 1 * scale
|
|
|
+ ctx.strokeStyle = '#FFFFFF'
|
|
|
+ ctx.stroke()
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.warn('商家头像绘制失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // 2. 绘制二维码图片(保持原有逻辑,增加加载成功后的边框效果)
|
|
|
- if (qrCodeImg) {
|
|
|
- try {
|
|
|
- let imgSrc = qrCodeImg
|
|
|
- let tempFilePath = ''
|
|
|
+function getPosterLayout(scale, yScale = scale) {
|
|
|
+ const cardTop = 370 * yScale
|
|
|
+ const qrTop = 660 * yScale
|
|
|
+ return {
|
|
|
+ cardTop,
|
|
|
+ titleY: 420 * yScale,
|
|
|
+ amountBaselineY: 510 * yScale,
|
|
|
+ ruleY: 578 * yScale,
|
|
|
+ qrTop,
|
|
|
+ qrSize: 220 * scale,
|
|
|
+ infoStartY: 920 * yScale,
|
|
|
+ footerStartY: 1190 * yScale,
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // 检查是否已经是临时文件路径
|
|
|
- const isTempFile = typeof qrCodeImg === 'string' && !qrCodeImg.startsWith('data:image')
|
|
|
+// 绘制二维码区域
|
|
|
+async function drawQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale, yScale) {
|
|
|
+ const { qrTop: qrY, qrSize } = getPosterLayout(scale, yScale)
|
|
|
+ const qrX = (width - qrSize) / 2
|
|
|
+
|
|
|
+ // 清除阴影
|
|
|
+ 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}`
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 如果是临时文件路径,直接使用
|
|
|
- if (isTempFile) {
|
|
|
- tempFilePath = qrCodeImg
|
|
|
+ // 尝试使用临时文件方式
|
|
|
+ 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('图片加载事件触发但尺寸无效'))
|
|
|
+ }
|
|
|
}
|
|
|
- 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 = ''
|
|
|
- }
|
|
|
+ qrImage.onerror = (err) => {
|
|
|
+ clearTimeout(timeout)
|
|
|
+ console.error('加载二维码图片失败:', err)
|
|
|
+ reject(err)
|
|
|
}
|
|
|
|
|
|
- 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('图片尺寸无效')
|
|
|
- }
|
|
|
+ // 优先使用临时文件路径,否则使用base64数据
|
|
|
+ qrImage.src = res.path
|
|
|
+ },
|
|
|
+ fail: (fail) => {
|
|
|
+ reject(new Error('获取图片信息失败'))
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
|
|
|
- // 绘制图片(正方形,不需要裁剪)
|
|
|
- ctx.save()
|
|
|
- ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
|
|
|
+ const finalWidth = qrImage.naturalWidth || qrImage.width
|
|
|
+ const finalHeight = qrImage.naturalHeight || qrImage.height
|
|
|
|
|
|
+ // 最后再检查一次图片尺寸
|
|
|
+ if (finalWidth <= 0 || finalHeight <= 0) {
|
|
|
+ throw new Error('图片尺寸无效')
|
|
|
+ }
|
|
|
|
|
|
- // 图片绘制成功后,添加一个精致的内边框
|
|
|
- ctx.restore()
|
|
|
- ctx.beginPath()
|
|
|
- ctx.rect(qrX, qrY, qrSize, qrSize)
|
|
|
- ctx.lineWidth = 2 * scale
|
|
|
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)'
|
|
|
- ctx.stroke()
|
|
|
+ // 绘制图片
|
|
|
+ ctx.save()
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.arc(centerX, centerY, qrRadius, 0, Math.PI * 2)
|
|
|
+ ctx.clip()
|
|
|
+ ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize)
|
|
|
|
|
|
- console.log('二维码绘制成功')
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- console.error('绘制二维码失败:', error)
|
|
|
|
|
|
- // 增加更详细的错误日志
|
|
|
- if (typeof qrCodeImg === 'string') {
|
|
|
- console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
|
|
|
- }
|
|
|
- }
|
|
|
+ // 图片绘制成功后,添加一个精致的内边框
|
|
|
+ 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)
|
|
|
|
|
|
- // 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 = `长期有效`
|
|
|
+ // 增加更详细的错误日志
|
|
|
+ if (typeof qrCodeImg === 'string') {
|
|
|
+ console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
|
|
|
+ }
|
|
|
}
|
|
|
- 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}元`
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制二维码区域(广誉源)
|
|
|
+async function drawGyyQRCodeArea(ctx, canvas, qrCodeImg, width, height, scale, yScale) {
|
|
|
+ const { qrTop: qrY, qrSize } = getPosterLayout(scale, yScale)
|
|
|
+ const qrX = (width - qrSize) / 2
|
|
|
+
|
|
|
+ // 清除阴影
|
|
|
+ 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('二维码绘制成功')
|
|
|
}
|
|
|
- else if (couponDetail.value.type === '3') {
|
|
|
- ruleText = `满${couponDetail.value.ruleMinSpendAmount}元可用`
|
|
|
+ catch (error) {
|
|
|
+ console.error('绘制二维码失败:', error)
|
|
|
+
|
|
|
+ // 增加更详细的错误日志
|
|
|
+ if (typeof qrCodeImg === 'string') {
|
|
|
+ console.log('二维码数据源类型:', qrCodeImg.startsWith('data:image') ? 'base64' : '临时文件路径')
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- ctx.fillText(ruleText, width / 2, qrY + qrSize + 65 * scale)
|
|
|
}
|
|
|
|
|
|
// 绘制底部信息
|
|
|
function drawFooter(ctx, width, height, scale) {
|
|
|
- // 计算二维码区域的位置
|
|
|
- const qrSize = 200 * scale
|
|
|
- const qrY = 435 * scale
|
|
|
+ // 计算二维码区域的位置
|
|
|
+ const qrSize = 200 * scale
|
|
|
+ const qrY = 435 * scale
|
|
|
|
|
|
- // 底部信息显示在二维码下方,与二维码提示文字保持适当间距
|
|
|
- const footerY = qrY + qrSize + 80 * scale // 在"长按识别小程序码领取"文字下方40*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)
|
|
|
+ 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
|
|
|
+ const path = `pages/home/index?from=share&name=${encodeURIComponent('券中心')}`
|
|
|
+ uni.share({
|
|
|
+ provider: "weixin",
|
|
|
+ scene: "WXSceneSession", // 好友会话
|
|
|
+ type: 5, // 5 代表分享小程序卡片
|
|
|
+ title: "优惠券来了", // 卡片标题
|
|
|
+ summary: "快来看看这张超值券", // 卡片描述
|
|
|
+ imageUrl: shareImageUrl.value, // 卡片封面图(90*90px)
|
|
|
+ miniProgram: {
|
|
|
+ id: "gh_bea9780e04b4", // 小程序的原始ID,以 'gh_' 开头
|
|
|
+ path: path, // 小程序内具体页面路径
|
|
|
+ type: 0, // 0: 正式版, 1: 开发版, 2: 体验版
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ console.log('分享小程序卡片成功', res);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('分享失败', err);
|
|
|
}
|
|
|
+ });
|
|
|
+}
|
|
|
|
|
|
- uni.showShareImageMenu({
|
|
|
- path: shareImageUrl.value,
|
|
|
- needShowEntrance: false,
|
|
|
- success: async (res) => {
|
|
|
- console.log('分享成功', res)
|
|
|
- await markSuccess(shareRecordId.value)
|
|
|
- },
|
|
|
- fail: (err) => {
|
|
|
- console.log('分享失败', err)
|
|
|
- },
|
|
|
- })
|
|
|
+function handleImagePreview() {
|
|
|
+ if (!shareImageUrl.value)
|
|
|
+ return
|
|
|
+
|
|
|
+ uni.previewImage({
|
|
|
+ urls: [shareImageUrl.value],
|
|
|
+ current: shareImageUrl.value,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
// 保存图片到相册
|
|
|
async function handleSaveImage() {
|
|
|
- if (!shareImageUrl.value) {
|
|
|
- uni.showToast({
|
|
|
- title: '请先生成分享图片',
|
|
|
- icon: 'none',
|
|
|
- })
|
|
|
- return
|
|
|
+ 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',
|
|
|
+ })
|
|
|
}
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- uni.showLoading({
|
|
|
- title: '保存中...',
|
|
|
- mask: true,
|
|
|
+async function handleCopyShareLink() {
|
|
|
+ if (copyLinkLoading.value)
|
|
|
+ return
|
|
|
+
|
|
|
+ if (!shareId.value) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '缺少优惠券信息',
|
|
|
+ icon: 'none',
|
|
|
})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ copyLinkLoading.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!shareLink.value) {
|
|
|
+ const result = await getCouponShareLink({
|
|
|
+ templateId: shareId.value,
|
|
|
+ pathUrl: 'pages/home/index',
|
|
|
+ })
|
|
|
+ shareLink.value = result || ''
|
|
|
+ }
|
|
|
|
|
|
- try {
|
|
|
- await uni.saveImageToPhotosAlbum({
|
|
|
- filePath: shareImageUrl.value,
|
|
|
- })
|
|
|
+ if (!shareLink.value) {
|
|
|
+ throw new Error('empty share link')
|
|
|
+ }
|
|
|
|
|
|
- uni.hideLoading()
|
|
|
+ uni.setClipboardData({
|
|
|
+ data: shareLink.value,
|
|
|
+ success: () => {
|
|
|
uni.showToast({
|
|
|
- title: '保存成功',
|
|
|
- icon: 'success',
|
|
|
+ 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',
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '复制失败,请重试',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('获取分享链接失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: '获取分享链接失败,请重试',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ copyLinkLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 长按图片操作
|
|
|
function handleLongPressImage() {
|
|
|
- uni.showActionSheet({
|
|
|
- itemList: ['保存到相册', '分享给好友'],
|
|
|
- success: (res) => {
|
|
|
- if (res.tapIndex === 0) {
|
|
|
- handleSaveImage()
|
|
|
- }
|
|
|
- else if (res.tapIndex === 1) {
|
|
|
- handleShareFriend()
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
+ 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;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
+ 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);
|
|
|
- /* 适配安全区域 */
|
|
|
+ // 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;
|
|
|
+ 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;
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
|
|
|
.back-icon {
|
|
|
- font-size: 36rpx;
|
|
|
- color: #ffffff;
|
|
|
- font-weight: bold;
|
|
|
+ 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;
|
|
|
+ font-family: Inter, Inter;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
|
|
|
/* 右侧占位 */
|
|
|
.right-space {
|
|
|
- width: 60rpx;
|
|
|
+ width: 60rpx;
|
|
|
}
|
|
|
|
|
|
/* 安全区域 */
|
|
|
.safe-top {
|
|
|
- background: linear-gradient(-46deg, #ff7a75 0%, #ed6b66 98%);
|
|
|
+ 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;
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 1;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: stretch;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ padding-left: 0;
|
|
|
+ padding-right: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
/* 预览图片容器 */
|
|
|
.preview-image-wrapper {
|
|
|
- width: 100%;
|
|
|
- // max-width: 600rpx;
|
|
|
- position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
/* 预览图片 */
|
|
|
.qc-code-img {
|
|
|
- position: absolute;
|
|
|
- top: -9999px;
|
|
|
- left: -9999px;
|
|
|
- opacity: 0;
|
|
|
+ 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;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: block;
|
|
|
}
|
|
|
|
|
|
.preview-image:active {
|
|
|
- transform: scale(1.02);
|
|
|
- box-shadow: 0 15rpx 50rpx rgba(0, 0, 0, 0.2);
|
|
|
+ opacity: 0.96;
|
|
|
}
|
|
|
|
|
|
/* 图片操作提示 */
|
|
|
.image-tips {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- margin-top: 20rpx;
|
|
|
- padding: 0 20rpx;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
}
|
|
|
|
|
|
.tip-text {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #666666;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #666666;
|
|
|
}
|
|
|
|
|
|
/* 生成图片按钮容器 */
|
|
|
.generate-btn-container {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- width: 100%;
|
|
|
+ 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;
|
|
|
+ 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);
|
|
|
+ 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;
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ background: transparent;
|
|
|
+ padding: 28rpx 70rpx calc(28rpx + env(safe-area-inset-bottom));
|
|
|
+ z-index: 20;
|
|
|
}
|
|
|
|
|
|
/* 分享按钮 */
|
|
|
.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;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: transparent;
|
|
|
+ border: 0;
|
|
|
+ border-radius: 0;
|
|
|
+ padding: 0;
|
|
|
+ width: 136rpx;
|
|
|
+ line-height: 1;
|
|
|
+ overflow: visible;
|
|
|
}
|
|
|
|
|
|
.share-btn:active {
|
|
|
- background: #fff5f0;
|
|
|
- transform: translateY(2rpx);
|
|
|
+ opacity: 0.82;
|
|
|
}
|
|
|
|
|
|
-/* 分享好友按钮 */
|
|
|
-.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;
|
|
|
+.share-btn::after {
|
|
|
+ border: 0;
|
|
|
}
|
|
|
|
|
|
/* 按钮图标 */
|
|
|
-.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;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #111111;
|
|
|
+ line-height: 38rpx;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-/* 按钮描述 */
|
|
|
-.btn-desc {
|
|
|
- font-size: 22rpx;
|
|
|
- opacity: 0.8;
|
|
|
+.share-btn[disabled] {
|
|
|
+ opacity: 0.55;
|
|
|
+ color: #111111;
|
|
|
+ background: transparent;
|
|
|
}
|
|
|
|
|
|
/* 额外提示 */
|
|
|
.extra-tips {
|
|
|
- background: #fffbe6;
|
|
|
- border: 2rpx solid #ffeaa7;
|
|
|
- border-radius: 15rpx;
|
|
|
- padding: 25rpx;
|
|
|
+ background: #fffbe6;
|
|
|
+ border: 2rpx solid #ffeaa7;
|
|
|
+ border-radius: 15rpx;
|
|
|
+ padding: 25rpx;
|
|
|
}
|
|
|
|
|
|
.tip-content {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #856404;
|
|
|
- line-height: 36rpx;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666666;
|
|
|
}
|
|
|
|
|
|
/* 隐藏canvas */
|
|
|
.canvas-hide {
|
|
|
- position: absolute;
|
|
|
- top: -9999px;
|
|
|
- left: -9999px;
|
|
|
- opacity: 0;
|
|
|
+ position: absolute;
|
|
|
+ top: -9999px;
|
|
|
+ left: -9999px;
|
|
|
+ opacity: 0;
|
|
|
}
|
|
|
|
|
|
/* 动画 */
|
|
|
@keyframes spin {
|
|
|
- 0% {
|
|
|
- transform: rotate(0deg);
|
|
|
- }
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
|
|
|
- 100% {
|
|
|
- transform: rotate(360deg);
|
|
|
- }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
@media screen and (max-height: 1000rpx) {
|
|
|
- .preview-area {
|
|
|
- min-height: 600rpx;
|
|
|
- }
|
|
|
+ .preview-area {
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
|
|
|
- .action-area {
|
|
|
- padding: 30rpx 20rpx;
|
|
|
- }
|
|
|
+ .action-area {
|
|
|
+ padding-top: 24rpx;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|