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

@@ -4,10 +4,23 @@
* 技术支持: 存客宝
*/
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { cleanSingleLineField } = require('../../utils/contentParser')
/** 与首页固定「卡若」获客位重复时从横滑列表剔除(含历史误写「卡路」) */
function isKaruoHostDuplicateName(displayName) {
const s = String(displayName || '').trim()
return s === '卡若' || s === '卡路'
}
/** 超级个体无头像占位:仅展示中文首字,避免头像圆里出现英文字母 */
function superAvatarLetter(displayName) {
const s = String(displayName || '').trim()
if (!s) return '会'
const ch = s[0]
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
}
Page({
data: {
@@ -31,8 +44,8 @@ Page({
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 最新章节(动态计算
latestSection: null,
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters
bannerSection: null,
latestLabel: '最新更新',
// 内容概览
@@ -66,8 +79,7 @@ Page({
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
featuredExpandedLoading: false,
featuredSectionsFull: [], // 精选排行榜全量(最多 50默认只展示前 3 条
// 功能配置(搜索开关)
searchEnabled: true,
@@ -136,28 +148,22 @@ Page({
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
// 并行请求 VIP 会员和普通用户,合并后取前 4 个VIP 优先)
const [vipRes, usersRes] = await Promise.all([
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/users?limit=20', silent: true }).catch(() => null)
])
// 仅走后端 VIP 列表排序vip_sort、vip_activated_at不在端上拼普通用户
const vipRes = await app.request({ url: '/api/miniprogram/vip/members?limit=24', silent: true }).catch(() => null)
let members = []
if (vipRes && vipRes.success && Array.isArray(vipRes.data) && vipRes.data.length > 0) {
members = vipRes.data.slice(0, 4).map(u => ({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员',
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '')
}
if (members.length < 4 && usersRes && usersRes.success && Array.isArray(usersRes.data)) {
const existIds = new Set(members.map(m => m.id))
const extra = usersRes.data
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
.slice(0, 4 - members.length)
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
members = members.concat(extra)
members = vipRes.data.map(u => {
const raw = u.name || u.nickname || u.vipName || u.vip_name || '会员'
const name = cleanSingleLineField(raw) || '会员'
return {
id: u.id,
name,
avatar: u.avatar || '',
isVip: true,
avatarLetter: superAvatarLetter(name)
}
}).filter((m) => !isKaruoHostDuplicateName(m.name))
console.log('[Index] 超级个体(后端排序):', members.length, '人')
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
@@ -166,48 +172,79 @@ Page({
}
},
// 精选推荐 + 最新更新 + 最新列表:一次请求 recommended + latest-chapters避免重复
// 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法latest=updated_at
async loadFeaturedAndLatest() {
try {
const excludeFixed = (c) => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
const tagClassForTag = (tag) => (tag === '热门' ? 'tag-hot' : 'tag-rec')
const toSectionFromRanking = (s) => {
const tag = s.tag || '精选'
return {
id: s.id || s.section_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,
tagClass: tagClassForTag(tag)
}
}
const fallbackTags = ['热门', '推荐', '精选']
const toSectionFromHot = (s, i) => {
const tag = fallbackTags[i % 3]
return {
id: s.id || s.section_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,
tagClass: tagClassForTag(tag)
}
}
const toSection = (s, i, tagMap = ['热门', '推荐', '精选']) => ({
id: s.id || s.section_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: s.tag || tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
})
const [recRes, latestRes] = await Promise.all([
app.request({ url: '/api/miniprogram/book/recommended', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/book/recommended?limit=50', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
])
// 1. 精选推荐recommended → hot 兜底
let featured = []
// 1. 精选推荐一次拉全量≤50默认只显示 3 条;点列表下三角展开(与「最新新增」一致
let featuredFull = []
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => toSection(s, i))
featuredFull = recRes.data.map((s) => toSectionFromRanking(s))
}
if (featured.length === 0) {
if (featuredFull.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) featured = hotList.slice(0, 3).map((s, i) => toSection(s, i))
if (hotList.length > 0) featuredFull = hotList.map((s, i) => toSectionFromHot(s, i))
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
if (featured.length > 0) this.setData({ featuredSections: featured })
if (featuredFull.length > 0) {
this.setData({
featuredSectionsFull: featuredFull,
featuredSections: featuredFull.slice(0, 3),
featuredExpanded: false
})
} else {
this.setData({
featuredSectionsFull: [],
featuredSections: [],
featuredExpanded: false
})
}
// 2. 最新更新 + 最新列表(共用 latest-chapters 数据)
// 2. Banner 推荐:优先取 recommended 第一条,回退 latest 第一条
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(excludeFixed)
if (latestList.length > 0) {
// 按更新时间倒序,最新在前(与后台展示一致)
const latestList = [...rawList].sort((a, b) => {
const ta = new Date(a.updatedAt || a.updated_at || 0).getTime()
const tb = new Date(b.updatedAt || b.updated_at || 0).getTime()
return tb - ta
})
if (featuredFull.length > 0) {
this.setData({ bannerSection: featuredFull[0] })
} else if (latestList.length > 0) {
const l = latestList[0]
this.setData({
latestSection: {
bannerSection: {
id: l.id,
mid: l.mid ?? l.MID ?? 0,
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
@@ -334,12 +371,6 @@ 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()
if (!phone && !wechatId) {
@@ -439,11 +470,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 })
@@ -501,50 +527,24 @@ Page({
wx.switchTab({ url: '/pages/match/match' })
},
// 精选推荐:展开/折叠
async toggleFeaturedExpanded() {
if (this.data.featuredExpandedLoading) return
trackClick('home', 'tab_click', this.data.featuredExpanded ? '精选收起' : '精选展开')
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 })
}
// 精选推荐:列表下方小三角展开(数据已在 loadFeaturedAndLatest 一次拉齐)
expandFeaturedChapters() {
if (this.data.featuredExpanded) return
const full = this.data.featuredSectionsFull || []
if (full.length <= 3) return
trackClick('home', 'tab_click', '精选展开_底部三角')
this.setData({ featuredExpanded: true, featuredSections: full })
},
// 最新新增:展开/折叠(默认 5 条,点击展开剩余
toggleLatestExpanded() {
trackClick('home', 'tab_click', this.data.latestExpanded ? '最新收起' : '最新展开')
const expanded = !this.data.latestExpanded
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
// 最新新增:列表下方小三角展开(无「收起」,展开后整页向下滚动查看
expandLatestChapters() {
if (this.data.latestExpanded) return
trackClick('home', 'tab_click', '最新展开_底部三角')
const full = this.data.latestChapters || []
this.setData({
latestExpanded: true,
displayLatestChapters: full
})
},
goToMemberDetail(e) {

View File

@@ -4,12 +4,12 @@
<!-- 自定义导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 顶部区域按设计稿S 图标 + 标题副标题 | 点击链接卡若 -->
<!-- 顶部区域:中文标识 + 标题副标题 | 链接卡若 -->
<view class="header">
<view class="header-content">
<view class="logo-section">
<view class="logo-icon">
<text class="logo-text">S</text>
<text class="logo-text"></text>
</view>
<view class="logo-info">
<text class="logo-title-text">卡若创业派对</text>
@@ -19,7 +19,7 @@
<view class="header-right">
<view class="contact-btn" bindtap="onLinkKaruo">
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
<text class="contact-text">点击链接卡若</text>
<text class="contact-name">点击链接卡若</text>
</view>
</view>
</view>
@@ -35,13 +35,13 @@
<!-- 主内容区 -->
<view class="main-content">
<!-- Banner卡片 - 最新章节(异步加载 -->
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
<!-- Banner 推荐卡片(优先 recommended API 第一条 -->
<view class="banner-card" wx:if="{{bannerSection}}" bindtap="goToRead" data-id="{{bannerSection.id}}" data-mid="{{bannerSection.mid}}">
<view class="banner-glow"></view>
<view class="banner-tag">推荐</view>
<view class="banner-title">{{latestSection.title}}</view>
<view class="banner-title">{{bannerSection.title}}</view>
<view class="banner-action">
<text class="banner-action-text">点击阅读123</text>
<text class="banner-action-text">点击阅读</text>
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
</view>
</view>
@@ -53,10 +53,11 @@
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏 -->
<!-- 超级个体:与匹配页一致,仅 VIP 横向列表(无首位特例 -->
<view class="section" wx:if="{{!auditMode}}">
<view class="section-header">
<text class="section-title">超级个体</text>
<text class="section-subtitle">获客入口</text>
</view>
<!-- 加载中:骨架动画 -->
<view wx:if="{{superMembersLoading}}" class="super-loading">
@@ -76,12 +77,16 @@
wx:key="id"
bindtap="goToMemberDetail"
data-id="{{item.id}}"
hover-class="super-item-hover"
hover-stay-time="80"
>
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
<text class="super-avatar-text" wx:else>{{(item.name && item.name[0]) || '会'}}</text>
<view class="super-item-stack">
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
<text class="super-avatar-text" wx:else>{{item.avatarLetter}}</text>
</view>
<text class="super-name">{{item.name}}</text>
</view>
<text class="super-name">{{item.name}}</text>
</view>
</view>
</scroll-view>
@@ -92,14 +97,10 @@
</view>
</view>
<!-- 精选推荐(带 tag支持展开更多 -->
<!-- 精选推荐:默认 3 条,列表下三角展开更多(与最新新增一致 -->
<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>
<icon name="{{featuredExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
</view>
</view>
<view class="featured-list">
<view
@@ -111,30 +112,29 @@
data-mid="{{item.mid}}"
>
<view class="featured-content">
<view class="featured-meta">
<text class="featured-id brand-color">{{item.id}}</text>
<text class="featured-tag {{item.tagClass || 'tag-rec'}}" wx:if="{{item.tag}}">{{item.tag}}</text>
<view class="featured-meta" wx:if="{{item.tag}}">
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag}}</text>
</view>
<text class="featured-title">{{item.title}}</text>
</view>
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.6)" customClass="featured-arrow"></icon>
</view>
</view>
<view
class="latest-expand-hint"
wx:if="{{!featuredExpanded && featuredSectionsFull.length > 3}}"
bindtap="expandFeaturedChapters"
hover-class="latest-expand-hint-hover"
hover-stay-time="80"
>
<view class="latest-expand-triangle"></view>
</view>
</view>
<!-- 最新新增(时间线样式,支持展开更多 -->
<!-- 最新新增(时间线样式;超过 5 条时在列表下方点小三角展开,展开后随页面滚动看全部 -->
<view class="section" wx:if="{{latestChapters.length > 0}}">
<view class="section-header latest-header">
<text class="section-title">最新新增</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>
<icon name="{{latestExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
</view>
</view>
</view>
<view class="timeline-wrap">
<view class="timeline-line"></view>
@@ -149,6 +149,16 @@
</view>
</view>
</view>
<!-- 仅文案「展开更多」去掉:下方居中轻点小三角,点一次展开剩余条目 -->
<view
class="latest-expand-hint"
wx:if="{{latestChapters.length > 5 && !latestExpanded}}"
bindtap="expandLatestChapters"
hover-class="latest-expand-hint-hover"
hover-stay-time="80"
>
<view class="latest-expand-triangle"></view>
</view>
</view>
</view>

View File

@@ -63,26 +63,28 @@
.contact-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 8rpx 20rpx 8rpx 12rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 40rpx;
font-size: 24rpx;
font-weight: 500;
gap: 8rpx;
padding: 12rpx 16rpx 10rpx;
background: rgba(255, 255, 255, 0.06);
border: 1rpx solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
color: #ffffff;
}
.contact-avatar {
width: 48rpx;
height: 48rpx;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 2rpx solid rgba(0, 206, 209, 0.35);
}
.contact-text {
font-size: 24rpx;
.contact-name {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
}
.logo-title {
@@ -330,6 +332,12 @@
color: #ffffff;
}
.section-subtitle {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.42);
font-weight: 400;
}
.section-more {
display: flex;
align-items: center;
@@ -609,9 +617,18 @@
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
min-width: 140rpx;
}
.super-item-hover {
opacity: 0.88;
}
.super-item-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
width: 100%;
}
.super-scroll .super-avatar {
width: 112rpx;
@@ -692,6 +709,7 @@
.daily-badge-wrap {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
/* 设计稿 1:1橙底白字 rounded-full */
.daily-badge {
@@ -701,10 +719,29 @@
font-weight: 700;
padding: 8rpx 20rpx;
border-radius: 999rpx;
margin-left: 8rpx;
box-shadow: 0 4rpx 16rpx rgba(246, 173, 85, 0.3);
}
/* 列表下方:仅小三角,点击展开(替代标题栏「展开更多」) */
.latest-expand-hint {
display: flex;
justify-content: center;
align-items: center;
padding: 8rpx 0 16rpx;
margin-top: 8rpx;
}
.latest-expand-hint-hover {
opacity: 0.65;
}
/* 向下小三角 */
.latest-expand-triangle {
width: 0;
height: 0;
border-left: 16rpx solid transparent;
border-right: 16rpx solid transparent;
border-top: 20rpx solid rgba(0, 206, 209, 0.85);
}
/* 设计稿 1:1pl-3 竖线 left-3 top-2 bottom-2 w-[1px] bg-gray-800 */
.timeline-wrap {
position: relative;