2026-03-07 22:58:43 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Soul创业派对 - 目录页
|
|
|
|
|
|
* 开发: 卡若
|
|
|
|
|
|
* 技术支持: 存客宝
|
|
|
|
|
|
* 数据: 完整真实文章标题
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const app = getApp()
|
2026-03-17 12:15:08 +08:00
|
|
|
|
const { trackClick } = require('../../utils/trackClick')
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
Page({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
// 系统信息
|
|
|
|
|
|
statusBarHeight: 44,
|
|
|
|
|
|
navBarHeight: 88,
|
|
|
|
|
|
|
|
|
|
|
|
// 用户状态
|
|
|
|
|
|
isLoggedIn: false,
|
|
|
|
|
|
hasFullBook: false,
|
2026-03-10 11:04:34 +08:00
|
|
|
|
isVip: false,
|
2026-03-07 22:58:43 +08:00
|
|
|
|
purchasedSections: [],
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 懒加载:篇章列表(不含章节详情),展开时再请求 chapters-by-part
|
2026-03-08 08:00:39 +08:00
|
|
|
|
totalSections: 0,
|
|
|
|
|
|
bookData: [],
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 展开状态
|
2026-03-07 22:58:43 +08:00
|
|
|
|
expandedPart: null,
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 已加载的篇章章节缓存 { partId: chapters }
|
|
|
|
|
|
_loadedChapters: {},
|
|
|
|
|
|
|
|
|
|
|
|
// 固定模块 id -> mid(序言/尾声/附录,供 goToRead 传 mid)
|
|
|
|
|
|
fixedSectionsMap: {},
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 附录
|
|
|
|
|
|
appendixList: [
|
|
|
|
|
|
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
|
|
|
|
|
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
|
|
|
|
|
|
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
|
|
|
|
|
],
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 每日新增章节(懒加载后暂无,可后续用 latest-chapters 补充)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
dailyChapters: [],
|
|
|
|
|
|
|
|
|
|
|
|
// book/parts 加载中
|
2026-03-17 13:17:49 +08:00
|
|
|
|
partsLoading: true,
|
|
|
|
|
|
|
|
|
|
|
|
// 功能配置(搜索开关)
|
|
|
|
|
|
searchEnabled: true
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
wx.showShareMenu({ withShareTimeline: true })
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
statusBarHeight: app.globalData.statusBarHeight,
|
|
|
|
|
|
navBarHeight: app.globalData.navBarHeight
|
|
|
|
|
|
})
|
|
|
|
|
|
this.updateUserStatus()
|
2026-03-10 11:04:34 +08:00
|
|
|
|
this.loadVipStatus()
|
2026-03-14 18:04:05 +08:00
|
|
|
|
this.loadParts()
|
2026-03-17 12:15:08 +08:00
|
|
|
|
this.loadDailyChapters()
|
2026-03-17 13:17:49 +08:00
|
|
|
|
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.request({ url: '/api/miniprogram/config', silent: true })
|
|
|
|
|
|
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 })
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 懒加载:仅拉取篇章列表 + totalSections + fixedSections
|
2026-03-17 11:44:36 +08:00
|
|
|
|
// 优先 book/parts,404 或失败时降级为 all-chapters 推导
|
2026-03-14 18:04:05 +08:00
|
|
|
|
async loadParts() {
|
2026-03-17 11:44:36 +08:00
|
|
|
|
this.setData({ partsLoading: true })
|
2026-03-07 22:58:43 +08:00
|
|
|
|
try {
|
2026-03-17 11:44:36 +08:00
|
|
|
|
let res
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('[Chapters] book/parts 失败,降级 all-chapters:', e?.message || e)
|
|
|
|
|
|
res = null
|
|
|
|
|
|
}
|
|
|
|
|
|
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 || []
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 降级:从 all-chapters 推导 parts
|
|
|
|
|
|
const allRes = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
|
|
|
|
|
const list = (allRes?.data || allRes?.chapters || [])
|
|
|
|
|
|
totalSections = list.length
|
|
|
|
|
|
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
|
|
|
|
|
const exclude = (c) => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
|
|
|
|
|
const partMap = new Map()
|
|
|
|
|
|
list.filter(exclude).forEach(c => {
|
|
|
|
|
|
const pid = c.partId || c.part_id || 'default'
|
|
|
|
|
|
const ptitle = c.partTitle || c.part_title || '未分类'
|
|
|
|
|
|
if (!partMap.has(pid)) partMap.set(pid, { id: pid, title: ptitle, subtitle: '', chapterCount: 0 })
|
|
|
|
|
|
partMap.get(pid).chapterCount++
|
|
|
|
|
|
})
|
|
|
|
|
|
parts = Array.from(partMap.values())
|
2026-03-08 08:00:39 +08:00
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
2026-03-14 18:04:05 +08:00
|
|
|
|
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) => ({
|
|
|
|
|
|
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,
|
2026-03-17 11:44:36 +08:00
|
|
|
|
_loadedChapters: {},
|
|
|
|
|
|
partsLoading: false
|
2026-03-14 18:04:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('[Chapters] 加载篇章失败:', e)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
this.setData({ bookData: [], totalSections: 0, partsLoading: false })
|
2026-03-14 18:04:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 展开时懒加载该篇章的章节(含 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 => {
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const cid = r.chapterId || r.chapter_id || 'chapter-1'
|
2026-03-14 18:04:05 +08:00
|
|
|
|
if (!chMap.has(cid)) {
|
|
|
|
|
|
chMap.set(cid, {
|
2026-03-07 22:58:43 +08:00
|
|
|
|
id: cid,
|
|
|
|
|
|
title: r.chapterTitle || r.chapter_title || '未分类',
|
|
|
|
|
|
sections: []
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-14 18:04:05 +08:00
|
|
|
|
const ch = chMap.get(cid)
|
|
|
|
|
|
const isPremium = r.editionPremium === true || r.edition_premium === true || r.edition_premium === 1 || r.edition_premium === '1'
|
2026-03-07 22:58:43 +08:00
|
|
|
|
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,
|
2026-03-10 11:04:34 +08:00
|
|
|
|
isNew: r.isNew === true || r.is_new === true,
|
|
|
|
|
|
isPremium
|
2026-03-07 22:58:43 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-03-14 18:04:05 +08:00
|
|
|
|
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)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
})
|
2026-03-14 18:04:05 +08:00
|
|
|
|
app.globalData.bookData = bookDataFlat
|
|
|
|
|
|
wx.setStorageSync('bookData', bookDataFlat)
|
|
|
|
|
|
this.setData({ bookData, _loadedChapters: loaded })
|
2026-03-08 08:00:39 +08:00
|
|
|
|
} catch (e) {
|
2026-03-14 18:04:05 +08:00
|
|
|
|
console.log('[Chapters] 加载章节失败:', e)
|
2026-03-08 08:00:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onPullDownRefresh() {
|
2026-03-17 12:15:08 +08:00
|
|
|
|
Promise.all([this.loadParts(), this.loadDailyChapters()])
|
|
|
|
|
|
.then(() => wx.stopPullDownRefresh())
|
|
|
|
|
|
.catch(() => wx.stopPullDownRefresh())
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 每日新增:用 latest-chapters 接口,展示最近更新章节
|
|
|
|
|
|
async loadDailyChapters() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
|
|
|
|
|
|
const list = (res && res.data) ? res.data : []
|
|
|
|
|
|
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
|
|
|
|
|
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
|
|
|
|
|
const daily = list
|
|
|
|
|
|
.filter(exclude)
|
|
|
|
|
|
.slice(0, 10)
|
|
|
|
|
|
.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,
|
|
|
|
|
|
price: c.price ?? 1,
|
|
|
|
|
|
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
this.setData({ dailyChapters: daily })
|
|
|
|
|
|
} catch (e) { console.log('[Chapters] 加载每日新增失败:', e) }
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-03-10 11:04:34 +08:00
|
|
|
|
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) {
|
|
|
|
|
|
// 静默失败不影响目录展示
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户状态
|
|
|
|
|
|
updateUserStatus() {
|
2026-03-10 11:04:34 +08:00
|
|
|
|
const { isLoggedIn, hasFullBook, purchasedSections, isVip } = app.globalData
|
|
|
|
|
|
this.setData({ isLoggedIn, hasFullBook, purchasedSections, isVip })
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 切换展开状态,展开时懒加载该篇章章节
|
|
|
|
|
|
async togglePart(e) {
|
2026-03-17 12:15:08 +08:00
|
|
|
|
trackClick('chapters', 'tab_click', e.currentTarget.dataset.id || '篇章')
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const partId = e.currentTarget.dataset.id
|
2026-03-14 18:04:05 +08:00
|
|
|
|
const isExpanding = this.data.expandedPart !== partId
|
2026-03-07 22:58:43 +08:00
|
|
|
|
this.setData({
|
2026-03-14 18:04:05 +08:00
|
|
|
|
expandedPart: isExpanding ? partId : null
|
2026-03-07 22:58:43 +08:00
|
|
|
|
})
|
2026-03-14 18:04:05 +08:00
|
|
|
|
if (isExpanding) await this.loadChaptersByPart(partId)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
|
|
|
|
|
goToRead(e) {
|
|
|
|
|
|
const id = e.currentTarget.dataset.id
|
|
|
|
|
|
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
2026-03-17 12:15:08 +08:00
|
|
|
|
trackClick('chapters', 'card_click', id || '章节')
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const q = mid ? `mid=${mid}` : `id=${id}`
|
|
|
|
|
|
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已购买
|
2026-03-10 11:04:34 +08:00
|
|
|
|
hasPurchased(sectionId, isPremium) {
|
|
|
|
|
|
if (this.data.isVip) return true
|
|
|
|
|
|
if (!isPremium && this.data.hasFullBook) return true
|
2026-03-07 22:58:43 +08:00
|
|
|
|
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
|
2026-03-17 12:15:08 +08:00
|
|
|
|
trackClick('chapters', 'nav_click', '搜索')
|
2026-03-07 22:58:43 +08:00
|
|
|
|
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}` : '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|