feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本
- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整 - soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新 - soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL - .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle Made-with: Cursor
This commit is contained in:
@@ -18,6 +18,7 @@ const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
@@ -120,10 +121,64 @@ Page({
|
||||
|
||||
// 好友从代付分享进入:待自动领取的 requestSn
|
||||
pendingGiftRequestSn: '',
|
||||
|
||||
// 朋友圈单页模式(scene 1154 / systemInfo.mode):无法登录与支付,仅引导「前往小程序」
|
||||
readSinglePageMode: false,
|
||||
// 朋友圈单页付费墙:说明默认收起,点「购买本章」后展开极简文案 + 底栏箭头(无长 Modal)
|
||||
momentsPaywallExpanded: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否处于朋友圈等「单页预览」环境。
|
||||
* 兼容:部分机型/基础库首帧 getSystemInfoSync().mode 未就绪,需结合 launch/enter scene 1154、getWindowInfo。
|
||||
* 命中时同步 app.globalData.isSinglePageMode,保证 ensureFullAppForAuth 与页内 wx:if 一致。
|
||||
*/
|
||||
_detectReadSinglePage() {
|
||||
try {
|
||||
const launch = typeof wx.getLaunchOptionsSync === 'function' ? wx.getLaunchOptionsSync() : null
|
||||
if (launch && Number(launch.scene) === 1154) {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const enter = typeof wx.getEnterOptionsSync === 'function' ? wx.getEnterOptionsSync() : null
|
||||
if (enter && Number(enter.scene) === 1154) {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const win = typeof wx.getWindowInfo === 'function' ? wx.getWindowInfo() : null
|
||||
if (win && win.mode === 'singlePage') {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
if (sys && sys.mode === 'singlePage') {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
return !!app.globalData.isSinglePageMode
|
||||
},
|
||||
|
||||
/** 单页模式下点「购买本章」:触觉反馈 + 展开极简说明;引导靠页内文案 + 底栏箭头,不再弹长 Modal */
|
||||
onUnlockTapInSinglePage() {
|
||||
trackClick('read', 'btn_click', '单页_解锁引导')
|
||||
try {
|
||||
wx.vibrateShort({ type: 'light' })
|
||||
} catch (e) {}
|
||||
if (this._detectReadSinglePage() && typeof this.setData === 'function') {
|
||||
this.setData({ readSinglePageMode: true, momentsPaywallExpanded: true })
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
const sp = this._detectReadSinglePage()
|
||||
this.setData({
|
||||
auditMode: app.globalData.auditMode || false,
|
||||
readSinglePageMode: sp,
|
||||
...(sp ? {} : { momentsPaywallExpanded: false }),
|
||||
})
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
@@ -186,7 +241,9 @@ Page({
|
||||
sectionMid: mid || null,
|
||||
loading: true,
|
||||
accessState: 'unknown',
|
||||
pendingGiftRequestSn: giftRequestSn || ''
|
||||
pendingGiftRequestSn: giftRequestSn || '',
|
||||
readSinglePageMode: this._detectReadSinglePage(),
|
||||
momentsPaywallExpanded: false,
|
||||
})
|
||||
|
||||
if (ref) {
|
||||
@@ -234,9 +291,10 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
// 【标准流程】4. 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
readingTracker.init(id)
|
||||
} else {
|
||||
app.touchRecentSection(id)
|
||||
}
|
||||
|
||||
// 5. 导航:文章详情已带 prev/next
|
||||
@@ -375,6 +433,7 @@ Page({
|
||||
if (accessManager.canAccessFullContent(accessState)) {
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
app.touchRecentSection(id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败,尝试本地缓存:', e)
|
||||
@@ -393,6 +452,7 @@ Page({
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
})
|
||||
app.touchRecentSection(id)
|
||||
console.log('[Read] 从本地缓存加载成功')
|
||||
return
|
||||
}
|
||||
@@ -698,8 +758,8 @@ Page({
|
||||
}
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号(必填),以便对方联系您',
|
||||
title: '补全手机号',
|
||||
content: '请填写手机号(必填),便于对方联系您。',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
@@ -784,11 +844,7 @@ Page({
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '朋友圈单页',
|
||||
content: '当前为朋友圈单页,无法发起代付支付。请点击底部「前往小程序」进入完整版后再操作。',
|
||||
showCancel: false
|
||||
})
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -932,17 +988,9 @@ Page({
|
||||
return { title, path }
|
||||
},
|
||||
|
||||
// 底部「分享到朋友圈」按钮点击:微信不支持 button open-type=shareTimeline,只能通过右上角菜单分享,点击时引导用户
|
||||
onShareTimelineTap() {
|
||||
wx.showToast({
|
||||
title: '请点击右上角「...」→ 分享到朋友圈',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
},
|
||||
|
||||
// 右下角悬浮按钮:分享到朋友圈(复制文案 + 引导点右上角)
|
||||
// 分享到朋友圈:复制摘要 + 引导用户用右上角「···」发圈(无 open-type=shareTimeline)
|
||||
shareToMoments() {
|
||||
trackClick('read', 'btn_click', '分享到朋友圈_' + (this.data.sectionId || ''))
|
||||
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
|
||||
const raw = (this.data.content || '')
|
||||
.replace(/<[^>]+>/g, '\n')
|
||||
@@ -961,7 +1009,7 @@ Page({
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '分享到朋友圈',
|
||||
content: '文案已复制。\n\n请点击右上角「···」菜单,选择「分享到朋友圈」即可发布。',
|
||||
content: '已复制发圈文案(非分享给好友)。\n\n请点击右上角「···」→「分享到朋友圈」粘贴发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
@@ -988,21 +1036,10 @@ Page({
|
||||
|
||||
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
|
||||
showLoginModal() {
|
||||
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为朋友圈单页,仅支持部分浏览。想登录继续阅读,请点击底部「前往小程序」后再操作。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 检测单页模式失败,回退为正常登录流程:', e)
|
||||
// 单页模式无法弹登录组件:页内已说明「前往小程序」,不再弹 Modal
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.setData({ showLoginModal: true })
|
||||
@@ -1086,6 +1123,10 @@ Page({
|
||||
|
||||
// 购买章节 - 直接调起支付
|
||||
async handlePurchaseSection() {
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
trackClick('read', 'btn_click', '购买章节_' + this.data.sectionId)
|
||||
console.log('[Pay] 点击购买章节按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
@@ -1105,6 +1146,10 @@ Page({
|
||||
|
||||
// 购买全书 - 直接调起支付
|
||||
async handlePurchaseFullBook() {
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
console.log('[Pay] 点击购买全书按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
|
||||
@@ -1123,6 +1168,14 @@ Page({
|
||||
// 处理支付 - 调用真实微信支付接口
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
} catch (e) {}
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
@@ -1132,10 +1185,10 @@ Page({
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '购买前请先完善头像和昵称',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
title: '设置头像与昵称',
|
||||
content: '支付订单会关联你的对外展示信息,请先设置头像与昵称,避免账单与对方看到默认占位。',
|
||||
confirmText: '去设置',
|
||||
cancelText: '关闭',
|
||||
success: resolve
|
||||
})
|
||||
})
|
||||
@@ -1294,7 +1347,7 @@ Page({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
cancelText: '关闭',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
@@ -1414,6 +1467,7 @@ Page({
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
checkAndExecute('after_pay', this)
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
@@ -1499,6 +1553,25 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
/** 海报 canvas 在弹层渲染后偶现取不到 node,多次重试 */
|
||||
async _queryPosterCanvasNode(maxTry = 10, delayMs = 100) {
|
||||
for (let i = 0; i < maxTry; i++) {
|
||||
const node = await new Promise((resolve) => {
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (res && res[0] && res[0].node) resolve(res[0])
|
||||
else resolve(null)
|
||||
})
|
||||
})
|
||||
if (node) return node
|
||||
await new Promise((r) => setTimeout(r, delayMs))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 生成海报(Canvas 2D API)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
@@ -1509,6 +1582,7 @@ Page({
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(resolve)
|
||||
else setTimeout(resolve, 50)
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
|
||||
try {
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
@@ -1526,18 +1600,12 @@ Page({
|
||||
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 canvasNode = await this._queryPosterCanvasNode()
|
||||
if (!canvasNode) {
|
||||
throw new Error('canvas node not found')
|
||||
}
|
||||
|
||||
const canvas = canvasNode.node
|
||||
const ctx = canvas.getContext('2d')
|
||||
let dpr = 2
|
||||
try {
|
||||
if (typeof wx.getWindowInfo === 'function') {
|
||||
@@ -1548,73 +1616,100 @@ Page({
|
||||
} catch (_) {
|
||||
dpr = 2
|
||||
}
|
||||
const width = 300
|
||||
const height = 450
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
// 布局尺寸:优先用节点测量;为 0 时回退 300×450(避免真机 query 过早得到 0 导致空白)
|
||||
const layoutW = (canvasNode.width && canvasNode.width > 1) ? Math.round(canvasNode.width) : 300
|
||||
const layoutH = (canvasNode.height && canvasNode.height > 1) ? Math.round(canvasNode.height) : 450
|
||||
canvas.width = Math.max(1, Math.floor(layoutW * dpr))
|
||||
canvas.height = Math.max(1, Math.floor(layoutH * dpr))
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('canvas 2d not supported')
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
const paintPoster = async () => {
|
||||
const w = layoutW
|
||||
const h = layoutH
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, h)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, w, 4)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('卡若创业派对', 20, 35)
|
||||
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText2d(ctx, title, width - 40)
|
||||
let y = 70
|
||||
titleLines.forEach(line => { ctx.fillText(line, 20, y); y += 26 })
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText2d(ctx, title, w - 40)
|
||||
let y = 70
|
||||
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.strokeStyle = 'rgba(255,255,255,0.1)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(w - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
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.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.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
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)
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
||||
y += 30
|
||||
let paras = Array.isArray(contentParagraphs) ? contentParagraphs.filter(Boolean) : []
|
||||
if (!paras.length && this.data.content) {
|
||||
const plain = String(this.data.content)
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (plain) paras = [plain.slice(0, 400)]
|
||||
}
|
||||
const rawSummary = paras.slice(0, 3).join(' ').trim() || '卡若创业派对 · 真实商业故事'
|
||||
const summary = rawSummary.length > 160 ? rawSummary.slice(0, 157) + '...' : rawSummary
|
||||
const summaryLines = this.wrapText2d(ctx, summary, w - 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, h - 100, w, 100)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, h - 60)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, h - 38)
|
||||
|
||||
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, w - 85, h - 85, 70, 70)
|
||||
} catch (_) {
|
||||
this.drawQRPlaceholder2d(ctx, w, h)
|
||||
}
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof canvas.requestAnimationFrame === 'function') {
|
||||
await new Promise((resolve, reject) => {
|
||||
canvas.requestAnimationFrame(() => {
|
||||
paintPoster().then(resolve).catch(reject)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
await paintPoster()
|
||||
}
|
||||
|
||||
wx.hideLoading()
|
||||
|
||||
@@ -89,21 +89,21 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享操作区 -->
|
||||
<!-- 分享区:仅好友/海报/代付;完整小程序发圈见右下角悬浮钮 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<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 btn-poster-inline" bindtap="generatePoster">
|
||||
<button plain class="action-share-native action-tile-unified" open-type="share" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="share" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">分享给好友</text>
|
||||
</button>
|
||||
<button plain class="action-share-native action-tile-unified" bindtap="generatePoster" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="image" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
</button>
|
||||
<button plain class="action-share-native action-tile-unified" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="gift" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
|
||||
@@ -122,23 +122,36 @@
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录:显示购买按钮(朋友圈/分享场景) -->
|
||||
<view class="paywall">
|
||||
<!-- 付费墙 - 未登录:完整小程序登录+价;朋友圈单页与正文同款「购买本章 ¥1」,点后再展开极简说明 -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并购买后阅读全文</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>
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<text class="paywall-title">解锁全文</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥1</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-desc paywall-desc--moments-expanded" wx:if="{{momentsPaywallExpanded}}">预览不可付款,请点底部「前往小程序」。</text>
|
||||
</block>
|
||||
|
||||
<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>
|
||||
<block wx:else>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并支付 ¥1 后阅读全文</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>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -182,39 +195,47 @@
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</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>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<text class="paywall-title">解锁全文</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥1</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-desc paywall-desc--moments-expanded" wx:if="{{momentsPaywallExpanded}}">预览不可付款,请点底部「前往小程序」。</text>
|
||||
</block>
|
||||
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
<!-- 代付分享:帮好友购买(审核模式隐藏) -->
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="40" color="#00CED1" customClass="gift-share-icon"></icon>
|
||||
<text class="gift-share-text">代付分享</text>
|
||||
</view>
|
||||
<block wx:else>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</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 class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="40" color="#00CED1" customClass="gift-share-icon"></icon>
|
||||
<text class="gift-share-text">代付分享</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -270,9 +291,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content poster-modal" catchtap="stopPropagation">
|
||||
<!-- 海报生成弹窗:居中 + z-index 高于右下角悬浮,避免「空白」错觉 -->
|
||||
<view class="modal-overlay modal-overlay-center" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content modal-content-center poster-modal" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">生成海报</text>
|
||||
<view class="modal-close" bindtap="closePosterModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
@@ -357,8 +378,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<icon name="share" size="44" color="#0f172a" customClass="fab-share-icon"></icon>
|
||||
<!-- 单页预览(朋友圈):指向底栏「前往小程序」,字少;完整小程序仍保留发圈悬浮钮 -->
|
||||
<view class="singlepage-launch-pointer" wx:if="{{readSinglePageMode && momentsPaywallExpanded}}" aria-hidden="true">
|
||||
<view class="singlepage-launch-pointer__arrow">↘</view>
|
||||
</view>
|
||||
|
||||
<view class="fab-share-moments" wx:if="{{!readSinglePageMode}}" bindtap="shareToMoments" hover-class="fab-share-moments-hover" aria-label="分享到朋友圈">
|
||||
<icon name="share" size="44" color="#ffffff" customClass="fab-share-moments-icon"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -280,6 +280,36 @@
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:与完整小程序同款购买行,留白略紧 */
|
||||
.paywall--single-preview {
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
.paywall--single-preview .paywall-icon {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.paywall--single-preview .paywall-title {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.paywall-desc--moments-expanded {
|
||||
margin-top: 28rpx !important;
|
||||
margin-bottom: 0 !important;
|
||||
font-size: 26rpx !important;
|
||||
line-height: 1.45;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:未点解锁前的一行轻提示 */
|
||||
.paywall-hint-compact {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 36rpx;
|
||||
line-height: 1.55;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.paywall-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
@@ -360,6 +390,33 @@
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.paywall-singlepage-note {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 朋友圈单页付费墙底部:与正文文末「分享赚收益」文案一致 */
|
||||
.paywall-share-earn-wrap {
|
||||
margin-top: 28rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
text-align: center;
|
||||
}
|
||||
.paywall-share-earn-wrap .share-tip-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.paywall-share-earn-sub {
|
||||
margin-top: 12rpx !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paywall-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
@@ -470,7 +527,14 @@
|
||||
.action-row-inline {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16rpx;
|
||||
align-items: stretch;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 底部三按钮:同一底纹与描边(好友 / 海报 / 代付) */
|
||||
.action-tile-unified {
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.28) !important;
|
||||
}
|
||||
|
||||
.action-btn-inline {
|
||||
@@ -489,21 +553,38 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.action-btn-inline::after {
|
||||
/* 分享给好友:原生 button + open-type=share,样式与 action-btn-inline 对齐 */
|
||||
.action-share-native {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 96rpx;
|
||||
margin: 0;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
line-height: normal;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.action-share-native::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
button.action-share-native {
|
||||
color: inherit;
|
||||
}
|
||||
.action-share-native-hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
.action-btn-inline::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
@@ -597,7 +678,8 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* 高于右下角悬浮钮,避免弹层被盖住或 canvas 不可见 */
|
||||
z-index: 10050;
|
||||
}
|
||||
|
||||
.modal-overlay-center {
|
||||
@@ -1201,6 +1283,9 @@
|
||||
/* ===== 海报弹窗 ===== */
|
||||
.poster-modal {
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.poster-preview {
|
||||
@@ -1251,44 +1336,54 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 右下角悬浮分享按钮 ===== */
|
||||
.fab-share {
|
||||
/* ===== 右下角:分享到朋友圈(固定悬浮小圆钮,不在文末分享行) ===== */
|
||||
.fab-share-moments {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
width:70rpx!important;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 70rpx;
|
||||
border-radius: 60rpx;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
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;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.42);
|
||||
z-index: 9980;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
border: none;
|
||||
.fab-share-moments-hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
.fab-share-moments:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
padding:16rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-share-icon {
|
||||
.fab-share-moments-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
/* 朋友圈单页:点「购买本章」后,箭头指向底栏「前往小程序」(字少,仅符号) */
|
||||
.singlepage-launch-pointer {
|
||||
position: fixed;
|
||||
right: 48rpx;
|
||||
bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
z-index: 99985;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.singlepage-launch-pointer__arrow {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
color: #00CED1;
|
||||
text-shadow: 0 0 20rpx rgba(0, 206, 209, 0.55);
|
||||
transform: rotate(0deg);
|
||||
animation: singlepage-launch-pulse 1.25s ease-in-out infinite;
|
||||
}
|
||||
@keyframes singlepage-launch-pulse {
|
||||
0%, 100% { opacity: 0.75; transform: translate(0, 0) scale(1); }
|
||||
50% { opacity: 1; transform: translate(8rpx, 10rpx) scale(1.06); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user