feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本

- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整
- soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新
- soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL
- .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle

Made-with: Cursor
This commit is contained in:
卡若
2026-03-23 18:38:23 +08:00
parent cb6e2bff56
commit fa3da12b16
82 changed files with 5621 additions and 2723 deletions

View File

@@ -26,6 +26,7 @@ Page({
// 展开状态
expandedPart: null,
bookCollapsed: false,
// 已加载的篇章章节缓存 { partId: chapters }
_loadedChapters: {},
@@ -47,7 +48,11 @@ Page({
partsLoading: true,
// 功能配置(搜索开关)
searchEnabled: true
searchEnabled: true,
// mp_config.mpUi.chaptersPage
chaptersBookTitle: '一场SOUL的创业实验场',
chaptersBookSubtitle: '来自Soul派对房的真实商业故事'
},
onLoad() {
@@ -62,10 +67,20 @@ Page({
this.loadFeatureConfig()
},
_applyChaptersMpUi() {
const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {}
this.setData({
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
'来自Soul派对房的真实商业故事'
})
},
async loadFeatureConfig() {
try {
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
this._applyChaptersMpUi()
return
}
const res = await app.getConfig()
@@ -74,8 +89,10 @@ Page({
if (!app.globalData.features) app.globalData.features = {}
app.globalData.features.searchEnabled = searchEnabled
this.setData({ searchEnabled })
this._applyChaptersMpUi()
} catch (e) {
this.setData({ searchEnabled: true })
this._applyChaptersMpUi()
}
},
@@ -92,7 +109,6 @@ Page({
totalSections = res.totalSections ?? 0
fixedSections = res.fixedSections || []
}
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
const fixedMap = {}
fixedSections.forEach(f => { fixedMap[f.id] = f.mid })
const appendixList = [
@@ -100,13 +116,14 @@ Page({
{ id: 'appendix-2', title: '附录2创业者自检清单', mid: fixedMap['appendix-2'] },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源', mid: fixedMap['appendix-3'] }
]
const bookData = parts.map((p, idx) => ({
const bookData = parts.map((p) => ({
id: p.id,
number: numbers[idx] || String(idx + 1),
icon: p.icon || '',
title: p.title,
subtitle: p.subtitle || '',
chapterCount: p.chapterCount || 0,
chapters: [] // 展开时懒加载
chapters: [],
alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
}))
app.globalData.totalSections = totalSections
this.setData({
@@ -186,6 +203,7 @@ Page({
},
onShow() {
this._applyChaptersMpUi()
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
@@ -225,7 +243,11 @@ Page({
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

View File

@@ -34,18 +34,21 @@
</view>
</view>
<!-- 书籍信息卡 -->
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}">
<!-- 书籍信息卡(点击折叠/展开除"每日派对干货"外的篇章) -->
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}" bindtap="toggleBookCollapse">
<view class="book-icon">
<view class="book-icon-inner"><icon name="book-open" size="56" color="#ffffff"></icon></view>
</view>
<view class="book-info">
<text class="book-title">一场SOUL的创业实验场</text>
<text class="book-subtitle">来自Soul派对房的真实商业故事</text>
<text class="book-title">{{chaptersBookTitle}}</text>
<text class="book-subtitle">{{chaptersBookSubtitle}}</text>
</view>
<view class="book-count">
<text class="count-value brand-color">{{totalSections}}</text>
<text class="count-label">章节</text>
<view class="book-right-area">
<view class="book-count">
<text class="count-value brand-color">{{totalSections}}</text>
<text class="count-label">章节</text>
</view>
<text class="book-collapse-hint">{{bookCollapsed ? '展开 ▸' : '折叠 ▾'}}</text>
</view>
</view>
@@ -65,11 +68,12 @@
<!-- 篇章列表 -->
<view class="part-list">
<view class="part-item" wx:for="{{bookData}}" wx:key="id">
<view class="part-item" wx:for="{{bookData}}" wx:key="id" wx:if="{{!bookCollapsed || item.alwaysShow}}">
<!-- 篇章标题 -->
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
<view class="part-left">
<view class="part-icon">{{item.number}}</view>
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFit"/>
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
<view class="part-info">
<text class="part-title">{{item.title}}</text>
<text class="part-subtitle">{{item.subtitle}}</text>

View File

@@ -195,20 +195,11 @@
color: rgba(255, 255, 255, 0.4);
}
.book-count {
text-align: right;
}
.count-value {
font-size: 40rpx;
font-weight: 700;
display: block;
}
.count-label {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);
}
.book-right-area { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
.book-count { text-align: right; }
.count-value { font-size: 40rpx; font-weight: 700; display: block; }
.count-label { font-size: 20rpx; color: rgba(255, 255, 255, 0.4); }
.book-collapse-hint { font-size: 20rpx; color: #00CED1; opacity: 0.7; }
/* ===== 目录内容 ===== */
.chapters-content {
@@ -365,6 +356,9 @@
color: #ffffff;
flex-shrink: 0;
}
.part-icon-img {
width: 64rpx; height: 64rpx; border-radius: 16rpx; flex-shrink: 0;
}
.part-info {
display: flex;