feat: 运营-用户功能四大需求完整实现

1. 客资中心:Dashboard 聚合 CKB 线索+提交记录,联表用户信息
2. @置顶:Person 三端(后端+管理端+小程序)置顶功能,首页优先展示
3. 存客宝场景:一键检查并自动启用所有场景获客计划
4. 去重增强:后端聚合 dupCount,管理端展示重复标记和统计
5. 首页文案:"最新更新"→"推荐","开始阅读"→"点击阅读"

Made-with: Cursor
This commit is contained in:
卡若
2026-03-19 16:20:46 +08:00
parent 01d700aab2
commit 80e397f7ac
17 changed files with 1330 additions and 1130 deletions

View File

@@ -33,7 +33,7 @@ Page({
// 最新章节(动态计算)
latestSection: null,
latestLabel: '最新更新',
latestLabel: '推荐',
// 内容概览
partsList: [
@@ -135,29 +135,63 @@ Page({
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
// 并行请求 VIP 会员和普通用户,合并后取前 4 个VIP 优先)
const [vipRes, usersRes] = await Promise.all([
const [pinnedRes, vipRes, usersRes] = await Promise.all([
app.request({ url: '/api/miniprogram/persons/pinned', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/users?limit=20', 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, '人')
const usedIds = new Set()
// 1. 后台置顶人物优先(最多 4 个)
if (pinnedRes && pinnedRes.success && Array.isArray(pinnedRes.persons)) {
pinnedRes.persons.slice(0, 4).forEach(p => {
const id = p.userId || p.personId
members.push({
id,
personId: p.personId,
name: p.nickname || p.name || '置顶',
avatar: p.avatar || '',
isVip: true,
isPinned: true
})
usedIds.add(id)
})
}
// 2. VIP 会员补位
if (members.length < 4 && vipRes && vipRes.success && Array.isArray(vipRes.data)) {
vipRes.data.forEach(u => {
if (members.length >= 4) return
if (usedIds.has(u.id)) return
members.push({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员',
avatar: u.avatar || '',
isVip: true,
isPinned: false
})
usedIds.add(u.id)
})
}
// 3. 普通用户兜底
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)
usersRes.data
.filter(u => u.avatar && u.nickname && !usedIds.has(u.id))
.forEach(u => {
if (members.length >= 4) return
members.push({
id: u.id,
name: u.nickname,
avatar: u.avatar,
isVip: u.is_vip === 1,
isPinned: false
})
})
}
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '人 (置顶', members.filter(m => m.isPinned).length, '人)')
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)

View File

@@ -38,18 +38,18 @@
<!-- Banner卡片 - 最新章节(异步加载) -->
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
<view class="banner-glow"></view>
<view class="banner-tag">最新更新</view>
<view class="banner-tag">推荐</view>
<view class="banner-title">{{latestSection.title}}</view>
<view class="banner-action">
<text class="banner-action-text">开始阅读</text>
<text class="banner-action-text">点击阅读</text>
<icon name="chevron-right" size="32" color="#fff" customClass="banner-arrow"></icon>
</view>
</view>
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
<view class="banner-glow"></view>
<view class="banner-tag">最新更新</view>
<view class="banner-tag">推荐</view>
<view class="banner-title">加载中...</view>
<view class="banner-action"><text class="banner-action-text">开始阅读</text><icon name="chevron-right" size="32" color="#fff" customClass="banner-arrow"></icon></view>
<view class="banner-action"><text class="banner-action-text">点击阅读</text><icon name="chevron-right" size="32" color="#fff" customClass="banner-arrow"></icon></view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
@@ -70,15 +70,16 @@
<scroll-view wx:elif="{{superMembers.length > 0}}" class="super-scroll" scroll-x>
<view class="super-scroll-inner">
<view
class="super-item-h"
class="super-item-h {{item.isPinned ? 'super-item-pinned' : ''}}"
wx:for="{{superMembers}}"
wx:key="id"
bindtap="goToMemberDetail"
data-id="{{item.id}}"
>
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}} {{item.isPinned ? 'super-avatar-pinned' : ''}}">
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
<view class="pinned-badge" wx:if="{{item.isPinned}}">★</view>
</view>
<text class="super-name">{{item.name}}</text>
</view>

View File

@@ -634,10 +634,11 @@
gap: 10rpx;
}
.super-avatar {
position: relative;
width: 108rpx;
height: 108rpx;
border-radius: 50%;
overflow: hidden;
overflow: visible;
background: rgba(0,206,209,0.1);
display: flex;
align-items: center;
@@ -648,10 +649,33 @@
border: 3rpx solid #FFD700;
box-shadow: 0 0 12rpx rgba(255,215,0,0.3);
}
.super-avatar-pinned {
border: 3rpx solid #38bdac;
box-shadow: 0 0 16rpx rgba(56, 189, 172, 0.4);
}
.super-item-pinned .super-name {
color: #38bdac;
}
.pinned-badge {
position: absolute;
bottom: -4rpx;
right: -4rpx;
width: 28rpx;
height: 28rpx;
background: #38bdac;
border-radius: 50%;
font-size: 18rpx;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.super-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.super-avatar-text {
font-size: 40rpx;

File diff suppressed because it is too large Load Diff