新增匹配次数管理功能,优化用户匹配体验。通过服务端计算用户的匹配配额,更新用户状态以反映剩余匹配次数。同时,调整匹配页面逻辑,确保在匹配次数用尽时提示用户购买更多次数。更新相关API以支持匹配记录的存储与查询,提升系统稳定性。

This commit is contained in:
2026-02-11 16:53:17 +08:00
parent ecee1bb2bb
commit a1dcf599ee
18 changed files with 422 additions and 229 deletions

View File

@@ -38,6 +38,7 @@ Page({
todayMatchCount: 0,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchesRemaining: FREE_MATCH_LIMIT,
showQuotaExhausted: false,
needPayToMatch: false,
// 匹配状态
@@ -88,8 +89,7 @@ Page({
})
this.loadMatchConfig()
this.loadStoredContact()
this.loadTodayMatchCount()
this.initUserStatus()
this.refreshMatchCountAndStatus()
},
onShow() {
@@ -102,7 +102,7 @@ Page({
}
}
this.loadStoredContact()
this.initUserStatus()
this.refreshMatchCountAndStatus()
},
// 加载匹配配置
@@ -147,49 +147,46 @@ Page({
})
},
// 加载今日匹配次数
loadTodayMatchCount() {
try {
const today = new Date().toISOString().split('T')[0]
const stored = wx.getStorageSync('match_count_data')
if (stored) {
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
if (data.date === today) {
this.setData({ todayMatchCount: data.count })
// 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威)
async refreshMatchCountAndStatus() {
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
try {
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`)
if (res.success && res.data) {
app.globalData.matchCount = res.data.matchCount ?? 0
app.globalData.matchQuota = res.data.matchQuota || null
}
} catch (e) {
console.log('[Match] 拉取 matchQuota 失败:', e)
}
} catch (e) {
console.error('加载匹配次数失败:', e)
}
this.initUserStatus()
},
// 保存今日匹配次数
saveTodayMatchCount(count) {
const today = new Date().toISOString().split('T')[0]
wx.setStorageSync('match_count_data', { date: today, count })
},
// 初始化用户状态
// 初始化用户状态matchQuota 服务端纯计算:订单+match_records
initUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
// 获取额外购买的匹配次数
const extraMatches = wx.getStorageSync('extra_match_count') || 0
// 总匹配次数 = 每日免费(3) + 额外购买次数
// 全书用户无限制
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount)
const needPayToMatch = !hasFullBook && matchesRemaining <= 0
const quota = app.globalData.matchQuota
// 今日剩余次数、今日已用:来自服务端 matchQuota未登录无法计算不能显示已用完
const remainToday = quota?.remainToday ?? 0
const matchesUsedToday = quota?.matchesUsedToday ?? 0
const purchasedRemain = quota?.purchasedRemain ?? 0
const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT)
// 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」
const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
this.setData({
isLoggedIn,
hasFullBook,
hasPurchased: true, // 所有用户都可以使用匹配功能
hasPurchased: true,
todayMatchCount: matchesUsedToday,
totalMatchesAllowed,
matchesRemaining,
matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)),
needPayToMatch,
extraMatches
showQuotaExhausted,
extraMatches: purchasedRemain
})
},
@@ -224,7 +221,7 @@ Page({
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/catalog/catalog' })
wx.switchTab({ url: '/pages/chapters/chapters' })
}
}
})
@@ -257,12 +254,12 @@ Page({
return
}
// 创业合伙类型 - 真正的匹配功能
if (this.data.needPayToMatch) {
// 创业合伙类型 - 真正的匹配功能(仅当登录且服务端确认次数用尽时,弹出购买)
if (this.data.showQuotaExhausted) {
this.setData({ showUnlockModal: true })
return
}
this.startMatch()
},
@@ -314,6 +311,7 @@ Page({
// 开始匹配 - 只匹配数据库中的真实用户
async startMatch() {
this.loadStoredContact()
this.setData({
isMatching: true,
matchAttempts: 0,
@@ -325,20 +323,28 @@ Page({
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 1000)
// 从数据库获取真实用户匹配
// 从数据库获取真实用户匹配(后端会校验剩余次数)
let matchedUser = null
let quotaExceeded = false
try {
const ui = app.globalData.userInfo || {}
const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim()
const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim()
const res = await app.request('/api/miniprogram/match/users', {
method: 'POST',
data: {
matchType: this.data.selectedType,
userId: app.globalData.userInfo?.id || ''
userId: app.globalData.userInfo?.id || '',
phone,
wechatId
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
} else if (res.code === 'QUOTA_EXCEEDED') {
quotaExceeded = true
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
@@ -348,7 +354,22 @@ Page({
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
clearInterval(timer)
// 次数用尽(后端校验)
if (quotaExceeded) {
this.setData({ isMatching: false })
wx.showModal({
title: '今日匹配次数已用完',
content: '请购买更多次数继续匹配',
confirmText: '去购买',
success: (r) => {
if (r.confirm) this.setData({ showUnlockModal: true })
}
})
this.refreshMatchCountAndStatus()
return
}
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
@@ -360,23 +381,17 @@ Page({
})
return
}
// 增加今日匹配次数
const newCount = this.data.todayMatchCount + 1
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
// 匹配成功:从服务端刷新配额(后端已写入 match_records
this.setData({
isMatching: false,
currentMatch: matchedUser,
todayMatchCount: newCount,
matchesRemaining,
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
needPayToMatch: false
})
this.saveTodayMatchCount(newCount)
this.refreshMatchCountAndStatus()
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
}, delay)
},
@@ -665,7 +680,8 @@ Page({
try {
const result = await app.login()
if (result) {
this.initUserStatus()
// 登录成功后必须拉取 matchQuota否则无法正确显示剩余次数
await this.refreshMatchCountAndStatus()
this.setData({ showLoginModal: false, agreeProtocol: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
@@ -777,7 +793,24 @@ Page({
this.setData({ showUnlockModal: false })
},
// 购买匹配次数
// 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试)
async pollOrderAndRefresh(orderSn) {
const maxAttempts = 12
const interval = 800
for (let i = 0; i < maxAttempts; i++) {
try {
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
if (r?.data?.status === 'paid') {
await this.refreshMatchCountAndStatus()
return
}
} catch (_) {}
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval))
}
await this.refreshMatchCountAndStatus()
},
// 购买匹配次数(与购买章节逻辑一致,写入订单)
async buyMatchCount() {
this.setData({ showUnlockModal: false })
@@ -793,16 +826,17 @@ Page({
return
}
const matchPrice = this.data.matchPrice || 1
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
// 调用支付接口购买匹配次数productType: match订单类型购买匹配次数
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
productType: 'match',
productId: 'match_1',
amount: 1,
amount: matchPrice,
description: '匹配次数x1',
userId: app.globalData.userInfo?.id || '',
referralCode: referralCode || undefined
@@ -810,6 +844,7 @@ Page({
})
if (res.success && res.data?.payParams) {
const orderSn = res.data.orderSn
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
@@ -818,13 +853,9 @@ Page({
fail: reject
})
})
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '购买成功', icon: 'success' })
this.initUserStatus()
// 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机)
this.pollOrderAndRefresh(orderSn)
} else {
throw new Error(res.error || '创建订单失败')
}
@@ -832,14 +863,13 @@ Page({
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
// 测试模式
// 测试模式(无支付环境时本地模拟)
wx.showModal({
title: '支付服务暂不可用',
content: '是否使用测试模式购买?',
success: (res) => {
if (res.confirm) {
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1
wx.showToast({ title: '测试购买成功', icon: 'success' })
this.initUserStatus()
}

View File

@@ -15,8 +15,8 @@
<!-- 顶部留白,让内容往下 -->
<view style="height: 30rpx;"></view>
<!-- 匹配提示条 - 简化显示 -->
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
<!-- 匹配提示条 - 仅登录且服务端确认次数用尽时显示 -->
<view class="match-tip-bar" wx:if="{{showQuotaExhausted}}">
<text class="tip-icon">⚡</text>
<text class="tip-text">今日免费次数已用完</text>
<view class="tip-btn" bindtap="showUnlockModal">购买次数</view>

View File

@@ -110,6 +110,27 @@ Page({
}
},
// 登录后刷新购买状态(与 match/read 一致,避免其他页面用旧数据)
async refreshPurchaseStatus() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
if (res.success && res.data) {
app.globalData.hasFullBook = res.data.hasFullBook || false
app.globalData.purchasedSections = res.data.purchasedSections || []
app.globalData.matchCount = res.data.matchCount ?? 0
app.globalData.matchQuota = res.data.matchQuota || null
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = res.data.hasFullBook
userInfo.purchasedSections = res.data.purchasedSections
wx.setStorageSync('userInfo', userInfo)
}
} catch (e) {
console.log('[My] 刷新购买状态失败:', e)
}
},
// 初始化用户状态
initUserStatus() {
const { isLoggedIn, userInfo } = app.globalData
@@ -538,6 +559,7 @@ Page({
try {
const result = await app.login()
if (result) {
await this.refreshPurchaseStatus()
this.initUserStatus()
this.setData({ showLoginModal: false, agreeProtocol: false })
wx.showToast({ title: '登录成功', icon: 'success' })
@@ -566,6 +588,7 @@ Page({
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
await this.refreshPurchaseStatus()
this.initUserStatus()
this.setData({ showLoginModal: false })
wx.showToast({ title: '登录成功', icon: 'success' })

View File

@@ -15,6 +15,10 @@ Page({
this.loadOrders()
},
onShow() {
this.loadOrders()
},
async loadOrders() {
this.setData({ loading: true })
try {

View File

@@ -880,7 +880,9 @@ Page({
// 更新全局购买状态
app.globalData.hasFullBook = res.data.hasFullBook
app.globalData.purchasedSections = res.data.purchasedSections || []
app.globalData.matchCount = res.data.matchCount ?? 0
app.globalData.matchQuota = res.data.matchQuota || null
// 更新用户信息中的购买记录
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = res.data.hasFullBook
@@ -890,7 +892,8 @@ Page({
console.log('[Pay] ✅ 购买状态已刷新:', {
hasFullBook: res.data.hasFullBook,
purchasedCount: res.data.purchasedSections.length
purchasedCount: res.data.purchasedSections.length,
matchCount: res.data.matchCount
})
}
} catch (e) {

View File

@@ -59,6 +59,7 @@ Page({
showPosterModal: false,
isGeneratingPoster: false,
posterQrSrc: '',
posterQrFilePath: '',
posterReferralLink: '',
posterNickname: '',
posterNicknameInitial: '',
@@ -283,53 +284,6 @@ Page({
})
},
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
shareToWechat() {
const { referralCode } = this.data
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
// 与 Next.js 完全相同的文案
const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事55个真实案例讲透创业的底层逻辑。
👉 点击阅读: ${referralLink}
#创业 #商业思维 #Soul派对`
wx.setClipboardData({
data: shareText,
success: () => {
wx.showModal({
title: '朋友圈文案已复制!',
content: '打开微信 → 发朋友圈 → 粘贴即可',
showCancel: false,
confirmText: '知道了'
})
}
})
},
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
handleMoreShare() {
const { referralCode } = this.data
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
// 与 Next.js 完全相同的文案
const shareText = `我正在读《一场SOUL的创业实验场》每天6-9点的真实商业故事推荐给你${referralLink}`
wx.setClipboardData({
data: shareText,
success: () => {
wx.showToast({
title: '分享文案已复制',
icon: 'success',
duration: 2000
})
}
})
},
// 生成推广海报 - 1:1 对齐 Next.js 设计
async generatePoster() {
wx.showLoading({ title: '生成中...', mask: true })
@@ -352,15 +306,18 @@ Page({
},
})
if (!res || !res.success || !res.image) {
// 接口返回 { success, image: "data:image/png;base64,...", scene }
const imageData = res?.image || res?.data?.image
if (!res || !res.success || !imageData) {
console.error('[Poster] 生成小程序码失败:', res)
throw new Error(res?.error || '生成小程序码失败')
throw new Error(res?.error || res?.message || '生成小程序码失败')
}
// 后端返回的是 data:image/png;base64,... 需要先写入本地临时文件,再作为 <image> 的 src
const base64Data = String(res.image).replace(/^data:image\/\w+;base64,/, '')
// 小程序 image 组件支持 base64 格式,直接使用;同时写入本地供预览用
const base64Str = String(imageData).trim()
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/poster_qrcode_${Date.now()}.png`
const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, '')
await new Promise((resolve, reject) => {
fs.writeFile({
@@ -375,10 +332,10 @@ Page({
})
})
console.log('[Poster] 小程序码已保存到本地:', filePath)
// 优先用 base64 直接显示(兼容性更好);预览时用本地路径
this.setData({
posterQrSrc: filePath,
posterQrSrc: base64Str,
posterQrFilePath: filePath,
posterReferralLink: '', // 小程序版本不再使用 H5 链接
posterNickname: nickname,
posterNicknameInitial: (nickname || '用').charAt(0),
@@ -389,7 +346,7 @@ Page({
console.error('[Poster] 生成二维码失败:', e)
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'none' })
this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '' })
this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterQrFilePath: '', posterReferralLink: '' })
}
},
@@ -570,56 +527,15 @@ Page({
// 预览二维码
previewPosterQr() {
const { posterQrSrc } = this.data
if (!posterQrSrc) return
wx.previewImage({ urls: [posterQrSrc] })
const { posterQrSrc, posterQrFilePath } = this.data
const url = posterQrFilePath || posterQrSrc
if (!url) return
wx.previewImage({ urls: [url] })
},
// 阻止冒泡
stopPropagation() {},
// 分享到朋友圈 - 随机文案
shareToMoments() {
// 10条随机文案基于书的内容
const shareTexts = [
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法受用终身\n\n#流量 #副业 #创业派对`,
`📚 一个70后大健康私域一个月150万流水是怎么做到的\n\n答案在《Soul创业派对》第9章全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》创业者必读\n\n#云阿米巴 #商业思维 #创业`,
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
`🚀 AI工具推广一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会你发现了吗\n\n#AI #副业 #商业机会`,
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案\n\n#美业 #轻创业 #月入十万`,
`🌟 3000万流水是怎么跑出来的\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑值得反复看\n\n#电商 #创业 #商业系统`,
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`,
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案\n\n#职业规划 #创业 #未来`
]
// 随机选择一条文案
const randomIndex = Math.floor(Math.random() * shareTexts.length)
const shareText = shareTexts[randomIndex]
wx.setClipboardData({
data: shareText,
success: () => {
wx.showModal({
title: '文案已复制',
content: '请打开微信朋友圈,粘贴分享文案,配合推广海报一起发布效果更佳!\n\n再次点击可获取新的随机文案',
showCancel: false,
confirmText: '去发朋友圈'
})
}
})
},
// 提现 - 直接到微信零钱
async handleWithdraw() {
const availableEarnings = this.data.availableEarningsNum || 0
@@ -891,16 +807,6 @@ Page({
}
},
// 分享到朋友圈
onShareTimeline() {
console.log('[Referral] 分享到朋友圈,推荐码:', this.data.referralCode)
return {
title: `Soul创业派对 - 62个真实商业案例`,
query: `ref=${this.data.referralCode}`
// 不设置 imageUrl使用小程序默认截图
}
},
goBack() {
wx.navigateBack()
},

View File

@@ -9,9 +9,6 @@
<view class="nav-btn" bindtap="showNotification">
<image class="nav-icon" src="/assets/icons/bell.svg" mode="aspectFit"></image>
</view>
<view class="nav-btn" bindtap="showSettings">
<image class="nav-icon" src="/assets/icons/settings.svg" mode="aspectFit"></image>
</view>
</view>
<text class="nav-title">分销中心</text>
<view class="nav-right-placeholder"></view>
@@ -179,28 +176,6 @@
</view>
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
</view>
<view class="share-item" bindtap="shareToWechat">
<view class="share-icon wechat">
<image class="icon-share-btn" src="/assets/icons/message-circle.svg" mode="aspectFit"></image>
</view>
<view class="share-info">
<text class="share-title">分享到朋友圈</text>
<text class="share-desc">复制文案发朋友圈</text>
</view>
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
</view>
<view class="share-item" bindtap="handleMoreShare">
<view class="share-icon link">
<image class="icon-share-btn" src="/assets/icons/share.svg" mode="aspectFit"></image>
</view>
<view class="share-info">
<text class="share-title">更多分享方式</text>
<text class="share-desc">使用系统分享功能</text>
</view>
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
</view>
</view>
<!-- 收益明细 - 增强版 -->
@@ -239,14 +214,7 @@
</view>
</view>
<!-- 空状态 - 对齐 Next.js -->
<view class="empty-earnings" wx:if="{{earningsDetails.length === 0 && activeBindings.length === 0}}">
<view class="empty-icon-wrapper">
<image class="empty-gift-icon" src="/assets/icons/gift.svg" mode="aspectFit"></image>
</view>
<text class="empty-title">暂无收益记录</text>
<text class="empty-desc">分享专属链接,好友购买即可获得 {{shareRate}}% 返利</text>
</view>
</view>
<!-- 海报生成弹窗 - 优化小程序显示 -->
@@ -315,7 +283,7 @@
</view>
<!-- 二维码 -->
<view class="poster-qr-wrap" bindtap="previewPosterQr">
<view class="poster-qr-wrap">
<image
class="poster-qr-img"
src="{{posterQrSrc}}"

View File

@@ -236,12 +236,85 @@
/* ????- ?? Next.js */
.empty-earnings { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 64rpx 40rpx; text-align: center; margin-bottom: 24rpx; }
.empty-icon-wrapper { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(28, 28, 30, 0.8); display: flex; align-items: center; justify-content: center; margin: 0 auto 32rpx; }
.empty-gift-icon { width: 64rpx; height: 64rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.empty-title { font-size: 30rpx; font-weight: 500; color: #fff; display: block; margin-bottom: 16rpx; }
.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; line-height: 1.5; }
/* 空状态 - 暂无收益 */
.empty-earnings {
background: linear-gradient(160deg, rgba(28, 28, 30, 0.95) 0%, rgba(15, 33, 55, 0.9) 100%);
backdrop-filter: blur(40rpx);
border: 2rpx solid rgba(0, 206, 209, 0.15);
border-radius: 32rpx;
padding: 80rpx 48rpx;
margin-bottom: 24rpx;
text-align: center;
position: relative;
overflow: hidden;
}
.empty-earnings::before {
content: '';
position: absolute;
top: -80rpx;
left: 50%;
transform: translateX(-50%);
width: 320rpx;
height: 160rpx;
background: radial-gradient(ellipse at center, rgba(0, 206, 209, 0.08) 0%, transparent 70%);
pointer-events: none;
}
.empty-earnings-inner {
position: relative;
z-index: 1;
}
.empty-earnings-icon-wrap {
position: relative;
width: 160rpx;
height: 160rpx;
margin: 0 auto 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-earnings-glow {
position: absolute;
inset: -20rpx;
background: radial-gradient(circle, rgba(0, 206, 209, 0.2) 0%, rgba(255, 215, 0, 0.06) 50%, transparent 70%);
border-radius: 50%;
}
.empty-earnings-icon {
width: 96rpx;
height: 96rpx;
position: relative;
z-index: 1;
filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
}
.empty-earnings-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
display: block;
margin-bottom: 20rpx;
letter-spacing: 1rpx;
}
.empty-earnings-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.65);
display: block;
line-height: 1.6;
margin-bottom: 12rpx;
}
.empty-earnings-rate {
display: inline-block;
font-size: 52rpx;
font-weight: 800;
color: #00CED1;
letter-spacing: 4rpx;
text-shadow: 0 0 40rpx rgba(0, 206, 209, 0.4);
}
.empty-earnings-hint {
font-size: 24rpx;
color: rgba(255, 215, 0, 0.85);
display: block;
margin-top: 8rpx;
font-weight: 500;
}
/* ===== Loading 遮罩(备用) ===== */