442 lines
15 KiB
JavaScript
442 lines
15 KiB
JavaScript
/**
|
||
* 卡若创业派对 - 目录页
|
||
* 开发: 卡若
|
||
* 技术支持: 存客宝
|
||
* 数据: 完整真实文章标题
|
||
*/
|
||
|
||
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}` : '' }
|
||
}
|
||
})
|