/** * 卡若创业派对 - 目录页 * 开发: 卡若 * 技术支持: 存客宝 * 数据: 完整真实文章标题 */ const app = getApp() const { trackClick } = require('../../utils/trackClick') const { partEmojiForBodyIndex } = require('../../utils/partIcons.js') const { isSafeImageSrc } = require('../../utils/imageUrl.js') Page({ data: { statusBarHeight: 44, navBarHeight: 88, isLoggedIn: false, hasFullBook: false, isVip: false, purchasedSections: [], // 多书架构 bookList: [], expandedBookId: null, // 当前展开书的篇章数据(按 bookId 索引) _bookPartsData: {}, totalSections: 0, bookData: [], expandedPart: null, bookCollapsed: false, _loadedChapters: {}, _triangleAnimating: '', fixedSectionsMap: {}, appendixList: [ { id: 'appendix-1', title: '附录1|Soul派对房精选对话' }, { id: 'appendix-2', title: '附录2|创业者自检清单' }, { id: 'appendix-3', title: '附录3|本书提到的工具和资源' } ], partsLoading: true, searchEnabled: true, bookFreePreviewPercent: 100, chaptersBookTitle: '一场Soul的创业实验', chaptersBookSubtitle: '来自派对房的真实故事', chaptersNewBadgeText: 'NEW' }, onLoad() { wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight }) this.updateUserStatus() this.loadVipStatus() this.loadBooks() this.loadFeatureConfig() }, _applyChaptersMpUi() { const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {} const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {} const newBadgeText = String(c.newBadgeText || c.sectionNewBadgeText || h.latestSectionTitle || 'NEW').trim() || 'NEW' this.setData({ chaptersBookTitle: String(c.bookTitle || '一场Soul的创业实验').trim() || '一场Soul的创业实验', chaptersBookSubtitle: String(c.bookSubtitle || '来自派对房的真实故事').trim() || '来自派对房的真实故事', chaptersNewBadgeText: newBadgeText }) }, _normalizeBadgeText(v) { return String(v || '').trim().slice(0, 8) }, async loadFeatureConfig() { try { if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') { this.setData({ searchEnabled: app.globalData.features.searchEnabled }) const pct = app.globalData.bookFreePreviewPercent if (typeof pct === 'number') this.setData({ bookFreePreviewPercent: pct }) this._applyChaptersMpUi() return } const res = await app.getConfig() const features = (res && res.features) || {} const searchEnabled = features.searchEnabled !== false if (!app.globalData.features) app.globalData.features = {} app.globalData.features.searchEnabled = searchEnabled const previewPct = (typeof res?.bookFreePreviewPercent === 'number') ? res.bookFreePreviewPercent : 100 app.globalData.bookFreePreviewPercent = previewPct this.setData({ searchEnabled, bookFreePreviewPercent: previewPct }) this._applyChaptersMpUi() } catch (e) { this.setData({ searchEnabled: true }) this._applyChaptersMpUi() } }, async loadBooks() { this.setData({ partsLoading: true }) let books = [] try { const res = await app.request({ url: '/api/miniprogram/book/books', silent: true }) if (res?.success && Array.isArray(res.books)) { books = res.books } } catch (e) { console.log('[Chapters] 加载书列表失败,使用默认书:', e?.message || e) } if (books.length === 0) { books = [{ id: 1, bookKey: 'main', title: this.data.chaptersBookTitle || '一场Soul的创业实验', subtitle: this.data.chaptersBookSubtitle || '来自派对房的真实故事', icon: '', iconEmoji: '📖', sortOrder: 0, sectionCount: 0, }] } this.setData({ bookList: books.map(b => ({ id: b.id, bookKey: b.bookKey || 'main', title: b.title, subtitle: b.subtitle || '', icon: b.icon || '', iconEmoji: b.iconEmoji || '', sortOrder: b.sortOrder || 0, sectionCount: b.sectionCount || 0, })), expandedBookId: null, partsLoading: false, }) }, async loadPartsForBook(bookId) { const effectiveBookId = bookId > 0 ? bookId : 1 if (effectiveBookId > 0 && this.data._bookPartsData[effectiveBookId]) { this.setData({ bookData: this.data._bookPartsData[effectiveBookId].parts, totalSections: this.data._bookPartsData[effectiveBookId].total, fixedSectionsMap: this.data._bookPartsData[effectiveBookId].fixedMap, appendixList: this.data._bookPartsData[effectiveBookId].appendixList, }) return } this.setData({ partsLoading: true }) try { const url = `/api/miniprogram/book/parts${effectiveBookId > 0 ? '?bookId=' + effectiveBookId : ''}` const res = await app.request({ url, silent: true }) let parts = [] let totalSections = 0 let fixedSections = [] if (res?.success && Array.isArray(res.parts) && res.parts.length > 0) { parts = res.parts totalSections = res.totalSections ?? 0 fixedSections = res.fixedSections || [] } const fixedMap = {} fixedSections.forEach(f => { fixedMap[f.id] = f.mid }) const appendixList = [ { id: 'appendix-1', title: '附录1|Soul派对房精选对话', mid: fixedMap['appendix-1'] }, { id: 'appendix-2', title: '附录2|创业者自检清单', mid: fixedMap['appendix-2'] }, { id: 'appendix-3', title: '附录3|本书提到的工具和资源', mid: fixedMap['appendix-3'] } ] const bookData = parts.map((p, idx) => { let icon = String(p.icon || '').trim() if (icon && !isSafeImageSrc(icon)) icon = '' const iconEmoji = icon ? '' : partEmojiForBodyIndex(idx) const partBadgeText = this._normalizeBadgeText( p.badgeText || p.badge_text || p.partBadgeText || p.part_badge_text ) return { id: p.id, icon, iconEmoji, iconText: partBadgeText, title: p.title, subtitle: p.subtitle || '', chapterCount: p.chapterCount || 0, chapters: [], alwaysShow: false } }) app.globalData.totalSections = totalSections if (effectiveBookId > 0) { const cached = { ...this.data._bookPartsData } cached[effectiveBookId] = { parts: bookData, total: totalSections, fixedMap, appendixList } this.setData({ _bookPartsData: cached }) } this.setData({ bookData, totalSections, fixedSectionsMap: fixedMap, appendixList, _loadedChapters: {}, partsLoading: false, expandedPart: null }) } catch (e) { console.log('[Chapters] 加载篇章失败:', e) this.setData({ bookData: [], totalSections: 0, partsLoading: false }) } }, // 展开时懒加载该篇章的章节(含 mid,供阅读页 by-mid 请求) async loadChaptersByPart(partId) { if (this.data._loadedChapters[partId]) return try { const res = await app.request({ url: `/api/miniprogram/book/chapters-by-part?partId=${encodeURIComponent(partId)}`, silent: true }) const rows = (res && res.data) || [] const chMap = new Map() rows.forEach(r => { const cid = r.chapterId || r.chapter_id || 'chapter-1' if (!chMap.has(cid)) { chMap.set(cid, { id: cid, title: r.chapterTitle || r.chapter_title || '未分类', sections: [] }) } const ch = chMap.get(cid) ch.sections.push({ id: r.id, mid: r.mid ?? r.MID ?? 0, title: r.sectionTitle || r.section_title || r.title || '', isFree: r.isFree === true || (r.price !== undefined && r.price === 0), price: r.price ?? 1, isNew: r.isNew === true || r.is_new === true, newBadgeText: this._normalizeBadgeText( r.newBadgeText || r.new_badge_text || r.sectionBadgeText || r.section_badge_text || r.badgeText || r.badge_text ), isPremium: false }) }) const chapters = Array.from(chMap.values()) // 按免费展示比例标记:超出比例的章节标记为需付费 const freePercent = this.data.bookFreePreviewPercent ?? 100 if (freePercent < 100) { let allSections = [] chapters.forEach(ch => { allSections = allSections.concat(ch.sections) }) const freeCount = Math.floor(allSections.length * freePercent / 100) allSections.forEach((s, idx) => { if (idx >= freeCount && !s.isFree) { s.needPay = true } }) } chapters.forEach((ch) => { const n = ch.sections.length ch.sectionVisibleLimit = n === 0 ? 0 : Math.min(5, n) }) const loaded = { ...this.data._loadedChapters, [partId]: chapters } const bookData = this.data.bookData.map(p => p.id === partId ? { ...p, chapters } : p ) const bookDataFlat = app.globalData.bookData || [] rows.forEach(r => { const idx = bookDataFlat.findIndex(c => c.id === r.id) if (idx >= 0) bookDataFlat[idx] = { ...bookDataFlat[idx], ...r } else bookDataFlat.push(r) }) app.globalData.bookData = bookDataFlat wx.setStorage({ key: 'bookData', data: bookDataFlat }) // 异步写入,避免阻塞主线程 this.setData({ bookData, _loadedChapters: loaded }) } catch (e) { console.log('[Chapters] 加载章节失败:', e) } }, async toggleBook(e) { const bookId = Number(e.currentTarget.dataset.bookId) trackClick('chapters', 'tab_click', `book_${bookId}`) const isExpanding = this.data.expandedBookId !== bookId this.setData({ expandedBookId: isExpanding ? bookId : null, expandedPart: null }) if (isExpanding) { await this.loadPartsForBook(bookId) // 自动展开第一个篇章(二级默认展开) if (this.data.bookData.length > 0) { const firstPartId = this.data.bookData[0].id this.setData({ expandedPart: firstPartId }) await this.loadChaptersByPart(firstPartId) } } }, onPullDownRefresh() { this.setData({ _bookPartsData: {} }) this.loadBooks() .then(() => wx.stopPullDownRefresh()) .catch(() => wx.stopPullDownRefresh()) }, onShow() { this._applyChaptersMpUi() // 设置TabBar选中状态 if (typeof this.getTabBar === 'function' && this.getTabBar()) { const tabBar = this.getTabBar() if (tabBar.updateSelected) { tabBar.updateSelected() } else { tabBar.setData({ selected: 1 }) } } this.updateUserStatus() this.loadVipStatus() }, // 拉取 VIP 状态(isVip=会员,hasFullBook=365读书会权益) async loadVipStatus() { const userId = app.globalData.userInfo?.id if (!userId) return try { const res = await app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true, timeout: 3000 }) if (res?.success) { app.globalData.isVip = !!res.data?.isVip app.globalData.vipExpireDate = res.data?.expireDate || '' this.setData({ isVip: app.globalData.isVip }) const userInfo = app.globalData.userInfo || {} userInfo.isVip = app.globalData.isVip userInfo.vipExpireDate = app.globalData.vipExpireDate wx.setStorageSync('userInfo', userInfo) } } catch (e) { // 静默失败不影响目录展示 } }, // 更新用户状态 updateUserStatus() { const { isLoggedIn, hasFullBook, purchasedSections, isVip } = app.globalData this.setData({ isLoggedIn, hasFullBook, purchasedSections, isVip }) }, toggleBookCollapse() { trackClick('chapters', 'btn_click', '折叠书名') this.setData({ bookCollapsed: !this.data.bookCollapsed }) }, async togglePart(e) { trackClick('chapters', 'tab_click', e.currentTarget.dataset.id || '篇章') const partId = e.currentTarget.dataset.id const isExpanding = this.data.expandedPart !== partId this.setData({ expandedPart: isExpanding ? partId : null }) if (isExpanding) await this.loadChaptersByPart(partId) }, expandSectionChapter(e) { const partId = e.currentTarget.dataset.partId const chapterId = e.currentTarget.dataset.chapterId if (!partId || !chapterId) return trackClick('chapters', 'tab_click', '目录_子章展开5条') const part = this.data.bookData.find((p) => p.id === partId) const chapter = part && (part.chapters || []).find((c) => c.id === chapterId) if (!chapter || !chapter.sections || chapter.sections.length === 0) return const total = chapter.sections.length const cur = typeof chapter.sectionVisibleLimit === 'number' ? chapter.sectionVisibleLimit : Math.min(5, total) const next = Math.min(cur + 5, total) if (next === cur) return const bookData = this.data.bookData.map((p) => { if (p.id !== partId) return p return { ...p, chapters: (p.chapters || []).map((ch) => ch.id === chapterId ? { ...ch, sectionVisibleLimit: next } : ch ), } }) // 先去掉动画 class 再打上,便于连续点击重复触发动画 this.setData({ _triangleAnimating: '', bookData }) setTimeout(() => { this.setData({ _triangleAnimating: chapterId }) setTimeout(() => { if (this.data._triangleAnimating === chapterId) { this.setData({ _triangleAnimating: '' }) } }, 480) }, 30) }, // 跳转到阅读页(优先传 mid,与分享逻辑一致) goToRead(e) { const id = e.currentTarget.dataset.id const mid = e.currentTarget.dataset.mid trackClick('chapters', 'card_click', id || '章节') const q = mid ? `mid=${mid}` : `id=${id}` wx.navigateTo({ url: `/pages/read/read?${q}` }) }, hasPurchased(sectionId) { if (this.data.isVip) return true if (this.data.hasFullBook) return true return this.data.purchasedSections.includes(sectionId) }, // 返回首页 goBack() { wx.switchTab({ url: '/pages/index/index' }) }, // 跳转到搜索页 goToSearch() { if (!this.data.searchEnabled) return trackClick('chapters', 'nav_click', '搜索') wx.navigateTo({ url: '/pages/search/search' }) }, onShareAppMessage() { const ref = app.getMyReferralCode() return { title: '卡若创业派对 - 目录', path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters' } }, onShareTimeline() { const ref = app.getMyReferralCode() return { title: '卡若创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' } } })