Merge branch 'devlop' into yongxu-dev
# Conflicts: # .cursor/skills/miniprogram-dev/SKILL.md resolved by devlop version # miniprogram/pages/index/index.js resolved by devlop version # miniprogram/pages/index/index.wxml resolved by devlop version # miniprogram/pages/my/my.wxml resolved by devlop version # miniprogram/pages/read/read.wxml resolved by devlop version # miniprogram/pages/read/read.wxss resolved by devlop version # soul-admin/dist/index.html resolved by devlop version # soul-admin/src/pages/content/ChapterTree.tsx resolved by devlop version # soul-admin/src/pages/content/ContentPage.tsx resolved by devlop version # soul-admin/src/pages/distribution/DistributionPage.tsx resolved by devlop version # soul-api/internal/handler/book.go resolved by devlop version # soul-api/internal/handler/ckb.go resolved by devlop version # soul-api/internal/handler/db_book.go resolved by devlop version # soul-api/internal/handler/db_ckb_leads.go resolved by devlop version # soul-api/internal/handler/db_person.go resolved by devlop version # soul-api/internal/model/chapter.go resolved by devlop version
This commit is contained in:
@@ -13,14 +13,44 @@
|
||||
* - contentSegments 解析每行,mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead
|
||||
*/
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
import readingTracker from '../../utils/readingTracker'
|
||||
const accessManager = require('../../utils/chapterAccessManager')
|
||||
const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
/** 阅读页解析正文用:人物字典 + #标签(与 /config/read-extras 一致) */
|
||||
function getContentParseConfig() {
|
||||
const g = getApp().globalData || {}
|
||||
const raw = Array.isArray(g.mentionPersons) ? g.mentionPersons : []
|
||||
const persons = raw.map((p) => ({
|
||||
personId: p.personId || '',
|
||||
token: p.token || '',
|
||||
name: (p.name || '').trim(),
|
||||
label: (p.label || '').trim(),
|
||||
aliases: p.aliases != null ? String(p.aliases) : '',
|
||||
}))
|
||||
const linkTags = Array.isArray(g.linkTagsConfig) ? g.linkTagsConfig : []
|
||||
return { persons, linkTags }
|
||||
}
|
||||
|
||||
/** 补全 mentionDisplay,避免旧数据无字段;昵称去空白防「@ 名」 */
|
||||
function normalizeMentionSegments(segments) {
|
||||
if (!Array.isArray(segments)) return []
|
||||
return segments.map((row) => {
|
||||
if (!Array.isArray(row)) return row
|
||||
return row.map((seg) => {
|
||||
if (!seg || seg.type !== 'mention') return seg
|
||||
const nick = String(seg.nickname || '')
|
||||
.replace(/^[\s\u00a0\u200b\u3000]+/g, '')
|
||||
.replace(/[\s\u00a0\u200b\u3000]+$/g, '')
|
||||
return { ...seg, nickname: nick, mentionDisplay: '@' + nick }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
@@ -312,6 +342,8 @@ Page({
|
||||
async loadContent(id, accessState, prefetchedChapter) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
try {
|
||||
await app.getReadExtras()
|
||||
const parseCfg = getContentParseConfig()
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
let res = prefetchedChapter
|
||||
if (!res || !res.content) {
|
||||
@@ -328,13 +360,13 @@ Page({
|
||||
// 已解锁用 data.content(完整内容),未解锁用 content(预览);先 determineAccessState 再 loadContent 保证顺序正确
|
||||
const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content
|
||||
if (res && displayContent) {
|
||||
const { lines, segments } = contentParser.parseContent(displayContent)
|
||||
const { lines, segments } = contentParser.parseContent(displayContent, parseCfg)
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const updates = {
|
||||
content: displayContent,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
@@ -355,13 +387,14 @@ Page({
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
const { lines, segments } = contentParser.parseContent(cached.content)
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(cached.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
this.setData({
|
||||
content: cached.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
@@ -460,8 +493,9 @@ Page({
|
||||
},
|
||||
|
||||
// 设置章节内容(兼容纯文本/Markdown 与 TipTap HTML)
|
||||
setChapterContent(res) {
|
||||
const { lines, segments } = contentParser.parseContent(res.content)
|
||||
async setChapterContent(res) {
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(res.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
@@ -478,7 +512,7 @@ Page({
|
||||
content: res.content,
|
||||
previewContent: lines.slice(0, previewCount).join('\n'),
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
// 导航栏、分享等使用的文章标题,同样统一为 sectionTitle
|
||||
@@ -504,7 +538,7 @@ Page({
|
||||
if (currentRetry >= maxRetries) {
|
||||
this.setData({
|
||||
contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
|
||||
contentSegments: contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments,
|
||||
contentSegments: normalizeMentionSegments(contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments),
|
||||
previewParagraphs: ['内容加载失败']
|
||||
})
|
||||
return
|
||||
@@ -514,7 +548,7 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
await this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
return
|
||||
@@ -680,12 +714,6 @@ Page({
|
||||
})
|
||||
return
|
||||
}
|
||||
// 2 分钟内只能点一次(与后端限频一致,与首页链接卡若共用)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
@@ -881,10 +909,8 @@ Page({
|
||||
#创业派对 #私域运营 #商业案例`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({ title: '文案已复制', icon: 'success' })
|
||||
}
|
||||
data: shareText
|
||||
// 不额外 showToast:系统已有「内容已复制」,避免与自定义文案叠两层
|
||||
})
|
||||
},
|
||||
|
||||
@@ -935,12 +961,17 @@ Page({
|
||||
wx.setClipboardData({
|
||||
data: copyText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请点击右上角「···」菜单,选择「分享到朋友圈」即可发布',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
// 系统会在 setClipboardData 成功后自动 toast「内容已复制」,与下方引导弹窗重复,先关掉再弹窗
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '分享到朋友圈',
|
||||
content: '文案已复制。\n\n请点击右上角「···」菜单,选择「分享到朋友圈」即可发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
|
||||
@@ -1099,7 +1130,28 @@ Page({
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
// 检查金额是否有效
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
const avatar = userInfo.avatarUrl || ''
|
||||
const nickname = userInfo.nickname || userInfo.nickName || ''
|
||||
const needProfile = !avatar || avatar.includes('default') || avatar.includes('132') || !nickname || nickname === '微信用户'
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '购买前请先完善头像和昵称',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: resolve
|
||||
})
|
||||
})
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
console.error('[Pay] 金额无效:', amount)
|
||||
wx.showToast({ title: '价格信息错误', icon: 'none' })
|
||||
@@ -1246,13 +1298,13 @@ Page({
|
||||
// 支付接口失败时,显示客服联系方式
|
||||
wx.showModal({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
|
||||
content: '微信支付正在审核中,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => {
|
||||
wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
}
|
||||
@@ -1294,13 +1346,13 @@ Page({
|
||||
// 支付失败,可能是参数错误或权限问题
|
||||
wx.showModal({
|
||||
title: '支付失败',
|
||||
content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
|
||||
content: '微信支付暂不可用,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
@@ -1453,18 +1505,22 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报
|
||||
// 生成海报(Canvas 2D API)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true }, () => resolve())
|
||||
})
|
||||
await new Promise((resolve) => {
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(resolve)
|
||||
else setTimeout(resolve, 50)
|
||||
})
|
||||
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数,优先 mid 与新链接一致)
|
||||
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
@@ -1473,109 +1529,102 @@ Page({
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
if (qrRes.success && qrRes.image) qrcodeImage = qrRes.image
|
||||
} catch (_) {}
|
||||
|
||||
const canvasNode = await new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (res && res[0] && res[0].node) resolve(res[0])
|
||||
else reject(new Error('canvas node not found'))
|
||||
})
|
||||
})
|
||||
|
||||
const canvas = canvasNode.node
|
||||
const ctx = canvas.getContext('2d')
|
||||
let dpr = 2
|
||||
try {
|
||||
if (typeof wx.getWindowInfo === 'function') {
|
||||
dpr = wx.getWindowInfo().pixelRatio || 2
|
||||
} else if (wx.getSystemInfoSync) {
|
||||
dpr = wx.getSystemInfoSync().pixelRatio || 2
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
} catch (_) {
|
||||
dpr = 2
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
const titleLines = this.wrapText2d(ctx, title, width - 40)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
titleLines.forEach(line => { ctx.fillText(line, 20, y); y += 26 })
|
||||
|
||||
ctx.strokeStyle = '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)')
|
||||
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.fillStyle = '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)')
|
||||
const summaryLines = this.wrapText2d(ctx, summary, width - 40)
|
||||
summaryLines.slice(0, 6).forEach(line => { ctx.fillText(line, 20, y); y += 20 })
|
||||
|
||||
ctx.fillStyle = 'rgba(0,206,209,0.1)'
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
if (qrcodeImage) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
fs.writeFileSync(filePath, base64Data, 'base64')
|
||||
const img = canvas.createImage()
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
img.src = filePath
|
||||
})
|
||||
ctx.drawImage(img, width - 85, height - 85, 70, 70)
|
||||
} catch (_) {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
@@ -1583,21 +1632,19 @@ Page({
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
drawQRPlaceholder(ctx, width, height) {
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
drawQRPlaceholder2d(ctx, width, height) {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.beginPath()
|
||||
ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.setFontSize(9)
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.font = '9px sans-serif'
|
||||
ctx.fillText('扫码', width - 57, height - 52)
|
||||
ctx.fillText('阅读', width - 57, height - 40)
|
||||
},
|
||||
|
||||
// 文字换行处理
|
||||
wrapText(ctx, text, maxWidth, fontSize) {
|
||||
|
||||
wrapText2d(ctx, text, maxWidth) {
|
||||
const lines = []
|
||||
let line = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
@@ -1619,39 +1666,47 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
// 保存海报到相册(Canvas 2D)
|
||||
savePoster() {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const canvas = res[0].node
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
success: (r2) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: r2.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (m) => {
|
||||
if (m.confirm) wx.openSetting()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
}, this)
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
|
||||
@@ -54,11 +54,9 @@
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
|
||||
<text user-select wx:if="{{!(item.length === 1 && item[0].type === 'image')}}"><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<text wx:if="{{seg.type === 'text'}}" user-select>{{seg.text}}</text>
|
||||
<text wx:elif="{{seg.type === 'mention'}}" class="mention" user-select bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">@{{seg.nickname}}</text>
|
||||
<text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" user-select bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text>
|
||||
<image wx:elif="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -94,23 +92,17 @@
|
||||
<!-- 分享操作区 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<view class="action-btn-inline" bindtap="onShareTimelineTap">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-share-inline" bindtap="onShareTimelineTap">
|
||||
<icon name="megaphone" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</view>
|
||||
<view class="action-btn-inline" bindtap="generatePoster">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/image.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<icon name="image" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/gift.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
@@ -130,15 +122,23 @@
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<!-- 付费墙 - 未登录:显示购买按钮(朋友圈/分享场景) -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读{{previewPercent}}%,登录后查看完整内容</text>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并购买后阅读全文</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">手机号一键登录</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal" style="margin-top:12px">
|
||||
<text class="login-btn-text">手机号登录后购买</text>
|
||||
</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -185,7 +185,7 @@
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读{{previewPercent}}%,购买后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</text>
|
||||
|
||||
<!-- 购买选项(审核模式隐藏) -->
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
@@ -280,7 +280,7 @@
|
||||
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
<canvas type="2d" id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
|
||||
<view class="poster-actions">
|
||||
@@ -359,6 +359,6 @@
|
||||
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<image class="fab-circle-icon" src="/assets/icons/circle.svg" mode="aspectFit"></image>
|
||||
<icon name="share" size="44" color="#0f172a" customClass="fab-share-icon"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -479,10 +479,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
@@ -492,28 +493,31 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-text-small {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.share-tip-inline {
|
||||
@@ -655,6 +659,9 @@
|
||||
}
|
||||
|
||||
/* ===== 代付分享 ===== */
|
||||
.btn-gift-inline {
|
||||
/* 与 btn-share-inline 同风格 */
|
||||
}
|
||||
.gift-share-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1248,15 +1255,20 @@
|
||||
.fab-share {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
width:70rpx!important;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 70rpx;
|
||||
border-radius: 60rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
@@ -1265,10 +1277,18 @@
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.fab-circle-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
.fab-icon {
|
||||
padding:16rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-share-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user