Files
Mycontent/miniprogram/pages/index/index.js
乘风 83a420f1b9 feat: implement audit mode restrictions across various pages
- Added checks for audit mode in VIP navigation and member detail pages, preventing access and displaying appropriate messages.
- Updated UI elements to conditionally render based on audit mode, enhancing user experience by hiding VIP-related options when in audit mode.
- Introduced loading indicators on the VIP page to manage user expectations during content validation.
- Enhanced error handling for image uploads in the article editor, ensuring a smoother content creation process.

This update aims to improve the overall user experience by enforcing access restrictions and providing clear feedback in audit mode.
2026-04-24 18:23:34 +08:00

1032 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 卡若创业派对 - 首页
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { cleanSingleLineField } = require('../../utils/contentParser')
const { navigateMpPath } = require('../../utils/mpNavigate.js')
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
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'
/** 与首页固定「卡若」获客位重复时从横滑列表剔除(含历史误写「卡路」) */
function isKaruoHostDuplicateName(displayName) {
const s = String(displayName || '').trim()
return s === '卡若' || s === '卡路'
}
/** 超级个体无头像占位:仅展示中文首字,避免头像圆里出现英文字母 */
function superAvatarLetter(displayName) {
const s = String(displayName || '').trim()
if (!s) return '会'
const ch = s[0]
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
}
/** 旧 OSS 存 16personalities .svg 时改为 png 目录,与后端 resolveAvatarURL 一致 */
function normalizeLegacyMbtiAvatarUrl(u) {
if (!u || typeof u !== 'string') return u
const lower = u.toLowerCase()
if (!lower.includes('mbti-avatar')) return u
let s = u.replace(/mbti-avatars-16personalities-male/g, 'mbti-avatars-png-male')
s = s.replace(/mbti-avatars-16personalities-female/g, 'mbti-avatars-png-female')
if (/\.svg$/i.test(s)) s = s.replace(/\.svg$/i, '.png')
return s
}
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 dFallback = base[Math.min(i, base.length - 1)]
const rn = String(row.name != null ? row.name : '').trim()
let id = row.id != null ? String(row.id).trim() : ''
let d = dFallback
// 无 id / 空 id 时禁止按下标套默认 id后台顺序多为「人气票、小心心、嘉年华」下标1会变成 gift_star
// 导致中间显示小心心¥9.9 却带 gift_star 去支付变成¥29.8。优先按礼物名称、再按 micOrder 对齐默认档。
if (!id) {
const byName = rn ? base.find((g) => g.name === rn) : undefined
if (byName) {
d = byName
id = byName.id
} else {
let mo = row.micOrder != null ? parseInt(String(row.micOrder), 10) : NaN
if (!Number.isNaN(mo)) {
const byOrder = base.find((g) => Number(g.micOrder) === mo)
if (byOrder) {
d = byOrder
id = byOrder.id
}
}
if (!id) {
d = dFallback
id = 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 12 档为 index 11 档为 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: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户信息
isLoggedIn: false,
hasFullBook: false,
readCount: 0,
// 书籍数据totalSections 来自 book/parts初始用 app.getTotalSections() 兜底)
totalSections: 0, // onLoad 后由 loadBookData 更新
bookData: [],
// 推荐章节(来自 recommended/hot API初始为空避免占位错误
featuredSections: [],
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters
bannerSection: null,
/** 首屏 Banner 请求完成前为 true完成后为 false避免接口全失败时长期显示「加载中」 */
bannerLoading: true,
latestLabel: '最新更新',
// 内容概览
partsList: [
{ id: 'part-1', number: '一', title: '真实的人', subtitle: '人与人之间的底层逻辑' },
{ id: 'part-2', number: '二', title: '真实的行业', subtitle: '电商、内容、传统行业解析' },
{ id: 'part-3', number: '三', title: '真实的错误', subtitle: '我和别人犯过的错' },
{ id: 'part-4', number: '四', title: '真实的赚钱', subtitle: '底层结构与真实案例' },
{ id: 'part-5', number: '五', title: '真实的社会', subtitle: '未来职业与商业生态' }
],
// 超级个体VIP会员
superMembers: [],
superMembersLoading: true,
// 收益排行榜
earningsBoard: [],
earningsBoardEnabled: false,
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
latestChapters: [],
displayLatestChapters: [],
// 篇章数(从 bookData 计算)
partCount: 0,
// 加载状态
loading: true,
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 精选排行榜全量(最多 50默认只展示前 3 条
// 功能配置(搜索开关)
searchEnabled: true,
// 审核模式:隐藏支付相关入口
auditMode: false,
// mp_config.mpUi.homePage后台系统设置 mpUi
mpUiLogoTitle: '卡若创业派对',
mpUiLogoSubtitle: '来自派对房的真实故事',
/** 仅当有置顶 @人物时展示,文案与头像由 _applyHomeMpUi 写入 */
mpUiLinkKaruoText: '',
mpUiLinkKaruoDisplay: DEFAULT_KARUO_LINK_AVATAR,
mpUiSearchPlaceholder: '搜索章节标题或内容...',
mpUiBannerTag: '推荐',
mpUiBannerReadMore: '点击阅读',
mpUiSuperTitle: '超级个体',
mpUiPickTitle: '精选推荐',
mpUiLatestTitle: '最新新增',
/** 后台 @列表置顶人物:有则右上角展示绑定用户头像 + @名称,点击走 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: '' },
/** 文旅/落地体验链:未登录时弹一键登录,成功后继续打赏/上麦/留资 */
showLoginModal: false,
showPrivacyModal: false,
loginModalDesc: '登录后可参与打赏、上麦与链接嘉宾',
pendingAfterLogin: null,
},
onLoad(options) {
console.log('[Index] ===== onLoad 触发 =====')
// 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
wx.showShareMenu({ withShareTimeline: true })
this.loadFeatureConfig()
this.initData()
},
async onShow() {
console.log('[Index] onShow 触发')
try {
await app.getConfig(true)
} catch (_) {}
this.setData({ auditMode: app.globalData.auditMode || false })
this._refreshHomeEntryFlags()
void this.loadHomePinnedPerson()
// 首页 tab 再次展示时重拉超级个体(避免首进网络失败或仅 onLoad 拉一次后长期空白)
if (!app.globalData.auditMode) {
void this.loadSuperMembers()
}
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
// 主动触发配置加载
if (tabBar && tabBar.loadFeatureConfig) {
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
tabBar.loadFeatureConfig()
}
// 更新选中状态
if (tabBar && tabBar.updateSelected) {
tabBar.updateSelected()
} else if (tabBar) {
tabBar.setData({ selected: 0 })
}
} else {
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
// 更新用户状态
this.updateUserStatus()
},
// 初始化数据:首次进页面并行异步加载,加快首屏展示
initData() {
this.setData({ loading: false })
this.loadBookData()
this.loadFeaturedAndLatest()
this.loadSuperMembers()
this.loadEarningsBoard()
},
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
// 仅走后端 VIP 列表排序vip_sort、vip_activated_at不在端上拼普通用户
const vipRes = await app.request({ url: '/api/miniprogram/vip/members?limit=24', silent: true }).catch(() => null)
let members = []
if (vipRes && vipRes.success && Array.isArray(vipRes.data) && vipRes.data.length > 0) {
members = vipRes.data.map(u => {
const raw = u.name || u.nickname || u.vipName || u.vip_name || '会员'
const name = cleanSingleLineField(raw) || '会员'
return {
id: u.id,
name,
avatar: u.avatar || '',
isVip: true,
avatarLetter: superAvatarLetter(name)
}
}).filter((m) => !isKaruoHostDuplicateName(m.name))
console.log('[Index] 超级个体(后端排序):', members.length, '人')
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)
this.setData({ superMembersLoading: false })
}
},
async loadEarningsBoard() {
try {
const res = await app.request({ url: '/api/miniprogram/earnings/leaderboard', silent: true }).catch(() => null)
if (res && res.success && res.enabled && Array.isArray(res.data) && res.data.length > 0) {
const board = res.data.map(item => ({
rank: item.rank,
nickname: item.nickname || '分享达人',
avatar: normalizeLegacyMbtiAvatarUrl(item.avatar || ''),
avatarLetter: superAvatarLetter(item.nickname || '创')
}))
this.setData({ earningsBoard: board, earningsBoardEnabled: true })
console.log('[Index] 收益排行榜:', board.length, '人')
} else {
this.setData({ earningsBoardEnabled: res && res.enabled === false ? false : true })
}
} catch (e) {
console.log('[Index] 加载收益排行榜失败:', e)
}
},
// 精选推荐 + 最新更新 + 最新列表顺序以后端为准recommended=排行榜算法latest=updated_at
async loadFeaturedAndLatest() {
this.setData({ bannerLoading: true })
try {
const tagClassForTag = (tag) => (tag === '热门' ? 'tag-hot' : 'tag-rec')
const toSectionFromRanking = (s) => {
const tag = s.tag || '精选'
return {
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
part: (s.part_title || s.partTitle || '').replace(/[_|]/g, ' ').trim(),
tag,
tagClass: tagClassForTag(tag)
}
}
const fallbackTags = ['热门', '推荐', '精选']
const toSectionFromHot = (s, i) => {
const tag = fallbackTags[i % 3]
return {
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
part: (s.part_title || s.partTitle || '').replace(/[_|]/g, ' ').trim(),
tag,
tagClass: tagClassForTag(tag)
}
}
const [recRes, latestRes] = await Promise.all([
app.request({ url: '/api/miniprogram/book/recommended?limit=50', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
])
// 1. 精选推荐一次拉全量≤50默认只显示 3 条;点列表下三角展开(与「最新新增」一致)
let featuredFull = []
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featuredFull = recRes.data.map((s) => toSectionFromRanking(s))
}
if (featuredFull.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) featuredFull = hotList.map((s, i) => toSectionFromHot(s, i))
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
if (featuredFull.length > 0) {
this.setData({
featuredSectionsFull: featuredFull,
featuredSections: featuredFull.slice(0, 3),
featuredExpanded: false
})
} else {
this.setData({
featuredSectionsFull: [],
featuredSections: [],
featuredExpanded: false
})
}
// 2. Banner 推荐:优先取 recommended 第一条,回退 latest 第一条
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
// 按更新时间倒序,最新在前(与后台展示一致)
const latestList = [...rawList].sort((a, b) => {
const ta = new Date(a.updatedAt || a.updated_at || 0).getTime()
const tb = new Date(b.updatedAt || b.updated_at || 0).getTime()
return tb - ta
})
if (featuredFull.length > 0) {
this.setData({ bannerSection: featuredFull[0] })
} else if (latestList.length > 0) {
const l = latestList[0]
this.setData({
bannerSection: {
id: l.id,
mid: l.mid ?? l.MID ?? 0,
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
part: l.part_title || l.partTitle || ''
}
})
}
const latestChapters = latestList.slice(0, 20).map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
desc: '',
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
const display = this.data.latestExpanded ? latestChapters : latestChapters.slice(0, 5)
this.setData({ latestChapters, displayLatestChapters: display })
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
} finally {
this.setData({ bannerLoading: false })
}
},
async loadBookData() {
try {
const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
if (res?.success) {
const total = res.totalSections ?? 0
const parts = res.parts || []
app.globalData.totalSections = (total != null && total > 0) ? total : app.getTotalSections()
this.setData({
totalSections: app.globalData.totalSections,
partCount: parts.length || 5
})
}
} catch (e) {
this.setData({ totalSections: app.getTotalSections(), partCount: 5 })
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || app.getTotalSections())
this.setData({
isLoggedIn,
hasFullBook,
readCount
})
},
// 跳转到目录
goToChapters() {
trackClick('home', 'nav_click', '阅读进度')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
_applyHomeMpUi() {
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
const baseTitle = String(h.logoTitle || '卡若创业派对').trim() || '卡若创业派对'
const prefix = String(h.pinnedTitlePrefix != null ? h.pinnedTitlePrefix : '派对会员').trim()
const tpl = String(h.pinnedMainTitleTemplate || '').trim()
const patch = {
mpUiLogoTitle: baseTitle,
mpUiLogoSubtitle: String(h.logoSubtitle || '来自派对房的真实故事').trim() || '来自派对房的真实故事',
mpUiSearchPlaceholder: String(h.searchPlaceholder || '搜索章节标题或内容...').trim() || '搜索章节标题或内容...',
mpUiBannerTag: String(h.bannerTag || '推荐').trim() || '推荐',
mpUiBannerReadMore: String(h.bannerReadMoreText || '点击阅读').trim() || '点击阅读',
mpUiSuperTitle: String(h.superSectionTitle || '超级个体').trim() || '超级个体',
mpUiPickTitle: String(h.pickSectionTitle || '精选推荐').trim() || '精选推荐',
mpUiLatestTitle: String(h.latestSectionTitle || '最新新增').trim() || '最新新增',
}
const pinned = this.data.homePinnedPerson
if (pinned && pinned.token) {
const displayAv =
pinned.avatar && isSafeImageSrc(pinned.avatar) ? pinned.avatar : DEFAULT_KARUO_LINK_AVATAR
const nm = pinned.name || '好友'
patch.mpUiLinkKaruoText = `点击链接${nm}`
patch.mpUiLinkKaruoDisplay = displayAv
let mainTitle = baseTitle
if (tpl) {
mainTitle = tpl
.replace(/\{\{name\}\}/g, nm)
.replace(/\{\{prefix\}\}/g, prefix)
.trim() || baseTitle
} else if (prefix) {
mainTitle = baseTitle
} else {
mainTitle = `@${nm}`
}
patch.mpUiLogoTitle = mainTitle
} else {
patch.mpUiLinkKaruoText = ''
patch.mpUiLinkKaruoDisplay = DEFAULT_KARUO_LINK_AVATAR
}
this.setData(patch, () => {
try {
wx.setNavigationBarTitle({ title: patch.mpUiLogoTitle || '首页' })
} catch (_) {}
this._syncHeaderCornerAction()
})
},
/** 拉取后台置顶 @人物,合并到首页右上角「链接」区;同时拿到该人的 homeEntryConfig */
async loadHomePinnedPerson() {
let pinnedPerson = null
let override
try {
const res = await app.request({ url: '/api/miniprogram/ckb/pinned-person', silent: true })
if (res && res.success && res.data && res.data.token) {
const name = cleanSingleLineField(res.data.name) || '好友'
let av = String(res.data.avatar || '').trim()
if (!isSafeImageSrc(av)) av = ''
pinnedPerson = {
token: String(res.data.token).trim(),
name,
avatar: av,
homeEntryConfig: res.data.homeEntryConfig || null,
}
if (pinnedPerson.homeEntryConfig) {
override = {}
if (pinnedPerson.homeEntryConfig.linkKaruoReward) override.linkKaruoReward = pinnedPerson.homeEntryConfig.linkKaruoReward
if (pinnedPerson.homeEntryConfig.liveMicSchedule) override.liveMicSchedule = pinnedPerson.homeEntryConfig.liveMicSchedule
}
} else {
pinnedPerson = null
}
} catch (e) {
console.log('[Index] pinned-person:', e)
pinnedPerson = null
}
this.setData({ homePinnedPerson: pinnedPerson }, () => {
this._applyHomeMpUi()
this._refreshHomeEntryFlags(override)
})
},
async loadFeatureConfig() {
try {
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.mpConfig || {}
app.globalData.auditMode = !!mp.auditMode
}
await app.getAuditMode()
const searchEnabled = app.globalData.features?.searchEnabled !== false
this.setData({
searchEnabled,
auditMode: app.globalData.auditMode || false
})
this._applyHomeMpUi()
this._refreshHomeEntryFlags()
} catch (e) {
try {
await app.getAuditMode()
} catch (_) {}
this.setData({
searchEnabled: app.globalData.features?.searchEnabled !== false,
auditMode: app.globalData.auditMode || false
})
this._applyHomeMpUi()
this._refreshHomeEntryFlags()
}
await this.loadHomePinnedPerson()
},
_refreshHomeEntryFlags(override) {
const audit = !!app.globalData.auditMode
// 有置顶人物时:入口开关严格按该人物配置,不再回退全局,避免“人字拖未开上麦却显示申请上麦”。
// 无置顶人物时:沿用全局 mpUi.homePage。
const hasPinned = !!(this.data.homePinnedPerson && this.data.homePinnedPerson.token)
const personCfg = override || ((this.data.homePinnedPerson && this.data.homePinnedPerson.homeEntryConfig) || null)
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
const rw = hasPinned
? ((personCfg && personCfg.linkKaruoReward) || {})
: (h.linkKaruoReward || {})
const schReal = hasPinned
? ((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'
const rwUi = this.data.linkKaruoRewardUi || {}
label = String(rwUi.entryButtonText || '支持一下').trim() || '支持一下'
emoji = ''
}
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')
},
/**
* 体验环节未登录:弹公用 login-modal单页模式仍引导「前往小程序」
* @param {{ type: 'tip', tipSource: string } | { type: 'link_karuo' }} pending
*/
_showHomeLoginModal(pending, desc) {
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('[Index] 检测单页模式失败,回退为登录弹窗:', e)
}
this.setData({
pendingAfterLogin: pending,
loginModalDesc: desc || '登录后可参与打赏、上麦与链接嘉宾',
showLoginModal: true,
})
},
onLoginModalClose() {
this.setData({ showLoginModal: false, showPrivacyModal: false, pendingAfterLogin: null })
},
onLoginModalPrivacyAgree() {
this.setData({ showPrivacyModal: false })
},
onLoginModalSuccess() {
const pending = this.data.pendingAfterLogin
this.setData({ showLoginModal: false, pendingAfterLogin: null })
this.updateUserStatus()
wx.showToast({ title: '登录成功', icon: 'success' })
if (!pending) return
const run = (fn) => {
if (typeof wx.nextTick === 'function') wx.nextTick(fn)
else setTimeout(fn, 0)
}
if (pending.type === 'tip' && pending.tipSource) {
run(() => this._openHorizontalGiftModal(pending.tipSource))
} else if (pending.type === 'link_karuo') {
run(() => this.onLinkKaruo())
}
},
/** 打开横排礼物弹窗(上麦/打赏统一入口) */
_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) {
const desc =
tipSource === 'live_mic'
? '登录后可申请上麦并完成礼遇支付'
: '登录后可打赏支持'
this._showHomeLoginModal({ type: 'tip', tipSource }, desc)
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
trackClick('home', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页(传 mid与分享一致无 mid 时传 id
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid
trackClick('home', 'card_click', id || '章节')
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},
// 跳转到匹配页
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
goToVip() {
if (app.globalData.auditMode) {
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
return
}
trackClick('home', 'btn_click', '加入创业派对')
wx.navigateTo({ url: '/pages/vip/vip' })
},
async onLinkKaruo() {
const pinned = this.data.homePinnedPerson
if (!pinned || !pinned.token) return
trackClick('home', 'btn_click', '置顶@人物留资')
if (!app.globalData.isLoggedIn) {
this._showHomeLoginModal({ type: 'link_karuo' }, '登录后可向嘉宾留下联系方式')
return
}
await submitCkbLead(getApp(), {
targetUserId: pinned.token,
targetNickname: pinned.name,
source: 'home_pinned_person',
})
},
goToSuperList() {
wx.switchTab({ url: '/pages/match/match' })
},
// 精选推荐:列表下方小三角展开(数据已在 loadFeaturedAndLatest 一次拉齐)
expandFeaturedChapters() {
if (this.data.featuredExpanded) return
const full = this.data.featuredSectionsFull || []
if (full.length <= 3) return
trackClick('home', 'tab_click', '精选展开_底部三角')
this.setData({ featuredExpanded: true, featuredSections: full })
},
// 最新新增:列表下方小三角展开(无「收起」,展开后整页向下滚动查看)
expandLatestChapters() {
if (this.data.latestExpanded) return
trackClick('home', 'tab_click', '最新展开_底部三角')
const full = this.data.latestChapters || []
this.setData({
latestExpanded: true,
displayLatestChapters: full
})
},
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
trackClick('home', 'card_click', '超级个体_' + (id || ''))
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
},
// 跳转到我的页面
goToMy() {
wx.switchTab({ url: '/pages/my/my' })
},
// 下拉刷新(等待各异步加载完成后再结束)
async onPullDownRefresh() {
try {
await app.getConfig(true)
} catch (_) {}
this.setData({ auditMode: app.globalData.auditMode || false })
this._refreshHomeEntryFlags()
await Promise.all([
this.loadBookData(),
this.loadFeaturedAndLatest(),
this.loadSuperMembers()
])
this.updateUserStatus()
wx.stopPullDownRefresh()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: '卡若创业派对 - 真实商业故事',
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: '卡若创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
}
})