/** * 卡若创业派对 - 首页 * 开发: 卡若 * 技术支持: 存客宝 */ 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 } = require('../../utils/soulBridge') /** 置顶人物无头像时的占位图 */ 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 : '会' } 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, 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, // 展开状态(首页精选/最新) 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, }, 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 }) void this.loadHomePinnedPerson() // 设置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 { // 仅走后端 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 }) } }, // 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法,latest=updated_at) async loadFeaturedAndLatest() { 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) } }, 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 = `${prefix} · ${nm}` } 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 (_) {} }, /** 拉取后台置顶 @人物,合并到首页右上角「链接」区 */ async loadHomePinnedPerson() { 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 = '' this.setData({ homePinnedPerson: { token: String(res.data.token).trim(), name, avatar: av, }, }) } else { this.setData({ homePinnedPerson: null }) } } catch (e) { console.log('[Index] pinned-person:', e) this.setData({ homePinnedPerson: null }) } this._applyHomeMpUi() }, 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) || {} 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) || {} 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() } catch (e) { try { await app.getAuditMode() } catch (_) {} this.setData({ searchEnabled: app.globalData.features?.searchEnabled !== false, auditMode: app.globalData.auditMode || false }) this._applyHomeMpUi() } await this.loadHomePinnedPerson() }, // 跳转到搜索页 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() { const pinned = this.data.homePinnedPerson if (!pinned || !pinned.token) return trackClick('home', 'btn_click', '置顶@人物留资') 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() { 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}` : '' } } })