feat(miniprogram): 用 karuo-316 替换小程序目录

- 从 github/karuo-316 覆盖 miniprogram/
- 排除 project.private.config.json 与 *.backup

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 18:25:24 +08:00
parent 982622368a
commit 34f7c7bbdc
34 changed files with 450 additions and 891 deletions

View File

@@ -4,9 +4,9 @@
* 技术支持: 存客宝
*/
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
Page({
data: {
@@ -20,15 +20,13 @@ Page({
readCount: 0,
// 书籍数据
totalSections: 62,
totalSections: 0,
bookData: [],
// 推荐章节
featuredSections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 精选推荐按热度排行默认显示3篇可展开更多
featuredSections: [],
featuredSectionsAll: [],
featuredExpanded: false,
// 最新章节(动态计算)
latestSection: null,
@@ -47,9 +45,10 @@ Page({
superMembers: [],
superMembersLoading: true,
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
// 最新新增章节
latestChapters: [],
displayLatestChapters: [],
latestChaptersExpanded: false,
latestChaptersAll: [],
// 篇章数(从 bookData 计算)
partCount: 0,
@@ -57,26 +56,12 @@ Page({
// 加载状态
loading: true,
// 审核模式
auditMode: false,
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: '',
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
featuredExpandedLoading: false,
// 功能配置(搜索开关)
searchEnabled: true
leadPhone: ''
},
onLoad(options) {
console.log('[Index] ===== onLoad 触发 =====')
// 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
@@ -85,26 +70,19 @@ Page({
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
wx.showShareMenu({ withShareTimeline: true })
this.loadFeatureConfig()
this.initData()
},
onShow() {
console.log('[Index] onShow 触发')
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
// 主动触发配置加载
if (tabBar && tabBar.loadFeatureConfig) {
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
tabBar.loadFeatureConfig()
}
@@ -114,24 +92,24 @@ Page({
} else if (tabBar) {
tabBar.setData({ selected: 0 })
}
} else {
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
// 同步审核模式
this.setData({ auditMode: app.globalData.auditMode })
// 更新用户状态
this.updateUserStatus()
// 规则引擎:首页展示时检查(填头像、分享引导等)
checkAndExecute('page_show', this)
},
// 初始化数据:首次进页面并行异步加载,加快首屏展示
initData() {
this.setData({ loading: false })
this.loadBookData()
this.loadFeaturedFromServer()
this.loadSuperMembers()
this.loadLatestChapters()
Promise.all([
this.loadBookData(),
this.loadFeaturedFromServer(),
this.loadSuperMembers(),
this.loadLatestChapters()
]).finally(() => {
this.setData({ loading: false })
})
},
async loadSuperMembers() {
@@ -149,13 +127,8 @@ Page({
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) {
console.log('[Index] 超级个体加载成功:', members.length, '人')
}
}
} catch (e) {
console.log('[Index] vip/members 请求失败:', e)
}
} catch (e) {}
// 不足 4 个则用有头像的普通用户补充
if (members.length < 4) {
try {
@@ -172,73 +145,36 @@ Page({
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)
this.setData({ superMembersLoading: false })
}
},
// 从服务端获取精选推荐、最新更新stitch_soulbook/recommended、book/latest-chapters
// 从服务端获取精选推荐(按热度排行)和最新更新
async loadFeaturedFromServer() {
try {
// 1. 精选推荐:优先用 book/recommended按阅读量+算法,带 热门/推荐/精选 标签)
let featured = []
// 1. 精选推荐: book/hot 获取热度排行数据
try {
const recRes = await app.request({ url: '/api/miniprogram/book/recommended', silent: true })
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => ({
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
if (hotRes && hotRes.success && Array.isArray(hotRes.data) && hotRes.data.length > 0) {
const tagClassMap = { '热门': 'tag-hot', '推荐': 'tag-rec', '精选': 'tag-rec' }
const all = hotRes.data.map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: s.tag || ['热门', '推荐', '精选'][i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
tag: s.tag || '',
tagClass: tagClassMap[s.tag] || 'tag-rec',
hotScore: s.hotScore || s.hot_score || 0,
hotRank: s.hotRank || (i + 1),
price: s.price ?? 1,
}))
this.setData({ featuredSections: featured })
this.setData({
featuredSectionsAll: all,
featuredSections: all.slice(0, 3),
featuredExpanded: false,
})
}
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
// 兜底:无 recommended 时从 book/hot 取前3
if (featured.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = hotList.slice(0, 3).map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
if (featured.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (valid.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = valid
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
.slice(0, 3)
.map((s, i) => ({
id: s.id,
mid: s.mid ?? s.MID ?? 0,
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
part: (s.part_title || s.partTitle || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
}
} catch (e) {}
// 2. 最新更新:用 book/latest-chapters 取第1条排除「序言」「尾声」「附录」
try {
@@ -260,7 +196,6 @@ Page({
})
}
} catch (e) {
// 兜底:从 all-chapters 取
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
@@ -280,9 +215,7 @@ Page({
})
}
}
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
}
} catch (e) {}
},
async loadBookData() {
@@ -293,7 +226,7 @@ Page({
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
this.setData({
bookData: chapters,
totalSections: res.total || chapters.length || 62,
totalSections: res.total || chapters.length || app.globalData.totalSections || 0,
partCount: partIds.size || 5
})
}
@@ -305,7 +238,7 @@ Page({
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
const readCount = Math.min(app.getReadCount(), this.data.totalSections || app.globalData.totalSections || 0)
this.setData({
isLoggedIn,
hasFullBook,
@@ -315,35 +248,20 @@ Page({
// 跳转到目录
goToChapters() {
trackClick('home', 'nav_click', '目录')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
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 })
}
},
// 跳转到搜索页
goToSearch() {
if (!this.data.searchEnabled) return
trackClick('home', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页(优先传 mid与分享逻辑一致
goToRead(e) {
const id = e.currentTarget.dataset.id
trackClick('home', 'card_click', e.currentTarget.dataset.id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
@@ -355,10 +273,16 @@ Page({
},
goToVip() {
trackClick('home', 'btn_click', 'VIP')
wx.navigateTo({ url: '/pages/vip/vip' })
},
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
async onLinkKaruo() {
trackClick('home', 'btn_click', '链接卡若')
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
@@ -373,23 +297,31 @@ Page({
return
}
const userId = app.globalData.userInfo.id
// 2 分钟内只能点一次(与后端限频一致)
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
let avatar = (app.globalData.userInfo.avatar || app.globalData.userInfo.avatarUrl || '').trim()
if (!phone && !wechatId) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || '').trim()
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
if (!avatar) avatar = (profileRes.data.avatar || '').trim()
}
} catch (e) {}
}
if ((!phone && !wechatId) || !avatar) {
wx.showModal({
title: '完善资料',
content: !avatar ? '请先设置头像和填写联系方式,以便对方联系您' : '请先填写手机号或微信号,以便对方联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
if (phone || wechatId) {
wx.showLoading({ title: '提交中...', mask: true })
try {
@@ -405,8 +337,12 @@ Page({
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
wx.showModal({
title: '提交成功',
content: '卡若会主动添加你微信,请注意你的微信消息',
showCancel: false,
confirmText: '好的'
})
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
@@ -469,11 +405,6 @@ Page({
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
wx.showLoading({ title: '提交中...', mask: true })
@@ -490,7 +421,6 @@ Page({
wx.hideLoading()
this.setData({ showLeadModal: false, leadPhone: '' })
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
// 同步手机号到用户资料
try {
if (userId) {
@@ -519,6 +449,7 @@ Page({
},
async submitLead() {
trackClick('home', 'btn_click', '提交留资')
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
@@ -531,75 +462,76 @@ Page({
wx.switchTab({ url: '/pages/match/match' })
},
// 精选推荐:展开/折叠
async toggleFeaturedExpanded() {
if (this.data.featuredExpandedLoading) return
if (this.data.featuredExpanded) {
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
this.setData({ featuredExpanded: false, featuredSections: collapsed })
return
}
if (this.data.featuredSectionsFull.length > 0) {
this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull })
return
}
this.setData({ featuredExpandedLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
const list = (res && res.data) ? res.data : []
const tagMap = ['热门', '推荐', '精选']
const full = list.map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i % 3] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec'
}))
this.setData({
featuredSectionsFull: full,
featuredSections: full,
featuredExpanded: true,
featuredExpandedLoading: false
})
} catch (e) {
console.log('[Index] 加载精选更多失败:', e)
this.setData({ featuredExpandedLoading: false })
}
},
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
toggleLatestExpanded() {
const expanded = !this.data.latestExpanded
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
},
// 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章
async loadLatestChapters() {
try {
const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
const list = (res && res.data) ? res.data : []
let chapters = app.globalData.bookData || []
if (!Array.isArray(chapters) || chapters.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
chapters = (res && res.data) || (res && res.chapters) || []
}
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
const latest = list
.filter(exclude)
.slice(0, 20)
.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,
desc: '', // latest-chapters 不返回 content避免大表全量加载
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
const display = this.data.latestExpanded ? latest : latest.slice(0, 5)
this.setData({ latestChapters: latest, displayLatestChapters: display })
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
if (candidates.length === 0) {
candidates = chapters.filter(exclude)
}
const sessionNum = (c) => {
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
const m = title.match(/第\s*(\d+)\s*场/) || title.match(/第(\d+)场/)
if (m) return parseInt(m[1], 10)
const id = c.id != null ? String(c.id) : ''
if (/^\d+$/.test(id)) return parseInt(id, 10)
return 0
}
const mapChapter = (c) => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
let desc = ''
if (rawContent && rawContent.length > 0) {
const clean = rawContent.replace(/^#[\d.]+\s*/, '').trim()
desc = clean.length > 36 ? clean.slice(0, 36) + '...' : clean
}
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
desc,
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
}
const sorted = candidates.sort((a, b) => {
const na = sessionNum(a)
const nb = sessionNum(b)
if (na !== nb) return nb - na
return new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0)
})
const latestAll = sorted.slice(0, 10).map(mapChapter)
this.setData({
latestChaptersAll: latestAll,
latestChapters: latestAll.slice(0, 5),
latestChaptersExpanded: false,
})
} catch (e) {}
},
toggleLatestExpand() {
const all = this.data.latestChaptersAll || []
if (this.data.latestChaptersExpanded) {
this.setData({ latestChapters: all.slice(0, 5), latestChaptersExpanded: false })
} else {
this.setData({ latestChapters: all, latestChaptersExpanded: true })
}
},
toggleFeaturedExpand() {
const all = this.data.featuredSectionsAll || []
if (this.data.featuredExpanded) {
this.setData({ featuredSections: all.slice(0, 3), featuredExpanded: false })
} else {
this.setData({ featuredSections: all, featuredExpanded: true })
}
},
goToMemberDetail(e) {