Files
soul-yongping/miniprogram/pages/index/index.js
卡若 708547d0dd feat: 数据概览简化 + 用户管理增加余额/提现列
- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额
- 数据概览:移除余额统计区块(余额改在用户管理中展示)
- 数据概览:恢复转化率卡片(唯一付费用户/总用户)
- 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额
- 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段
- 后端:User model 添加 WalletBalance 非数据库字段
- 包含之前的小程序埋点和管理后台点击统计面板

Made-with: Cursor
2026-03-15 15:57:09 +08:00

583 lines
20 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.

/**
* Soul创业派对 - 首页
* 开发: 卡若
* 技术支持: 存客宝
*/
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户信息
isLoggedIn: false,
hasFullBook: false,
readCount: 0,
// 书籍数据
totalSections: 62,
bookData: [],
// 精选推荐按热度排行默认显示3篇可展开更多
featuredSections: [],
featuredSectionsAll: [],
featuredExpanded: false,
// 最新章节(动态计算)
latestSection: null,
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,
// 最新新增章节
latestChapters: [],
latestChaptersExpanded: false,
latestChaptersAll: [],
// 篇章数(从 bookData 计算)
partCount: 0,
// 加载状态
loading: true,
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: ''
},
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.initData()
},
onShow() {
console.log('[Index] onShow 触发')
// 设置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()
// 规则引擎:首页展示时检查(填头像、分享引导等)
checkAndExecute('page_show', this)
},
initData() {
Promise.all([
this.loadBookData(),
this.loadFeaturedFromServer(),
this.loadSuperMembers(),
this.loadLatestChapters()
]).finally(() => {
this.setData({ loading: false })
})
},
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
// 优先加载 VIP 会员(购买 1980 fullbook/vip 订单的用户)
let members = []
try {
const res = await app.request({ url: '/api/miniprogram/vip/members', silent: true })
if (res && res.success && res.data) {
// 不再过滤无头像用户,无头像时用首字母展示
members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员', // 超级个体:用户资料优先,随「我的」修改实时生效
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) {
console.log('[Index] 超级个体加载成功:', members.length, '人')
}
}
} catch (e) {
console.log('[Index] vip/members 请求失败:', e)
}
// 不足 4 个则用有头像的普通用户补充
if (members.length < 4) {
try {
const dbRes = await app.request({ url: '/api/miniprogram/users?limit=20', silent: true })
if (dbRes && dbRes.success && dbRes.data) {
const existIds = new Set(members.map(m => m.id))
const extra = (Array.isArray(dbRes.data) ? dbRes.data : [])
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
.slice(0, 4 - members.length)
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
members = members.concat(extra)
}
} catch (e) {}
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)
this.setData({ superMembersLoading: false })
}
},
// 从服务端获取精选推荐(按热度排行)和最新更新
async loadFeaturedFromServer() {
try {
// 1. 精选推荐:从 book/hot 获取热度排行数据
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
if (hotRes && hotRes.success && Array.isArray(hotRes.data) && hotRes.data.length > 0) {
const tagClassMap = { '热门': 'tag-hot', '推荐': 'tag-rec', '精选': 'tag-rec' }
const all = hotRes.data.map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: s.tag || '',
tagClass: tagClassMap[s.tag] || 'tag-rec',
hotScore: s.hotScore || s.hot_score || 0,
hotRank: s.hotRank || (i + 1),
price: s.price ?? 1,
}))
this.setData({
featuredSectionsAll: all,
featuredSections: all.slice(0, 3),
featuredExpanded: false,
})
}
} catch (e) { console.log('[Index] book/hot 失败:', e) }
// 2. 最新更新:用 book/latest-chapters 取第1条排除「序言」「尾声」「附录」
try {
const latestRes = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(l => {
const pt = (l.part_title || l.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (latestList.length > 0) {
const l = latestList[0]
this.setData({
latestSection: {
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 || ''
}
})
}
} catch (e) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (valid.length > 0) {
valid.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
const latest = valid[0]
this.setData({
latestSection: {
id: latest.id,
mid: latest.mid ?? latest.MID ?? 0,
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
part: latest.part_title || latest.partTitle || ''
}
})
}
}
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
}
},
async loadBookData() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
this.setData({
bookData: chapters,
totalSections: res.total || chapters.length || 62,
partCount: partIds.size || 5
})
}
} catch (e) {
console.error('加载书籍数据失败:', e)
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
this.setData({
isLoggedIn,
hasFullBook,
readCount
})
},
// 跳转到目录
goToChapters() {
trackClick('home', 'nav_click', '目录')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到搜索页
goToSearch() {
trackClick('home', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页(优先传 mid与分享逻辑一致
goToRead(e) {
const id = e.currentTarget.dataset.id
trackClick('home', 'card_click', e.currentTarget.dataset.id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},
// 跳转到匹配页
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
goToVip() {
trackClick('home', 'btn_click', 'VIP')
wx.navigateTo({ url: '/pages/vip/vip' })
},
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
async onLinkKaruo() {
trackClick('home', 'btn_click', '链接卡若')
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再链接卡若',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
}
})
return
}
const userId = app.globalData.userInfo.id
// 2 分钟内只能点一次(与后端限频一致)
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
if (!phone && !wechatId) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || '').trim()
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
}
} catch (e) {}
}
if (phone || wechatId) {
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/index-lead',
method: 'POST',
data: {
userId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
}
return
}
this.setData({ showLeadModal: true, leadPhone: '' })
},
closeLeadModal() {
this.setData({ showLeadModal: false, leadPhone: '' })
},
// 阻止弹窗内部点击事件冒泡到遮罩层
stopPropagation() {},
onLeadPhoneInput(e) {
this.setData({ leadPhone: (e.detail.value || '').trim() })
},
// 一键获取手机号(微信能力),成功后直接提交链接卡若
async onGetPhoneNumberForLead(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
wx.showToast({ title: '未获取到手机号,请手动输入', icon: 'none' })
return
}
const code = e.detail.code
if (!code) {
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
wx.showLoading({ title: '获取中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/phone',
method: 'POST',
data: { code, userId }
})
wx.hideLoading()
if (res && res.success && res.phoneNumber) {
await this._submitLeadWithPhone(res.phoneNumber)
} else {
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
wx.showToast({ title: err.message || '获取失败,请手动输入', icon: 'none' })
}
},
// 内部:用手机号提交链接卡若(一键获取与手动输入共用)
async _submitLeadWithPhone(phone) {
const p = (phone || '').trim().replace(/\s/g, '')
if (!p || p.length < 11) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/index-lead',
method: 'POST',
data: {
userId,
phone: p,
name: (app.globalData.userInfo?.nickname || '').trim() || undefined,
},
})
wx.hideLoading()
this.setData({ showLeadModal: false, leadPhone: '' })
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
// 同步手机号到用户资料
try {
if (userId) {
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId, phone: p },
})
if (app.globalData.userInfo) {
app.globalData.userInfo.phone = p
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.setStorageSync('user_phone', p)
}
} catch (e) {
console.log('[Index] 同步手机号到用户资料失败:', e && e.message)
}
wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
}
},
async submitLead() {
trackClick('home', 'btn_click', '提交留资')
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
return
}
await this._submitLeadWithPhone(phone)
},
goToSuperList() {
wx.switchTab({ url: '/pages/match/match' })
},
async loadLatestChapters() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
if (candidates.length === 0) {
candidates = chapters.filter(exclude)
}
const sessionNum = (c) => {
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
const m = title.match(/第\s*(\d+)\s*场/) || title.match(/第(\d+)场/)
if (m) return parseInt(m[1], 10)
const id = c.id != null ? String(c.id) : ''
if (/^\d+$/.test(id)) return parseInt(id, 10)
return 0
}
const mapChapter = (c) => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
let desc = ''
if (rawContent && rawContent.length > 0) {
const clean = rawContent.replace(/^#[\d.]+\s*/, '').trim()
desc = clean.length > 36 ? clean.slice(0, 36) + '...' : clean
}
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
desc,
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
}
const sorted = candidates.sort((a, b) => {
const na = sessionNum(a)
const nb = sessionNum(b)
if (na !== nb) return nb - na
return new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0)
})
const latestAll = sorted.slice(0, 10).map(mapChapter)
this.setData({
latestChaptersAll: latestAll,
latestChapters: latestAll.slice(0, 5),
latestChaptersExpanded: false,
})
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},
toggleLatestExpand() {
const all = this.data.latestChaptersAll || []
if (this.data.latestChaptersExpanded) {
this.setData({ latestChapters: all.slice(0, 5), latestChaptersExpanded: false })
} else {
this.setData({ latestChapters: all, latestChaptersExpanded: true })
}
},
toggleFeaturedExpand() {
const all = this.data.featuredSectionsAll || []
if (this.data.featuredExpanded) {
this.setData({ featuredSections: all.slice(0, 3), featuredExpanded: false })
} else {
this.setData({ featuredSections: all, featuredExpanded: true })
}
},
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
},
// 跳转到我的页面
goToMy() {
wx.switchTab({ url: '/pages/my/my' })
},
// 下拉刷新(等待各异步加载完成后再结束)
async onPullDownRefresh() {
await Promise.all([
this.loadBookData(),
this.loadFeaturedFromServer(),
this.loadSuperMembers(),
this.loadLatestChapters()
])
this.updateUserStatus()
wx.stopPullDownRefresh()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 真实商业故事',
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
}
})