Files
soul-yongping/miniprogram/pages/index/index.js
卡若 80e397f7ac feat: 运营-用户功能四大需求完整实现
1. 客资中心:Dashboard 聚合 CKB 线索+提交记录,联表用户信息
2. @置顶:Person 三端(后端+管理端+小程序)置顶功能,首页优先展示
3. 存客宝场景:一键检查并自动启用所有场景获客计划
4. 去重增强:后端聚合 dupCount,管理端展示重复标记和统计
5. 首页文案:"最新更新"→"推荐","开始阅读"→"点击阅读"

Made-with: Cursor
2026-03-19 16:20:46 +08:00

609 lines
21 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')
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户信息
isLoggedIn: false,
hasFullBook: false,
readCount: 0,
// 书籍数据
totalSections: 62,
bookData: [],
// 推荐章节
featuredSections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 最新章节(动态计算)
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: [],
displayLatestChapters: [],
// 篇章数(从 bookData 计算)
partCount: 0,
// 加载状态
loading: true,
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: '',
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
featuredExpandedLoading: false,
// 功能配置(搜索开关)
searchEnabled: true,
// 审核模式:隐藏支付相关入口
auditMode: false
},
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()
},
onShow() {
console.log('[Index] onShow 触发')
this.setData({ auditMode: app.globalData.auditMode || false })
// 设置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()
},
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
const [pinnedRes, vipRes, usersRes] = await Promise.all([
app.request({ url: '/api/miniprogram/persons/pinned', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/users?limit=20', silent: true }).catch(() => null)
])
let members = []
const usedIds = new Set()
// 1. 后台置顶人物优先(最多 4 个)
if (pinnedRes && pinnedRes.success && Array.isArray(pinnedRes.persons)) {
pinnedRes.persons.slice(0, 4).forEach(p => {
const id = p.userId || p.personId
members.push({
id,
personId: p.personId,
name: p.nickname || p.name || '置顶',
avatar: p.avatar || '',
isVip: true,
isPinned: true
})
usedIds.add(id)
})
}
// 2. VIP 会员补位
if (members.length < 4 && vipRes && vipRes.success && Array.isArray(vipRes.data)) {
vipRes.data.forEach(u => {
if (members.length >= 4) return
if (usedIds.has(u.id)) return
members.push({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员',
avatar: u.avatar || '',
isVip: true,
isPinned: false
})
usedIds.add(u.id)
})
}
// 3. 普通用户兜底
if (members.length < 4 && usersRes && usersRes.success && Array.isArray(usersRes.data)) {
usersRes.data
.filter(u => u.avatar && u.nickname && !usedIds.has(u.id))
.forEach(u => {
if (members.length >= 4) return
members.push({
id: u.id,
name: u.nickname,
avatar: u.avatar,
isVip: u.is_vip === 1,
isPinned: false
})
})
}
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '人 (置顶', members.filter(m => m.isPinned).length, '人)')
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)
this.setData({ superMembersLoading: false })
}
},
// 精选推荐 + 最新更新 + 最新列表:一次请求 recommended + latest-chapters避免重复
async loadFeaturedAndLatest() {
try {
const excludeFixed = (c) => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
}
const toSection = (s, i, tagMap = ['热门', '推荐', '精选']) => ({
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: s.tag || tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
})
const [recRes, latestRes] = await Promise.all([
app.request({ url: '/api/miniprogram/book/recommended', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
])
// 1. 精选推荐recommended → hot 兜底)
let featured = []
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => toSection(s, i))
}
if (featured.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) featured = hotList.slice(0, 3).map((s, i) => toSection(s, i))
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
if (featured.length > 0) this.setData({ featuredSections: featured })
// 2. 最新更新 + 最新列表(共用 latest-chapters 数据)
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(excludeFixed)
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 || ''
}
})
}
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)
}
},
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 || 62
this.setData({
totalSections: app.globalData.totalSections,
partCount: parts.length || 5
})
}
} catch (e) {
this.setData({ totalSections: app.globalData.totalSections || 62, partCount: 5 })
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
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' })
},
async loadFeatureConfig() {
try {
const hasCachedFeatures = app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean'
if (hasCachedFeatures) {
this.setData({
searchEnabled: app.globalData.features.searchEnabled,
auditMode: app.globalData.auditMode || false
})
return
}
const res = await app.getConfig()
const features = (res && res.features) || {}
const mp = (res && res.mpConfig) || {}
const searchEnabled = features.searchEnabled !== false
const auditMode = !!mp.auditMode
if (!app.globalData.features) app.globalData.features = {}
app.globalData.features.searchEnabled = searchEnabled
app.globalData.auditMode = auditMode
this.setData({ searchEnabled, auditMode })
} catch (e) {
this.setData({ searchEnabled: true, auditMode: app.globalData.auditMode || false })
}
},
// 跳转到搜索页
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() {
trackClick('home', 'btn_click', '加入创业派对')
wx.navigateTo({ url: '/pages/vip/vip' })
},
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() {
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 toggleFeaturedExpanded() {
if (this.data.featuredExpandedLoading) return
trackClick('home', 'tab_click', this.data.featuredExpanded ? '精选收起' : '精选展开')
if (this.data.featuredExpanded) {
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
this.setData({ featuredExpanded: false, featuredSections: collapsed })
return
}
if (this.data.featuredSectionsFull.length > 0) {
this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull })
return
}
this.setData({ featuredExpandedLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
const list = (res && res.data) ? res.data : []
const tagMap = ['热门', '推荐', '精选']
const full = list.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: tagMap[i % 3] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec'
}))
this.setData({
featuredSectionsFull: full,
featuredSections: full,
featuredExpanded: true,
featuredExpandedLoading: false
})
} catch (e) {
console.log('[Index] 加载精选更多失败:', e)
this.setData({ featuredExpandedLoading: false })
}
},
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
toggleLatestExpanded() {
trackClick('home', 'tab_click', this.data.latestExpanded ? '最新收起' : '最新展开')
const expanded = !this.data.latestExpanded
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
},
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() {
await Promise.all([
this.loadBookData(),
this.loadFeaturedAndLatest(),
this.loadSuperMembers()
])
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}` : '' }
}
})