Merge branch 'yongpxu-soul' of https://github.com/fnvtk/Mycontent into yongpxu-soul
# Conflicts: # miniprogram/app.js resolved by origin/yongpxu-soul(远端) version # miniprogram/app.json resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/addresses/addresses.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/addresses/edit.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/agreement/agreement.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/chapters/chapters.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/index/index.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/index/index.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/match/match.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.wxss resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/privacy/privacy.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/purchases/purchases.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/read/read.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/read/read.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.wxss resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/settings/settings.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/withdraw-records/withdraw-records.js resolved by origin/yongpxu-soul(远端) version # miniprogram/project.config.json resolved by origin/yongpxu-soul(远端) version # miniprogram/project.private.config.json resolved by origin/yongpxu-soul(远端) version # miniprogram/utils/chapterAccessManager.js resolved by origin/yongpxu-soul(远端) version
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
import readingTracker from '../../utils/readingTracker'
|
||||
const { buildScene, parseScene } = require('../../utils/scene.js')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
@@ -57,12 +56,6 @@ Page({
|
||||
sectionPrice: 1,
|
||||
fullBookPrice: 9.9,
|
||||
totalSections: 62,
|
||||
// 好友优惠展示
|
||||
userDiscount: 5,
|
||||
hasReferralDiscount: false,
|
||||
showDiscountHint: false,
|
||||
displaySectionPrice: 1,
|
||||
displayFullBookPrice: 9.9,
|
||||
|
||||
// 弹窗
|
||||
showShareModal: false,
|
||||
@@ -73,41 +66,21 @@ Page({
|
||||
isGeneratingPoster: false,
|
||||
|
||||
// 免费章节
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
|
||||
|
||||
// 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用)
|
||||
shareImagePath: ''
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
// 官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与海报生成 buildScene 闭环
|
||||
const sceneStr = (options && options.scene) || ''
|
||||
const parsed = parseScene(sceneStr)
|
||||
const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0)
|
||||
const id = options.id || parsed.id || app.globalData.initialSectionId
|
||||
const ref = options.ref || parsed.ref
|
||||
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
|
||||
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
|
||||
|
||||
console.log('[Read] onLoad:', { options, sceneRaw: sceneStr || undefined, parsed, mid, id, ref })
|
||||
console.log('[Read] onLoad options:', options)
|
||||
|
||||
if (!mid && !id) {
|
||||
console.warn('[Read] 未获取到章节 mid/id,options:', options)
|
||||
wx.showToast({ title: '章节参数缺失', icon: 'none' })
|
||||
this.setData({ accessState: 'error', loading: false })
|
||||
return
|
||||
}
|
||||
|
||||
const { id, ref } = options
|
||||
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight,
|
||||
sectionId: '', // 加载后填充
|
||||
sectionMid: mid || null,
|
||||
sectionId: id,
|
||||
loading: true,
|
||||
accessState: 'unknown'
|
||||
})
|
||||
|
||||
// 处理推荐码绑定(异步不阻塞)
|
||||
if (ref) {
|
||||
console.log('[Read] 检测到推荐码:', ref)
|
||||
wx.setStorageSync('referral_code', ref)
|
||||
@@ -115,66 +88,35 @@ Page({
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
// 有推荐人 = ref/ referral_code 或 用户信息中有推荐人绑定
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || ref || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const showDiscountHint = userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
// 【标准流程】1. 拉取最新配置(免费列表、价格)
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9
|
||||
})
|
||||
|
||||
// 先拉取章节获取 id(mid 时必需;id 时可直接用)
|
||||
let resolvedId = id
|
||||
let prefetchedChapter = null
|
||||
if (mid && !id) {
|
||||
const chRes = await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
|
||||
if (chRes && chRes.id) {
|
||||
resolvedId = chRes.id
|
||||
prefetchedChapter = chRes
|
||||
}
|
||||
}
|
||||
this.setData({ sectionId: resolvedId })
|
||||
|
||||
const accessState = await accessManager.determineAccessState(resolvedId, config.freeChapters)
|
||||
// 【标准流程】2. 确定权限状态
|
||||
const accessState = await accessManager.determineAccessState(id, config.freeChapters)
|
||||
const canAccess = accessManager.canAccessFullContent(accessState)
|
||||
|
||||
this.setData({
|
||||
accessState,
|
||||
canAccess,
|
||||
isLoggedIn: !!app.globalData.userInfo?.id,
|
||||
showPaywall: !canAccess,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? 0
|
||||
showPaywall: !canAccess
|
||||
})
|
||||
|
||||
await this.loadContent(mid, resolvedId, accessState, prefetchedChapter)
|
||||
// 【标准流程】3. 加载内容
|
||||
await this.loadContent(id, accessState)
|
||||
|
||||
// 【标准流程】4. 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
readingTracker.init(resolvedId)
|
||||
readingTracker.init(id)
|
||||
}
|
||||
|
||||
this.loadNavigation(resolvedId)
|
||||
// 5. 加载导航
|
||||
this.loadNavigation(id)
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Read] 初始化失败:', e)
|
||||
@@ -217,9 +159,8 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 【重构】加载章节内容。mid 优先用 by-mid 接口,id 用旧接口;prefetched 避免重复请求
|
||||
async loadContent(mid, id, accessState, prefetched) {
|
||||
console.log('[Read] loadContent 请求参数:', { mid, id, accessState: accessState, prefetched: !!prefetched })
|
||||
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
|
||||
async loadContent(id, accessState) {
|
||||
try {
|
||||
const section = this.getSectionInfo(id)
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
@@ -227,43 +168,26 @@ Page({
|
||||
section.price = sectionPrice
|
||||
}
|
||||
this.setData({ section })
|
||||
|
||||
let res = prefetched
|
||||
if (!res) {
|
||||
res = mid
|
||||
? await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
|
||||
: await app.request(`/api/miniprogram/book/chapter/${id}`)
|
||||
}
|
||||
|
||||
// 从 API 获取内容
|
||||
const res = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
|
||||
|
||||
if (res && res.content) {
|
||||
const lines = res.content.split('\n').filter(line => line.trim())
|
||||
const previewCount = Math.ceil(lines.length * 0.2)
|
||||
const updates = {
|
||||
|
||||
this.setData({
|
||||
content: res.content,
|
||||
contentParagraphs: lines,
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
}
|
||||
if (res.mid) updates.sectionMid = res.mid
|
||||
this.setData(updates)
|
||||
})
|
||||
|
||||
// 如果有权限,标记为已读
|
||||
if (accessManager.canAccessFullContent(accessState)) {
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
// 始终用接口返回的 price/isFree 更新 section(不写死 1 元)
|
||||
const section = this.data.section || {}
|
||||
if (res.price !== undefined && res.price !== null) section.price = Number(res.price)
|
||||
if (res.isFree !== undefined) section.isFree = !!res.isFree
|
||||
// 0元即免费:接口返回 price 为 0 或 isFree 为 true 时,不展示付费墙
|
||||
const isFreeByPrice = res.price === 0 || res.price === '0' || Number(res.price) === 0
|
||||
const isFreeByFlag = res.isFree === true
|
||||
if (isFreeByPrice || isFreeByFlag) {
|
||||
this.setData({ section, showPaywall: false, canAccess: true, accessState: 'free' })
|
||||
app.markSectionAsRead(id)
|
||||
} else {
|
||||
this.setData({ section })
|
||||
}
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败:', e)
|
||||
@@ -307,12 +231,12 @@ Page({
|
||||
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
|
||||
}
|
||||
|
||||
// 普通章节:price 不写死,由 loadContent 从 config/接口 填充
|
||||
// 普通章节
|
||||
return {
|
||||
id: id,
|
||||
title: this.getSectionTitle(id),
|
||||
isFree: id === '1.1',
|
||||
price: undefined
|
||||
price: 1
|
||||
}
|
||||
},
|
||||
|
||||
@@ -335,6 +259,48 @@ Page({
|
||||
}
|
||||
return titles[id] || `章节 ${id}`
|
||||
},
|
||||
|
||||
// 加载内容 - 三级降级方案:API → 本地缓存 → 备用API
|
||||
async loadContent(id) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
|
||||
// 1. 优先从API获取
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 5000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
// 成功后缓存到本地
|
||||
wx.setStorageSync(cacheKey, res)
|
||||
console.log('[Read] 从API加载成功:', id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] API加载失败,尝试本地缓存:', e.message)
|
||||
}
|
||||
|
||||
// 2. API失败,尝试从本地缓存读取
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
this.setChapterContent(cached)
|
||||
console.log('[Read] 从本地缓存加载成功:', id)
|
||||
// 后台静默刷新
|
||||
this.silentRefresh(id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 本地缓存读取失败')
|
||||
}
|
||||
|
||||
// 3. 都失败,显示加载中并持续重试
|
||||
this.setData({
|
||||
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
|
||||
previewParagraphs: ['章节内容加载中...']
|
||||
})
|
||||
|
||||
// 延迟重试(最多3次)
|
||||
this.retryLoadContent(id, 3)
|
||||
},
|
||||
|
||||
// 带超时的章节请求
|
||||
fetchChapterWithTimeout(id, timeout = 5000) {
|
||||
@@ -397,11 +363,9 @@ 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) {
|
||||
@@ -412,7 +376,7 @@ Page({
|
||||
},
|
||||
|
||||
|
||||
// 加载导航(prevSection/nextSection 含 mid 时用于跳转,否则用 id)
|
||||
// 加载导航
|
||||
loadNavigation(id) {
|
||||
const sectionOrder = [
|
||||
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
|
||||
@@ -428,19 +392,14 @@ Page({
|
||||
'11.1', '11.2', '11.3', '11.4', '11.5',
|
||||
'epilogue'
|
||||
]
|
||||
const bookData = app.globalData.bookData || []
|
||||
const idToMid = {}
|
||||
bookData.forEach(ch => {
|
||||
if (ch.id && ch.mid) idToMid[ch.id] = ch.mid
|
||||
})
|
||||
|
||||
|
||||
const currentIndex = sectionOrder.indexOf(id)
|
||||
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
|
||||
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
|
||||
|
||||
|
||||
this.setData({
|
||||
prevSection: prevId ? { id: prevId, mid: idToMid[prevId], title: this.getSectionTitle(prevId) } : null,
|
||||
nextSection: nextId ? { id: nextId, mid: idToMid[nextId], title: this.getSectionTitle(nextId) } : null
|
||||
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
|
||||
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
|
||||
})
|
||||
},
|
||||
|
||||
@@ -460,6 +419,21 @@ Page({
|
||||
this.setData({ showShareModal: false })
|
||||
},
|
||||
|
||||
// 复制链接
|
||||
copyLink() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => {
|
||||
wx.showToast({ title: '链接已复制', icon: 'success' })
|
||||
this.setData({ showShareModal: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 复制分享文案(朋友圈风格)
|
||||
copyShareText() {
|
||||
const { section } = this.data
|
||||
@@ -480,91 +454,33 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 绘制分享卡片图(标题+正文摘要),生成后供 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, sectionMid, shareImagePath } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const shareTitle = section?.title
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
|
||||
|
||||
// 分享标题优化
|
||||
const shareTitle = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 Soul创业派对 - 真实商业故事'
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
|
||||
|
||||
return {
|
||||
title: shareTitle,
|
||||
path,
|
||||
imageUrl: shareImagePath || undefined
|
||||
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
|
||||
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
|
||||
}
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return this.getShareConfig()
|
||||
},
|
||||
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
const { section, sectionId, sectionMid } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
|
||||
return {
|
||||
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
|
||||
query: ref ? `${q}&ref=${ref}` : q
|
||||
query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -643,34 +559,9 @@ Page({
|
||||
// 1. 刷新用户购买状态(从 orders 表拉取最新)
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
|
||||
// 2. 重新拉取免费列表、价格与用户推荐人状态
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
})
|
||||
// 2. 重新拉取免费列表(极端情况:刚登录时当前章节可能改免费了)
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({ freeIds: config.freeChapters })
|
||||
|
||||
// 3. 重新判断当前章节权限
|
||||
const newAccessState = await accessManager.determineAccessState(
|
||||
@@ -688,7 +579,7 @@ Page({
|
||||
|
||||
// 4. 如果已解锁,重新加载内容并初始化阅读追踪
|
||||
if (canAccess) {
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
readingTracker.init(this.data.sectionId)
|
||||
}
|
||||
|
||||
@@ -713,7 +604,7 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const price = this.data.section?.price ?? this.data.sectionPrice ?? 1
|
||||
const price = this.data.section?.price || 1
|
||||
console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price })
|
||||
wx.hideLoading()
|
||||
await this.processPayment('section', this.data.sectionId, price)
|
||||
@@ -759,7 +650,6 @@ Page({
|
||||
// 更新本地购买状态
|
||||
app.globalData.hasFullBook = checkRes.data.hasFullBook
|
||||
app.globalData.purchasedSections = checkRes.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = checkRes.data.sectionMidMap || {}
|
||||
|
||||
// 检查是否已购买
|
||||
if (type === 'section' && sectionId) {
|
||||
@@ -844,8 +734,7 @@ Page({
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
paymentData = res.data.payParams
|
||||
paymentData._orderSn = res.data.orderSn
|
||||
console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
|
||||
console.log('[Pay] 获取支付参数成功:', paymentData)
|
||||
} else {
|
||||
throw new Error(res.error || res.message || '创建订单失败')
|
||||
}
|
||||
@@ -878,12 +767,11 @@ Page({
|
||||
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
|
||||
|
||||
try {
|
||||
const orderSn = paymentData._orderSn
|
||||
await this.callWechatPay(paymentData)
|
||||
|
||||
// 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
|
||||
// 4. 【标准流程】支付成功后刷新权限并解锁内容
|
||||
console.log('[Pay] 微信支付成功!')
|
||||
await this.onPaymentSuccess(orderSn)
|
||||
await this.onPaymentSuccess()
|
||||
|
||||
} catch (payErr) {
|
||||
console.error('[Pay] 微信支付调起失败:', payErr)
|
||||
@@ -919,34 +807,13 @@ 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(orderSn) {
|
||||
async onPaymentSuccess() {
|
||||
wx.showLoading({ title: '确认购买中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify)
|
||||
if (orderSn) {
|
||||
const paid = await this.pollOrderUntilPaid(orderSn)
|
||||
if (!paid) {
|
||||
console.warn('[Pay] 轮询超时,仍尝试刷新')
|
||||
}
|
||||
} else {
|
||||
await this.sleep(1500)
|
||||
}
|
||||
// 1. 等待服务端处理支付回调(1-2秒)
|
||||
await this.sleep(2000)
|
||||
|
||||
// 2. 刷新用户购买状态
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
@@ -976,7 +843,7 @@ Page({
|
||||
})
|
||||
|
||||
// 4. 重新加载全文
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
|
||||
// 5. 初始化阅读追踪
|
||||
if (canAccess) {
|
||||
@@ -1013,10 +880,7 @@ Page({
|
||||
// 更新全局购买状态
|
||||
app.globalData.hasFullBook = res.data.hasFullBook
|
||||
app.globalData.purchasedSections = res.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
|
||||
|
||||
// 更新用户信息中的购买记录
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = res.data.hasFullBook
|
||||
@@ -1026,8 +890,7 @@ Page({
|
||||
|
||||
console.log('[Pay] ✅ 购买状态已刷新:', {
|
||||
hasFullBook: res.data.hasFullBook,
|
||||
purchasedCount: res.data.purchasedSections.length,
|
||||
matchCount: res.data.matchCount
|
||||
purchasedCount: res.data.purchasedSections.length
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1051,19 +914,17 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到上一篇
|
||||
goToPrev() {
|
||||
const s = this.data.prevSection
|
||||
if (s) {
|
||||
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
|
||||
wx.redirectTo({ url: `/pages/read/read?${q}` })
|
||||
if (this.data.prevSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到下一篇
|
||||
goToNext() {
|
||||
const s = this.data.nextSection
|
||||
if (s) {
|
||||
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
|
||||
wx.redirectTo({ url: `/pages/read/read?${q}` })
|
||||
if (this.data.nextSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1072,120 +933,134 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
|
||||
// 生成海报
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
const safeParagraphs = contentParagraphs || []
|
||||
|
||||
// 与 utils/scene 闭环:生成 scene 用 buildScene,扫码后用 parseScene 解析
|
||||
let qrcodeTempPath = null
|
||||
try {
|
||||
const refVal = userId ? String(userId).slice(0, 12) : ''
|
||||
const scene = buildScene({
|
||||
mid: sectionMid || undefined,
|
||||
id: sectionMid ? undefined : (sectionId || ''),
|
||||
ref: refVal || undefined
|
||||
})
|
||||
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)
|
||||
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 }
|
||||
})
|
||||
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.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
@@ -1223,21 +1098,11 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册:画布 300x450 兼容 iOS,导出 2 倍 600x900 提升清晰度(宽高比 2:3 不变)
|
||||
// 保存海报到相册
|
||||
savePoster() {
|
||||
const width = 300
|
||||
const height = 450
|
||||
const exportScale = 2
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
destWidth: width * exportScale,
|
||||
destHeight: height * exportScale,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
if (!res.tempFilePath) {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
@@ -1245,25 +1110,25 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
|
||||
if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (sres) => {
|
||||
if (sres.confirm) wx.openSetting()
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[savePoster] canvasToTempFilePath fail:', err)
|
||||
wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
},
|
||||
@@ -1286,33 +1151,9 @@ Page({
|
||||
wx.showLoading({ title: '重试中...', mask: true })
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
})
|
||||
// 重新拉取配置
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({ freeIds: config.freeChapters })
|
||||
|
||||
// 重新判断权限
|
||||
const newAccessState = await accessManager.determineAccessState(
|
||||
@@ -1328,7 +1169,7 @@ Page({
|
||||
})
|
||||
|
||||
// 重新加载内容
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
|
||||
// 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
|
||||
@@ -166,16 +166,7 @@
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<view class="btn-price-row" wx:if="{{hasReferralDiscount}}">
|
||||
<text class="btn-original-price">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price brand-color">¥{{displaySectionPrice}}</text>
|
||||
<text class="btn-discount-tag">省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<view class="btn-price-row" wx:elif="{{showDiscountHint}}">
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-discount-tag">好友链接立省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<text wx:else class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
@@ -185,9 +176,8 @@
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-original-price" wx:if="{{hasReferralDiscount}}">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-price">¥{{hasReferralDiscount ? displayFullBookPrice : (fullBookPrice || 9.9)}}</text>
|
||||
<text class="btn-discount">{{hasReferralDiscount ? '省' + userDiscount + '%' : '省82%'}}</text>
|
||||
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -256,7 +246,7 @@
|
||||
<view class="modal-close" bindtap="closePosterModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报预览:画布 300x450 避免 iOS transform 裁切;保存时导出 2 倍分辨率 -->
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
@@ -307,7 +297,4 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user