优化小程序推荐码处理逻辑,支持通过扫码场景解析推荐码和初始章节ID。新增获取用户邀请码的功能以便于分享。更新分享配置,确保分享时自动带上推荐码。调整部分页面逻辑以提升用户体验。

This commit is contained in:
2026-02-12 15:09:52 +08:00
parent c57866ffe0
commit 448e908855
40 changed files with 1068 additions and 318 deletions

View File

@@ -77,5 +77,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 关于作者',
path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
}
}
})

View File

@@ -119,5 +119,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 收货地址',
path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
}
}
})

View File

@@ -197,5 +197,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 编辑地址',
path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
}
}
})

View File

@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 用户协议',
path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
}
}
})

View File

@@ -257,5 +257,13 @@ Page({
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 目录',
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
}
}
})

View File

@@ -56,10 +56,9 @@ Page({
navBarHeight: app.globalData.navBarHeight
})
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
// 处理分享参数与扫码 scene(推荐码绑定)
if (options && (options.ref || options.scene)) {
app.handleReferralCode(options)
}
// 初始化数据
@@ -211,5 +210,13 @@ Page({
await this.initData()
this.updateUserStatus()
wx.stopPullDownRefresh()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 真实商业故事',
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
}
}
})

View File

@@ -891,5 +891,13 @@ Page({
},
// 阻止事件冒泡
preventBubble() {}
preventBubble() {},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 找伙伴',
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
}
}
})

View File

@@ -676,5 +676,13 @@ Page({
},
// 阻止冒泡
stopPropagation() {}
stopPropagation() {},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 我的',
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
}
}
})

View File

@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 隐私政策',
path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
}
}
})

View File

@@ -45,5 +45,13 @@ Page({
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
goBack() { wx.navigateBack() }
goBack() { wx.navigateBack() },
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 我的订单',
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
}
}
})

View File

