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