mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-09-20 11:52:02 +08:00
- 新增银行卡管理页面,支持用户查看与管理银行卡。 - 实现添加、删除及设置默认银行卡功能。 - 优化邀请与加入页面,展示邀请人信息并改进用户提示。 - 更新海报与二维码生成逻辑,提升用户体验。 - 重构多个组件,提高可读性与可维护性。
290 lines
7.4 KiB
TypeScript
290 lines
7.4 KiB
TypeScript
import { nextTick } from 'vue'
|
|
import DrawPoster from '@/js_sdk/u-draw-poster'
|
|
|
|
export const POSTER_WIDTH = 630
|
|
export const POSTER_HEIGHT = 1000
|
|
|
|
const QR_SIZE = 180
|
|
const AVATAR_SIZE = 86
|
|
const PADDING_H = 24
|
|
const MEMBER_LEFT = PADDING_H + 16
|
|
const BOTTOM_RATIO = 0.06
|
|
const MEMBER_ROW_GAP = 12
|
|
const NAME_FONT_SIZE = 30
|
|
const SLOGAN_FONT_SIZE = 24
|
|
const NAME_LINE_HEIGHT = 38
|
|
const SLOGAN_LINE_HEIGHT = 32
|
|
|
|
// #ifdef MP-WEIXIN
|
|
const st2 = (size: number) => size * 2
|
|
// #endif
|
|
// #ifndef MP-WEIXIN
|
|
const st2 = (size: number) => size
|
|
// #endif
|
|
|
|
export interface DistributionPosterOptions {
|
|
backgroundImage?: string
|
|
memberAvatar?: string
|
|
memberName?: string
|
|
slogan?: string
|
|
qrImage: string
|
|
textColor?: string
|
|
showMemberInfo?: boolean
|
|
}
|
|
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
export async function resolveImagePath(url: string) {
|
|
if (!url) {
|
|
throw new Error('图片地址无效')
|
|
}
|
|
if (
|
|
url.startsWith('data:') ||
|
|
url.startsWith('wxfile://') ||
|
|
url.startsWith('http://tmp/') ||
|
|
url.startsWith('https://tmp/') ||
|
|
url.startsWith('file://')
|
|
) {
|
|
return url
|
|
}
|
|
const res = await uni.downloadFile({ url })
|
|
if (res.statusCode === 200 && res.tempFilePath) {
|
|
return res.tempFilePath
|
|
}
|
|
throw new Error('图片下载失败,请检查图片域名配置')
|
|
}
|
|
|
|
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
|
|
return new Promise((resolve, reject) => {
|
|
uni.getImageInfo({
|
|
src,
|
|
success: (res) => resolve({ width: res.width, height: res.height }),
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
function calcCoverBox(imgW: number, imgH: number, boxW: number, boxH: number) {
|
|
const scale = Math.max(boxW / imgW, boxH / imgH)
|
|
const width = imgW * scale
|
|
const height = imgH * scale
|
|
return {
|
|
x: (boxW - width) / 2,
|
|
y: (boxH - height) / 2,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
export function clearDrawPosterCache(selector: string) {
|
|
const pages = getCurrentPages()
|
|
const page = pages[pages.length - 1] as any
|
|
if (!page) return
|
|
delete page[`#${selector}__dp`]
|
|
delete page[`${selector}__dp`]
|
|
}
|
|
|
|
async function waitCanvasReady() {
|
|
await nextTick()
|
|
await sleep(150)
|
|
}
|
|
|
|
async function drawCircleImage(ctx: any, src: string, x: number, y: number, size: number) {
|
|
const radius = size / 2
|
|
const cx = x + radius
|
|
const cy = y + radius
|
|
ctx.save()
|
|
ctx.beginPath()
|
|
ctx.arc(st2(cx), st2(cy), st2(radius), 0, Math.PI * 2)
|
|
ctx.clip()
|
|
await ctx.drawImage(src, st2(x), st2(y), st2(size), st2(size))
|
|
ctx.restore()
|
|
ctx.save()
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
|
|
ctx.lineWidth = st2(2)
|
|
ctx.beginPath()
|
|
ctx.arc(st2(cx), st2(cy), st2(radius - 1), 0, Math.PI * 2)
|
|
ctx.stroke()
|
|
ctx.restore()
|
|
}
|
|
|
|
export async function composeDistributionPoster(
|
|
selector: string,
|
|
componentThis: any,
|
|
options: DistributionPosterOptions
|
|
): Promise<string> {
|
|
const {
|
|
backgroundImage = '',
|
|
memberAvatar = '',
|
|
memberName = '分销员',
|
|
slogan = '',
|
|
qrImage,
|
|
textColor = '#1f2a44',
|
|
showMemberInfo = true,
|
|
} = options
|
|
|
|
if (!qrImage) {
|
|
throw new Error('葵花码未生成')
|
|
}
|
|
|
|
clearDrawPosterCache(selector)
|
|
await waitCanvasReady()
|
|
|
|
const dp = await DrawPoster.build({
|
|
selector,
|
|
componentThis,
|
|
loading: false,
|
|
debugging: false,
|
|
})
|
|
|
|
if (!dp?.canvas || !dp?.ctx) {
|
|
throw new Error('画布初始化失败,请重试')
|
|
}
|
|
|
|
// #ifdef MP-WEIXIN
|
|
dp.canvas.width = st2(POSTER_WIDTH)
|
|
dp.canvas.height = st2(POSTER_HEIGHT)
|
|
// #endif
|
|
|
|
const localQr = await resolveImagePath(qrImage)
|
|
let localBg = ''
|
|
if (backgroundImage) {
|
|
try {
|
|
localBg = await resolveImagePath(backgroundImage)
|
|
} catch (error) {
|
|
console.warn('poster background download failed', error)
|
|
}
|
|
}
|
|
let localAvatar = ''
|
|
if (showMemberInfo && memberAvatar) {
|
|
try {
|
|
localAvatar = await resolveImagePath(memberAvatar)
|
|
} catch (error) {
|
|
console.warn('poster avatar download failed', error)
|
|
}
|
|
}
|
|
|
|
const bottomOffset = POSTER_HEIGHT * BOTTOM_RATIO
|
|
const rowBottom = POSTER_HEIGHT - bottomOffset
|
|
const qrX = POSTER_WIDTH - PADDING_H - QR_SIZE
|
|
const qrY = rowBottom - QR_SIZE
|
|
|
|
await dp.draw((ctx: any) => {
|
|
ctx.fillStyle = '#ffffff'
|
|
ctx.fillRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT))
|
|
})
|
|
|
|
if (localBg) {
|
|
await dp.draw(async (ctx: any) => {
|
|
const { width: imgW, height: imgH } = await getImageInfo(localBg)
|
|
const fit = calcCoverBox(imgW, imgH, POSTER_WIDTH, POSTER_HEIGHT)
|
|
await ctx.drawImage(
|
|
localBg,
|
|
st2(fit.x),
|
|
st2(fit.y),
|
|
st2(fit.width),
|
|
st2(fit.height)
|
|
)
|
|
})
|
|
}
|
|
|
|
await dp.draw(async (ctx: any) => {
|
|
ctx.fillStyle = '#ffffff'
|
|
ctx.fillRoundRect(st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE), st2(12))
|
|
await ctx.drawImage(localQr, st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE))
|
|
})
|
|
|
|
if (showMemberInfo) {
|
|
await dp.draw(async (ctx: any) => {
|
|
const textMaxWidth = qrX - MEMBER_LEFT - 16
|
|
const memberBlockHeight =
|
|
AVATAR_SIZE +
|
|
MEMBER_ROW_GAP +
|
|
NAME_LINE_HEIGHT +
|
|
(slogan ? MEMBER_ROW_GAP + SLOGAN_LINE_HEIGHT : 0)
|
|
const blockHeight = Math.max(memberBlockHeight, QR_SIZE)
|
|
const blockTop = rowBottom - blockHeight
|
|
|
|
const avatarX = MEMBER_LEFT
|
|
const avatarY = blockTop
|
|
const nameY = avatarY + AVATAR_SIZE + MEMBER_ROW_GAP
|
|
const sloganY = nameY + NAME_LINE_HEIGHT + MEMBER_ROW_GAP
|
|
|
|
if (localAvatar) {
|
|
await drawCircleImage(ctx, localAvatar, avatarX, avatarY, AVATAR_SIZE)
|
|
} else {
|
|
const radius = AVATAR_SIZE / 2
|
|
ctx.save()
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
|
|
ctx.beginPath()
|
|
ctx.arc(st2(avatarX + radius), st2(avatarY + radius), st2(radius), 0, Math.PI * 2)
|
|
ctx.fill()
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
|
|
ctx.lineWidth = st2(2)
|
|
ctx.stroke()
|
|
ctx.restore()
|
|
}
|
|
|
|
ctx.textAlign = 'left'
|
|
ctx.textBaseline = 'top'
|
|
ctx.fillStyle = textColor
|
|
ctx.font = `bold ${st2(NAME_FONT_SIZE)}px PingFang SC`
|
|
ctx.fillWarpText({
|
|
text: memberName,
|
|
maxWidth: st2(textMaxWidth),
|
|
x: st2(avatarX),
|
|
y: st2(nameY),
|
|
layer: 1,
|
|
lineHeight: st2(NAME_LINE_HEIGHT),
|
|
})
|
|
|
|
if (slogan) {
|
|
ctx.font = `${st2(SLOGAN_FONT_SIZE)}px PingFang SC`
|
|
ctx.fillWarpText({
|
|
text: slogan,
|
|
maxWidth: st2(textMaxWidth),
|
|
x: st2(avatarX),
|
|
y: st2(sloganY),
|
|
layer: 1,
|
|
lineHeight: st2(SLOGAN_LINE_HEIGHT),
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const path = await dp.createImagePath()
|
|
if (!path || path === '---stop createImagePath---') {
|
|
throw new Error('海报图片导出失败')
|
|
}
|
|
return path
|
|
}
|
|
|
|
export function savePosterToAlbum(filePath: string) {
|
|
if (!filePath) return
|
|
uni.saveImageToPhotosAlbum({
|
|
filePath,
|
|
success: () => {
|
|
uni.showToast({ title: '已保存到相册', icon: 'none' })
|
|
},
|
|
fail: (err: any) => {
|
|
const errMsg = err?.errMsg || ''
|
|
if (errMsg.includes('auth deny') || errMsg.includes('authorize')) {
|
|
uni.showModal({
|
|
title: '提示',
|
|
content: '需要您授权保存图片到相册',
|
|
confirmText: '去设置',
|
|
success: (modalRes) => {
|
|
if (modalRes.confirm) {
|
|
uni.openSetting({})
|
|
}
|
|
},
|
|
})
|
|
return
|
|
}
|
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
|
},
|
|
})
|
|
}
|