@@ -66,12 +66,18 @@ Page({
isGeneratingPoster: false,
// 免费章节
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
// 分享卡片图canvas 生成后写入,供 onShareAppMessage 使用)
shareImagePath: ''
},
async onLoad(options) {
const { id, ref } = options
// 扫码进入时 id 可能在 app.globalData.initialSectionIdscene 里 id=1.1 由 app 解析)
let id = options.id || app.globalData.initialSectionId
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
const ref = options.ref
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight,
@@ -268,10 +274,11 @@ Page({
try {
const res = await this.fetchChapterWithTimeout(id, 5000)
if (res && res.content) {
this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(res)
// 成功后缓存到本地
wx.setStorageSync(cacheKey, res)
console.log('[Read] 从API加载成功:', id)
setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -282,10 +289,11 @@ Page({
try {
const cached = wx.getStorageSync(cacheKey)
if (cached && cached.content) {
this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(cached)
console.log('[Read] 从本地缓存加载成功:', id)
// 后台静默刷新
this.silentRefresh(id)
setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -363,9 +371,11 @@ Page({
try {
const res = await this.fetchChapterWithTimeout(id, 8000)
if (res && res.content) {
this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(res)
wx.setStorageSync(`chapter_${id}`, res)
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -454,33 +464,91 @@ Page({
})
},
// 分享到微信 - 自动带分享人ID
onShareAppMessage() {
const { section, sectionId } = this.data
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
// 分享标题优化
const shareTitle = section?.title
// 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用
drawShareCard() {
const { section, sectionId, contentParagraphs } = this.data
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
const raw = (contentParagraphs && contentParagraphs.length)
? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim()
: ''
const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事')
const ctx = wx.createCanvasContext('shareCardCanvas', this)
const w = 500
const h = 400
// 白底
ctx.setFillStyle('#ffffff')
ctx.fillRect(0, 0, w, h)
// 顶部:平台名
ctx.setFillStyle('#333333')
ctx.setFontSize(14)
ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36)
// 深色内容区(模拟参考图效果)
const boxX = 24
const boxY = 52
const boxW = w - 48
const boxH = 300
ctx.setFillStyle('#2c2c2e')
ctx.fillRect(boxX, boxY, boxW, boxH)
// 文章标题(白字)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(15)
const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15)
let y = boxY + 28
titleLines.slice(0, 2).forEach(line => {
ctx.fillText(line, boxX + 16, y)
y += 22
})
y += 8
// 正文摘要(浅灰)
ctx.setFillStyle('rgba(255,255,255,0.88)')
ctx.setFontSize(12)
const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12)
excerptLines.slice(0, 8).forEach(line => {
ctx.fillText(line, boxX + 16, y)
y += 20
})
// 底部:小程序标识
ctx.setFillStyle('#999999')
ctx.setFontSize(11)
ctx.fillText('小程序', 24, h - 16)
ctx.draw(false, () => {
wx.canvasToTempFilePath({
canvasId: 'shareCardCanvas',
fileType: 'png',
success: (res) => {
this.setData({ shareImagePath: res.tempFilePath })
}
}, this)
})
},
// 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
getShareConfig() {
const { section, sectionId, shareImagePath } = this.data
const ref = app.getMyReferralCode()
const shareTitle = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
: '📚 Soul创业派对 - 真实商业故事'
const path = ref
? `/pages/read/read?id=${sectionId}&ref=${ref}`
: `/pages/read/read?id=${sectionId}`
return {
title: shareTitle,
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
path,
imageUrl: shareImagePath || undefined
}
},
// 分享到朋友圈
onShareAppMessage() {
return this.getShareConfig()
},
onShareTimeline() {
const { section, sectionId } = this.data
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || ''
const ref = app.getMyReferralCode()
return {
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
query: ref ? `id=${sectionId}&ref=${ref}` : `id=${sectionId}`
}
},
@@ -734,7 +802,8 @@ Page({
if (res.success && res.data?.payParams) {
paymentData = res.data.payParams
console.log('[Pay] 获取支付参数成功:', paymentData)
paymentData._orderSn = res.data.orderSn
console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
} else {
throw new Error(res.error || res.message || '创建订单失败')
}
@@ -767,11 +836,12 @@ Page({
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
try {
const orderSn = paymentData._orderSn
await this.callWechatPay(paymentData)
// 4. 【标准流程】支付成功后刷新权限并解锁内容
// 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
console.log('[Pay] 微信支付成功!')
await this.onPaymentSuccess()
await this.onPaymentSuccess(orderSn)
} catch (payErr) {
console.error('[Pay] 微信支付调起失败:', payErr)
@@ -807,13 +877,34 @@ Page({
}
},
// 轮询订单状态,确认 paid 后刷新权限并解锁
async pollOrderUntilPaid(orderSn) {
const maxAttempts = 15
const interval = 800
for (let i = 0; i < maxAttempts; i++) {
try {
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
if (r?.data?.status === 'paid') return true
} catch (_) {}
if (i < maxAttempts - 1) await this.sleep(interval)
}
return false
},
// 【新增】支付成功后的标准处理流程
async onPaymentSuccess() {
async onPaymentSuccess(orderSn) {
wx.showLoading({ title: '确认购买中...', mask: true })
try {
// 1. 等待服务端处理支付回调1-2秒
await this.sleep(2000)
// 1. 轮询订单状态直到已支付GET pay 会主动同步本地订单,不依赖 PayNotify
if (orderSn) {
const paid = await this.pollOrderUntilPaid(orderSn)
if (!paid) {
console.warn('[Pay] 轮询超时,仍尝试刷新')
}
} else {
await this.sleep(1500)
}
// 2. 刷新用户购买状态
await accessManager.refreshUserPurchaseStatus()
@@ -936,134 +1027,115 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 生成海报
// 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
async generatePoster() {
wx.showLoading({ title: '生成中...' })
this.setData({ showPosterModal: true, isGeneratingPoster: true })
const { section, contentParagraphs, sectionId } = this.data
const userInfo = app.globalData.userInfo
const userId = userInfo?.id || ''
const safeParagraphs = contentParagraphs || []
// 通过 GET 接口下载二维码图片,得到 tempFilePath 便于开发工具与真机统一用 drawImage 绘制
let qrcodeTempPath = null
try {
const ctx = wx.createCanvasContext('posterCanvas', this)
const { section, contentParagraphs, sectionId } = this.data
const userInfo = app.globalData.userInfo
const userId = userInfo?.id || ''
// 获取小程序码(带推荐人参数)
let qrcodeImage = null
try {
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
const qrRes = await app.request('/api/miniprogram/qrcode', {
method: 'POST',
data: { scene, page: 'pages/read/read', width: 280 }
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`
const baseUrl = app.globalData.baseUrl || ''
const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
qrcodeTempPath = await new Promise((resolve) => {
wx.downloadFile({
url,
success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null),
fail: () => resolve(null)
})
if (qrRes.success && qrRes.image) {
qrcodeImage = qrRes.image
}
} catch (e) {
console.log('[Poster] 获取小程序码失败,使用占位符')
}
// 海报尺寸 300x450
const width = 300
const height = 450
// 背景渐变
const grd = ctx.createLinearGradient(0, 0, 0, height)
grd.addColorStop(0, '#1a1a2e')
grd.addColorStop(1, '#16213e')
ctx.setFillStyle(grd)
ctx.fillRect(0, 0, width, height)
// 顶部装饰条
ctx.setFillStyle('#00CED1')
ctx.fillRect(0, 0, width, 4)
// 标题区域
ctx.setFillStyle('#ffffff')
ctx.setFontSize(14)
ctx.fillText('📚 Soul创业派对', 20, 35)
// 章节标题
ctx.setFontSize(18)
ctx.setFillStyle('#ffffff')
const title = section?.title || '精彩内容'
const titleLines = this.wrapText(ctx, title, width - 40, 18)
let y = 70
titleLines.forEach(line => {
ctx.fillText(line, 20, y)
y += 26
})
// 分隔线
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
ctx.beginPath()
ctx.moveTo(20, y + 10)
ctx.lineTo(width - 20, y + 10)
ctx.stroke()
// 内容摘要
ctx.setFontSize(12)
ctx.setFillStyle('rgba(255,255,255,0.8)')
y += 30
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
summaryLines.slice(0, 6).forEach(line => {
ctx.fillText(line, 20, y)
y += 20
})
// 底部区域背景
ctx.setFillStyle('rgba(0,206,209,0.1)')
ctx.fillRect(0, height - 100, width, 100)
// 左侧提示文字
ctx.setFillStyle('#ffffff')
ctx.setFontSize(13)
ctx.fillText('长按识别小程序码', 20, height - 60)
ctx.setFillStyle('rgba(255,255,255,0.6)')
ctx.setFontSize(11)
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
// 绘制小程序码或占位符
const drawQRCode = () => {
return new Promise((resolve) => {
if (qrcodeImage) {
// 下载base64图片并绘制
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
fs.writeFile({
filePath,
data: base64Data,
encoding: 'base64',
success: () => {
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
resolve()
},
fail: () => {
this.drawQRPlaceholder(ctx, width, height)
resolve()
}
})
} else {
this.drawQRPlaceholder(ctx, width, height)
resolve()
}
})
}
await drawQRCode()
ctx.draw(true, () => {
wx.hideLoading()
this.setData({ isGeneratingPoster: false })
})
} catch (e) {
console.error('生成海报失败:', e)
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'none' })
this.setData({ showPosterModal: false, isGeneratingPoster: false })
console.log('[Poster] 获取小程序码失败,使用占位符')
}
const doDraw = () => {
try {
const ctx = wx.createCanvasContext('posterCanvas', this)
const width = 300
const height = 450
const grd = ctx.createLinearGradient(0, 0, 0, height)
grd.addColorStop(0, '#1a1a2e')
grd.addColorStop(1, '#16213e')
ctx.setFillStyle(grd)
ctx.fillRect(0, 0, width, height)
ctx.setFillStyle('#00CED1')
ctx.fillRect(0, 0, width, 4)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(14)
ctx.fillText('📚 Soul创业派对', 20, 35)
ctx.setFontSize(18)
ctx.setFillStyle('#ffffff')
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
const titleLines = this.wrapText(ctx, title, width - 40, 18)
let y = 70
titleLines.forEach(line => {
ctx.fillText(line, 20, y)
y += 26
})
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
ctx.beginPath()
ctx.moveTo(20, y + 10)
ctx.lineTo(width - 20, y + 10)
ctx.stroke()
ctx.setFontSize(12)
ctx.setFillStyle('rgba(255,255,255,0.8)')
y += 30
const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150)
const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事'
const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12)
summaryLines.slice(0, 6).forEach(line => {
ctx.fillText(line, 20, y)
y += 20
})
ctx.setFillStyle('rgba(0,206,209,0.1)')
ctx.fillRect(0, height - 100, width, 100)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(13)
ctx.fillText('长按识别小程序码', 20, height - 60)
ctx.setFillStyle('rgba(255,255,255,0.6)')
ctx.setFontSize(11)
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
const drawQRCode = () => {
return new Promise((resolve) => {
if (qrcodeTempPath) {
ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70)
} else {
this.drawQRPlaceholder(ctx, width, height)
}
resolve()
})
}
drawQRCode().then(() => {
ctx.draw(true, () => {
wx.hideLoading()
this.setData({ isGeneratingPoster: false })
})
})
} catch (e) {
console.error('生成海报失败:', e)
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'none' })
this.setData({ showPosterModal: false, isGeneratingPoster: false })
}
}
setTimeout(doDraw, 400)
},
// 绘制小程序码占位符
@@ -1101,11 +1173,20 @@ Page({
this.setData({ showPosterModal: false })
},
// 保存海报到相册
// 保存海报到相册(与海报绘制尺寸一致,必须传 destWidth/destHeight 否则部分机型导出失败)
savePoster() {
const width = 300
const height = 450
wx.canvasToTempFilePath({
canvasId: 'posterCanvas',
destWidth: width,
destHeight: height,
fileType: 'png',
success: (res) => {
if (!res.tempFilePath) {
wx.showToast({ title: '生成图片失败', icon: 'none' })
return
}
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
@@ -1113,25 +1194,25 @@ Page({
this.setData({ showPosterModal: false })
},
fail: (err) => {
if (err.errMsg.includes('auth deny')) {
console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
wx.showModal({
title: '提示',
content: '需要相册权限才能保存海报',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
success: (sres) => {
if (sres.confirm) wx.openSetting()
}
})
} else {
wx.showToast({ title: '保存失败', icon: 'none' })
wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
}
}
})
},
fail: () => {
wx.showToast({ title: '生成图片失败', icon: 'none' })
fail: (err) => {
console.error('[savePoster] canvasToTempFilePath fail:', err)
wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
}
}, this)
},

View File

@@ -297,4 +297,7 @@
<button class="fab-share" open-type="share">
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
</button>
<!-- 分享卡片用 canvas离屏绘制用于生成分享图 -->
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
</view>

View File

@@ -912,6 +912,14 @@
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
}
/* 分享卡片 canvas离屏绘制不展示给用户 */
.share-card-canvas {
position: fixed;
left: -600px;
top: 0;
z-index: -1;
}
.poster-actions {
display: flex;
gap: 24rpx;

View File

@@ -796,14 +796,13 @@ Page({
})
},
// 分享 - 带推荐码
// 分享 - 带自己的邀请码(与 app.getMyReferralCode 一致)
onShareAppMessage() {
console.log('[Referral] 分享给好友,推荐码:', this.data.referralCode)
const app = getApp()
const ref = app.getMyReferralCode() || this.data.referralCode
return {
title: 'Soul创业派对 - 来自派对房的真实商业故事',
path: `/pages/index/index?ref=${this.data.referralCode}`
// 不设置 imageUrl使用小程序默认截图
// 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
}
},

View File

@@ -0,0 +1,157 @@
/**
* Soul创业派对 - 扫码解析页
* 扫描二维码/条形码,展示解析内容
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
// 最近一次解析结果
lastResult: null,
scanType: '',
charSet: '',
// 小程序码解析(路径、参数)
parsedPath: null,
parsedQuery: [],
canNavigate: false,
// 历史记录
history: []
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
this.loadHistory()
},
loadHistory() {
try {
const history = wx.getStorageSync('scanHistory') || []
this.setData({ history })
} catch (e) {
console.log('加载扫码历史失败:', e)
}
},
saveToHistory(result, scanType, charSet) {
const item = { result, scanType, charSet, time: new Date().toLocaleString() }
let history = wx.getStorageSync('scanHistory') || []
history = [item, ...history].slice(0, 10)
wx.setStorageSync('scanHistory', history)
this.setData({ history })
},
// 解析小程序码内容path?key=val 或 path
parseMiniProgramCode(result) {
if (!result || typeof result !== 'string') return { path: null, query: [], canNavigate: false }
const idx = result.indexOf('?')
let path = idx >= 0 ? result.slice(0, idx) : result
const qs = idx >= 0 ? result.slice(idx + 1) : ''
path = path.replace(/^\//, '').trim()
const query = []
if (qs) {
qs.split('&').forEach(pair => {
const eq = pair.indexOf('=')
const k = eq >= 0 ? pair.slice(0, eq) : pair
const v = eq >= 0 ? pair.slice(eq + 1) : ''
try {
if (k) query.push({ key: decodeURIComponent(k), value: decodeURIComponent(v) })
} catch (_) {
if (k) query.push({ key: k, value: v })
}
})
}
const isMiniProgramPath = /^pages\/[\w-]+\/[\w-]+$/.test(path)
return { path: path || null, query, canNavigate: isMiniProgramPath }
},
// 发起扫码(支持小程序码)
doScan() {
wx.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success: (res) => {
const { result, scanType, charSet } = res
const parsed = this.parseMiniProgramCode(result)
this.setData({
lastResult: result,
scanType: scanType || '未知',
charSet: charSet || '',
parsedPath: parsed.path,
parsedQuery: parsed.query,
canNavigate: parsed.canNavigate
})
this.saveToHistory(result, scanType, charSet)
},
fail: (err) => {
if (err.errMsg && err.errMsg.includes('cancel')) {
return
}
wx.showToast({ title: err.errMsg || '扫码失败', icon: 'none' })
}
})
},
// 复制内容
copyResult() {
const { lastResult } = this.data
if (!lastResult) {
wx.showToast({ title: '暂无解析内容', icon: 'none' })
return
}
wx.setClipboardData({
data: lastResult,
success: () => wx.showToast({ title: '已复制', icon: 'success' })
})
},
// 清空当前结果
clearResult() {
this.setData({
lastResult: null, scanType: '', charSet: '',
parsedPath: null, parsedQuery: [], canNavigate: false
})
},
// 打开解析出的小程序页面
openParsedPage() {
const { parsedPath, parsedQuery } = this.data
if (!parsedPath || !this.data.canNavigate) {
wx.showToast({ title: '非本小程序页面', icon: 'none' })
return
}
const queryStr = parsedQuery.length
? '?' + parsedQuery.map(q => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join('&')
: ''
const url = `/${parsedPath}${queryStr}`
const tabPages = ['pages/index/index', 'pages/chapters/chapters', 'pages/match/match', 'pages/my/my']
if (tabPages.includes(parsedPath)) {
wx.switchTab({ url: `/${parsedPath}` })
} else {
wx.navigateTo({ url })
}
},
// 清空历史
clearHistory() {
wx.setStorageSync('scanHistory', [])
this.setData({ history: [], lastResult: null, scanType: '', charSet: '' })
wx.showToast({ title: '已清空', icon: 'success' })
},
// 点击历史项复制
onHistoryItemTap(e) {
const result = e.currentTarget.dataset.result
if (!result) return
wx.setClipboardData({
data: result,
success: () => wx.showToast({ title: '已复制', icon: 'success' })
})
},
goBack() {
wx.navigateBack()
}
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"扫码解析"}

View File

@@ -0,0 +1,81 @@
<!--pages/scan/scan.wxml-->
<!--扫码解析页 - 扫描二维码/条形码展示解析内容-->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<text class="nav-title">扫码解析</text>
</view>
</view>
<!-- 主内容 -->
<view class="main-content" style="padding-top: {{statusBarHeight + 56}}px;">
<!-- 扫码按钮 -->
<view class="scan-action">
<view class="scan-btn" bindtap="doScan">
<text class="scan-icon">📷</text>
<text class="scan-text">扫描小程序码 / 二维码</text>
</view>
</view>
<!-- 解析结果 -->
<view class="result-card" wx:if="{{lastResult}}">
<view class="result-header">
<text class="result-label">解析内容</text>
<view class="result-actions">
<view class="action-btn" bindtap="copyResult">复制</view>
<view class="action-btn" wx:if="{{canNavigate}}" bindtap="openParsedPage">打开</view>
<view class="action-btn secondary" bindtap="clearResult">清空</view>
</view>
</view>
<!-- 小程序码解析:路径 + 参数(仅当为 pages/ 路径时展示) -->
<view class="parsed-section" wx:if="{{parsedPath && (parsedPath.indexOf('pages/') === 0 || parsedQuery.length > 0)}}">
<view class="parsed-row">
<text class="parsed-label">路径</text>
<text class="parsed-value">{{parsedPath}}</text>
</view>
<view class="parsed-row" wx:for="{{parsedQuery}}" wx:key="key" wx:for-item="q">
<text class="parsed-label">{{q.key}}</text>
<text class="parsed-value">{{q.value}}</text>
</view>
</view>
<view class="result-meta" wx:if="{{scanType}}">
<text class="meta-item">类型: {{scanType}}</text>
<text class="meta-item" wx:if="{{charSet}}">字符集: {{charSet}}</text>
</view>
<scroll-view class="result-content" scroll-y="{{true}}">
<text class="result-text" selectable="{{true}}">{{lastResult}}</text>
</scroll-view>
</view>
<!-- 无结果提示 -->
<view class="empty-tip" wx:else>
<text class="empty-icon">📷</text>
<text class="empty-text">点击上方按钮扫码</text>
<text class="empty-desc">支持小程序码、二维码、条形码</text>
</view>
<!-- 扫码历史 -->
<view class="history-section" wx:if="{{history.length > 0}}">
<view class="history-header">
<text class="history-title">扫码历史</text>
<view class="clear-history" bindtap="clearHistory">清空</view>
</view>
<view class="history-list">
<view
class="history-item"
wx:for="{{history}}"
wx:key="index"
bindtap="onHistoryItemTap"
data-result="{{item.result}}"
>
<text class="history-content">{{item.result}}</text>
<text class="history-time">{{item.time}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,248 @@
/* 扫码解析页样式 */
.page {
min-height: 100vh;
background: linear-gradient(180deg, #0a0a0a 0%, #111111 100%);
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.nav-content {
display: flex;
align-items: center;
padding: 8rpx 24rpx;
height: 88rpx;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 40rpx;
color: #00CED1;
}
.nav-title {
flex: 1;
font-size: 34rpx;
font-weight: 600;
color: #fff;
text-align: center;
margin-right: 60rpx;
}
.main-content {
padding: 24rpx;
}
/* 扫码按钮 */
.scan-action {
padding: 60rpx 0;
}
.scan-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.15) 100%);
border: 2rpx solid rgba(0, 206, 209, 0.4);
border-radius: 32rpx;
padding: 80rpx 48rpx;
}
.scan-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.scan-text {
font-size: 32rpx;
color: #00CED1;
font-weight: 500;
}
/* 解析结果卡片 */
.result-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 24rpx;
padding: 32rpx;
margin-top: 32rpx;
border: 1rpx solid rgba(255, 255, 255, 0.08);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.result-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
}
.result-actions {
display: flex;
gap: 24rpx;
}
.action-btn {
font-size: 26rpx;
color: #00CED1;
padding: 8rpx 20rpx;
}
.action-btn.secondary {
color: rgba(255, 255, 255, 0.5);
}
/* 小程序码解析:路径+参数 */
.parsed-section {
background: rgba(0, 206, 209, 0.08);
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
border: 1rpx solid rgba(0, 206, 209, 0.2);
}
.parsed-row {
display: flex;
align-items: baseline;
margin-bottom: 12rpx;
}
.parsed-row:last-child {
margin-bottom: 0;
}
.parsed-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
min-width: 120rpx;
flex-shrink: 0;
}
.parsed-value {
font-size: 26rpx;
color: #00CED1;
word-break: break-all;
}
.result-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 20rpx;
}
.meta-item {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
.result-content {
max-height: 400rpx;
background: rgba(0, 0, 0, 0.3);
border-radius: 16rpx;
padding: 24rpx;
}
.result-text {
font-size: 28rpx;
color: #fff;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
}
/* 无结果提示 */
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
opacity: 0.5;
}
.empty-text {
font-size: 30rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8rpx;
}
.empty-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
}
/* 扫码历史 */
.history-section {
margin-top: 48rpx;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.history-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
}
.clear-history {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
.history-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.history-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
padding: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.06);
}
.history-content {
font-size: 26rpx;
color: #fff;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-time {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
margin-top: 8rpx;
display: block;
}

View File

@@ -105,5 +105,13 @@ Page({
// 返回上一页
goBack() {
wx.navigateBack()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 搜索',
path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search'
}
}
})

View File

@@ -493,5 +493,13 @@ Page({
// 跳转到地址管理页
goToAddresses() {
wx.navigateTo({ url: '/pages/addresses/addresses' })
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 设置',
path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
}
}
})

View File

@@ -119,5 +119,13 @@ Page({
wx.hideLoading()
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
}
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 提现记录',
path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
}
}
})