Update project documentation and enhance user interaction features

- Added a new entry for user interaction habit analysis based on agent transcripts, summarizing key insights into communication styles and preferences.
- Updated project indices to reflect the latest developments, including the addition of a wallet balance feature and enhancements to the mini program's user interface for better user experience.
- Improved the handling of loading states in the chapters page, ensuring a smoother user experience during data retrieval.
- Implemented a gift payment sharing feature, allowing users to share payment requests with friends for collaborative purchases.
This commit is contained in:
Alex-larget
2026-03-17 11:44:36 +08:00
parent b971420090
commit 0d12ab1d07
65 changed files with 3836 additions and 180 deletions

View File

@@ -9,8 +9,8 @@ App({
globalData: {
// API 基础地址(切换环境时注释/取消注释)
// baseUrl: 'https://soulapi.quwanzhi.com',
// baseUrl: 'http://localhost:8080', // 本地调试
baseUrl: 'https://souldev.quwanzhi.com', // 测试环境
baseUrl: 'http://localhost:8080', // 本地调试
// baseUrl: 'https://souldev.quwanzhi.com', // 测试环境
// 小程序配置 - 真实AppID

View File

@@ -16,13 +16,16 @@
"pages/addresses/addresses",
"pages/addresses/edit",
"pages/withdraw-records/withdraw-records",
"pages/wallet/wallet",
"pages/vip/vip",
"pages/member-detail/member-detail",
"pages/mentors/mentors",
"pages/mentor-detail/mentor-detail",
"pages/profile-show/profile-show",
"pages/profile-edit/profile-edit",
"pages/avatar-nickname/avatar-nickname"
"pages/avatar-nickname/avatar-nickname",
"pages/gift-pay/detail",
"pages/gift-pay/list"
],
"window": {
"backgroundTextStyle": "light",
@@ -58,7 +61,10 @@
]
},
"usingComponents": {},
"navigateToMiniProgramAppIdList": [],
"navigateToMiniProgramAppIdList": [
"wx6489c26045912fe1",
"wx3d15ed02e98b04e3"
],
"__usePrivacyCheck__": true,
"lazyCodeLoading": "requiredComponents",
"style": "v2",

View File

@@ -40,7 +40,10 @@ Page({
],
// 每日新增章节(懒加载后暂无,可后续用 latest-chapters 补充)
dailyChapters: []
dailyChapters: [],
// book/parts 加载中
partsLoading: true
},
onLoad() {
@@ -55,16 +58,40 @@ Page({
},
// 懒加载:仅拉取篇章列表 + totalSections + fixedSections
// 优先 book/parts404 或失败时降级为 all-chapters 推导
async loadParts() {
this.setData({ partsLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
if (!res?.success) {
this.setData({ bookData: [], totalSections: 0 })
return
let res
try {
res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
} catch (e) {
console.log('[Chapters] book/parts 失败,降级 all-chapters:', e?.message || e)
res = null
}
let parts = []
let totalSections = 0
let fixedSections = []
if (res?.success && Array.isArray(res.parts) && res.parts.length > 0) {
parts = res.parts
totalSections = res.totalSections ?? 0
fixedSections = res.fixedSections || []
} else {
// 降级:从 all-chapters 推导 parts
const allRes = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const list = (allRes?.data || allRes?.chapters || [])
totalSections = list.length
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = (c) => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
const partMap = new Map()
list.filter(exclude).forEach(c => {
const pid = c.partId || c.part_id || 'default'
const ptitle = c.partTitle || c.part_title || '未分类'
if (!partMap.has(pid)) partMap.set(pid, { id: pid, title: ptitle, subtitle: '', chapterCount: 0 })
partMap.get(pid).chapterCount++
})
parts = Array.from(partMap.values())
}
const parts = res.parts || []
const totalSections = res.totalSections ?? 0
const fixedSections = res.fixedSections || []
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
const fixedMap = {}
fixedSections.forEach(f => { fixedMap[f.id] = f.mid })
@@ -87,11 +114,12 @@ Page({
totalSections,
fixedSectionsMap: fixedMap,
appendixList,
_loadedChapters: {}
_loadedChapters: {},
partsLoading: false
})
} catch (e) {
console.log('[Chapters] 加载篇章失败:', e)
this.setData({ bookData: [], totalSections: 0 })
this.setData({ bookData: [], totalSections: 0, partsLoading: false })
}
},

View File

@@ -17,8 +17,14 @@
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 目录加载中 -->
<view class="parts-loading" wx:if="{{partsLoading}}">
<view class="parts-loading-spinner"></view>
<text class="parts-loading-text">加载目录中...</text>
</view>
<!-- 书籍信息卡 -->
<view class="book-info-card card-gradient">
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}">
<view class="book-icon">
<view class="book-icon-inner">📚</view>
</view>
@@ -33,7 +39,7 @@
</view>
<!-- 目录内容 -->
<view class="chapters-content">
<view class="chapters-content" wx:if="{{!partsLoading}}">
<!-- 序言(优先传 mid阅读页用 by-mid 请求) -->
<view class="chapter-item" bindtap="goToRead" data-id="preface" data-mid="{{fixedSectionsMap.preface}}">
<view class="item-left">

View File

@@ -75,6 +75,34 @@
width: 100%;
}
/* ===== 目录加载中 ===== */
.parts-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
gap: 24rpx;
}
.parts-loading-spinner {
width: 64rpx;
height: 64rpx;
border: 6rpx solid rgba(255, 255, 255, 0.1);
border-top-color: #00CED1;
border-radius: 50%;
animation: parts-spin 0.8s linear infinite;
}
.parts-loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
@keyframes parts-spin {
to { transform: rotate(360deg); }
}
/* ===== 书籍信息卡 ===== */
.book-info-card {
display: flex;

View File

@@ -0,0 +1,114 @@
/**
* Soul创业派对 - 代付详情页
* 好友打开后看到订单信息,点击「帮他付款」完成代付
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
requestSn: '',
detail: null,
loading: true,
paying: false
},
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
const requestSn = (options.requestSn || '').trim()
if (!requestSn) {
wx.showToast({ title: '代付链接无效', icon: 'none' })
setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 1500)
return
}
this.setData({ requestSn })
this.loadDetail()
},
async loadDetail() {
const { requestSn } = this.data
if (!requestSn) return
this.setData({ loading: true })
try {
const res = await app.request(`/api/miniprogram/gift-pay/detail?requestSn=${encodeURIComponent(requestSn)}`)
if (res && res.success) {
this.setData({ detail: res, loading: false })
} else {
this.setData({ loading: false })
wx.showToast({ title: res?.error || '加载失败', icon: 'none' })
}
} catch (e) {
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
async doPay() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => wx.switchTab({ url: '/pages/my/my' }), 1500)
return
}
const openId = app.globalData.openId || ''
if (!openId) {
wx.showToast({ title: '请先完成微信授权', icon: 'none' })
return
}
const { requestSn, detail } = this.data
if (!requestSn || !detail) return
this.setData({ paying: true })
wx.showLoading({ title: '创建订单中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/gift-pay/pay',
method: 'POST',
data: {
requestSn,
openId,
userId: app.globalData.userInfo?.id || ''
}
})
wx.hideLoading()
if (!res || !res.success || !res.data?.payParams) {
throw new Error(res?.error || '创建订单失败')
}
const payParams = res.data.payParams
payParams._orderSn = res.data.orderSn
await new Promise((resolve, reject) => {
wx.requestPayment({
...payParams,
signType: payParams.signType || 'MD5',
success: resolve,
fail: reject
})
})
wx.showToast({ title: '代付成功', icon: 'success' })
this.setData({ paying: false })
setTimeout(() => {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
}, 1500)
} catch (e) {
this.setData({ paying: false })
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消支付', icon: 'none' })
} else {
wx.showToast({ title: e.message || e.error || '支付失败', icon: 'none' })
}
}
},
goBack() {
app.goBackOrToHome()
},
onShareAppMessage() {
const { requestSn } = this.data
return {
title: '好友请你帮忙代付 - Soul创业派对',
path: requestSn ? `/pages/gift-pay/detail?requestSn=${requestSn}` : '/pages/gift-pay/detail'
}
}
})

View File

@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"usingComponents": {}
}

View File

@@ -0,0 +1,52 @@
<!-- Soul创业派对 - 代付详情页 -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-back" bindtap="goBack">
<text class="back-arrow">←</text>
</view>
<view class="nav-info">
<text class="nav-title">帮他付款</text>
</view>
<view class="nav-right-placeholder"></view>
</view>
</view>
<view class="content" style="padding-top: calc({{statusBarHeight}}px + 88rpx);">
<block wx:if="{{loading}}">
<view class="loading-box">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</block>
<block wx:elif="{{detail}}">
<view class="card">
<view class="card-header">
<text class="card-title">代付订单</text>
<text class="initiator">{{detail.initiatorNickname || '好友'}} 请你帮忙付款</text>
</view>
<view class="card-body">
<view class="row">
<text class="label">商品</text>
<text class="value">{{detail.description || '-'}}</text>
</view>
<view class="row amount-row">
<text class="label">金额</text>
<text class="amount">¥{{detail.amount ? detail.amount.toFixed(2) : '0.00'}}</text>
</view>
</view>
</view>
<view class="tips">
<text>付款后,{{detail.initiatorNickname || '好友'}}将获得对应权益</text>
</view>
<button class="pay-btn" bindtap="doPay" disabled="{{paying}}">
{{paying ? '支付中...' : '帮他付款'}}
</button>
</block>
<block wx:else>
<view class="empty">
<text>代付请求不存在或已处理</text>
</view>
</block>
</view>
</view>

View File

@@ -0,0 +1,160 @@
/* Soul创业派对 - 代付详情页 */
.page {
min-height: 100vh;
background: #000;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.9);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
height: 88rpx;
}
.nav-back {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #1c1c1e;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
}
.content {
padding: 32rpx;
}
.loading-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
border-top-color: #00CED1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 24rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
.card {
background: #1c1c1e;
border-radius: 24rpx;
overflow: hidden;
margin-bottom: 32rpx;
}
.card-header {
padding: 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
}
.card-title {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 8rpx;
}
.initiator {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.card-body {
padding: 32rpx;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.row:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
.value {
font-size: 28rpx;
color: #fff;
}
.amount-row .amount {
font-size: 40rpx;
font-weight: 700;
color: #00CED1;
}
.tips {
padding: 0 8rpx 32rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
}
.pay-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
color: #fff;
font-size: 32rpx;
font-weight: 600;
border-radius: 48rpx;
border: none;
}
.pay-btn[disabled] {
opacity: 0.6;
}
.empty {
text-align: center;
padding: 120rpx 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}

View File

@@ -0,0 +1,89 @@
/**
* Soul创业派对 - 我的代付
* Tab: 我发起的 / 我帮付的
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
tab: 'requests',
requests: [],
payments: [],
loading: false
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
this.loadData()
},
onShow() {
if (this.data.requests.length > 0 || this.data.payments.length > 0) {
this.loadData()
}
},
switchTab(e) {
const tab = e.currentTarget.dataset.tab || 'requests'
this.setData({ tab })
this.loadData()
},
async loadData() {
const userId = app.globalData.userInfo?.id || ''
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.setData({ loading: true })
try {
if (this.data.tab === 'requests') {
const res = await app.request(`/api/miniprogram/gift-pay/my-requests?userId=${encodeURIComponent(userId)}`)
this.setData({ requests: (res && res.list) || [], loading: false })
} else {
const res = await app.request(`/api/miniprogram/gift-pay/my-payments?userId=${encodeURIComponent(userId)}`)
this.setData({ payments: (res && res.list) || [], loading: false })
}
} catch (e) {
this.setData({ loading: false })
}
},
async cancelRequest(e) {
const requestSn = e.currentTarget.dataset.sn
if (!requestSn) return
const ok = await new Promise(r => {
wx.showModal({ title: '取消代付', content: '确定取消该代付请求?', success: res => r(res.confirm) })
})
if (!ok) return
try {
const res = await app.request({
url: '/api/miniprogram/gift-pay/cancel',
method: 'POST',
data: { requestSn, userId: app.globalData.userInfo?.id }
})
if (res && res.success) {
wx.showToast({ title: '已取消', icon: 'success' })
this.loadData()
} else {
wx.showToast({ title: res?.error || '取消失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '取消失败', icon: 'none' })
}
},
shareRequest(e) {
const requestSn = e.currentTarget.dataset.sn
wx.showToast({ title: '请点击右上角「...」分享给好友', icon: 'none', duration: 2500 })
},
goBack() {
app.goBackOrToHome()
},
onShareAppMessage() {
return { title: '我的代付 - Soul创业派对', path: '/pages/gift-pay/list' }
}
})

View File

@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"usingComponents": {}
}

View File

@@ -0,0 +1,64 @@
<!-- Soul创业派对 - 我的代付 -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-back" bindtap="goBack">
<text class="back-arrow">←</text>
</view>
<view class="nav-info">
<text class="nav-title">我的代付</text>
</view>
<view class="nav-right-placeholder"></view>
</view>
</view>
<view class="tabs" style="padding-top: calc({{statusBarHeight}}px + 88rpx);">
<view class="tab {{tab === 'requests' ? 'active' : ''}}" data-tab="requests" bindtap="switchTab">我发起的</view>
<view class="tab {{tab === 'payments' ? 'active' : ''}}" data-tab="payments" bindtap="switchTab">我帮付的</view>
</view>
<view class="content">
<block wx:if="{{loading}}">
<view class="loading-box">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</block>
<block wx:elif="{{tab === 'requests'}}">
<block wx:if="{{requests.length === 0}}">
<view class="empty">暂无发起的代付</view>
</block>
<block wx:else>
<view class="card" wx:for="{{requests}}" wx:key="requestSn">
<view class="card-row">
<text class="desc">{{item.description}}</text>
<text class="amount">¥{{item.amount}}</text>
</view>
<view class="card-row">
<text class="status {{item.status}}">{{item.status === 'pending' ? '待支付' : item.status === 'paid' ? '已支付' : item.status === 'cancelled' ? '已取消' : '已过期'}}</text>
<view class="actions" wx:if="{{item.status === 'pending'}}">
<text class="action-text" bindtap="shareRequest" data-sn="{{item.requestSn}}">分享</text>
<text class="action-text cancel" bindtap="cancelRequest" data-sn="{{item.requestSn}}">取消</text>
</view>
</view>
</view>
</block>
</block>
<block wx:else>
<block wx:if="{{payments.length === 0}}">
<view class="empty">暂无帮付记录</view>
</block>
<block wx:else>
<view class="card" wx:for="{{payments}}" wx:key="requestSn">
<view class="card-row">
<text class="desc">{{item.description}}</text>
<text class="amount">¥{{item.amount}}</text>
</view>
<view class="card-row">
<text class="status {{item.status}}">{{item.status === 'paid' ? '已支付' : item.status}}</text>
</view>
</view>
</block>
</block>
</view>
</view>

View File

@@ -0,0 +1,160 @@
/* Soul创业派对 - 我的代付 */
.page {
min-height: 100vh;
background: #000;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.9);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
height: 88rpx;
}
.nav-back {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #1c1c1e;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
}
.tabs {
display: flex;
padding: 24rpx 32rpx;
gap: 24rpx;
background: #000;
}
.tab {
flex: 1;
text-align: center;
padding: 20rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
border-radius: 12rpx;
background: #1c1c1e;
}
.tab.active {
color: #00CED1;
background: rgba(0, 206, 209, 0.15);
}
.content {
padding: 0 32rpx 32rpx;
}
.loading-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 0;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
border-top-color: #00CED1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 24rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
.empty {
text-align: center;
padding: 80rpx 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
}
.card {
background: #1c1c1e;
border-radius: 16rpx;
padding: 24rpx 32rpx;
margin-bottom: 24rpx;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.card-row:last-child {
margin-bottom: 0;
}
.desc {
font-size: 28rpx;
color: #fff;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.amount {
font-size: 32rpx;
font-weight: 600;
color: #00CED1;
margin-left: 16rpx;
}
.status {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
}
.status.paid {
color: #00CED1;
}
.actions {
display: flex;
gap: 24rpx;
}
.action-text {
font-size: 26rpx;
color: #00CED1;
}
.action-text.cancel {
color: rgba(255, 255, 255, 0.5);
}

View File

@@ -47,8 +47,9 @@ Page({
superMembers: [],
superMembersLoading: true,
// 最新新增章节
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
latestChapters: [],
displayLatestChapters: [],
// 篇章数(从 bookData 计算)
partCount: 0,
@@ -58,7 +59,13 @@ Page({
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: ''
leadPhone: '',
// 展开状态(首页精选/最新)
featuredExpanded: false,
latestExpanded: false,
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
featuredExpandedLoading: false
},
onLoad(options) {
@@ -180,7 +187,25 @@ Page({
}
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
// 兜底:无 recommended 时从 book/hot 取前3
if (featured.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = hotList.slice(0, 3).map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
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) || []
@@ -482,6 +507,50 @@ Page({
wx.switchTab({ url: '/pages/match/match' })
},
// 精选推荐:展开/折叠
async toggleFeaturedExpanded() {
if (this.data.featuredExpandedLoading) return
if (this.data.featuredExpanded) {
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
this.setData({ featuredExpanded: false, featuredSections: collapsed })
return
}
if (this.data.featuredSectionsFull.length > 0) {
this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull })
return
}
this.setData({ featuredExpandedLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
const list = (res && res.data) ? res.data : []
const tagMap = ['热门', '推荐', '精选']
const full = list.map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i % 3] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec'
}))
this.setData({
featuredSectionsFull: full,
featuredSections: full,
featuredExpanded: true,
featuredExpandedLoading: false
})
} catch (e) {
console.log('[Index] 加载精选更多失败:', e)
this.setData({ featuredExpandedLoading: false })
}
},
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
toggleLatestExpanded() {
const expanded = !this.data.latestExpanded
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
},
// 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章
async loadLatestChapters() {
try {
@@ -491,7 +560,7 @@ Page({
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
const latest = list
.filter(exclude)
.slice(0, 10)
.slice(0, 20)
.map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
@@ -504,7 +573,8 @@ Page({
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
this.setData({ latestChapters: latest })
const display = this.data.latestExpanded ? latest : latest.slice(0, 5)
this.setData({ latestChapters: latest, displayLatestChapters: display })
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},

View File

@@ -52,6 +52,19 @@
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 阅读进度(设计稿:最新更新→阅读进度→超级个体) -->
<view class="progress-card" wx:if="{{isLoggedIn}}" bindtap="goToChapters">
<view class="progress-header">
<text class="progress-title">阅读进度</text>
<text class="progress-count">已读 {{readCount}}/{{totalSections}}</text>
</view>
<view class="progress-bar-wrapper">
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{readCount && totalSections ? (readCount / totalSections * 100) : 0}}%;"></view>
</view>
</view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
<view class="section">
<view class="section-header">
@@ -91,10 +104,14 @@
</view>
</view>
<!-- 精选推荐(带 tag已去掉「查看全部」 -->
<!-- 精选推荐(带 tag支持展开更多 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选推荐</text>
<view class="section-more" wx:if="{{featuredSections.length > 0}}" bindtap="toggleFeaturedExpanded">
<text class="more-text">{{featuredExpandedLoading ? '加载中...' : (featuredExpanded ? '收起' : '展开更多')}}</text>
<text class="more-arrow">{{featuredExpanded ? '▲' : '▼'}}</text>
</view>
</view>
<view class="featured-list">
<view
@@ -117,18 +134,24 @@
</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>
<view class="section-header-right">
<view class="daily-badge-wrap">
<text class="daily-badge">+{{latestChapters.length}}</text>
</view>
<view class="section-more" wx:if="{{latestChapters.length > 5}}" bindtap="toggleLatestExpanded">
<text class="more-text">{{latestExpanded ? '收起' : '展开更多'}}</text>
<text class="more-arrow">{{latestExpanded ? '▲' : '▼'}}</text>
</view>
</view>
</view>
<view class="timeline-wrap">
<view class="timeline-line"></view>
<view class="timeline-list">
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{displayLatestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-row">

View File

@@ -688,6 +688,12 @@
margin-bottom: 32rpx;
}
.section-header-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.daily-badge-wrap {
display: inline-flex;
align-items: center;

View File

@@ -76,6 +76,9 @@ Page({
// 设置入口:开发版、体验版显示
showSettingsEntry: false,
// 我的余额
walletBalanceText: '--',
},
onLoad() {
@@ -148,6 +151,7 @@ Page({
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
this.loadWalletBalance()
} else {
const guestReadCount = app.getReadCount()
this.setData({
@@ -762,8 +766,10 @@ Page({
const routes = {
orders: '/pages/purchases/purchases',
giftPay: '/pages/gift-pay/list',
referral: '/pages/referral/referral',
withdrawRecords: '/pages/withdraw-records/withdraw-records',
wallet: '/pages/wallet/wallet',
about: '/pages/about/about',
settings: '/pages/settings/settings'
}
@@ -848,6 +854,18 @@ Page({
} catch (e) { console.log('[My] VIP查询失败', e) }
},
async loadWalletBalance() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
if (res?.success && res.data) {
const balance = res.data.balance || 0
this.setData({ walletBalanceText: balance.toFixed(2) })
}
} catch (e) { console.log('[My] 余额查询失败', e) }
},
// 头像点击:已登录弹出选项(微信头像 / 相册)
onAvatarTap() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
@@ -915,6 +933,12 @@ Page({
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},
// 进入个人资料展示页enhanced_professional_profile展示页内可再进编辑
goToProfileShow() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/profile-show/profile-show' })
},
async handleWithdraw() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
const amount = parseFloat(this.data.pendingEarnings)

View File

@@ -34,7 +34,13 @@
<view class="profile-meta">
<view class="profile-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
<view class="profile-name-actions">
<view class="profile-edit-btn" bindtap="goToProfileShow">
<image class="profile-edit-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
<text class="profile-edit-text">编辑</text>
</view>
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
</view>
</view>
<view class="vip-tags">
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
@@ -57,6 +63,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">{{walletBalanceText}}</text>
<text class="profile-stat-label">我的余额</text>
</view>
</view>
</view>
</view>
@@ -147,6 +157,13 @@
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="handleMenuTap" data-id="giftPay">
<view class="menu-left">
<view class="menu-icon-wrap icon-gold"><image class="menu-icon-img" src="/assets/icons/gift.svg" mode="aspectFit"/></view>
<text class="menu-text">我的代付</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
<view class="menu-left">
<view class="menu-icon-wrap icon-blue"><image class="menu-icon-img" src="/assets/icons/info-blue.svg" mode="aspectFit"/></view>

View File

@@ -59,6 +59,10 @@
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
.profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
.profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
.profile-name-actions { display: flex; align-items: center; gap: 16rpx; flex-shrink: 0; }
.profile-edit-btn { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 16rpx; background: rgba(255,255,255,0.08); border-radius: 12rpx; }
.profile-edit-icon { width: 28rpx; height: 28rpx; opacity: 0.7; }
.profile-edit-text { font-size: 24rpx; color: rgba(255,255,255,0.7); }
.user-name {
font-size: 44rpx; font-weight: bold; color: #fff;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
@@ -178,6 +182,8 @@
.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-gold { background: rgba(200,161,70,0.2); }
.icon-gold .menu-icon-img { width: 32rpx; height: 32rpx; }
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }

View File

@@ -65,6 +65,9 @@ Page({
// 弹窗
showShareModal: false,
showGiftShareModal: false,
shareMode: '', // 'gift' = 代付分享onShareAppMessage 返回 gift-pay/detail
giftRequestSn: '', // 代付请求号,分享时用
showLoginModal: false,
agreeProtocol: false,
showPosterModal: false,
@@ -72,7 +75,10 @@ Page({
isGeneratingPoster: false,
// 章节 mid扫码/海报分享用,便于分享 path 带 mid
sectionMid: null
sectionMid: null,
// 余额(用于余额支付)
walletBalance: 0,
},
async onLoad(options) {
@@ -88,12 +94,16 @@ Page({
}).catch(() => {})
}
// 支持 scene扫码、mid、id、ref
// 支持 scene扫码、mid、id、ref、gift代付
const sceneStr = (options && options.scene) || ''
const parsed = parseScene(sceneStr)
const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0)
let id = options.id || parsed.id || app.globalData.initialSectionId
const ref = options.ref || parsed.ref
const isGift = options.gift === '1' || options.gift === 'true'
if (isGift && ref) {
wx.setStorageSync('gift_for_ref', ref) // 代付模式:好友打开后,购买时传 giftFor后端待支持
}
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
@@ -647,6 +657,55 @@ Page({
this.setData({ showShareModal: false })
},
// 代付分享弹窗:创建代付请求后分享到代付页面
async showGiftShareModal() {
if (!app.globalData.userInfo?.id) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const { sectionId, sectionMid } = this.data
const productId = sectionId || ''
if (!productId) {
wx.showToast({ title: '章节信息异常', icon: 'none' })
return
}
wx.showLoading({ title: '创建代付请求...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/gift-pay/create',
method: 'POST',
data: {
userId: app.globalData.userInfo.id,
productType: 'section',
productId
}
})
wx.hideLoading()
if (res && res.success && res.requestSn) {
this.setData({ showGiftShareModal: true, giftRequestSn: res.requestSn })
} else {
wx.showToast({ title: res?.error || '创建失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '创建失败', icon: 'none' })
}
},
closeGiftShareModal() {
this.setData({ showGiftShareModal: false })
},
// 分享给好友(代付):引导用户点右上角,分享到代付详情页
shareGiftToFriend() {
this.setData({ shareMode: 'gift', showGiftShareModal: false })
wx.showToast({
title: '请点击右上角「...」→ 发送给好友',
icon: 'none',
duration: 2500
})
},
// 复制链接
copyLink() {
const userInfo = app.globalData.userInfo
@@ -682,17 +741,27 @@ Page({
})
},
// 分享到微信 - 自动带分享人ID优先用 mid扫码/海报闭环),无则用 id
// 分享到微信 - 自动带分享人IDshareMode=gift 时分享到代付详情页
onShareAppMessage() {
const { section, sectionId, sectionMid } = this.data
const { section, sectionId, sectionMid, shareMode, giftRequestSn } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const shareTitle = section?.title
let path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
let title = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
: '📚 Soul创业派对 - 真实商业故事'
if (shareMode === 'gift' && giftRequestSn) {
path = `/pages/gift-pay/detail?requestSn=${giftRequestSn}`
title = '好友请你帮忙代付 - Soul创业派对'
this.setData({ shareMode: '', giftRequestSn: '' })
} else {
title = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
: '📚 Soul创业派对 - 真实商业故事'
}
return {
title: shareTitle,
path: ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
title,
path
// 不设置 imageUrl使用当前阅读页截图作为分享卡片中间图片
}
},
@@ -706,16 +775,21 @@ Page({
})
},
// 分享到朋友圈:带文章标题,过长时截断(朋友圈卡片标题显示有限)
// 分享到朋友圈:带文章标题,过长时截断shareMode=gift 时 query 带 gift=1
onShareTimeline() {
const { section, sectionId, sectionMid, chapterTitle } = this.data
const { section, sectionId, sectionMid, chapterTitle, shareMode } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
let query = ref ? `${q}&ref=${ref}` : q
if (shareMode === 'gift' && ref) {
query = `${q}&ref=${ref}&gift=1`
this.setData({ shareMode: '' })
}
const articleTitle = (section?.title || chapterTitle || '').trim()
const title = articleTitle
? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle)
: 'Soul创业派对 - 真实商业故事'
return { title, query: ref ? `${q}&ref=${ref}` : q }
return { title, query }
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
@@ -926,6 +1000,39 @@ Page({
wx.showLoading({ title: '正在发起支付...', mask: true })
try {
// 0. 尝试余额支付(若余额足够)
const userId = app.globalData.userInfo?.id
const referralCode = wx.getStorageSync('referral_code') || ''
if (userId) {
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
const balance = balanceRes?.data?.balance || 0
if (balance >= amount) {
const productId = type === 'section' ? sectionId : (type === 'fullbook' ? 'fullbook' : '')
const consumeRes = await app.request({
url: '/api/miniprogram/balance/consume',
method: 'POST',
data: {
userId,
productType: type,
productId: type === 'section' ? sectionId : (type === 'fullbook' ? 'fullbook' : 'vip_annual'),
amount,
referralCode: referralCode || undefined
}
})
if (consumeRes?.success) {
wx.hideLoading()
this.setData({ isPaying: false })
wx.showToast({ title: '购买成功', icon: 'success' })
await this.onPaymentSuccess()
return
}
}
} catch (e) {
console.warn('[Pay] 余额支付失败,改用微信支付:', e)
}
}
// 1. 先获取openId (支付必需)
let openId = app.globalData.openId || wx.getStorageSync('openId')

View File

@@ -93,6 +93,10 @@
<text class="action-icon-small">🖼️</text>
<text class="action-text-small">生成海报</text>
</view>
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn}}">
<text class="action-icon-small">🎁</text>
<text class="action-text-small">代付分享</text>
</view>
</view>
</view>
@@ -187,6 +191,11 @@
</view>
<text class="paywall-tip">分享给好友一起学习,还能赚取佣金</text>
<!-- 代付分享:让好友帮我买 -->
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn}}">
<text class="gift-share-icon">🎁</text>
<text class="gift-share-text">找好友代付</text>
</view>
</view>
<!-- 章节导航 -->
@@ -289,6 +298,23 @@
</view>
</view>
<!-- 代付分享弹窗:分享到代付页面,好友打开后帮他付款 -->
<view class="modal-overlay" wx:if="{{showGiftShareModal}}" bindtap="closeGiftShareModal">
<view class="modal-content share-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">找好友代付</text>
<view class="modal-close" bindtap="closeGiftShareModal">✕</view>
</view>
<text class="share-modal-desc">分享给好友,好友打开后点击「帮他付款」即可为你代付本章</text>
<view class="share-modal-actions">
<view class="share-modal-btn" bindtap="shareGiftToFriend">
<text class="btn-icon">👤</text>
<text>分享给好友</text>
</view>
</view>
</view>
</view>
<!-- 支付中提示 -->
<view class="modal-overlay" wx:if="{{isPaying}}" catchtap="">
<view class="loading-box">

View File

@@ -586,6 +586,48 @@
color: rgba(255, 255, 255, 0.6);
}
/* ===== 代付分享 ===== */
.btn-gift-inline {
/* 与 btn-share-inline 同风格 */
}
.gift-share-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 24rpx;
padding: 20rpx;
background: rgba(255, 215, 0, 0.08);
border-radius: 24rpx;
border: 1rpx solid rgba(255, 215, 0, 0.2);
}
.gift-share-icon { font-size: 32rpx; }
.gift-share-text { font-size: 28rpx; color: #FFD700; }
.share-modal-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
display: block;
margin-bottom: 32rpx;
line-height: 1.5;
}
.share-modal-actions {
display: flex;
gap: 24rpx;
}
.share-modal-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
padding: 32rpx 24rpx;
background: rgba(255, 255, 255, 0.06);
border-radius: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.1);
}
.share-modal-btn .btn-icon { font-size: 48rpx; }
.share-modal-btn text:last-child { font-size: 26rpx; color: rgba(255, 255, 255, 0.8); }
/* ===== 分享弹窗 ===== */
.share-link-box {
padding: 32rpx;

View File

@@ -36,7 +36,7 @@ Page({
// 加载热门章节(从服务器获取点击量高的章节)
async loadHotChapters() {
try {
const res = await app.request('/api/miniprogram/book/hot')
const res = await app.request('/api/miniprogram/book/hot?limit=50')
const list = (res && res.data) || (res && res.chapters) || []
if (list.length > 0) {
const hotChapters = list.map((c, i) => ({

View File

@@ -84,7 +84,37 @@ Page({
}
}
this.setData({ purchasing: true })
const amount = this.data.price
try {
// 0. 尝试余额支付(若余额足够)
const referralCode = wx.getStorageSync('referral_code') || ''
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
const balance = balanceRes?.data?.balance || 0
if (balance >= amount) {
const consumeRes = await app.request({
url: '/api/miniprogram/balance/consume',
method: 'POST',
data: {
userId,
productType: 'vip',
productId: 'vip_annual',
amount,
referralCode: referralCode || undefined
}
})
if (consumeRes?.success) {
this.setData({ purchasing: false })
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
await this._onVipPaymentSuccess()
return
}
}
} catch (e) {
console.warn('[VIP] 余额支付失败,改用微信支付:', e)
}
// 1. 微信支付
const payRes = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -92,7 +122,7 @@ Page({
userId,
productType: 'vip',
productId: 'vip_annual',
amount: this.data.price,
amount,
description: '卡若创业派对VIP年度会员365天'
}
})

View File

@@ -0,0 +1,134 @@
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
statusBarHeight: 44,
balance: 0,
balanceText: '0.00',
transactions: [],
loading: true,
rechargeAmounts: [10, 30, 50, 100],
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),
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: Math.abs(t.amount || 0).toFixed(2),
amountSign: (t.amount || 0) >= 0 ? '+' : '-',
description: t.type === 'recharge' ? '充值' : t.type === 'consume' ? '阅读消费' : t.type === 'refund' ? '退款' : '其他',
createdAt: t.createdAt ? new Date(t.createdAt).toLocaleString('zh-CN') : '--',
}))
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
let openId = app.globalData.openId
if (!openId) {
openId = await app.getOpenId()
}
if (!openId) {
wx.showToast({ title: '获取支付凭证失败,请重新登录', icon: 'none', duration: 2500 })
return
}
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) {
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: openId,
productType: 'balance_recharge',
productId: res.data.orderSn,
amount: amount,
description: `余额充值 ¥${amount}`,
userId: userId,
}
})
const params = (payRes && payRes.data && payRes.data.payParams) ? payRes.data.payParams : (payRes && payRes.payParams ? payRes.payParams : null)
if (params) {
wx.requestPayment({
...params,
success: async () => {
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: payRes?.error || '创建支付失败', icon: 'none' })
}
}
} catch (e) {
wx.hideLoading()
console.error('[Wallet] recharge error', e)
wx.showToast({ title: '充值失败:' + (e.message || e.errMsg || '网络异常'), icon: 'none', duration: 3000 })
}
},
goBack() {
wx.navigateBack()
},
})

View File

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

View File

@@ -0,0 +1,78 @@
<!-- 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>
<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 || '--'}}</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,256 @@
/* 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;
}
.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,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 || ''
app.request({
url: '/api/miniprogram/track',
method: 'POST',
data: {
userId: userId || undefined,
action,
target,
extraData: Object.assign({ module, page: module }, extra || {})
},
silent: true
}).catch(() => {})
}
module.exports = { trackClick }