chore: 清理敏感与开发文档,仅同步代码

- 永久忽略并从仓库移除 开发文档/
- 移除并忽略 .env 与小程序私有配置
- 同步小程序/管理端/API与脚本改动

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 17:50:12 +08:00
parent 868b0a10d9
commit 76965adb23
443 changed files with 24175 additions and 64154 deletions

View File

@@ -23,8 +23,12 @@ Page({
totalSections: 62,
bookData: [],
// 推荐章节:以服务端推荐算法为准,不再预置写死内容
featuredSections: [],
// 推荐章节
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: '真实的赚钱' }
],
// 最新章节(动态计算)
latestSection: null,
@@ -43,8 +47,9 @@ Page({
superMembers: [],
superMembersLoading: true,
// 最新新增章节
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
latestChapters: [],
displayLatestChapters: [],
// 篇章数(从 bookData 计算)
partCount: 0,
@@ -52,9 +57,21 @@ Page({
// 加载状态
loading: true,
// 审核模式
auditMode: false,
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: ''
leadPhone: '',
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
featuredExpandedLoading: false,
// 功能配置(搜索开关)
searchEnabled: true
},
onLoad(options) {
@@ -73,6 +90,7 @@ Page({
}
wx.showShareMenu({ withShareTimeline: true })
this.loadFeatureConfig()
this.initData()
},
@@ -100,6 +118,9 @@ Page({
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
// 同步审核模式
this.setData({ auditMode: app.globalData.auditMode })
// 更新用户状态
this.updateUserStatus()
},
@@ -176,7 +197,25 @@ Page({
}
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
// 兜底:无 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) || []
@@ -201,10 +240,14 @@ Page({
}
}
// 2. 最新更新:用 book/latest-chapters 取第1条
// 2. 最新更新:用 book/latest-chapters 取第1条(排除「序言」「尾声」「附录」)
try {
const latestRes = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
const latestList = (latestRes && latestRes.data) ? latestRes.data : []
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(l => {
const pt = (l.part_title || l.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (latestList.length > 0) {
const l = latestList[0]
this.setData({
@@ -275,8 +318,26 @@ Page({
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
wx.navigateTo({ url: '/pages/search/search' })
},
@@ -297,10 +358,6 @@ Page({
wx.navigateTo({ url: '/pages/vip/vip' })
},
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
async onLinkKaruo() {
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
@@ -316,7 +373,12 @@ Page({
return
}
const userId = app.globalData.userInfo.id
const leadKey = 'karuo_lead_' + userId
// 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()
if (!phone && !wechatId) {
@@ -329,15 +391,10 @@ Page({
} catch (e) {}
}
if (phone || wechatId) {
const hasLead = wx.getStorageSync(leadKey)
if (hasLead) {
wx.showToast({ title: '已提交联系方式,卡若会尽快联系你', icon: 'none' })
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
url: '/api/miniprogram/ckb/index-lead',
method: 'POST',
data: {
userId,
@@ -348,7 +405,7 @@ Page({
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync(leadKey, true)
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
@@ -366,38 +423,91 @@ Page({
this.setData({ showLeadModal: false, leadPhone: '' })
},
// 阻止弹窗内部点击事件冒泡到遮罩层
stopPropagation() {},
onLeadPhoneInput(e) {
this.setData({ leadPhone: (e.detail.value || '').trim() })
},
async submitLead() {
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
// 一键获取手机号(微信能力),成功后直接提交链接卡若
async onGetPhoneNumberForLead(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
wx.showToast({ title: '未获取到手机号,请手动输入', icon: 'none' })
return
}
if (phone.length < 11) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
const code = e.detail.code
if (!code) {
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
wx.showLoading({ title: '获取中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/phone',
method: 'POST',
data: { code, userId }
})
wx.hideLoading()
if (res && res.success && res.phoneNumber) {
await this._submitLeadWithPhone(res.phoneNumber)
} else {
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
wx.showToast({ title: err.message || '获取失败,请手动输入', icon: 'none' })
}
},
// 内部:用手机号提交链接卡若(一键获取与手动输入共用)
async _submitLeadWithPhone(phone) {
const p = (phone || '').trim().replace(/\s/g, '')
if (!p || p.length < 11) {
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
const leadKey = userId ? ('karuo_lead_' + userId) : ''
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
url: '/api/miniprogram/ckb/index-lead',
method: 'POST',
data: {
userId,
phone,
name: (app.globalData.userInfo?.nickname || '').trim() || undefined
}
phone: p,
name: (app.globalData.userInfo?.nickname || '').trim() || undefined,
},
})
wx.hideLoading()
this.setData({ showLeadModal: false, leadPhone: '' })
if (res && res.success) {
if (leadKey) wx.setStorageSync(leadKey, true)
wx.setStorageSync('lead_last_submit_ts', Date.now())
// 同步手机号到用户资料
try {
if (userId) {
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId, phone: p },
})
if (app.globalData.userInfo) {
app.globalData.userInfo.phone = p
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.setStorageSync('user_phone', p)
}
} catch (e) {
console.log('[Index] 同步手机号到用户资料失败:', e && e.message)
}
wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
@@ -408,58 +518,87 @@ Page({
}
},
async submitLead() {
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
return
}
await this._submitLeadWithPhone(phone)
},
goToSuperList() {
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/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
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('附录')
// stitch_soul优先取 isNew 标记的章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
if (candidates.length === 0) {
candidates = chapters.filter(exclude)
}
// 解析「第X场」用于倒序最新场次大放在最上方
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 latest = 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)
})
.slice(0, 10)
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 || ''
const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
// 描述仅用正文摘要,避免 #id 或标题重复;截取 36 字
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,
desc: '', // latest-chapters 不返回 content避免大表全量加载
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
this.setData({ latestChapters: latest })
const display = this.data.latestExpanded ? latest : latest.slice(0, 5)
this.setData({ latestChapters: latest, displayLatestChapters: display })
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},

View File

@@ -24,8 +24,8 @@
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar" bindtap="goToSearch">
<!-- 搜索栏(根据配置显示) -->
<view class="search-bar" wx:if="{{searchEnabled}}" bindtap="goToSearch">
<view class="search-icon-wrap">
<text class="search-icon-text">🔍</text>
</view>
@@ -52,6 +52,19 @@
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 阅读进度(设计稿:最新更新→阅读进度→超级个体) -->
<view class="progress-card" wx:if="{{isLoggedIn}}" bindtap="goToChapters">
<view class="progress-header">
<text class="progress-title">阅读进度</text>
<text class="progress-count">已读 {{readCount}}/{{totalSections}}</text>
</view>
<view class="progress-bar-wrapper">
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{readCount && totalSections ? (readCount / totalSections * 100) : 0}}%;"></view>
</view>
</view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
<view class="section">
<view class="section-header">
@@ -87,14 +100,18 @@
<!-- 已加载无数据 -->
<view wx:else class="super-empty">
<text class="super-empty-text">成为会员,展示你的项目</text>
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
<view class="super-empty-btn" bindtap="goToVip" wx:if="{{!auditMode}}">加入创业派对 →</view>
</view>
</view>
<!-- 精选推荐(带 tag已去掉「查看全部」 -->
<!-- 精选推荐(带 tag支持展开更多 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选推荐</text>
<view class="section-more" wx:if="{{featuredSections.length > 0}}" bindtap="toggleFeaturedExpanded">
<text class="more-text">{{featuredExpandedLoading ? '加载中...' : (featuredExpanded ? '收起' : '展开更多')}}</text>
<text class="more-arrow">{{featuredExpanded ? '▲' : '▼'}}</text>
</view>
</view>
<view class="featured-list">
<view
@@ -117,18 +134,24 @@
</view>
</view>
<!-- 最新新增(时间线样式) -->
<!-- 最新新增(时间线样式,支持展开更多 -->
<view class="section" wx:if="{{latestChapters.length > 0}}">
<view class="section-header latest-header">
<text class="section-title">最新新增</text>
<view class="daily-badge-wrap">
<text class="daily-badge">+{{latestChapters.length}}</text>
<view class="section-header-right">
<view class="daily-badge-wrap">
<text class="daily-badge">+{{latestChapters.length}}</text>
</view>
<view class="section-more" wx:if="{{latestChapters.length > 5}}" bindtap="toggleLatestExpanded">
<text class="more-text">{{latestExpanded ? '收起' : '展开更多'}}</text>
<text class="more-arrow">{{latestExpanded ? '▲' : '▼'}}</text>
</view>
</view>
</view>
<view class="timeline-wrap">
<view class="timeline-line"></view>
<view class="timeline-list">
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{displayLatestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-row">
@@ -150,12 +173,17 @@
<!-- 底部留白 -->
<view class="bottom-space"></view>
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时) -->
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时):一键获取 + 手动输入 -->
<view class="lead-mask" wx:if="{{showLeadModal}}" catchtap="closeLeadModal">
<view class="lead-box" catchtap="">
<!-- 使用 catchtap="stopPropagation" 阻止内部点击冒泡到遮罩层,避免点击输入框时弹窗被关闭 -->
<view class="lead-box" catchtap="stopPropagation">
<text class="lead-title">留下联系方式</text>
<text class="lead-desc">方便卡若与您联系</text>
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
<button class="lead-get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumberForLead">一键获取手机号</button>
<text class="lead-divider">或手动输入</text>
<view class="lead-input-wrap">
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
</view>
<view class="lead-actions">
<button class="lead-btn lead-btn-cancel" bindtap="closeLeadModal">取消</button>
<button class="lead-btn lead-btn-submit" bindtap="submitLead">提交</button>

View File

@@ -445,6 +445,7 @@
font-size: 32rpx;
color: rgba(255, 255, 255, 0.3);
margin-top: 8rpx;
flex-shrink: 0;
}
/* ===== 内容概览列表 ===== */
@@ -688,6 +689,12 @@
margin-bottom: 32rpx;
}
.section-header-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.daily-badge-wrap {
display: inline-flex;
align-items: center;
@@ -838,6 +845,64 @@
height: 26rpx;
}
/* 展开/收起按钮 */
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 20rpx 0;
margin-top: 8rpx;
}
.expand-text {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
}
.expand-icon {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
}
/* 热度排行 */
.hot-list {
background: #1c1c1e;
border-radius: 24rpx;
overflow: hidden;
border: 2rpx solid rgba(255, 255, 255, 0.04);
}
.hot-item {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
}
.hot-item:last-child {
border-bottom: none;
}
.hot-rank {
width: 48rpx;
font-size: 28rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.4);
margin-right: 20rpx;
}
.hot-rank-top {
color: #38bdac;
}
.hot-title {
flex: 1;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.hot-price {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.4);
margin-left: 16rpx;
}
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;
@@ -877,19 +942,42 @@
display: block;
font-size: 26rpx;
color: #A0AEC0;
margin-bottom: 24rpx;
}
.lead-get-phone-btn {
width: 100%;
height: 88rpx;
background: rgba(56, 189, 172, 0.2);
border: 2rpx solid rgba(56, 189, 172, 0.5);
border-radius: 16rpx;
font-size: 30rpx;
color: #38bdac;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
line-height: normal;
}
.lead-get-phone-btn::after { border: none; }
.lead-divider {
display: block;
font-size: 24rpx;
color: #6B7280;
text-align: center;
margin-bottom: 16rpx;
}
.lead-input-wrap {
padding: 16rpx 24rpx;
background: #0a1628;
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
margin-bottom: 32rpx;
}
.lead-input {
width: 100%;
height: 88rpx;
background: #0a1628;
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 0 24rpx;
box-sizing: border-box;
font-size: 30rpx;
color: #ffffff;
margin-bottom: 32rpx;
background: transparent;
}
.lead-actions {
display: flex;
@@ -898,7 +986,11 @@
.lead-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
/* 使用 flex 垂直居中文本,避免小程序默认 padding 导致按钮文字下沉 */
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;