feat: 数据概览简化 + 用户管理增加余额/提现列

- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额
- 数据概览:移除余额统计区块(余额改在用户管理中展示)
- 数据概览:恢复转化率卡片(唯一付费用户/总用户)
- 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额
- 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段
- 后端:User model 添加 WalletBalance 非数据库字段
- 包含之前的小程序埋点和管理后台点击统计面板

Made-with: Cursor
This commit is contained in:
卡若
2026-03-15 15:57:09 +08:00
parent 991e17698c
commit 708547d0dd
52 changed files with 3161 additions and 1103 deletions

View File

@@ -8,9 +8,9 @@ const { parseScene } = require('./utils/scene.js')
App({
globalData: {
// API基础地址 - 连接真实后端
// baseUrl: 'https://soulapi.quwanzhi.com',
baseUrl: 'https://soulapi.quwanzhi.com',
// baseUrl: 'https://souldev.quwanzhi.com',
baseUrl: 'http://localhost:8080',
// baseUrl: 'http://localhost:8080',
// 小程序配置 - 真实AppID

View File

@@ -21,7 +21,8 @@
"pages/mentors/mentors",
"pages/mentor-detail/mentor-detail",
"pages/profile-show/profile-show",
"pages/profile-edit/profile-edit"
"pages/profile-edit/profile-edit",
"pages/wallet/wallet"
],
"window": {
"backgroundTextStyle": "light",

View File

@@ -6,6 +6,7 @@
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -206,6 +207,7 @@ Page({
// 切换展开状态
togglePart(e) {
trackClick('chapters', 'tab_click', e.currentTarget.dataset.id || '篇章')
const partId = e.currentTarget.dataset.id
this.setData({
expandedPart: this.data.expandedPart === partId ? null : partId
@@ -216,6 +218,7 @@ Page({
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
trackClick('chapters', 'card_click', id || '章节')
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},
@@ -234,6 +237,7 @@ Page({
// 跳转到搜索页
goToSearch() {
trackClick('chapters', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},

View File

@@ -7,6 +7,8 @@
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
Page({
data: {
@@ -23,12 +25,10 @@ Page({
totalSections: 62,
bookData: [],
// 推荐章节
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: '真实的赚钱' }
],
// 精选推荐按热度排行默认显示3篇可展开更多
featuredSections: [],
featuredSectionsAll: [],
featuredExpanded: false,
// 最新章节(动态计算)
latestSection: null,
@@ -49,6 +49,8 @@ Page({
// 最新新增章节
latestChapters: [],
latestChaptersExpanded: false,
latestChaptersAll: [],
// 篇章数(从 bookData 计算)
partCount: 0,
@@ -106,6 +108,9 @@ Page({
// 更新用户状态
this.updateUserStatus()
// 规则引擎:首页展示时检查(填头像、分享引导等)
checkAndExecute('page_show', this)
},
initData() {
@@ -162,50 +167,32 @@ Page({
}
},
// 从服务端获取精选推荐、最新更新stitch_soulbook/recommended、book/latest-chapters
// 从服务端获取精选推荐(按热度排行)和最新更新
async loadFeaturedFromServer() {
try {
// 1. 精选推荐:优先用 book/recommended按阅读量+算法,带 热门/推荐/精选 标签)
let featured = []
// 1. 精选推荐: book/hot 获取热度排行数据
try {
const recRes = await app.request({ url: '/api/miniprogram/book/recommended', silent: true })
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => ({
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
if (hotRes && hotRes.success && Array.isArray(hotRes.data) && hotRes.data.length > 0) {
const tagClassMap = { '热门': 'tag-hot', '推荐': 'tag-rec', '精选': 'tag-rec' }
const all = hotRes.data.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: s.tag || ['热门', '推荐', '精选'][i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
tag: s.tag || '',
tagClass: tagClassMap[s.tag] || 'tag-rec',
hotScore: s.hotScore || s.hot_score || 0,
hotRank: s.hotRank || (i + 1),
price: s.price ?? 1,
}))
this.setData({ featuredSections: featured })
this.setData({
featuredSectionsAll: all,
featuredSections: all.slice(0, 3),
featuredExpanded: false,
})
}
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
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) || []
const valid = chapters.filter(c => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (valid.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = valid
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
.slice(0, 3)
.map((s, i) => ({
id: s.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: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
}
} catch (e) { console.log('[Index] book/hot 失败:', e) }
// 2. 最新更新:用 book/latest-chapters 取第1条排除「序言」「尾声」「附录」
try {
@@ -227,7 +214,6 @@ Page({
})
}
} catch (e) {
// 兜底:从 all-chapters 取
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
@@ -282,17 +268,20 @@ Page({
// 跳转到目录
goToChapters() {
trackClick('home', 'nav_click', '目录')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到搜索页
goToSearch() {
trackClick('home', 'nav_click', '搜索')
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页(优先传 mid与分享逻辑一致
goToRead(e) {
const id = e.currentTarget.dataset.id
trackClick('home', 'card_click', e.currentTarget.dataset.id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
@@ -304,6 +293,7 @@ Page({
},
goToVip() {
trackClick('home', 'btn_click', 'VIP')
wx.navigateTo({ url: '/pages/vip/vip' })
},
@@ -312,6 +302,7 @@ Page({
},
async onLinkKaruo() {
trackClick('home', 'btn_click', '链接卡若')
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
@@ -472,6 +463,7 @@ Page({
},
async submitLead() {
trackClick('home', 'btn_click', '提交留资')
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
@@ -490,12 +482,10 @@ Page({
const chapters = (res && res.data) || (res && res.chapters) || []
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+)场/)
@@ -504,37 +494,57 @@ Page({
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)
.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,
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
this.setData({ latestChapters: latest })
const mapChapter = (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()
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,
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
}
const sorted = 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)
})
const latestAll = sorted.slice(0, 10).map(mapChapter)
this.setData({
latestChaptersAll: latestAll,
latestChapters: latestAll.slice(0, 5),
latestChaptersExpanded: false,
})
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},
toggleLatestExpand() {
const all = this.data.latestChaptersAll || []
if (this.data.latestChaptersExpanded) {
this.setData({ latestChapters: all.slice(0, 5), latestChaptersExpanded: false })
} else {
this.setData({ latestChapters: all, latestChaptersExpanded: true })
}
},
toggleFeaturedExpand() {
const all = this.data.featuredSectionsAll || []
if (this.data.featuredExpanded) {
this.setData({ featuredSections: all.slice(0, 3), featuredExpanded: false })
} else {
this.setData({ featuredSections: all, featuredExpanded: true })
}
},
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })

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>
<view class="banner-arrow">→</view>
</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><view class="banner-arrow">→</view></view>
<view class="banner-action"><text class="banner-action-text">点击阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
@@ -91,8 +91,8 @@
</view>
</view>
<!-- 精选推荐(带 tag已去掉「查看全部」 -->
<view class="section">
<!-- 精选推荐(按热度排行默认3篇可展开更多 -->
<view class="section" wx:if="{{featuredSections.length > 0}}">
<view class="section-header">
<text class="section-title">精选推荐</text>
</view>
@@ -107,22 +107,25 @@
>
<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'}}">{{item.tag || '精选'}}</text>
<text class="featured-tag {{item.tagClass || 'tag-rec'}}" wx:if="{{item.tag}}">{{item.tag}}</text>
</view>
<text class="featured-title">{{item.title}}</text>
</view>
<view class="featured-arrow"></view>
</view>
</view>
<view class="expand-btn" bindtap="toggleFeaturedExpand" wx:if="{{(featuredSectionsAll.length || 0) > 3}}">
<text class="expand-text">{{featuredExpanded ? '收起' : '展开更多'}}</text>
<text class="expand-icon">{{featuredExpanded ? '∧' : ''}}</text>
</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>
<text class="daily-badge">+{{latestChaptersAll.length || latestChapters.length}}</text>
</view>
</view>
<view class="timeline-wrap">
@@ -132,19 +135,19 @@
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-row">
<view class="timeline-left">
<text class="latest-new-tag">NEW</text>
<text class="timeline-title">{{item.title}}</text>
</view>
<view class="timeline-right">
<text class="timeline-price">¥{{item.price}}</text>
</view>
<text class="timeline-title">{{item.title}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 展开/收起按钮 -->
<view class="expand-btn" bindtap="toggleLatestExpand" wx:if="{{(latestChaptersAll.length || 0) > 5}}">
<text class="expand-text">{{latestChaptersExpanded ? '收起' : '展开更多'}}</text>
<text class="expand-icon">{{latestChaptersExpanded ? '∧' : ''}}</text>
</view>
</view>
</view>
<!-- 底部留白 -->

View File

@@ -445,6 +445,7 @@
font-size: 32rpx;
color: rgba(255, 255, 255, 0.3);
margin-top: 8rpx;
flex-shrink: 0;
}
/* ===== 内容概览列表 ===== */
@@ -838,6 +839,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;

View File

@@ -5,6 +5,7 @@
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
// 默认匹配类型配置
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
@@ -103,9 +104,7 @@ Page({
// 加载匹配配置
async loadMatchConfig() {
try {
const res = await app.request({ url: '/api/miniprogram/match/config', silent: true, method: 'GET',
method: 'GET'
})
const res = await app.request({ url: '/api/miniprogram/match/config', silent: true, method: 'GET' })
if (res.success && res.data) {
// 更新全局配置,导师顾问类型强制显示「导师顾问」
@@ -196,6 +195,7 @@ Page({
// 选择匹配类型
selectType(e) {
trackClick('match', 'tab_click', e.currentTarget.dataset.type || '类型选择')
const typeId = e.currentTarget.dataset.type
const type = MATCH_TYPES.find(t => t.id === typeId)
this.setData({
@@ -206,6 +206,7 @@ Page({
// 点击匹配按钮
async handleMatchClick() {
trackClick('match', 'btn_click', '开始匹配')
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
// 导师顾问:先播匹配动画,动画完成后再跳转(不在此处直接跳)
@@ -363,7 +364,7 @@ Page({
}, 500)
// 1.5-3秒后导师顾问→跳转其他类型→弹窗
const delay = Math.random() * 1500 + 1500
const delay = Math.random() * 7000 + 3000
setTimeout(() => {
clearInterval(timer)
this.setData({ isMatching: false })
@@ -412,14 +413,15 @@ Page({
// 从数据库获取真实用户匹配
let matchedUser = null
try {
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
const res = await app.request({
url: '/api/miniprogram/match/users',
silent: true,
method: 'POST',
data: {
matchType: this.data.selectedType,
userId: app.globalData.userInfo?.id || ''
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
@@ -429,8 +431,8 @@ Page({
}
// 延迟显示结果(模拟匹配过程)
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
const delay = Math.random() * 7000 + 3000
const timeoutId = setTimeout(() => {
clearInterval(timer)
// 如果没有匹配到用户,提示用户
@@ -460,7 +462,6 @@ Page({
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
}, delay)
},
@@ -528,6 +529,7 @@ Page({
// 添加微信好友
handleAddWechat() {
trackClick('match', 'btn_click', '加好友')
if (!this.data.currentMatch) return
wx.setClipboardData({
@@ -578,6 +580,7 @@ Page({
// 提交加入
async handleJoinSubmit() {
trackClick('match', 'btn_click', '加入提交')
const { contactType, phoneNumber, wechatId, joinType, isJoining, canHelp, needHelp } = this.data
if (isJoining) return
@@ -668,6 +671,7 @@ Page({
// 购买匹配次数
async buyMatchCount() {
trackClick('match', 'btn_click', '购买次数')
this.setData({ showUnlockModal: false })
try {
@@ -744,11 +748,6 @@ Page({
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 打开设置
openSettings() {
wx.navigateTo({ url: '/pages/settings/settings' })
},
// 阻止事件冒泡
preventBubble() {},

View File

@@ -4,9 +4,7 @@
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-settings" bindtap="openSettings">
<text class="settings-icon">⚙️</text>
</view>
<view class="nav-left-placeholder"></view>
<text class="nav-title">找伙伴</text>
<view class="nav-right-placeholder"></view>
</view>

View File

@@ -27,15 +27,9 @@
padding: 0 32rpx;
}
.nav-settings {
.nav-left-placeholder {
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
border-radius: 50%;
background: #1c1c1e;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
@@ -51,10 +45,6 @@
flex-shrink: 0;
}
.settings-icon {
font-size: 36rpx;
}
.nav-placeholder {
width: 100%;
}

View File

@@ -5,6 +5,8 @@
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
Page({
data: {
@@ -69,6 +71,9 @@ Page({
contactWechat: '',
contactSaving: false,
pendingWithdraw: false,
// 我的余额wallet 页入口展示)
walletBalance: 0,
},
onLoad() {
@@ -79,6 +84,8 @@ Page({
})
this.loadFeatureConfig()
this.initUserStatus()
// 规则引擎:登录后检查(填头像等)
checkAndExecute('after_login', this)
},
onShow() {
@@ -134,6 +141,7 @@ Page({
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
this.loadWalletBalance()
} else {
this.setData({
isLoggedIn: false,
@@ -630,6 +638,7 @@ Page({
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
showLogin() {
trackClick('my', 'btn_click', '登录')
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
try {
const sys = wx.getSystemInfoSync()
@@ -677,6 +686,7 @@ Page({
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
async handleWechatLogin() {
trackClick('my', 'btn_click', '微信登录')
if (!this.data.agreeProtocol) {
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
@@ -729,6 +739,7 @@ Page({
// 点击菜单
handleMenuTap(e) {
trackClick('my', 'btn_click', e.currentTarget.dataset.id || '菜单')
const id = e.currentTarget.dataset.id
if (!this.data.isLoggedIn && id !== 'about') {
@@ -737,6 +748,7 @@ Page({
}
const routes = {
wallet: '/pages/wallet/wallet',
orders: '/pages/purchases/purchases',
referral: '/pages/referral/referral',
withdrawRecords: '/pages/withdraw-records/withdraw-records',
@@ -759,6 +771,7 @@ Page({
// 跳转到目录
goToChapters() {
trackClick('my', 'nav_click', '目录')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
@@ -769,11 +782,13 @@ Page({
// 跳转到匹配
goToMatch() {
trackClick('my', 'nav_click', '匹配')
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到推广中心(需登录)
goToReferral() {
trackClick('my', 'nav_click', '推广')
if (!this.data.isLoggedIn) {
this.showLogin()
return
@@ -783,6 +798,7 @@ Page({
// 跳转到找伙伴
goToMatch() {
trackClick('my', 'nav_click', '匹配')
wx.switchTab({ url: '/pages/match/match' })
},
@@ -801,6 +817,17 @@ Page({
})
},
async loadWalletBalance() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
if (res && res.data) {
this.setData({ walletBalance: (res.data.balance || 0).toFixed(2) })
}
} catch (e) {}
},
// VIP状态查询注意hasFullBook=9.9 买断,不等同 VIP
async loadVipStatus() {
const userId = app.globalData.userInfo?.id
@@ -881,12 +908,14 @@ Page({
},
goToVip() {
trackClick('my', 'nav_click', 'VIP')
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/vip/vip' })
},
// 进入个人资料编辑页stitch_soul
goToProfileEdit() {
trackClick('my', 'nav_click', '设置')
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},

View File

@@ -57,6 +57,10 @@
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
<text class="profile-stat-label">我的收益</text>
</view>
<view class="profile-stat" bindtap="handleMenuTap" data-id="wallet">
<text class="profile-stat-val">{{walletBalance > 0 ? '¥' + walletBalance : '0'}}</text>
<text class="profile-stat-label">我的余额</text>
</view>
</view>
</view>
</view>

View File

@@ -178,6 +178,10 @@
.icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; }
.icon-gray { background: rgba(156,163,175,0.15); }
.icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; }
.icon-amber { background: rgba(245,158,11,0.2); }
.menu-icon-emoji { font-size: 28rpx; }
.menu-right { display: flex; align-items: center; gap: 12rpx; }
.menu-balance { font-size: 26rpx; color: #4FD1C5; font-weight: 500; }
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }

View File

@@ -18,6 +18,8 @@ import readingTracker from '../../utils/readingTracker'
const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
const app = getApp()
Page({
@@ -181,6 +183,21 @@ Page({
// 5. 加载导航
this.loadNavigation(id)
// 6. 规则引擎:阅读前检查(填头像、绑手机等)
checkAndExecute('before_read', this)
// 7. 记录浏览行为到 user_tracks
const userId = app.globalData.userInfo?.id
if (userId) {
app.request('/api/miniprogram/track', {
method: 'POST',
data: { userId, action: 'view_chapter', target: id, extraData: { sectionId: id, mid: mid || '' } },
silent: true
}).catch(() => {})
// 更新全局阅读计数
app.globalData.readCount = (app.globalData.readCount || 0) + 1
}
} catch (e) {
console.error('[Read] 初始化失败:', e)
wx.showToast({ title: '加载失败,请重试', icon: 'none' })
@@ -783,6 +800,7 @@ Page({
// 分享到微信 - 自动带分享人ID优先用 mid扫码/海报闭环),无则用 id
onShareAppMessage() {
trackClick('read', 'btn_click', '分享_' + this.data.sectionId)
const { section, sectionId, sectionMid } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
@@ -808,11 +826,20 @@ Page({
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const articleTitle = (section?.title || chapterTitle || '').trim()
const title = articleTitle
? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle)
: 'Soul创业派对 - 真实商业故事'
? `📚 ${articleTitle.length > 24 ? articleTitle.slice(0, 24) + '...' : articleTitle}Soul创业派对`
: '📚 Soul创业派对 - 真实商业故事'
return { title, query: ref ? `${q}&ref=${ref}` : q }
},
shareToMoments() {
wx.showModal({
title: '分享到朋友圈',
content: '点击右上角「···」菜单,选择「分享到朋友圈」即可。\n\n朋友圈分享文案已自动生成。',
showCancel: false,
confirmText: '知道了',
})
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
showLoginModal() {
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
@@ -939,6 +966,7 @@ Page({
// 购买章节 - 直接调起支付
async handlePurchaseSection() {
trackClick('read', 'btn_click', '购买章节_' + this.data.sectionId)
console.log('[Pay] 点击购买章节按钮')
wx.showLoading({ title: '处理中...', mask: true })
@@ -1297,6 +1325,11 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
showPosterModal() {
this.setData({ showPosterModal: true })
this.generatePoster()
},
// 生成海报
async generatePoster() {
wx.showLoading({ title: '生成中...' })
@@ -1466,7 +1499,7 @@ Page({
this.setData({ showShareTip: false })
},
// 代付分享:余额帮好友解锁当前章节
// 代付分享:微信支付或余额帮好友解锁当前章节
async handleGiftPay() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({ title: '提示', content: '请先登录', confirmText: '去登录', success: (r) => { if (r.confirm) this.showLoginModal() } })
@@ -1476,46 +1509,101 @@ Page({
const userId = app.globalData.userInfo.id
const price = (this.data.section && this.data.section.price != null) ? this.data.section.price : (this.data.sectionPrice || 1)
const balRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }).catch(() => null)
const balance = (balRes && balRes.data) ? balRes.data.balance : 0
wx.showModal({
title: '代付分享',
content: `为好友代付本章 ¥${price}\n当前余额: ¥${balance.toFixed(2)}\n${balance < price ? '余额不足,请先充值' : '确认后将从余额扣除'}`,
confirmText: balance >= price ? '确认代付' : '去充值',
content: `为好友代付本章 ¥${price}\n\n支付后将生成代付链接,好友点击即可免费阅读`,
confirmText: '微信支付',
cancelText: '用余额',
success: async (res) => {
if (!res.confirm) return
if (balance < price) {
wx.navigateTo({ url: '/pages/wallet/wallet' })
return
}
wx.showLoading({ title: '处理中...' })
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
wx.hideLoading()
if (giftRes && giftRes.data && giftRes.data.giftCode) {
const giftCode = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已为好友代付 ¥${price},分享链接后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => {
if (r.confirm) {
this._giftCodeToShare = giftCode
wx.shareAppMessage()
}
if (!res.confirm && !res.cancel) return
if (res.confirm) {
// Direct WeChat Pay
wx.showLoading({ title: '创建订单...' })
try {
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: app.globalData.openId,
productType: 'gift',
productId: sectionId,
amount: price,
description: `代付解锁:${this.data.section?.title || sectionId}`,
userId: userId,
}
})
} else {
wx.showToast({ title: (giftRes && giftRes.error) || '代付失败', icon: 'none' })
wx.hideLoading()
if (payRes && payRes.payParams) {
wx.requestPayment({
...payRes.payParams,
success: async () => {
// After payment, create gift code via balance gift API
// First confirm recharge to add to balance, then deduct for gift
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已为好友代付 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
}
} catch (e) {
wx.showToast({ title: '生成分享链接失败', icon: 'none' })
}
},
fail: () => { wx.showToast({ title: '支付取消', icon: 'none' }) }
})
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
// Use balance (existing flow)
const balRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }).catch(() => null)
const balance = (balRes && balRes.data) ? balRes.data.balance : 0
if (balance < price) {
wx.showModal({
title: '余额不足',
content: `当前余额 ¥${balance.toFixed(2)},需要 ¥${price}\n请先充值`,
confirmText: '去充值',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/wallet/wallet' }) }
})
return
}
wx.showLoading({ title: '处理中...' })
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
wx.hideLoading()
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已从余额扣除 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '代付失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '代付失败', icon: 'none' })
}
}
})

View File

@@ -93,7 +93,7 @@
<text class="action-icon-small">🎁</text>
<text class="action-text-small">代付分享</text>
</view>
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
<view class="action-btn-inline btn-poster-inline" bindtap="showPosterModal">
<text class="action-icon-small">🖼️</text>
<text class="action-text-small">生成海报</text>
</view>
@@ -312,8 +312,8 @@
</view>
</view>
<!-- 右下角悬浮分享按钮 -->
<button class="fab-share" open-type="share">
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
</button>
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
<view class="fab-share" bindtap="shareToMoments">
<text class="fab-moments-icon">🌐</text>
</view>
</view>

View File

@@ -454,10 +454,16 @@
}
.btn-poster-inline {
background: rgba(255, 215, 0, 0.15);
border: 2rpx solid rgba(255, 215, 0, 0.3);
background: linear-gradient(135deg, #2d2d30 0%, #3d3d40 100%);
}
.btn-moments-inline {
background: linear-gradient(135deg, #1a4a2e, #0d3320);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.btn-moments-inline:active {
opacity: 0.7;
}
.action-icon-small {
font-size: 28rpx;
@@ -1003,6 +1009,10 @@
display: block;
}
.fab-moments-icon {
font-size: 48rpx;
}
/* ===== 分享提示文字(底部导航上方) ===== */
.share-tip-inline {
text-align: center;

View File

@@ -8,6 +8,7 @@
* - 收益统计90%归分发者)
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -257,6 +258,7 @@ Page({
// 切换Tab
switchTab(e) {
trackClick('referral', 'tab_click', e.currentTarget.dataset.tab || 'tab')
const tab = e.currentTarget.dataset.tab
let currentBindings = []
@@ -278,6 +280,7 @@ Page({
// 复制邀请链接
copyLink() {
trackClick('referral', 'btn_click', '复制链接')
const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
wx.setClipboardData({
data: link,
@@ -287,6 +290,7 @@ Page({
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
shareToWechat() {
trackClick('referral', 'btn_click', '分享朋友圈文案')
const { referralCode } = this.data
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
@@ -314,6 +318,7 @@ Page({
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
handleMoreShare() {
trackClick('referral', 'btn_click', '更多分享')
const { referralCode } = this.data
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
@@ -334,6 +339,7 @@ Page({
// 生成推广海报 - 1:1 对齐 Next.js 设计
async generatePoster() {
trackClick('referral', 'btn_click', '生成海报')
wx.showLoading({ title: '生成中...', mask: true })
this.setData({ showPosterModal: true, isGeneratingPoster: true })
@@ -524,6 +530,7 @@ Page({
// 保存海报
savePoster() {
trackClick('referral', 'btn_click', '保存海报')
const { posterQrSrc } = this.data
if (!posterQrSrc) {
wx.showToast({ title: '二维码未生成', icon: 'none' })
@@ -624,6 +631,7 @@ Page({
// 提现 - 直接到微信零钱
async handleWithdraw() {
trackClick('referral', 'btn_click', '提现')
const availableEarnings = this.data.availableEarningsNum || 0
const minWithdrawAmount = this.data.minWithdrawAmount || 10
const hasWechatId = this.data.hasWechatId
@@ -670,6 +678,7 @@ Page({
// 跳转提现记录页
goToWithdrawRecords() {
trackClick('referral', 'btn_click', '提现记录')
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
},

View File

@@ -3,6 +3,7 @@
* 搜索章节标题和内容
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -83,6 +84,7 @@ Page({
return
}
trackClick('search', 'btn_click', keyword.trim())
this.setData({ loading: true, searched: true })
try {
@@ -109,6 +111,7 @@ Page({
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
trackClick('search', 'card_click', id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},

View File

@@ -1,5 +1,6 @@
import accessManager from '../../utils/chapterAccessManager'
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -11,16 +12,16 @@ Page({
originalPrice: 6980,
/* 按 premium_membership_landing_v1 设计稿 */
contentRights: [
{ title: '解锁全部章节', desc: '365天全案精读', icon: '📖' },
{ title: '案例库', desc: '100+创业实战案例', icon: '📚' },
{ title: '智能纪要', desc: 'AI每日精华推送', icon: '💡' },
{ title: '会议纪要库', desc: '往期完整沉淀', icon: '📁' }
{ title: '解锁章节', desc: '全部章节365天读', icon: '📖' },
{ title: '创业项目', desc: '查看最新创业项目', icon: '📚' },
{ title: '每日纪要', desc: '专属团队每日总结', icon: '💡' },
{ title: '文内链接', desc: '文章提到你可被链接', icon: '📁' }
],
socialRights: [
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: '👥' },
{ title: '创业老板排行', desc: '项目曝光展示', icon: '📊' },
{ title: '链接资源', desc: '深度私域资源池', icon: '🔗' },
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: '✓' }
{ title: '匹配伙伴', desc: '1980次创业伙伴匹配', icon: '👥' },
{ title: '获得客资', desc: '加入创业伙伴获客资', icon: '🔗' },
{ title: '老板排行', desc: '项目曝光超级个体', icon: '📊' },
{ title: 'VIP标识', desc: '金色尊享光圈特权', icon: '✓' }
],
purchasing: false
},
@@ -64,6 +65,7 @@ Page({
},
async handlePurchase() {
trackClick('vip', 'btn_click', '购买VIP')
let userId = app.globalData.userInfo?.id
let openId = app.globalData.openId || app.globalData.userInfo?.open_id
if (!userId || !openId) {

View File

@@ -4,23 +4,22 @@
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">卡若创业派对</text>
<text class="nav-title">卡若创业派对VIP会员</text>
<view class="nav-placeholder-r"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 会员宣传区(已去掉 VIP PREMIUM 标签) -->
<view class="vip-hero {{isVip ? 'vip-hero-active' : ''}}">
<view class="vip-hero-title">加入卡若</view>
<view class="vip-hero-title gold">创业派对 会员</view>
<view class="vip-hero-title">加入卡若</view>
<view class="vip-hero-title gold">创业派对 VIP会员</view>
<text class="vip-hero-sub" wx:if="{{isVip}}">有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天)</text>
<text class="vip-hero-sub" wx:else>一次加入 尊享终身陪伴与成长</text>
</view>
<!-- 双列权益:内容权益 + 社交权益 -->
<view class="rights-grid">
<view class="rights-col">
<view class="rights-col-header">
<text class="rights-dot rights-dot-teal"></text>
<text class="rights-col-title">内容权益</text>
<text class="rights-col-title">会员权利</text>
</view>
<view class="benefit-card" wx:for="{{contentRights}}" wx:key="title">
<text class="benefit-icon">{{item.icon || '✓'}}</text>
@@ -33,7 +32,7 @@
<view class="rights-col">
<view class="rights-col-header">
<text class="rights-dot rights-dot-gold"></text>
<text class="rights-col-title rights-col-title-gold">社交权益</text>
<text class="rights-col-title rights-col-title-gold">派对权利</text>
</view>
<view class="benefit-card" wx:for="{{socialRights}}" wx:key="title">
<text class="benefit-icon benefit-icon-gold">{{item.icon || '✓'}}</text>
@@ -47,7 +46,7 @@
<!-- 底部固定购买按钮(非 VIP 时显示,用 view 避让 button 默认 margin -->
<view class="buy-footer" wx:if="{{!isVip}}">
<view class="buy-btn-fixed {{purchasing ? 'buy-btn-disabled' : ''}}" bindtap="handlePurchase">
{{purchasing ? "处理中..." : "¥" + price + "/年 加入创业派对"}}
{{purchasing ? "处理中..." : "立即支付" + price + " 加入创业派对"}}
</view>
</view>

View File

@@ -0,0 +1,167 @@
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
statusBarHeight: 0,
balance: 0,
balanceText: '0.00',
totalRecharged: '0.00',
totalGifted: '0.00',
totalRefunded: '0.00',
transactions: [],
loading: true,
rechargeAmounts: [10, 30, 50, 1000],
selectedAmount: 30,
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
this.loadBalance()
this.loadTransactions()
},
async loadBalance() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
if (res && res.data) {
this.setData({
balance: res.data.balance || 0,
balanceText: (res.data.balance || 0).toFixed(2),
totalRecharged: (res.data.totalRecharged || 0).toFixed(2),
totalGifted: (res.data.totalGifted || 0).toFixed(2),
totalRefunded: (res.data.totalRefunded || 0).toFixed(2),
loading: false,
})
}
} catch (e) {
this.setData({ loading: false })
}
},
async loadTransactions() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance/transactions?userId=${userId}`, silent: true })
if (res && res.data) {
const list = (res.data || []).map(t => ({
...t,
amountText: (t.amount || 0).toFixed(2),
amountSign: (t.amount || 0) >= 0 ? '+' : '',
description: t.description || (t.type === 'recharge' ? '充值' : t.type === 'gift' ? '赠送' : t.type === 'refund' ? '退款' : t.type === 'consume' ? '阅读消费' : '其他'),
}))
this.setData({ transactions: list })
}
} catch (e) {
console.warn('[Wallet] load transactions failed', e)
}
},
selectAmount(e) {
trackClick('wallet', 'tab_click', '选择金额' + (e.currentTarget.dataset.amount || ''))
this.setData({ selectedAmount: parseInt(e.currentTarget.dataset.amount) })
},
async handleRecharge() {
trackClick('wallet', 'btn_click', '充值')
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const userId = app.globalData.userInfo.id
const amount = this.data.selectedAmount
wx.showLoading({ title: '创建订单...' })
try {
const res = await app.request({
url: '/api/miniprogram/balance/recharge',
method: 'POST',
data: { userId, amount }
})
wx.hideLoading()
if (res && res.data && res.data.orderSn) {
// Trigger WeChat Pay for the recharge order
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: app.globalData.openId,
productType: 'balance_recharge',
productId: res.data.orderSn,
amount: amount,
description: `余额充值 ¥${amount}`,
userId: userId,
}
})
if (payRes && payRes.payParams) {
wx.requestPayment({
...payRes.payParams,
success: async () => {
// Confirm the recharge
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
},
fail: () => {
wx.showToast({ title: '支付取消', icon: 'none' })
}
})
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '充值失败', icon: 'none' })
}
},
async handleRefund() {
trackClick('wallet', 'btn_click', '退款')
if (this.data.balance <= 0) {
wx.showToast({ title: '余额为零', icon: 'none' })
return
}
const userId = app.globalData.userInfo.id
const balance = this.data.balance
const refundAmount = (balance * 0.9).toFixed(2)
wx.showModal({
title: '余额退款',
content: `退回全部余额 ¥${balance.toFixed(2)}\n实际到账 ¥${refundAmount}9折\n\n退款将在1-3个工作日内原路返回`,
confirmText: '确认退款',
success: async (res) => {
if (!res.confirm) return
wx.showLoading({ title: '处理中...' })
try {
const result = await app.request({
url: '/api/miniprogram/balance/refund',
method: 'POST',
data: { userId, amount: balance }
})
wx.hideLoading()
if (result && result.data) {
wx.showToast({ title: result.data.message || '退款成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '退款失败', icon: 'none' })
}
}
})
},
goBack() {
wx.navigateBack()
},
})

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "我的余额",
"navigationStyle": "custom"
}

View File

@@ -0,0 +1,88 @@
<!-- Soul创业派对 - 我的余额 -->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">我的余额</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-placeholder-block" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 余额卡片 -->
<view class="balance-card">
<view class="balance-main" wx:if="{{!loading}}">
<text class="balance-label">当前余额</text>
<text class="balance-value">¥{{balanceText}}</text>
<text class="balance-tip">充值后可直接用于解锁付费内容,消费记录会展示在下方。</text>
</view>
<view class="balance-skeleton" wx:else>
<text class="skeleton-text">加载中...</text>
</view>
</view>
<!-- 充值金额选择 -->
<view class="section">
<view class="section-head">
<text class="section-title">选择充值金额</text>
<text class="section-note">当前已选 ¥{{selectedAmount}}</text>
</view>
<view class="amount-grid">
<view
class="amount-card {{selectedAmount === item ? 'amount-card-active' : ''}}"
wx:for="{{rechargeAmounts}}"
wx:key="*this"
bindtap="selectAmount"
data-amount="{{item}}"
>
<view class="amount-card-top">
<text class="amount-card-value">¥{{item}}</text>
<view class="amount-card-check {{selectedAmount === item ? 'amount-card-check-active' : ''}}">
<view class="amount-card-check-dot" wx:if="{{selectedAmount === item}}"></view>
</view>
</view>
<text class="amount-card-desc">{{selectedAmount === item ? '已选中,点击充值' : '点击选择此金额'}}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-row">
<view class="btn btn-recharge" bindtap="handleRecharge">充值</view>
<view class="btn btn-refund" bindtap="handleRefund">退款(9折)</view>
</view>
<!-- 充值与消费记录 -->
<view class="section">
<view class="section-head">
<text class="section-title">充值/消费记录</text>
<text class="section-note">按时间倒序显示</text>
</view>
<view class="transactions" wx:if="{{transactions.length > 0}}">
<view
class="tx-item"
wx:for="{{transactions}}"
wx:key="id"
>
<view class="tx-icon {{item.type}}">
<text wx:if="{{item.type === 'recharge'}}">💰</text>
<text wx:elif="{{item.type === 'gift'}}">🎁</text>
<text wx:elif="{{item.type === 'refund'}}">↩️</text>
<text wx:elif="{{item.type === 'consume'}}">📖</text>
<text wx:else>•</text>
</view>
<view class="tx-info">
<text class="tx-desc">{{item.description}}</text>
<text class="tx-time">{{item.createdAt || item.created_at || '--'}}</text>
</view>
<text class="tx-amount {{item.amount >= 0 ? 'tx-amount-plus' : 'tx-amount-minus'}}">{{item.amountSign}}¥{{item.amountText}}</text>
</view>
</view>
<view class="tx-empty" wx:else>
<text>暂无充值或消费记录</text>
</view>
</view>
<view class="bottom-space"></view>
</view>

View File

@@ -0,0 +1,267 @@
/* Soul创业派对 - 我的余额 - 深色主题 */
.page {
min-height: 100vh;
background: #0a0a0a;
padding-bottom: 64rpx;
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(40rpx);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
height: 88rpx;
}
.nav-back {
width: 64rpx;
height: 64rpx;
background: #1c1c1e;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 40rpx;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.nav-placeholder {
width: 64rpx;
}
.nav-placeholder-block {
width: 100%;
}
/* 余额卡片 - 渐变背景 */
.balance-card {
margin: 24rpx 24rpx 32rpx;
background: linear-gradient(135deg, #1c1c1e 0%, rgba(56, 189, 172, 0.15) 100%);
border-radius: 32rpx;
padding: 40rpx 32rpx;
border: 2rpx solid rgba(56, 189, 172, 0.2);
}
.balance-main {
min-height: 220rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.balance-label {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8rpx;
}
.balance-value {
font-size: 72rpx;
font-weight: 700;
color: #38bdac;
letter-spacing: 2rpx;
}
.balance-tip {
margin-top: 18rpx;
font-size: 24rpx;
line-height: 1.7;
color: rgba(255, 255, 255, 0.58);
}
.balance-skeleton {
padding: 40rpx 0;
}
.skeleton-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
}
/* 区块标题 */
.section {
margin: 0 24rpx 32rpx;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.section-note {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.45);
}
/* 金额选择卡片 */
.amount-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20rpx;
}
.amount-card {
padding: 26rpx 24rpx;
background: linear-gradient(180deg, #19191b 0%, #151517 100%);
border-radius: 28rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
}
.amount-card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.amount-card-value {
font-size: 40rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
}
.amount-card-desc {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.46);
}
.amount-card-check {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.amount-card-check-active {
border-color: #38bdac;
background: rgba(56, 189, 172, 0.18);
}
.amount-card-check-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #38bdac;
}
.amount-card-active {
background: linear-gradient(180deg, rgba(56, 189, 172, 0.22) 0%, rgba(15, 30, 29, 0.95) 100%);
border-color: rgba(56, 189, 172, 0.95);
box-shadow: 0 0 0 2rpx rgba(56, 189, 172, 0.1);
}
.amount-card-active .amount-card-value {
color: #52d8c7;
}
.amount-card-active .amount-card-desc {
color: rgba(213, 255, 250, 0.72);
}
/* 操作按钮 */
.action-row {
display: flex;
gap: 24rpx;
margin: 0 24rpx 40rpx;
}
.btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
}
.btn-recharge {
background: #38bdac;
color: #0a0a0a;
}
.btn-refund {
background: #1c1c1e;
color: rgba(255, 255, 255, 0.9);
border: 2rpx solid rgba(56, 189, 172, 0.4);
}
/* 交易记录 */
.transactions {
background: #1c1c1e;
border-radius: 24rpx;
overflow: hidden;
border: 2rpx solid rgba(255, 255, 255, 0.04);
}
.tx-item {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
}
.tx-item:last-child {
border-bottom: none;
}
.tx-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 24rpx;
}
.tx-icon.recharge {
background: rgba(56, 189, 172, 0.2);
}
.tx-icon.gift {
background: rgba(255, 215, 0, 0.15);
}
.tx-icon.refund {
background: rgba(255, 255, 255, 0.1);
}
.tx-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.tx-desc {
font-size: 28rpx;
color: #fff;
}
.tx-time {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.45);
}
.tx-amount {
font-size: 30rpx;
font-weight: 600;
}
.tx-amount-plus {
color: #38bdac;
}
.tx-amount-minus {
color: rgba(255, 255, 255, 0.6);
}
.tx-empty {
padding: 60rpx;
text-align: center;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
background: #1c1c1e;
border-radius: 24rpx;
}
.bottom-space {
height: 48rpx;
}

View File

@@ -0,0 +1,179 @@
/**
* Soul创业派对 - 用户旅程规则引擎
* 在关键页面自动检查用户状态,引导完善信息
*
* 规则逻辑:
* 1. 注册后 → 引导填写头像和昵称
* 2. 浏览付费章节 → 引导绑定手机号
* 3. 完成匹配 → 引导填写 MBTI / 行业等
* 4. 购买全书 → 引导填写完整信息(进 VIP 群)
* 5. 累计浏览 5 章 → 引导分享推广
*/
const app = getApp()
const RULE_COOLDOWN_KEY = 'rule_engine_cooldown'
const COOLDOWN_MS = 60 * 1000
function isInCooldown(ruleId) {
try {
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
const ts = map[ruleId]
if (!ts) return false
return Date.now() - ts < COOLDOWN_MS
} catch { return false }
}
function setCooldown(ruleId) {
try {
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
map[ruleId] = Date.now()
wx.setStorageSync(RULE_COOLDOWN_KEY, map)
} catch {}
}
function getUserInfo() {
return app.globalData.userInfo || {}
}
function checkRule_FillAvatar() {
const user = getUserInfo()
if (!user.id) return null
const avatar = user.avatar || user.avatarUrl || ''
const nickname = user.nickname || ''
if (avatar && !avatar.includes('default') && nickname && nickname !== '微信用户' && !nickname.startsWith('微信用户')) {
return null
}
if (isInCooldown('fill_avatar')) return null
setCooldown('fill_avatar')
return {
ruleId: 'fill_avatar',
title: '完善个人信息',
message: '设置头像和昵称,让其他创业者更容易认识你',
action: 'navigate',
target: '/pages/profile-edit/profile-edit'
}
}
function checkRule_BindPhone() {
const user = getUserInfo()
if (!user.id) return null
if (user.phone) return null
if (isInCooldown('bind_phone')) return null
setCooldown('bind_phone')
return {
ruleId: 'bind_phone',
title: '绑定手机号',
message: '绑定手机号解锁更多功能,保障账户安全',
action: 'bind_phone',
target: null
}
}
function checkRule_FillProfile() {
const user = getUserInfo()
if (!user.id) return null
if (user.mbti && user.industry) return null
if (isInCooldown('fill_profile')) return null
setCooldown('fill_profile')
return {
ruleId: 'fill_profile',
title: '完善创业档案',
message: '填写 MBTI 和行业信息,帮你精准匹配创业伙伴',
action: 'navigate',
target: '/pages/profile-edit/profile-edit'
}
}
function checkRule_ShareAfter5Chapters() {
const user = getUserInfo()
if (!user.id) return null
const readCount = app.globalData.readCount || 0
if (readCount < 5) return null
if (isInCooldown('share_after_5')) return null
setCooldown('share_after_5')
return {
ruleId: 'share_after_5',
title: '邀请好友一起看',
message: '你已阅读 ' + readCount + ' 个章节,分享给好友可获得分销收益',
action: 'navigate',
target: '/pages/referral/referral'
}
}
/**
* 在指定场景下检查规则
* @param {string} scene - 场景after_login, before_read, after_match, after_pay, page_show
* @returns {object|null} - 触发的规则null 表示无需触发
*/
function checkRules(scene) {
const user = getUserInfo()
if (!user.id) return null
switch (scene) {
case 'after_login':
return checkRule_FillAvatar()
case 'before_read':
return checkRule_BindPhone() || checkRule_FillAvatar()
case 'after_match':
return checkRule_FillProfile()
case 'after_pay':
return checkRule_FillProfile()
case 'page_show':
return checkRule_FillAvatar() || checkRule_ShareAfter5Chapters()
default:
return null
}
}
/**
* 执行规则动作
* @param {object} rule - checkRules 返回的规则对象
* @param {object} pageInstance - 页面实例(用于绑定手机号等操作)
*/
function executeRule(rule, pageInstance) {
if (!rule) return
wx.showModal({
title: rule.title,
content: rule.message,
confirmText: '去完善',
cancelText: '稍后再说',
success: (res) => {
if (res.confirm) {
if (rule.action === 'navigate' && rule.target) {
wx.navigateTo({ url: rule.target })
} else if (rule.action === 'bind_phone' && pageInstance) {
if (typeof pageInstance.showPhoneBinding === 'function') {
pageInstance.showPhoneBinding()
}
}
}
_trackRuleAction(rule.ruleId, res.confirm ? 'confirm' : 'cancel')
}
})
}
function _trackRuleAction(ruleId, action) {
const userId = getUserInfo().id
if (!userId) return
app.request('/api/miniprogram/track', {
method: 'POST',
data: { userId, action: 'rule_trigger', target: ruleId, extraData: { result: action } },
silent: true
}).catch(() => {})
}
/**
* 在页面 onShow 中调用的便捷方法
* @param {string} scene
* @param {object} pageInstance
*/
function checkAndExecute(scene, pageInstance) {
const rule = checkRules(scene)
if (rule) {
setTimeout(() => executeRule(rule, pageInstance), 800)
}
}
module.exports = { checkRules, executeRule, checkAndExecute }

View File

@@ -0,0 +1,25 @@
const app = getApp()
/**
* 全局按钮/标签点击埋点
* @param {string} module 模块home|chapters|read|my|vip|wallet|match|referral|search|settings|about
* @param {string} action 行为tab_click|btn_click|nav_click|card_click|link_click 等
* @param {string} target 目标标识按钮文案、章节ID、标签名等
* @param {object} [extra] 附加数据
*/
function trackClick(module, action, target, extra) {
const userId = app.globalData.userInfo?.id || ''
if (!userId) return
app.request('/api/miniprogram/track', {
method: 'POST',
data: {
userId,
action,
target,
extraData: Object.assign({ module, page: module }, extra || {})
},
silent: true
}).catch(() => {})
}
module.exports = { trackClick }