feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调

- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go)
- 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken)
- 阅读页分享朋友圈复制与 toast 去重
- soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整
- 脚本:content_upload、miniprogram 上传辅助等

Made-with: Cursor
This commit is contained in:
卡若
2026-03-22 08:34:28 +08:00
parent 17ce20c8ee
commit 5724fba877
119 changed files with 8198 additions and 4369 deletions

View File

@@ -29,6 +29,9 @@ Page({
// 已加载的篇章章节缓存 { partId: chapters }
_loadedChapters: {},
// 小三角点击动画:当前触发的子章 id与 chapter.id 比对)
_triangleAnimating: '',
// 固定模块 id -> mid序言/尾声/附录,供 goToRead 传 mid
fixedSectionsMap: {},
@@ -152,6 +155,12 @@ Page({
})
})
const chapters = Array.from(chMap.values())
chapters.forEach(ch => ch.sections.reverse())
// 目录子章下列表:默认最多展示 5 条,点小三角每次再展开 5 条
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
@@ -227,6 +236,43 @@ Page({
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

View File

@@ -88,8 +88,8 @@
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
<view class="chapter-header">{{chapter.title}}</view>
<view class="section-list">
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section">
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section" wx:for-index="secIdx">
<view class="section-item" wx:if="{{secIdx < chapter.sectionVisibleLimit}}" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
<view class="section-left">
<view class="section-lock-wrap">
<icon wx:if="{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" name="lock-open" size="24" color="#00CED1" customClass="section-lock lock-open"></icon>
@@ -100,12 +100,14 @@
</view>
<view class="section-right">
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
<text wx:elif="{{isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已解锁</text>
<text wx:else class="section-price">¥{{section.price}}</text>
<text wx:elif="{{!(isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1)}}" class="section-price">¥{{section.price}}</text>
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="section-arrow"></icon>
</view>
</view>
</block>
<view class="section-expand-trigger" wx:if="{{chapter.sections.length > chapter.sectionVisibleLimit}}" bindtap="expandSectionChapter" data-part-id="{{item.id}}" data-chapter-id="{{chapter.id}}">
<view class="latest-expand-triangle {{_triangleAnimating === chapter.id ? 'tri-bounce' : ''}}"></view>
</view>
</view>
</block>
</view>

View File

@@ -577,6 +577,49 @@
color: rgba(255, 255, 255, 0.3);
}
/* ===== 展开三角 ===== */
.section-expand-trigger {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0 12rpx;
}
.latest-expand-triangle {
width: 0;
height: 0;
border-left: 18rpx solid transparent;
border-right: 18rpx solid transparent;
border-top: 14rpx solid rgba(0, 206, 209, 0.55);
opacity: 0.85;
transform-origin: 50% 0;
transition: border-top-color 0.15s ease;
}
.section-expand-trigger:active .latest-expand-triangle {
border-top-color: #00CED1;
}
@keyframes catalog-tri-nudge {
0% {
transform: translateY(0) scale(1);
opacity: 0.85;
}
40% {
transform: translateY(10rpx) scale(1.12);
opacity: 1;
border-top-color: #00CED1;
}
100% {
transform: translateY(0) scale(1);
opacity: 0.85;
}
}
.latest-expand-triangle.tri-bounce {
animation: catalog-tri-nudge 0.45s ease-out;
}
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;