feat: 数据概览简化 + 用户管理增加余额/提现列
- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额 - 数据概览:移除余额统计区块(余额改在用户管理中展示) - 数据概览:恢复转化率卡片(唯一付费用户/总用户) - 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额 - 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段 - 后端:User model 添加 WalletBalance 非数据库字段 - 包含之前的小程序埋点和管理后台点击统计面板 Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
|
||||
@@ -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_soul:book/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}` })
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
|
||||
@@ -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}` })
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
167
miniprogram/pages/wallet/wallet.js
Normal file
167
miniprogram/pages/wallet/wallet.js
Normal 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()
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/wallet/wallet.json
Normal file
4
miniprogram/pages/wallet/wallet.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的余额",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
88
miniprogram/pages/wallet/wallet.wxml
Normal file
88
miniprogram/pages/wallet/wallet.wxml
Normal 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>
|
||||
267
miniprogram/pages/wallet/wallet.wxss
Normal file
267
miniprogram/pages/wallet/wallet.wxss
Normal 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;
|
||||
}
|
||||
179
miniprogram/utils/ruleEngine.js
Normal file
179
miniprogram/utils/ruleEngine.js
Normal 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 }
|
||||
25
miniprogram/utils/trackClick.js
Normal file
25
miniprogram/utils/trackClick.js
Normal 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 }
|
||||
Reference in New Issue
Block a user