Files

347 lines
12 KiB
JavaScript
Raw Permalink Normal View History

/**
* 卡若创业派对 - 目录页
* 开发: 卡若
* 技术支持: 存客宝
* 数据: 完整真实文章标题
*/
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: [],
// 懒加载:篇章列表(不含章节详情),展开时再请求 chapters-by-part
totalSections: 0,
bookData: [],
// 展开状态
expandedPart: null,
bookCollapsed: false,
// 已加载的篇章章节缓存 { partId: chapters }
_loadedChapters: {},
// 小三角点击动画:当前触发的子章 id与 chapter.id 比对)
_triangleAnimating: '',
// 固定模块 id -> mid序言/尾声/附录,供 goToRead 传 mid
fixedSectionsMap: {},
// 附录
appendixList: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2创业者自检清单' },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源' }
],
// book/parts 加载中
2026-03-17 13:17:49 +08:00
partsLoading: true,
// 功能配置(搜索开关)
searchEnabled: true,
// mp_config.mpUi.chaptersPage
chaptersBookTitle: '一场SOUL的创业实验场',
chaptersBookSubtitle: '来自Soul派对房的真实商业故事'
},
onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
this.loadVipStatus()
this.loadParts()
2026-03-17 13:17:49 +08:00
this.loadFeatureConfig()
},
_applyChaptersMpUi() {
const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {}
this.setData({
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
'来自Soul派对房的真实商业故事'
})
},
2026-03-17 13:17:49 +08:00
async loadFeatureConfig() {
try {
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
this._applyChaptersMpUi()
2026-03-17 13:17:49 +08:00
return
}
const res = await app.getConfig()
2026-03-17 13:17:49 +08:00
const features = (res && res.features) || {}
const searchEnabled = features.searchEnabled !== false
if (!app.globalData.features) app.globalData.features = {}
app.globalData.features.searchEnabled = searchEnabled
this.setData({ searchEnabled })
this._applyChaptersMpUi()
2026-03-17 13:17:49 +08:00
} catch (e) {
this.setData({ searchEnabled: true })
this._applyChaptersMpUi()
2026-03-17 13:17:49 +08:00
}
},
// 懒加载:仅拉取篇章列表 + totalSections + fixedSectionsbook/parts不再用 all-chapters
async loadParts() {
this.setData({ partsLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/book/parts', 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: '附录1Soul派对房精选对话', 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)
return {
id: p.id,
icon,
iconEmoji,
title: p.title,
subtitle: p.subtitle || '',
chapterCount: p.chapterCount || 0,
chapters: [],
alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
}
})
app.globalData.totalSections = totalSections
this.setData({
bookData,
totalSections,
fixedSectionsMap: fixedMap,
appendixList,
_loadedChapters: {},
partsLoading: false
})
} 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)
const isPremium = r.editionPremium === true || r.edition_premium === true || r.edition_premium === 1 || r.edition_premium === '1'
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,
isPremium
})
})
const chapters = Array.from(chMap.values())
chapters.forEach(ch => ch.sections.reverse())
// 目录子章下列表:默认最多展示 5 条,点小三角每次再展开 5 条
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)
}
},
onPullDownRefresh() {
2026-03-17 18:22:06 +08:00
this.loadParts()
.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=9.9 买断)
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, isPremium) {
if (this.data.isVip) return true
if (!isPremium && this.data.hasFullBook) return true
return this.data.purchasedSections.includes(sectionId)
},
// 返回首页
goBack() {
wx.switchTab({ url: '/pages/index/index' })
},
// 跳转到搜索页
goToSearch() {
2026-03-17 13:17:49 +08:00
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}` : '' }
}
})