Files
lilishop-uniapp/pages/mine/distribution/poster.vue
田香琪 c12c76f6f5 feat(distribution): 添加分销员状态检查与加载指示
- 在多个分销相关页面中引入分销员状态检查,确保用户在访问前具备有效分销资格。
- 增加加载指示器以改善用户体验,提示用户正在加载数据。
- 更新相关逻辑以处理分销员资格的不同状态,确保用户在不符合条件时获得适当反馈。
2026-09-09 13:21:53 +08:00

428 lines
11 KiB
Vue

<template>
<view class="wrapper" :style="themeStyle">
<view v-if="pageLoading" class="state-wrap">
<text class="state-text">{{ pageLoadingText }}</text>
</view>
<view v-else-if="pageError" class="state-wrap">
<text class="state-text">{{ pageError }}</text>
<view class="retry-btn" @click="loadPosterSetting">重新加载</view>
</view>
<template v-else>
<view v-if="qrLoading || posterComposing" class="card state-card">
<text class="state-text">{{ posterComposing ? '海报合成中...' : qrLoadingText }}</text>
</view>
<view v-else-if="qrError || composeError" class="card state-card">
<text class="state-text">{{ composeError || qrError }}</text>
<view class="retry-btn" @click="retryGenerate">重新生成</view>
</view>
<image
v-else-if="posterImage"
class="poster-image"
:src="posterImage"
mode="widthFix"
show-menu-by-longpress
@click="previewPoster"
/>
</template>
<view v-if="posterImage" class="hint-fixed">长按保存分享</view>
<view class="canvas-hide">
<!-- #ifdef MP-WEIXIN -->
<canvas type="2d" id="qrCanvas" class="qr-canvas" />
<canvas type="2d" id="posterComposeCanvas" :style="composeCanvasStyle" />
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas canvas-id="qrCanvas" class="qr-canvas" />
<canvas canvas-id="posterComposeCanvas" id="posterComposeCanvas" :style="composeCanvasStyle" />
<!-- #endif -->
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, getCurrentInstance } from 'vue'
import { onLoad, onReady, onShareAppMessage } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import { getPromotionPoster } from '@/api/distribution'
import { assertActiveDistributor } from '@/utils/distributionStatus'
import { getMpCode } from '@/api/goods'
import { parseGoodsImageUrl } from '@/utils/filters.js'
import {
POSTER_WIDTH,
POSTER_HEIGHT,
composeDistributionPoster,
} from '@/utils/distributionPosterCanvas'
const DEFAULT_HOME_PAGE = 'pages/tabbar/home/index'
const QR_CANVAS_SIZE = 280
const COMPOSE_CANVAS_SELECTOR = 'posterComposeCanvas'
function getCanvasPixelRatio() {
return wx.getWindowInfo?.().pixelRatio || wx.getDeviceInfo?.().pixelRatio || 2
}
const instance = getCurrentInstance()
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const composeCanvasStyle = {
width: `${POSTER_WIDTH}px`,
height: `${POSTER_HEIGHT}px`,
}
let canvas2dNode: any = null
const pageLoading = ref(true)
const pageLoadingText = ref('加载海报设置...')
const pageError = ref('')
const qrLoading = ref(false)
const qrLoadingText = ref('葵花码生成中...')
const qrError = ref('')
const qrImage = ref('')
const posterImage = ref('')
const posterComposing = ref(false)
const composeError = ref('')
const posterContext = ref<Record<string, any>>({})
onLoad(() => {
loadPosterSetting()
})
onReady(() => {
// #ifdef MP-WEIXIN
initCanvas2d()
// #endif
})
onShareAppMessage(() => {
const ctx = posterContext.value
const sharePage = normalizeSharePage(ctx.sharePage)
return {
title: ctx.slogan || '邀请你一起逛逛',
path: `/${sharePage}`,
imageUrl: posterImage.value || qrImage.value || '',
}
})
function normalizeSharePage(page?: string) {
const value = String(page || DEFAULT_HOME_PAGE).trim()
return value.replace(/^\//, '')
}
function writeBase64ToTemp(base64: string): Promise<string> {
return new Promise((resolve, reject) => {
const raw = String(base64).trim()
if (!raw || raw.startsWith('{')) {
reject(new Error('葵花码数据无效'))
return
}
const data = raw.replace(/^data:image\/\w+;base64,/, '')
const filePath = `${wx.env.USER_DATA_PATH}/poster_qr_${Date.now()}.png`
wx.getFileSystemManager().writeFile({
filePath,
data,
encoding: 'base64',
success: () => resolve(filePath),
fail: () => reject(new Error('葵花码写入失败')),
})
})
}
function initCanvas2d() {
return new Promise<void>((resolve, reject) => {
if (canvas2dNode) {
resolve()
return
}
uni
.createSelectorQuery()
.in(instance?.proxy)
.select('#qrCanvas')
.fields({ node: true, size: true })
.exec((res: any[]) => {
const node = res?.[0]?.node
if (!node) {
reject(new Error('canvas 节点获取失败'))
return
}
canvas2dNode = node
const dpr = getCanvasPixelRatio()
node.width = QR_CANVAS_SIZE * dpr
node.height = QR_CANVAS_SIZE * dpr
resolve()
})
})
}
function exportQrByCanvas2d(qrPath: string): Promise<string> {
return new Promise((resolve, reject) => {
const canvas = canvas2dNode
const ctx = canvas.getContext('2d')
const dpr = getCanvasPixelRatio()
ctx.scale(dpr, dpr)
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
const img = canvas.createImage()
img.onload = () => {
ctx.drawImage(img, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
setTimeout(() => {
uni.canvasToTempFilePath(
{
canvas,
width: QR_CANVAS_SIZE,
height: QR_CANVAS_SIZE,
destWidth: QR_CANVAS_SIZE,
destHeight: QR_CANVAS_SIZE,
success: (file: any) => resolve(file.tempFilePath),
fail: () => reject(new Error('葵花码导出失败')),
},
instance?.proxy
)
}, 300)
}
img.onerror = () => reject(new Error('葵花码图片加载失败'))
img.src = qrPath
})
}
function exportQrByCanvasLegacy(qrPath: string): Promise<string> {
return new Promise((resolve, reject) => {
const ctx = uni.createCanvasContext('qrCanvas', instance?.proxy)
ctx.setFillStyle('#ffffff')
ctx.fillRect(0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
ctx.drawImage(qrPath, 0, 0, QR_CANVAS_SIZE, QR_CANVAS_SIZE)
ctx.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath(
{
canvasId: 'qrCanvas',
width: QR_CANVAS_SIZE,
height: QR_CANVAS_SIZE,
destWidth: QR_CANVAS_SIZE,
destHeight: QR_CANVAS_SIZE,
success: (file) => resolve(file.tempFilePath),
fail: () => reject(new Error('葵花码导出失败')),
},
instance?.proxy
)
}, 300)
})
})
}
async function exportQrByCanvas(qrPath: string): Promise<string> {
// #ifdef MP-WEIXIN
await initCanvas2d()
return exportQrByCanvas2d(qrPath)
// #endif
// #ifndef MP-WEIXIN
return exportQrByCanvasLegacy(qrPath)
// #endif
}
async function resolveQrDisplay(result: string) {
const tempPath = await writeBase64ToTemp(result)
qrImage.value = await exportQrByCanvas(tempPath)
}
async function composePoster() {
if (!qrImage.value) return
posterComposing.value = true
composeError.value = ''
posterImage.value = ''
try {
const ctx = posterContext.value
posterImage.value = await composeDistributionPoster(
COMPOSE_CANVAS_SELECTOR,
instance?.proxy || instance,
{
backgroundImage: backgroundUrl.value,
memberAvatar: avatarUrl.value,
memberName: ctx.memberName || '分销员',
slogan: ctx.slogan || '',
qrImage: qrImage.value,
textColor: ctx.textColor || '#1f2a44',
showMemberInfo: ctx.memberInfoVisible !== false,
}
)
} catch (error: any) {
console.error('compose poster failed', error)
composeError.value = error?.message || '海报合成失败,请稍后重试'
} finally {
posterComposing.value = false
}
}
async function loadPosterSetting() {
pageLoading.value = true
pageLoadingText.value = '加载海报设置...'
pageError.value = ''
qrImage.value = ''
posterImage.value = ''
qrError.value = ''
composeError.value = ''
const distributor = await assertActiveDistributor()
if (!distributor) {
return
}
try {
const res = await getPromotionPoster()
if (!res.data?.success || !res.data?.result) {
throw new Error(res.data?.message || '获取海报设置失败')
}
const result = res.data.result
posterContext.value = {
distributionId: result.distributionId || '',
memberName: result.memberName || '',
memberAvatar: result.memberAvatar || '',
backgroundImage: result.backgroundImage || '',
slogan: result.slogan || '发现好物,邀请你一起逛逛',
memberInfoVisible: result.memberInfoVisible !== false,
textColor: result.textColor || '#1f2a44',
sharePage: result.sharePage || DEFAULT_HOME_PAGE,
shareScene: result.shareScene || '',
}
} catch (error: any) {
console.error('load poster setting failed', error)
pageError.value = error?.message || '获取海报设置失败,请稍后重试'
return
} finally {
pageLoading.value = false
}
loadQrCode()
}
async function loadQrCode() {
if (!posterContext.value.distributionId && !posterContext.value.shareScene) {
qrError.value = '分销员信息异常,无法生成葵花码'
return
}
qrLoading.value = true
qrLoadingText.value = '葵花码生成中...'
qrError.value = ''
qrImage.value = ''
posterImage.value = ''
composeError.value = ''
try {
const ctx = posterContext.value
const codeRes = await getMpCode({
page: normalizeSharePage(ctx.sharePage),
scene: ctx.shareScene || `bind,${ctx.distributionId}`,
})
if (!codeRes.data?.success || !codeRes.data?.result) {
throw new Error(codeRes.data?.message || '葵花码生成失败')
}
await resolveQrDisplay(codeRes.data.result)
await composePoster()
} catch (error: any) {
console.error('load qr code failed', error)
qrError.value = error?.message || '葵花码生成失败,请稍后重试'
} finally {
qrLoading.value = false
}
}
function retryGenerate() {
if (qrImage.value && composeError.value) {
composePoster()
return
}
loadQrCode()
}
function previewPoster() {
if (!posterImage.value) return
uni.previewImage({
current: posterImage.value,
urls: [posterImage.value],
})
}
const avatarUrl = computed(() => parseGoodsImageUrl(posterContext.value.memberAvatar))
const backgroundUrl = computed(() => parseGoodsImageUrl(posterContext.value.backgroundImage))
</script>
<style lang="scss" scoped>
.wrapper {
min-height: 100vh;
padding: 24rpx;
box-sizing: border-box;
background: #f5f5f5;
}
.state-wrap {
padding: 160rpx 0;
text-align: center;
}
.state-text {
color: #999;
font-size: 28rpx;
}
.retry-btn {
display: inline-block;
margin-top: 24rpx;
padding: 12rpx 40rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
color: #fff;
font-size: 26rpx;
}
.card {
position: relative;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
aspect-ratio: 630 / 1000;
}
.state-card {
display: flex;
align-items: center;
justify-content: center;
}
.poster-image {
width: 100%;
border-radius: 16rpx;
display: block;
}
.hint-fixed {
margin-top: 24rpx;
color: #b3b3b3;
font-size: 24rpx;
text-align: center;
}
.canvas-hide {
position: fixed;
left: -9999px;
top: 0;
opacity: 0;
pointer-events: none;
}
.qr-canvas {
width: 280px;
height: 280px;
}
</style>