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:
Alex-larget
2026-03-24 14:27:07 +08:00
470 changed files with 60847 additions and 3748 deletions

View File

@@ -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)
})
},
// 阻止冒泡

View File

@@ -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>

View File

@@ -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;
}