zbb 1 Minggu lalu
induk
melakukan
1ec1f6e078
6 mengubah file dengan 140 tambahan dan 48 penghapusan
  1. 3 3
      pages/index/index.vue
  2. 3 2
      pages/mine/card.vue
  3. 3 2
      pages/mine/userCard.vue
  4. 5 2
      store/modules/user.js
  5. 17 0
      utils/index.js
  6. 109 39
      utils/poster.js

+ 3 - 3
pages/index/index.vue

@@ -71,10 +71,10 @@
 						<text class="tab-text">产品列表</text>
 						<view class="tab-indicator" v-if="currentTab === 'products'"></view>
 					</view> -->
-					<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
+				<!-- 	<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
 						<text class="tab-text">企业简介</text>
 						<view class="tab-indicator" v-if="currentTab === 'company'"></view>
-					</view>
+					</view> -->
 				</view>
 			</view>
 			<!-- 产品列表 -->
@@ -773,8 +773,8 @@ import {
 
 				::v-deep img {
 					max-width: 100% !important;
+					height: auto !important;
 				}
-
 				&:last-child {
 					margin-bottom: 0;
 				}

+ 3 - 2
pages/mine/card.vue

@@ -66,10 +66,10 @@
 						<text class="tab-text">产品列表</text>
 						<view class="tab-indicator" v-if="currentTab === 'products'"></view>
 					</view> -->
-					<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
+			<!-- 		<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
 						<text class="tab-text">企业简介</text>
 						<view class="tab-indicator" v-if="currentTab === 'company'"></view>
-					</view>
+					</view> -->
 				</view>
 			</view>
 			<!-- 产品列表 -->
@@ -806,6 +806,7 @@
 
 				::v-deep img {
 					max-width: 100% !important;
+					height: auto !important;
 				}
 
 				&:last-child {

+ 3 - 2
pages/mine/userCard.vue

@@ -65,10 +65,10 @@
 						<text class="tab-text">产品列表</text>
 						<view class="tab-indicator" v-if="currentTab === 'products'"></view>
 					</view> -->
-					<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
+				<!-- 	<view class="tab-item" :class="{ active: currentTab === 'company' }" @click="switchTab('company')">
 						<text class="tab-text">企业简介</text>
 						<view class="tab-indicator" v-if="currentTab === 'company'"></view>
-					</view>
+					</view> -->
 				</view>
 			</view>
 			<!-- 产品列表 -->
@@ -491,6 +491,7 @@
 
 				::v-deep img {
 					max-width: 100% !important;
+					height: auto !important;
 				}
 
 				&:last-child {

+ 5 - 2
store/modules/user.js

@@ -9,7 +9,9 @@ import {
 	getCardQrcode,
 	getCompanyInfo
 } from '@/api/card.js'
-
+import {
+formatRichText
+} from '@/utils/index.js'
 export const useUserStore = defineStore('user', {
 	state: () => ({
 		cardInfo: {
@@ -53,7 +55,8 @@ export const useUserStore = defineStore('user', {
 				} : {}
 				let res = await getCompanyInfo(parmas)
 				this.companyInfo = res.data
-				resolve(res.data)
+				this.companyInfo.introduce = formatRichText(res.data.introduce)
+				resolve(this.companyInfo)
 			})
 		},
 		queryCardQrcode(userId = null) {

+ 17 - 0
utils/index.js

@@ -0,0 +1,17 @@
+export const formatRichText = (html) => {
+    if (!html) return '';
+    let newContent = html;
+    // 1. 移除 img 标签中的 style 属性
+    newContent = newContent.replace(/<img[^>]*>/gi, function(match) {
+        match = match.replace(/style="[^"]+"/gi, '').replace(/style='[^']+'/gi, '');
+        match = match.replace(/width="[^"]+"/gi, '').replace(/width='[^']+'/gi, '');
+        match = match.replace(/height="[^"]+"/gi, '').replace(/height='[^']+'/gi, '');
+        return match;
+    });
+    // 2. 强制为 img 标签添加自适应样式,并处理为块级元素
+    newContent = newContent.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;"');
+    // 3. 将富文本中可能出现的 <text> 标签替换为 <span>
+    newContent = newContent.replace(/<text/gi, '<span').replace(/<\/text>/gi, '</span>');
+    
+    return newContent;
+};

+ 109 - 39
utils/poster.js

@@ -1,6 +1,7 @@
 /**
  * 名片海报生成工具
  * 使用 Canvas 2D 绘制名片海报
+ * 名片底部添加名片码(二维码)
  */
 
 const W = 750
@@ -21,13 +22,20 @@ const roundRect = (ctx, x, y, w, h, r) => {
 	ctx.closePath()
 }
 
-const loadImage = (src) => {
+/**
+ * 创建图片对象 - 必须使用 canvas.createImage()
+ * 微信小程序 Canvas 2D 中不能用 uni.createImage()
+ */
+const createCanvasImage = (canvas, src) => {
 	return new Promise((resolve) => {
 		if (!src) return resolve(null)
-		const img = uni.createImage()
-		img.src = src
+		const img = canvas.createImage()
 		img.onload = () => resolve(img)
-		img.onerror = () => resolve(null)
+		img.onerror = () => {
+			console.warn('图片加载失败:', src)
+			resolve(null)
+		}
+		img.src = src
 	})
 }
 
@@ -40,6 +48,51 @@ const downloadImage = async (url) => {
 	} catch { return null }
 }
 
+/**
+ * 将图片资源转换为Canvas可用的临时路径
+ */
+const resolveImageSrc = async (src) => {
+	if (!src) return null
+	if (src.startsWith('http')) {
+		try {
+			const res = await uni.downloadFile({ url: src })
+			if (res.statusCode === 200) return res.tempFilePath
+			return null
+		} catch { return null }
+	}
+	if (src.startsWith('/') || src.startsWith('.')) {
+		try {
+			const res = await uni.getImageInfo({ src })
+			return res.path
+		} catch { return null }
+	}
+	return src
+}
+
+/**
+ * base64 二维码转临时文件路径
+ */
+const qrBase64ToTempFile = async (base64Data) => {
+	if (!base64Data) return null
+	return new Promise((resolve) => {
+		try {
+			const pureBase64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
+			const fileName = `${Date.now()}_qrcode.png`
+			const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
+			const fs = uni.getFileSystemManager()
+			fs.writeFile({
+				filePath,
+				data: pureBase64,
+				encoding: 'base64',
+				success: () => resolve(filePath),
+				fail: () => resolve(null)
+			})
+		} catch {
+			resolve(null)
+		}
+	})
+}
+
 const drawCircleAvatar = (ctx, img, cx, cy, r) => {
 	ctx.save()
 	ctx.beginPath()
@@ -69,30 +122,25 @@ const drawAvatarFallback = (ctx, cardInfo, cx, cy, r) => {
 
 const drawContactRow = (ctx, emoji, text, x, y) => {
 	if (!text) return
-	// 图标背景圆
 	ctx.save()
 	ctx.beginPath()
 	ctx.arc(x + 20, y + 20, 20, 0, Math.PI * 2)
 	ctx.fillStyle = 'rgba(64, 128, 255, 0.08)'
 	ctx.fill()
 	ctx.restore()
-	// Emoji
 	ctx.font = '22px sans-serif'
 	ctx.textAlign = 'center'
 	ctx.textBaseline = 'middle'
 	ctx.fillText(emoji, x + 20, y + 20)
-	// 文字 - 超长时自动换行
 	ctx.font = '26px sans-serif'
 	ctx.textAlign = 'left'
 	ctx.textBaseline = 'middle'
 	ctx.fillStyle = '#555555'
-	// 计算可用宽度(图标到右边界)
-	const textMaxWidth = 480  // 联系方式文本最大宽度
+	const textMaxWidth = 480
 	const textX = x + 52
 	if (ctx.measureText(text).width > textMaxWidth) {
-		// 超长时换行显示(从垂直居中位置开始)
 		ctx.textBaseline = 'top'
-		const lineY = y + 10 // 和图标居中对齐的偏移
+		const lineY = y + 10
 		fillTextWrap(ctx, text, textX, lineY, textMaxWidth, 40, 'left')
 	} else {
 		ctx.fillText(text, textX, y + 20)
@@ -119,7 +167,7 @@ const fillTextWrap = (ctx, text, x, y, maxWidth, lineHeight, align = 'left') =>
 	return cy + lineHeight
 }
 
-export const generateCardPoster = async (cardInfo = {}) => {
+export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
 	return new Promise((resolve, reject) => {
 		const query = uni.createSelectorQuery()
 		query.select('#posterCanvas')
@@ -153,7 +201,7 @@ export const generateCardPoster = async (cardInfo = {}) => {
 
 				// ===== 3. 白色卡片 =====
 				const cardX = PADDING, cardY = 60
-				const cardW = W - PADDING * 2, cardH = 660
+				const cardW = W - PADDING * 2, cardH = 760
 				ctx.save()
 				ctx.shadowColor = 'rgba(0,0,0,0.08)'
 				ctx.shadowBlur = 24
@@ -179,15 +227,20 @@ export const generateCardPoster = async (cardInfo = {}) => {
 
 				// 背景图
 				try {
-					const cardBg = await loadImage('/static/image/home/usecard-bg.png')
-					if (cardBg) {
-						ctx.save()
-						roundRect(ctx, innerX, innerY, innerW, innerH, 16)
-						ctx.clip()
-						ctx.drawImage(cardBg, innerX, innerY, innerW, innerH)
-						ctx.restore()
+					const bgSrc = await resolveImageSrc('/static/image/home/usecard-bg.png')
+					if (bgSrc) {
+						const cardBg = await createCanvasImage(canvas, bgSrc)
+						if (cardBg) {
+							ctx.save()
+							roundRect(ctx, innerX, innerY, innerW, innerH, 16)
+							ctx.clip()
+							ctx.drawImage(cardBg, innerX, innerY, innerW, innerH)
+							ctx.restore()
+						}
 					}
-				} catch (e) {}
+				} catch (e) {
+					console.warn('加载名片背景图失败:', e)
+				}
 
 				// ===== 5. 头像(右上角)=====
 				const avatarCx = innerX + innerW - 100
@@ -203,7 +256,7 @@ export const generateCardPoster = async (cardInfo = {}) => {
 				let avatarImg = null
 				if (cardInfo.avatar) {
 					const localPath = await downloadImage(cardInfo.avatar)
-					avatarImg = await loadImage(localPath)
+					avatarImg = await createCanvasImage(canvas, localPath)
 				}
 				if (avatarImg) {
 					drawCircleAvatar(ctx, avatarImg, avatarCx, avatarCy, avatarR)
@@ -223,7 +276,6 @@ export const generateCardPoster = async (cardInfo = {}) => {
 				ctx.fillText(name, nameX, nameY)
 				const nameW = ctx.measureText(name).width
 
-				// 职位标签
 				if (cardInfo.postName) {
 					const tagX = nameX + nameW + 14
 					ctx.font = '22px sans-serif'
@@ -238,27 +290,19 @@ export const generateCardPoster = async (cardInfo = {}) => {
 					ctx.fillText(cardInfo.postName, tagX + 13, nameY + 11)
 				}
 
-				// 公司名称(最大宽度避开右上方头像)
+				// 公司名称
 				ctx.fillStyle = '#666666'
 				ctx.font = '26px sans-serif'
 				ctx.textAlign = 'left'
 				ctx.textBaseline = 'top'
-				const companyMaxW = innerW - 160
+				const companyMaxW = innerW - 200
 				const companyName = cardInfo.companyName || '公司名称'
-				// 如果公司名太长,手动截断
-				ctx.font = '26px sans-serif'
-				if (ctx.measureText(companyName).width > companyMaxW) {
-					let display = companyName
-					while (ctx.measureText(display + '...').width > companyMaxW && display.length > 0) {
-						display = display.substring(0, display.length - 1)
-					}
-					ctx.fillText(display + '...', innerX + 44, nameY + 60)
-				} else {
-					ctx.fillText(companyName, innerX + 44, nameY + 60)
-				}
+				const companyX = innerX + 44
+				const companyY = nameY + 60
+				const companyEndY = fillTextWrap(ctx, companyName, companyX, companyY, companyMaxW, 36, 'left')
 
 				// ===== 7. 联系方式 =====
-				const contactY = innerY + 160
+				const contactY = Math.max(companyEndY + 24, innerY + 160)
 				const contactX = innerX + 44
 				const contactGap = 60
 
@@ -273,14 +317,40 @@ export const generateCardPoster = async (cardInfo = {}) => {
 				drawContactRow(ctx, '✉', cardInfo.email, contactX, contactY + 20 + contactGap)
 				drawContactRow(ctx, '📍', cardInfo.companyAddress, contactX, contactY + 20 + contactGap * 2)
 
-				// ===== 8. 导出 =====
+				// ===== 8. 底部名片码 =====
+				const qrBoxSize = 130
+				const cardInnerBottom = cardY + cardH - 32
+				const qrBoxX = innerX + innerW - qrBoxSize - 44
+				const qrBoxY = cardInnerBottom - qrBoxSize - 44
+
+				// 灰色圆角背景框
+				ctx.save()
+				ctx.fillStyle = '#F5F7FA'
+				roundRect(ctx, qrBoxX - 8, qrBoxY - 8, qrBoxSize + 16, qrBoxSize + 16, 12)
+				ctx.fill()
+				ctx.restore()
+
+				// 加载二维码图片(使用 canvas.createImage())
+				try {
+					const qrLocalPath = await qrBase64ToTempFile(qrInfo.image)
+					if (qrLocalPath) {
+						const qrImg = await createCanvasImage(canvas, qrLocalPath)
+						if (qrImg) {
+							ctx.drawImage(qrImg, qrBoxX, qrBoxY, qrBoxSize, qrBoxSize)
+						}
+					}
+				} catch (e) {
+					console.warn('加载名片码失败:', e)
+				}
+
+				// ===== 9. 导出 =====
 				setTimeout(() => {
 					uni.canvasToTempFilePath({
 						canvas,
 						success: (res) => resolve(res.tempFilePath),
 						fail: (err) => reject(err)
 					})
-				}, 400)
+				}, 600)
 			})
 	})
 }