feat: 同步本地三端改动并清理上传凭证风险
整合小程序、管理端与后端的最新本地改动,补齐用户管理与首页入口相关能力;提交前已完成敏感信息扫描,并移除本地 gitea 远程 URL 中的明文凭证,避免隐私信息进入远程仓库。 Made-with: Cursor
This commit is contained in:
@@ -10,9 +10,9 @@ const mpPagePopups = require('./utils/mpPagePopups.js')
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
// baseUrl 手动切换(注释方式):
|
||||
// const API_BASE_URL = 'http://localhost:9090'
|
||||
const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
// baseUrl 手动切换(本地端口以 soul-api/.env 的 PORT 为准,当前仓库多为 8080;发版前改回线上)
|
||||
const API_BASE_URL = 'http://127.0.0.1:8080'
|
||||
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
// const API_BASE_URL = 'https://souldev.quwanzhi.com'
|
||||
|
||||
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
|
||||
@@ -548,6 +548,8 @@ App({
|
||||
const userId = this.globalData.userInfo.id
|
||||
const base = (this.globalData.baseUrl || '').replace(/\/$/, '')
|
||||
if (!base) return
|
||||
// 本机 API 无稳定 WSS 时跳过,减少控制台报错与无效重连
|
||||
if (/^(https?:\/\/)?(127\.0\.0\.1|localhost)(:\d+)?$/i.test(base)) return
|
||||
const wsUrl = base.replace(/^http/, 'ws') + '/ws/miniprogram'
|
||||
if (this._wsHeartbeatTimer) {
|
||||
clearInterval(this._wsHeartbeatTimer)
|
||||
@@ -1164,14 +1166,54 @@ App({
|
||||
}
|
||||
},
|
||||
|
||||
/** 是否为微信小程序支付可用的 openid */
|
||||
looksLikeWxOpenId(s) {
|
||||
const x = String(s || '').trim()
|
||||
if (x.length < 24 || x.length > 64) return false
|
||||
if (x.indexOf('user_') === 0) return false
|
||||
if (x.charAt(0) !== 'o') return false
|
||||
return /^o[a-zA-Z0-9_-]+$/.test(x)
|
||||
},
|
||||
|
||||
/** 从缓存 / userInfo 取合法微信 openid */
|
||||
resolvePayOpenId() {
|
||||
const u = this.globalData.userInfo || {}
|
||||
const pick = (v) => {
|
||||
const s = String(v || '').trim()
|
||||
return this.looksLikeWxOpenId(s) ? s : ''
|
||||
}
|
||||
return pick(this.globalData.openId) || pick(wx.getStorageSync('openId')) || pick(u.openId || u.open_id)
|
||||
},
|
||||
|
||||
/** 支付前确保拿到真实微信 openid */
|
||||
async ensurePayOpenId() {
|
||||
let oid = this.resolvePayOpenId()
|
||||
if (oid) {
|
||||
this.globalData.openId = oid
|
||||
try { wx.setStorageSync('openId', oid) } catch (_) {}
|
||||
return oid
|
||||
}
|
||||
try { await this.getOpenId() } catch (_) {}
|
||||
oid = this.resolvePayOpenId()
|
||||
if (oid) {
|
||||
this.globalData.openId = oid
|
||||
try { wx.setStorageSync('openId', oid) } catch (_) {}
|
||||
return oid
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
// 获取openId (支付必需)
|
||||
async getOpenId() {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
// 先检查缓存
|
||||
// 缓存中如果存的是 userId 等非法值,先清掉
|
||||
const cachedOpenId = wx.getStorageSync('openId')
|
||||
if (cachedOpenId) {
|
||||
if (cachedOpenId && !this.looksLikeWxOpenId(cachedOpenId)) {
|
||||
try { wx.removeStorageSync('openId') } catch (_) {}
|
||||
this.globalData.openId = null
|
||||
} else if (cachedOpenId && this.looksLikeWxOpenId(cachedOpenId)) {
|
||||
this.globalData.openId = cachedOpenId
|
||||
return cachedOpenId
|
||||
}
|
||||
|
||||
@@ -180,7 +180,6 @@ Page({
|
||||
})
|
||||
}
|
||||
const ch = chMap.get(cid)
|
||||
const isPremium = r.editionPremium === true || r.edition_premium === true || r.edition_premium === 1 || r.edition_premium === '1'
|
||||
ch.sections.push({
|
||||
id: r.id,
|
||||
mid: r.mid ?? r.MID ?? 0,
|
||||
@@ -191,7 +190,7 @@ Page({
|
||||
newBadgeText: this._normalizeBadgeText(
|
||||
r.newBadgeText || r.new_badge_text || r.sectionBadgeText || r.section_badge_text || r.badgeText || r.badge_text
|
||||
),
|
||||
isPremium
|
||||
isPremium: false
|
||||
})
|
||||
})
|
||||
const chapters = Array.from(chMap.values())
|
||||
@@ -327,10 +326,9 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 检查是否已购买
|
||||
hasPurchased(sectionId, isPremium) {
|
||||
hasPurchased(sectionId) {
|
||||
if (this.data.isVip) return true
|
||||
if (!isPremium && this.data.hasFullBook) return true
|
||||
if (this.data.hasFullBook) return true
|
||||
return this.data.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
|
||||
@@ -125,14 +125,15 @@ Page({
|
||||
wx.showToast({ title: '请先登录后再支付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let openId = app.globalData.openId || wx.getStorageSync('openId')
|
||||
wx.showLoading({ title: '获取支付凭证...', mask: true })
|
||||
const openId = await app.ensurePayOpenId()
|
||||
wx.hideLoading()
|
||||
if (!openId) {
|
||||
wx.showLoading({ title: '获取支付凭证...', mask: true })
|
||||
openId = await app.getOpenId()
|
||||
wx.hideLoading()
|
||||
}
|
||||
if (!openId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
wx.showModal({
|
||||
title: '无法拉起支付',
|
||||
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
let { requestSn, sectionId, detail, giftQuantity, isCreateMode } = this.data
|
||||
|
||||
@@ -9,7 +9,18 @@ const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser')
|
||||
const { navigateMpPath } = require('../../utils/mpNavigate.js')
|
||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
const { submitCkbLead } = require('../../utils/soulBridge')
|
||||
const {
|
||||
submitCkbLead,
|
||||
getReferralCodeForPay,
|
||||
requestWxJsapiPayment,
|
||||
syncOrderStatusQuery,
|
||||
} = require('../../utils/soulBridge')
|
||||
|
||||
const DEFAULT_TIP_GIFTS = [
|
||||
{ id: 'gift_lite', name: '小心心', price: 9.9, emoji: '❤️', icon: '', micOrder: 1, iconScale: 1, micFeatured: true },
|
||||
{ id: 'gift_star', name: '人气票', price: 99, emoji: '🎫', icon: '', micOrder: 0, iconScale: 1, micFeatured: false },
|
||||
{ id: 'gift_super', name: '嘉年华', price: 1980, emoji: '🎡', icon: '', micOrder: 2, iconScale: 1, micFeatured: false },
|
||||
]
|
||||
|
||||
/** 置顶人物无头像时的占位图 */
|
||||
const DEFAULT_KARUO_LINK_AVATAR = '/assets/images/karuo-link-avatar.png'
|
||||
@@ -28,6 +39,102 @@ function superAvatarLetter(displayName) {
|
||||
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
|
||||
}
|
||||
|
||||
function parseTimeToMinutes(t) {
|
||||
const m = String(t || '').trim().match(/^(\d{1,2}):(\d{2})$/)
|
||||
if (!m) return null
|
||||
const h = parseInt(m[1], 10)
|
||||
const min = parseInt(m[2], 10)
|
||||
if (h < 0 || h > 23 || min < 0 || min > 59) return null
|
||||
return h * 60 + min
|
||||
}
|
||||
|
||||
/** 是否在直播上麦时段(支持跨午夜 end < start) */
|
||||
function isNowInMicWindow(startStr, endStr) {
|
||||
const a = parseTimeToMinutes(startStr)
|
||||
const b = parseTimeToMinutes(endStr)
|
||||
if (a == null || b == null) return false
|
||||
const now = new Date()
|
||||
const cur = now.getHours() * 60 + now.getMinutes()
|
||||
if (a <= b) return cur >= a && cur < b
|
||||
return cur >= a || cur < b
|
||||
}
|
||||
|
||||
function normCfgBool(v) {
|
||||
return v === true || v === 1 || v === '1' || String(v).toLowerCase() === 'true'
|
||||
}
|
||||
|
||||
function normalizeTipGifts(raw) {
|
||||
const base = DEFAULT_TIP_GIFTS
|
||||
if (!Array.isArray(raw) || raw.length === 0) return base.map((g) => ({ ...g }))
|
||||
return raw.map((row, i) => {
|
||||
const d = base[Math.min(i, base.length - 1)]
|
||||
const id = String(row.id != null ? row.id : d.id).trim() || d.id
|
||||
const name = String(row.name != null ? row.name : d.name).trim() || d.name
|
||||
let price = parseFloat(row.price)
|
||||
if (Number.isNaN(price) || price <= 0) price = d.price
|
||||
const emoji = String(row.emoji != null ? row.emoji : d.emoji).trim() || d.emoji
|
||||
const icon = String(row.icon != null ? row.icon : '').trim()
|
||||
let micOrder = row.micOrder != null ? parseInt(String(row.micOrder), 10) : NaN
|
||||
if (Number.isNaN(micOrder)) micOrder = 999
|
||||
let iconScale = parseFloat(row.iconScale)
|
||||
if (Number.isNaN(iconScale) || iconScale <= 0) iconScale = 1
|
||||
const micFeatured =
|
||||
row.micFeatured === true ||
|
||||
row.micFeatured === 1 ||
|
||||
row.micFeatured === '1' ||
|
||||
String(row.micFeatured).toLowerCase() === 'true'
|
||||
return { id, name, price, emoji, icon, micOrder, iconScale, micFeatured }
|
||||
})
|
||||
}
|
||||
|
||||
function sortGiftsForMic(arr) {
|
||||
return [...arr].sort((a, b) => {
|
||||
const oa = a.micOrder != null && !Number.isNaN(Number(a.micOrder)) ? Number(a.micOrder) : 999
|
||||
const ob = b.micOrder != null && !Number.isNaN(Number(b.micOrder)) ? Number(b.micOrder) : 999
|
||||
if (oa !== ob) return oa - ob
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上麦横排:按礼物 micOrder 排序,最多 3 档;默认档优先 micFeatured,否则中间位
|
||||
* micEmojiRpx:默认档再乘 liveMicSchedule.micCenterEmojiScale
|
||||
*/
|
||||
function layoutMicHorizontalSlots(gifts, liveMicSchedule) {
|
||||
const fallback = DEFAULT_TIP_GIFTS.map((g) => ({ ...g }))
|
||||
let arr = Array.isArray(gifts) && gifts.length ? gifts : fallback
|
||||
arr = sortGiftsForMic(arr)
|
||||
const slotsRaw = arr.slice(0, 3)
|
||||
let defaultIndex = slotsRaw.length >= 2 ? 1 : 0
|
||||
const fi = slotsRaw.findIndex(
|
||||
(g) => g.micFeatured === true || g.micFeatured === 1 || g.micFeatured === '1' || String(g.micFeatured).toLowerCase() === 'true'
|
||||
)
|
||||
if (fi >= 0) defaultIndex = fi
|
||||
const def = slotsRaw[defaultIndex]
|
||||
const defaultId = def && def.id ? def.id : ''
|
||||
let centerScale = 1.55
|
||||
if (liveMicSchedule && liveMicSchedule.micCenterEmojiScale != null) {
|
||||
const v = parseFloat(String(liveMicSchedule.micCenterEmojiScale))
|
||||
if (!Number.isNaN(v) && v >= 1) centerScale = Math.min(v, 2.4)
|
||||
}
|
||||
const n = slotsRaw.length
|
||||
/** 几何中间列:3 档为 index 1;2 档为 index 1;1 档为 0 */
|
||||
const centerIdx = n <= 1 ? 0 : Math.floor(n / 2)
|
||||
const baseRpx = 44
|
||||
const slots = slotsRaw.map((g, idx) => {
|
||||
const per = g.iconScale != null && Number(g.iconScale) > 0 ? Number(g.iconScale) : 1
|
||||
const isDef = g.id === defaultId
|
||||
const isCenterSlot = idx === centerIdx
|
||||
let rpx = Math.round(baseRpx * per * (isDef ? centerScale : 1))
|
||||
if (isCenterSlot) rpx = Math.round(rpx * 1.22)
|
||||
rpx = Math.max(32, Math.min(110, rpx))
|
||||
const iconStr = String(g.icon || '').trim()
|
||||
const micShowIcon = !!(iconStr && isSafeImageSrc(iconStr))
|
||||
return { ...g, micEmojiRpx: rpx, micShowIcon, micIsCenterSlot: isCenterSlot }
|
||||
})
|
||||
return { slots, defaultId }
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
@@ -99,6 +206,42 @@ Page({
|
||||
|
||||
/** 后台 @列表置顶人物:有则右上角展示绑定用户头像 + @名称,点击走 ckb/lead */
|
||||
homePinnedPerson: null,
|
||||
|
||||
/** 首页入口:打赏 + 直播时段上麦(mpUi.homePage.linkKaruoReward / liveMicSchedule) */
|
||||
linkKaruoRewardEnabled: false,
|
||||
linkKaruoRewardUi: {
|
||||
entryButtonText: '支持一下',
|
||||
modalTitle: '打赏支持',
|
||||
modalSubtitle: '感谢支持,任选礼物与数量',
|
||||
payButtonText: '立即支付',
|
||||
leadLinkText: '留下联系方式',
|
||||
afterPayToast: '支付成功,感谢支持!',
|
||||
},
|
||||
liveMicSchedule: {
|
||||
enabled: false,
|
||||
micStart: '18:00',
|
||||
micEnd: '21:00',
|
||||
micEmoji: '🎤',
|
||||
micButtonText: '申请上麦',
|
||||
micModalTitle: '申请上麦',
|
||||
micModalContent: '',
|
||||
micAfterSubmitToast: '支付成功,感谢支持!',
|
||||
micCenterEmojiScale: 1.55,
|
||||
},
|
||||
homeMicEligible: false,
|
||||
tipGifts: [],
|
||||
showTipModal: false,
|
||||
tipHomeMicMode: false,
|
||||
tipModalTitle: '',
|
||||
tipModalSubtitle: '',
|
||||
selectedTipGiftId: '',
|
||||
tipQty: 1,
|
||||
tipLineTotal: '0.00',
|
||||
/** 上麦弹窗横排礼物(与后台 gifts 顺序一致) */
|
||||
micHorizontalSlots: [],
|
||||
micDefaultGiftId: '',
|
||||
/** 右上角唯一入口:由置顶超级个体决定 */
|
||||
headerCornerAction: { show: false, kind: '', label: '', emoji: '', avatar: '' },
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -121,9 +264,13 @@ Page({
|
||||
this.initData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
async onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
try {
|
||||
await app.getConfig(true)
|
||||
} catch (_) {}
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
this._refreshHomeEntryFlags()
|
||||
void this.loadHomePinnedPerson()
|
||||
|
||||
// 设置TabBar选中状态
|
||||
@@ -357,13 +504,15 @@ Page({
|
||||
patch.mpUiLinkKaruoText = ''
|
||||
patch.mpUiLinkKaruoDisplay = DEFAULT_KARUO_LINK_AVATAR
|
||||
}
|
||||
this.setData(patch)
|
||||
try {
|
||||
wx.setNavigationBarTitle({ title: patch.mpUiLogoTitle || '首页' })
|
||||
} catch (_) {}
|
||||
this.setData(patch, () => {
|
||||
try {
|
||||
wx.setNavigationBarTitle({ title: patch.mpUiLogoTitle || '首页' })
|
||||
} catch (_) {}
|
||||
this._syncHeaderCornerAction()
|
||||
})
|
||||
},
|
||||
|
||||
/** 拉取后台置顶 @人物,合并到首页右上角「链接」区 */
|
||||
/** 拉取后台置顶 @人物,合并到首页右上角「链接」区;同时拿到该人的 homeEntryConfig */
|
||||
async loadHomePinnedPerson() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/ckb/pinned-person', silent: true })
|
||||
@@ -376,6 +525,7 @@ Page({
|
||||
token: String(res.data.token).trim(),
|
||||
name,
|
||||
avatar: av,
|
||||
homeEntryConfig: res.data.homeEntryConfig || null,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -385,21 +535,28 @@ Page({
|
||||
console.log('[Index] pinned-person:', e)
|
||||
this.setData({ homePinnedPerson: null })
|
||||
}
|
||||
const pin = (this.data.homePinnedPerson || {})
|
||||
const override = {}
|
||||
if (pin.homeEntryConfig) {
|
||||
if (pin.homeEntryConfig.linkKaruoReward) override.linkKaruoReward = pin.homeEntryConfig.linkKaruoReward
|
||||
if (pin.homeEntryConfig.liveMicSchedule) override.liveMicSchedule = pin.homeEntryConfig.liveMicSchedule
|
||||
}
|
||||
this._applyHomeMpUi()
|
||||
this._refreshHomeEntryFlags(Object.keys(override).length ? override : undefined)
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const hasCachedFeatures = app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean'
|
||||
if (!hasCachedFeatures) {
|
||||
const res = await app.getConfig()
|
||||
const features = (res && res.features) || (res && res.data && res.data.features) || {}
|
||||
await app.getConfig(true)
|
||||
const res = app.globalData.configCache
|
||||
if (res) {
|
||||
const features = res.features || (res.data && res.data.features) || {}
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
if (typeof features.matchEnabled === 'boolean') app.globalData.features.matchEnabled = features.matchEnabled
|
||||
if (typeof features.referralEnabled === 'boolean') app.globalData.features.referralEnabled = features.referralEnabled
|
||||
const mp = (res && res.mpConfig) || {}
|
||||
const mp = res.mpConfig || {}
|
||||
app.globalData.auditMode = !!mp.auditMode
|
||||
}
|
||||
await app.getAuditMode()
|
||||
@@ -409,6 +566,7 @@ Page({
|
||||
auditMode: app.globalData.auditMode || false
|
||||
})
|
||||
this._applyHomeMpUi()
|
||||
this._refreshHomeEntryFlags()
|
||||
} catch (e) {
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
@@ -418,10 +576,218 @@ Page({
|
||||
auditMode: app.globalData.auditMode || false
|
||||
})
|
||||
this._applyHomeMpUi()
|
||||
this._refreshHomeEntryFlags()
|
||||
}
|
||||
await this.loadHomePinnedPerson()
|
||||
},
|
||||
|
||||
_refreshHomeEntryFlags() {
|
||||
const audit = !!app.globalData.auditMode
|
||||
// 优先读置顶超级个体的 per-person 配置,回退到全局 mpUi.homePage
|
||||
const personCfg = (this.data.homePinnedPerson && this.data.homePinnedPerson.homeEntryConfig) || null
|
||||
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
|
||||
const rw = (personCfg && personCfg.linkKaruoReward) || h.linkKaruoReward || {}
|
||||
const schReal = (personCfg && personCfg.liveMicSchedule) || h.liveMicSchedule || {}
|
||||
const rewardEnabled = normCfgBool(rw.enabled)
|
||||
const micEnabled = normCfgBool(schReal.enabled)
|
||||
const inWindow = isNowInMicWindow(schReal.micStart, schReal.micEnd)
|
||||
const homeMicEligible = !audit && micEnabled && inWindow
|
||||
const gifts = normalizeTipGifts(Array.isArray(rw.gifts) && rw.gifts.length ? rw.gifts : null)
|
||||
const tipGifts = gifts.length ? gifts : DEFAULT_TIP_GIFTS.map((g) => ({ ...g }))
|
||||
this.setData({
|
||||
linkKaruoRewardEnabled: rewardEnabled,
|
||||
linkKaruoRewardUi: {
|
||||
entryButtonText: String(rw.entryButtonText || '支持一下').trim() || '支持一下',
|
||||
modalTitle: String(rw.modalTitle || '打赏支持').trim() || '打赏支持',
|
||||
modalSubtitle: String(rw.modalSubtitle || '感谢支持,任选礼物与数量').trim() || '感谢支持,任选礼物与数量',
|
||||
payButtonText: String(rw.payButtonText || '立即支付').trim() || '立即支付',
|
||||
leadLinkText: String(rw.leadLinkText || '留下联系方式').trim() || '留下联系方式',
|
||||
afterPayToast: String(rw.afterPayToast || '支付成功,感谢支持!').trim() || '支付成功,感谢支持!',
|
||||
},
|
||||
liveMicSchedule: {
|
||||
enabled: micEnabled,
|
||||
micStart: String(schReal.micStart || '18:00').trim() || '18:00',
|
||||
micEnd: String(schReal.micEnd || '21:00').trim() || '21:00',
|
||||
micEmoji: String(schReal.micEmoji || '🎤').trim() || '🎤',
|
||||
micButtonText: String(schReal.micButtonText || '申请上麦').trim() || '申请上麦',
|
||||
micModalTitle: String(schReal.micModalTitle || '申请上麦').trim() || '申请上麦',
|
||||
micModalContent: String(
|
||||
schReal.micModalContent || '当前为直播时段,提交后工作人员将按顺序处理连麦申请。',
|
||||
).trim(),
|
||||
micAfterSubmitToast: String(schReal.micAfterSubmitToast || '支付成功,感谢支持!').trim() || '支付成功,感谢支持!',
|
||||
micCenterEmojiScale: (() => {
|
||||
const v = parseFloat(String(schReal.micCenterEmojiScale != null ? schReal.micCenterEmojiScale : '1.55'))
|
||||
if (Number.isNaN(v) || v < 1) return 1.55
|
||||
return Math.min(v, 2.4)
|
||||
})(),
|
||||
},
|
||||
homeMicEligible,
|
||||
tipGifts,
|
||||
})
|
||||
this._syncHeaderCornerAction()
|
||||
},
|
||||
|
||||
/** 右上角单一入口:上麦时段 > 置顶超级个体 > 打赏 */
|
||||
_syncHeaderCornerAction() {
|
||||
if (app.globalData.auditMode) {
|
||||
this.setData({ headerCornerAction: { show: false, kind: '', label: '', emoji: '', avatar: '' } })
|
||||
return
|
||||
}
|
||||
const mic = !!this.data.homeMicEligible
|
||||
const pinned = !!(this.data.homePinnedPerson && this.data.homePinnedPerson.token)
|
||||
const reward = !!this.data.linkKaruoRewardEnabled
|
||||
const sch = this.data.liveMicSchedule || {}
|
||||
let show = false, kind = '', label = '', emoji = '', avatar = ''
|
||||
if (mic) {
|
||||
show = true; kind = 'mic'
|
||||
label = String(sch.micButtonText || '申请上麦').trim() || '申请上麦'
|
||||
emoji = String(sch.micEmoji || '🎤').trim() || '🎤'
|
||||
} else if (pinned) {
|
||||
show = true; kind = 'ckb'
|
||||
label = String(this.data.mpUiLinkKaruoText || '').trim() || '链接卡若'
|
||||
avatar = this.data.mpUiLinkKaruoDisplay || DEFAULT_KARUO_LINK_AVATAR
|
||||
} else if (reward) {
|
||||
show = true; kind = 'reward'
|
||||
label = String(sch.micButtonText || '申请上麦').trim() || '申请上麦'
|
||||
emoji = String(sch.micEmoji || '🎤').trim() || '🎤'
|
||||
}
|
||||
this.setData({ headerCornerAction: { show, kind, label, emoji, avatar } })
|
||||
},
|
||||
|
||||
/** 右上角统一点击 */
|
||||
onHeaderCornerTap() {
|
||||
const a = this.data.headerCornerAction
|
||||
if (!a || !a.show) return
|
||||
if (a.kind === 'ckb') {
|
||||
this.onLinkKaruo()
|
||||
return
|
||||
}
|
||||
this._openHorizontalGiftModal(a.kind === 'mic' ? 'live_mic' : 'home_reward')
|
||||
},
|
||||
|
||||
/** 打开横排礼物弹窗(上麦/打赏统一入口) */
|
||||
_openHorizontalGiftModal(tipSource) {
|
||||
if (app.globalData.auditMode) return
|
||||
const label = tipSource === 'live_mic' ? '申请上麦' : '打赏支持'
|
||||
trackClick('home', tipSource === 'live_mic' ? 'live_mic_request' : 'btn_click', label)
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: `请先登录后再${label}`,
|
||||
confirmText: '去登录',
|
||||
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) },
|
||||
})
|
||||
return
|
||||
}
|
||||
const sch = this.data.liveMicSchedule
|
||||
const gifts = this.data.tipGifts || []
|
||||
const { slots, defaultId } = layoutMicHorizontalSlots(gifts, sch)
|
||||
const title = tipSource === 'live_mic'
|
||||
? String(sch.micModalTitle || '申请上麦').trim()
|
||||
: String(this.data.linkKaruoRewardUi.modalTitle || '打赏支持').trim()
|
||||
const sub = tipSource === 'live_mic'
|
||||
? String(sch.micModalContent || '当前为直播时段,提交后工作人员将按顺序处理连麦申请。').trim()
|
||||
: String(this.data.linkKaruoRewardUi.modalSubtitle || '感谢支持,点选礼物直接支付').trim()
|
||||
this.setData({
|
||||
showTipModal: true,
|
||||
tipHomeMicMode: tipSource === 'live_mic',
|
||||
tipModalTitle: title,
|
||||
tipModalSubtitle: sub,
|
||||
micHorizontalSlots: slots,
|
||||
micDefaultGiftId: defaultId,
|
||||
selectedTipGiftId: defaultId,
|
||||
tipQty: 1,
|
||||
})
|
||||
},
|
||||
|
||||
closeTipModal() {
|
||||
this.setData({ showTipModal: false, micHorizontalSlots: [], micDefaultGiftId: '' })
|
||||
},
|
||||
|
||||
stopTipModalBubble() {},
|
||||
|
||||
/** 横排礼物卡片点击 → 直接调起微信支付 */
|
||||
onHorizontalTipPay(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (!id) return
|
||||
const src = this.data.tipHomeMicMode ? 'live_mic' : 'home_reward'
|
||||
this._executeLinkKaruoTipPay(src, id, 1)
|
||||
},
|
||||
|
||||
async _executeLinkKaruoTipPay(tipSource, giftId, qty) {
|
||||
const gid = String(giftId || '').trim()
|
||||
const q = Math.max(1, parseInt(String(qty), 10) || 1)
|
||||
if (!gid) {
|
||||
wx.showToast({ title: '请选择礼物', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo?.id) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const openId = await app.ensurePayOpenId()
|
||||
if (!openId) {
|
||||
wx.showModal({
|
||||
title: '无法拉起支付',
|
||||
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
const g = (this.data.tipGifts || []).find((x) => String(x.id) === gid)
|
||||
const amount = g && Number(g.price) > 0 ? Math.round(Number(g.price) * q * 100) / 100 : 0
|
||||
if (amount <= 0) {
|
||||
wx.showToast({ title: '礼物价格无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '下单中...', mask: true })
|
||||
try {
|
||||
const referralCode = getReferralCodeForPay(app) || undefined
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/pay',
|
||||
method: 'POST',
|
||||
silent: true,
|
||||
data: {
|
||||
openId,
|
||||
productType: 'link_karuo_tip',
|
||||
productId: tipSource === 'live_mic' ? `live_mic|${gid}|${q}` : `${gid}|${q}`,
|
||||
amount,
|
||||
description: tipSource === 'live_mic' ? '上麦礼遇' : '首页打赏',
|
||||
userId: app.globalData.userInfo.id,
|
||||
referralCode,
|
||||
tipSource: tipSource === 'live_mic' ? 'live_mic' : 'home_reward',
|
||||
},
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (!res || !res.success || !res.data || !res.data.payParams) {
|
||||
const errText = (res && (res.error || res.message)) || '下单失败'
|
||||
wx.showToast({ title: String(errText).slice(0, 36), icon: 'none' })
|
||||
return
|
||||
}
|
||||
await requestWxJsapiPayment(res.data.payParams)
|
||||
const sn = res.data.orderSn
|
||||
if (sn) {
|
||||
try {
|
||||
await syncOrderStatusQuery(app, sn)
|
||||
} catch (_) {}
|
||||
}
|
||||
const toast =
|
||||
tipSource === 'live_mic'
|
||||
? this.data.liveMicSchedule.micAfterSubmitToast || '支付成功'
|
||||
: this.data.linkKaruoRewardUi.afterPayToast || '支付成功'
|
||||
wx.showToast({ title: toast, icon: 'success' })
|
||||
this.closeTipModal()
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
const msg = (e && e.errMsg) || (e && e.message) || '支付失败'
|
||||
if (String(msg).indexOf('cancel') >= 0) {
|
||||
wx.showToast({ title: '已取消支付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: String(msg).slice(0, 28), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
if (!this.data.searchEnabled) return
|
||||
@@ -496,6 +862,11 @@ Page({
|
||||
|
||||
// 下拉刷新(等待各异步加载完成后再结束)
|
||||
async onPullDownRefresh() {
|
||||
try {
|
||||
await app.getConfig(true)
|
||||
} catch (_) {}
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
this._refreshHomeEntryFlags()
|
||||
await Promise.all([
|
||||
this.loadBookData(),
|
||||
this.loadFeaturedAndLatest(),
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
<text class="logo-subtitle">{{mpUiLogoSubtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right" wx:if="{{!auditMode && homePinnedPerson && homePinnedPerson.token}}">
|
||||
<view class="contact-btn" catchtap="onLinkKaruo" hover-class="none">
|
||||
<image class="contact-avatar" src="{{mpUiLinkKaruoDisplay}}" mode="aspectFill"/>
|
||||
<text class="contact-name">{{mpUiLinkKaruoText}}</text>
|
||||
<!-- 右上角单一入口:由置顶超级个体决定,点击弹出横排礼物,点选即付 -->
|
||||
<view class="header-right" wx:if="{{!auditMode && headerCornerAction.show}}">
|
||||
<view class="contact-btn header-corner-btn" catchtap="onHeaderCornerTap" hover-class="none">
|
||||
<image wx:if="{{headerCornerAction.avatar}}" class="contact-avatar" src="{{headerCornerAction.avatar}}" mode="aspectFill"/>
|
||||
<text wx:elif="{{headerCornerAction.emoji}}" class="mic-entry-emoji">{{headerCornerAction.emoji}}</text>
|
||||
<text class="contact-name">{{headerCornerAction.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -161,6 +163,32 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 横排礼物弹层(上麦 / 打赏统一,与「申请上麦」截图一致,点选即付) -->
|
||||
<view class="tip-modal-mask" wx:if="{{showTipModal}}" catchtap="closeTipModal">
|
||||
<view class="tip-modal-panel tip-modal-panel--mic" catchtap="stopTipModalBubble">
|
||||
<view class="mic-modal-head">
|
||||
<text class="mic-modal-title">{{tipModalTitle}}</text>
|
||||
<view class="mic-modal-x" catchtap="closeTipModal">×</view>
|
||||
</view>
|
||||
<text class="mic-modal-desc" wx:if="{{tipModalSubtitle}}">{{tipModalSubtitle}}</text>
|
||||
<view class="mic-gift-row-h" wx:if="{{micHorizontalSlots.length > 0}}">
|
||||
<view
|
||||
wx:for="{{micHorizontalSlots}}"
|
||||
wx:key="id"
|
||||
class="mic-gift-card {{micDefaultGiftId === item.id ? 'mic-gift-card--default' : ''}} {{item.micIsCenterSlot ? 'mic-gift-card--mid' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
catchtap="onHorizontalTipPay"
|
||||
>
|
||||
<view class="mic-gift-default-tag" wx:if="{{micDefaultGiftId === item.id}}">默认</view>
|
||||
<image wx:if="{{item.micShowIcon}}" class="mic-gift-icon-img" src="{{item.icon}}" mode="aspectFit" style="width: {{item.micEmojiRpx}}rpx; height: {{item.micEmojiRpx}}rpx;"/>
|
||||
<text wx:else class="mic-gift-emoji" style="font-size: {{item.micEmojiRpx}}rpx;">{{item.emoji}}</text>
|
||||
<text class="mic-gift-name">{{item.name}}</text>
|
||||
<text class="mic-gift-price">¥{{item.price}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
|
||||
@@ -962,6 +962,291 @@
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 首页打赏 / 上麦弹层 ===== */
|
||||
.mic-entry-emoji {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tip-modal-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
z-index: 10050;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tip-modal-panel {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
padding: 36rpx 32rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 申请上麦:更宽横排三档 */
|
||||
.tip-modal-panel--mic {
|
||||
max-width: 710rpx;
|
||||
padding: 28rpx 24rpx 32rpx;
|
||||
border-color: rgba(0, 206, 209, 0.35);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.mic-modal-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
padding-right: 4rpx;
|
||||
}
|
||||
|
||||
.mic-modal-title {
|
||||
flex: 1;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
|
||||
.mic-modal-x {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 40rpx;
|
||||
line-height: 52rpx;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mic-modal-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 28rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mic-gift-row-h {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.mic-gift-card {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28rpx 12rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* 中间一列整体更大(与左右对比) */
|
||||
.mic-gift-card--mid {
|
||||
flex: 1.12;
|
||||
padding-top: 32rpx;
|
||||
padding-bottom: 24rpx;
|
||||
transform: scale(1.07);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mic-gift-card--default {
|
||||
border-color: rgba(0, 206, 209, 0.7);
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
box-shadow: 0 0 28rpx rgba(0, 206, 209, 0.22);
|
||||
}
|
||||
|
||||
.mic-gift-default-tag {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #0a1628;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(135deg, #00ced1 0%, #ffd700 100%);
|
||||
}
|
||||
|
||||
.mic-gift-emoji {
|
||||
line-height: 1.15;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.mic-gift-icon-img {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mic-gift-name {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-align: center;
|
||||
margin-bottom: 8rpx;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mic-gift-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #38bdac;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mic-gift-card--mid .mic-gift-name {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.mic-gift-card--mid .mic-gift-price {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.tip-modal-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.tip-modal-sub {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
text-align: center;
|
||||
margin-bottom: 28rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.tip-gift-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.tip-gift-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tip-gift-row-on {
|
||||
border-color: rgba(0, 206, 209, 0.55);
|
||||
background: rgba(0, 206, 209, 0.12);
|
||||
}
|
||||
|
||||
.tip-gift-left {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.tip-gift-price {
|
||||
font-size: 28rpx;
|
||||
color: #38bdac;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tip-qty-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tip-qty-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.tip-qty-ctrl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.tip-qty-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tip-qty-num {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
min-width: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tip-total-row {
|
||||
font-size: 28rpx;
|
||||
color: #38bdac;
|
||||
text-align: right;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.tip-pay-btn {
|
||||
width: 100% !important;
|
||||
background: linear-gradient(135deg, #00ced1 0%, #20b2aa 100%) !important;
|
||||
color: #000 !important;
|
||||
font-weight: 700;
|
||||
font-size: 30rpx;
|
||||
border-radius: 16rpx !important;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.tip-lead-link {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: rgba(0, 206, 209, 0.9);
|
||||
padding: 12rpx 0 8rpx;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tip-modal-close {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
|
||||
@@ -704,14 +704,13 @@ Page({
|
||||
this.setData({ showUnlockModal: false })
|
||||
|
||||
try {
|
||||
// 获取openId
|
||||
let openId = app.globalData.openId || wx.getStorageSync('openId')
|
||||
const openId = await app.ensurePayOpenId()
|
||||
if (!openId) {
|
||||
openId = await app.getOpenId()
|
||||
}
|
||||
|
||||
if (!openId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
wx.showModal({
|
||||
title: '无法拉起支付',
|
||||
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ const READ_UI_DEFAULTS = {
|
||||
payTapModalTitle: '解锁说明',
|
||||
payTapModalContent:
|
||||
'全文 ¥{price}。预览里无法完成支付:请先点屏幕底部「前往小程序」进入完整版,登录后再付款解锁。',
|
||||
fullUnlockTitle: '解锁完整内容',
|
||||
fullUnlockTitle: '加入365读书会',
|
||||
fullUnlockDesc: '可先上滑阅读预览;需要全文时,点下方「支付¥{price}」查看说明',
|
||||
fullLockedProgressText: '已阅读约 {percent}% ,购买后继续阅读',
|
||||
fullPaywallTip: '转发给需要的人,一起学习还能赚佣金',
|
||||
fullPaywallTip: '分享给好友,好友购买你可获得约 90% 收益',
|
||||
notLoginUnlockDesc: '已预览约 {percent}% 内容,登录并支付 ¥{price} 后阅读全文',
|
||||
notLoginPaywallTip: '分享给好友一起学习,还能赚取佣金',
|
||||
notLoginPaywallTip: '分享给好友,好友购买你可获得约 90% 收益',
|
||||
shareTipLine: '好友经你分享购买,你可获得约 90% 收益',
|
||||
momentsModalTitle: '分享到朋友圈',
|
||||
momentsModalContent:
|
||||
@@ -191,6 +191,7 @@ Page({
|
||||
hasFullBook: false,
|
||||
canAccess: false, // 保留兼容性,从 accessState 派生
|
||||
purchasedCount: 0,
|
||||
fullbookShowThreshold: 2,
|
||||
|
||||
// 阅读进度
|
||||
readingProgress: 0,
|
||||
@@ -491,9 +492,14 @@ Page({
|
||||
|
||||
try {
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const rawThreshold = config.fullbookShowThreshold ?? config.prices?.fullbookShowThreshold
|
||||
const threshold = (rawThreshold != null && Number(rawThreshold) > 0) ? Number(rawThreshold) : 2
|
||||
this.setData({
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9,
|
||||
purchasedCount: purchasedSections.length,
|
||||
fullbookShowThreshold: threshold,
|
||||
})
|
||||
|
||||
// 统一:先拉章节数据,用 isFree/price===0 判断免费
|
||||
@@ -1749,6 +1755,7 @@ Page({
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
this.setData({ purchasedCount: (res.data.purchasedSections || []).length })
|
||||
console.log('[Pay] ✅ 购买状态已刷新:', {
|
||||
hasFullBook: res.data.hasFullBook,
|
||||
purchasedCount: res.data.purchasedSections.length
|
||||
@@ -1756,7 +1763,6 @@ Page({
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Pay] 刷新购买状态失败:', e)
|
||||
// 刷新失败时不影响用户体验,只是记录日志
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2062,9 +2068,14 @@ Page({
|
||||
|
||||
try {
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
const purchasedSections2 = app.globalData.purchasedSections || []
|
||||
const rawTh2 = config.fullbookShowThreshold ?? config.prices?.fullbookShowThreshold
|
||||
const th2 = (rawTh2 != null && Number(rawTh2) > 0) ? Number(rawTh2) : 2
|
||||
this.setData({
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9,
|
||||
purchasedCount: purchasedSections2.length,
|
||||
fullbookShowThreshold: th2,
|
||||
})
|
||||
|
||||
// 重新拉取章节,用 isFree/price 判断免费
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '解锁完整内容'}}</text>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '加入365读书会'}}</text>
|
||||
<text class="paywall-desc paywall-desc--pre" wx:if="{{readBeforeLoginHint}}">{{readBeforeLoginHint}}</text>
|
||||
<text class="paywall-desc">{{readUi.notLoginUnlockDesc || '已预览部分内容,登录并支付后阅读全文'}}</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
@@ -170,7 +170,7 @@
|
||||
<view class="login-btn" bindtap="showLoginModal" style="margin-top:12px">
|
||||
<text class="login-btn-text">手机号登录后购买</text>
|
||||
</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.notLoginPaywallTip || '分享给好友一起学习,还能赚取佣金'}}</text>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.notLoginPaywallTip || '分享给好友,好友购买你可获得约 90% 收益'}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -245,12 +245,12 @@
|
||||
<text class="paywall-expanded-lead">{{readUi.singlePageExpandedHint || '预览页不能直接付款,务必先点底栏「前往小程序」。'}}</text>
|
||||
<view class="paywall-expanded-divider"></view>
|
||||
<text class="paywall-expanded-line">{{readUi.shareTipLine || '好友经你分享购买,你可获得约 90% 收益'}}</text>
|
||||
<text class="paywall-expanded-line">{{readUi.fullPaywallTip || '转发给需要的人,一起学习还能赚佣金'}}</text>
|
||||
<text class="paywall-expanded-line">{{readUi.fullPaywallTip || '分享给好友,好友购买你可获得约 90% 收益'}}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '解锁完整内容'}}</text>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '加入365读书会'}}</text>
|
||||
<text class="paywall-desc">{{readUi.fullUnlockDesc || '可先上滑阅读预览'}}</text>
|
||||
<text class="paywall-subdesc" wx:if="{{readUi.fullLockedProgressText}}">{{readUi.fullLockedProgressText}}</text>
|
||||
<text class="paywall-subdesc" wx:else>已阅读约 {{effectivePreviewPercent}}% ,购买后继续阅读</text>
|
||||
@@ -259,7 +259,7 @@
|
||||
<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="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= fullbookShowThreshold}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
@@ -271,7 +271,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.fullPaywallTip || '分享给好友一起学习,还能赚取佣金'}}</text>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.fullPaywallTip || '分享给好友,好友购买你可获得约 90% 收益'}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -68,15 +68,13 @@ Page({
|
||||
async handlePurchase() {
|
||||
trackClick('vip', 'btn_click', '开通VIP')
|
||||
let userId = app.globalData.userInfo?.id
|
||||
let openId = app.globalData.openId || app.globalData.userInfo?.open_id
|
||||
if (!userId || !openId) {
|
||||
if (!userId) {
|
||||
wx.showLoading({ title: '登录中...', mask: true })
|
||||
try {
|
||||
await app.login()
|
||||
userId = app.globalData.userInfo?.id
|
||||
openId = app.globalData.openId || app.globalData.userInfo?.open_id
|
||||
wx.hideLoading()
|
||||
if (!userId || !openId) {
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -86,6 +84,15 @@ Page({
|
||||
return
|
||||
}
|
||||
}
|
||||
const openId = await app.ensurePayOpenId()
|
||||
if (!openId) {
|
||||
wx.showModal({
|
||||
title: '无法拉起支付',
|
||||
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
// VIP 购买成功后再跳转资料页:购买前不拦截
|
||||
this.setData({ purchasing: true })
|
||||
const amount = this.data.price
|
||||
|
||||
@@ -83,10 +83,7 @@ Page({
|
||||
const userId = app.globalData.userInfo.id
|
||||
const amount = this.data.selectedAmount
|
||||
|
||||
let openId = app.globalData.openId
|
||||
if (!openId) {
|
||||
openId = await app.getOpenId()
|
||||
}
|
||||
const openId = await app.ensurePayOpenId()
|
||||
if (!openId) {
|
||||
wx.showToast({ title: '获取支付凭证失败,请重新登录', icon: 'none', duration: 2500 })
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "卡若创业派对 - 来自派对房的真实商业故事",
|
||||
"appid": "wxb8bbb2b10dec74aa",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
@@ -54,5 +54,7 @@
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
}
|
||||
},
|
||||
"projectname": "miniprogram",
|
||||
"libVersion": "3.13.2"
|
||||
}
|
||||
Reference in New Issue
Block a user