Files

271 lines
8.8 KiB
JavaScript
Raw Permalink 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创业派对 - 目录页
* 开发: 卡若
* 技术支持: 存客宝
* 数据: 完整真实文章标题
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户状态
isLoggedIn: false,
hasFullBook: false,
isVip: false,
purchasedSections: [],
// 懒加载:篇章列表(不含章节详情),展开时再请求 chapters-by-part
totalSections: 0,
bookData: [],
// 展开状态
expandedPart: null,
// 已加载的篇章章节缓存 { partId: chapters }
_loadedChapters: {},
// 固定模块 id -> mid序言/尾声/附录,供 goToRead 传 mid
fixedSectionsMap: {},
// 附录
appendixList: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2创业者自检清单' },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源' }
],
// book/parts 加载中
partsLoading: true,
// 功能配置(搜索开关)
searchEnabled: true
},
onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
this.loadVipStatus()
this.loadParts()
this.loadFeatureConfig()
},
async loadFeatureConfig() {
try {
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
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
this.setData({ searchEnabled })
} catch (e) {
this.setData({ searchEnabled: true })
}
},
// 懒加载:仅拉取篇章列表 + 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 numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
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) => ({
id: p.id,
number: numbers[idx] || String(idx + 1),
title: p.title,
subtitle: p.subtitle || '',
chapterCount: p.chapterCount || 0,
chapters: [] // 展开时懒加载
}))
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())
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() {
this.loadParts()
.then(() => wx.stopPullDownRefresh())
.catch(() => wx.stopPullDownRefresh())
},
onShow() {
// 设置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 })
},
// 切换展开状态,展开时懒加载该篇章章节
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)
},
// 跳转到阅读页(优先传 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() {
if (!this.data.searchEnabled) return
trackClick('chapters', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 目录',
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
}
})