miniprogram: 用永平版本替换(含超级个体、会员详情、提现等)

- 来源: 一场soul的创业实验-永平/soul/miniprogram
- 新增: addresses/agreement/privacy/withdraw-records 等页面
- 新增: components/icon, utils/chapterAccessManager, readingTracker
- 删除: 上传脚本、部署说明等冗余文件
- 同步永平最新结构和功能

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-02-24 14:35:58 +08:00
parent b038a042c2
commit e5e6ffd7b1
99 changed files with 8370 additions and 3550 deletions

View File

@@ -49,7 +49,7 @@ Page({
// 加载书籍统计
async loadBookStats() {
try {
const res = await app.request('/api/book/stats')
const res = await app.request('/api/miniprogram/book/stats')
if (res && res.success) {
this.setData({
'bookInfo.totalChapters': res.data?.totalChapters || 62,

View File

@@ -0,0 +1,123 @@
/**
* 收货地址列表页
* 参考 Next.js: app/view/my/addresses/page.tsx
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
isLoggedIn: false,
addressList: [],
loading: true
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
this.checkLogin()
},
onShow() {
if (this.data.isLoggedIn) {
this.loadAddresses()
}
},
// 检查登录状态
checkLogin() {
const isLoggedIn = app.globalData.isLoggedIn
const userId = app.globalData.userInfo?.id
if (!isLoggedIn || !userId) {
wx.showModal({
title: '需要登录',
content: '请先登录后再管理收货地址',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/my/my' })
} else {
wx.navigateBack()
}
}
})
return
}
this.setData({ isLoggedIn: true })
this.loadAddresses()
},
// 加载地址列表
async loadAddresses() {
const userId = app.globalData.userInfo?.id
if (!userId) return
this.setData({ loading: true })
try {
const res = await app.request(`/api/miniprogram/user/addresses?userId=${userId}`)
if (res.success && res.list) {
this.setData({
addressList: res.list,
loading: false
})
} else {
this.setData({ addressList: [], loading: false })
}
} catch (e) {
console.error('加载地址列表失败:', e)
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
// 编辑地址
editAddress(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/addresses/edit?id=${id}` })
},
// 删除地址
deleteAddress(e) {
const id = e.currentTarget.dataset.id
wx.showModal({
title: '确认删除',
content: '确定要删除该收货地址吗?',
confirmColor: '#FF3B30',
success: async (res) => {
if (res.confirm) {
try {
const result = await app.request(`/api/miniprogram/user/addresses/${id}`, {
method: 'DELETE'
})
if (result.success) {
wx.showToast({ title: '删除成功', icon: 'success' })
this.loadAddresses()
} else {
wx.showToast({ title: result.message || '删除失败', icon: 'none' })
}
} catch (e) {
console.error('删除地址失败:', e)
wx.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
},
// 新增地址
addAddress() {
wx.navigateTo({ url: '/pages/addresses/edit' })
},
// 返回
goBack() {
wx.navigateBack()
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationStyle": "custom",
"enablePullDownRefresh": false
}

View File

@@ -0,0 +1,66 @@
<!--收货地址列表页-->
<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 style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:elif="{{addressList.length === 0}}">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
<text class="empty-tip">点击下方按钮添加</text>
</view>
<!-- 地址列表 -->
<view class="address-list" wx:else>
<view
class="address-card"
wx:for="{{addressList}}"
wx:key="id"
>
<view class="address-header">
<text class="receiver-name">{{item.name}}</text>
<text class="receiver-phone">{{item.phone}}</text>
<text class="default-tag" wx:if="{{item.isDefault}}">默认</text>
</view>
<text class="address-text">{{item.fullAddress}}</text>
<view class="address-actions">
<view
class="action-btn edit-btn"
bindtap="editAddress"
data-id="{{item.id}}"
>
<text class="action-icon">✏️</text>
<text class="action-text">编辑</text>
</view>
<view
class="action-btn delete-btn"
bindtap="deleteAddress"
data-id="{{item.id}}"
>
<text class="action-icon">🗑️</text>
<text class="action-text">删除</text>
</view>
</view>
</view>
</view>
<!-- 新增按钮 -->
<view class="add-btn" bindtap="addAddress">
<text class="add-icon"></text>
<text class="add-text">新增收货地址</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,217 @@
/**
* 收货地址列表页样式
*/
.page {
min-height: 100vh;
background: #000000;
padding-bottom: 200rpx;
}
/* ===== 导航栏 ===== */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(40rpx);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
height: 88rpx;
}
.nav-back {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.nav-back:active {
background: rgba(255, 255, 255, 0.15);
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
line-height: 1;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.nav-placeholder {
width: 64rpx;
}
/* ===== 内容区 ===== */
.content {
padding: 32rpx;
}
/* ===== 加载状态 ===== */
.loading-state {
padding: 240rpx 0;
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 空状态 ===== */
.empty-state {
padding: 240rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 24rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 16rpx;
}
.empty-tip {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 地址列表 ===== */
.address-list {
margin-bottom: 24rpx;
}
.address-card {
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
padding: 32rpx;
margin-bottom: 24rpx;
}
/* 地址头部 */
.address-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
}
.receiver-name {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.receiver-phone {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
.default-tag {
font-size: 22rpx;
color: #00CED1;
background: rgba(0, 206, 209, 0.2);
padding: 6rpx 16rpx;
border-radius: 8rpx;
margin-left: auto;
}
/* 地址文本 */
.address-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
display: block;
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.05);
}
/* 操作按钮 */
.address-actions {
display: flex;
justify-content: flex-end;
gap: 32rpx;
}
.action-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 0;
}
.action-btn:active {
opacity: 0.6;
}
.edit-btn {
color: #00CED1;
}
.delete-btn {
color: #FF3B30;
}
.action-icon {
font-size: 28rpx;
}
.action-text {
font-size: 28rpx;
}
/* ===== 新增按钮 ===== */
.add-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 32rpx;
background: #00CED1;
border-radius: 24rpx;
font-weight: 600;
margin-top: 48rpx;
}
.add-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.add-icon {
font-size: 36rpx;
color: #000000;
}
.add-text {
font-size: 32rpx;
color: #000000;
}

View File

@@ -0,0 +1,201 @@
/**
* 地址编辑页(新增/编辑)
* 参考 Next.js: app/view/my/addresses/[id]/page.tsx
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
isEdit: false, // 是否为编辑模式
addressId: null,
// 表单数据
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false,
// 地区选择器
region: [],
saving: false
},
onLoad(options) {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
// 如果有 id 参数,则为编辑模式
if (options.id) {
this.setData({
isEdit: true,
addressId: options.id
})
this.loadAddress(options.id)
}
},
// 加载地址详情(编辑模式)
async loadAddress(id) {
wx.showLoading({ title: '加载中...', mask: true })
try {
const res = await app.request(`/api/miniprogram/user/addresses/${id}`)
if (res.success && res.data) {
const addr = res.data
this.setData({
name: addr.name || '',
phone: addr.phone || '',
province: addr.province || '',
city: addr.city || '',
district: addr.district || '',
detail: addr.detail || '',
isDefault: addr.isDefault || false,
region: [addr.province, addr.city, addr.district]
})
} else {
wx.showToast({ title: '加载失败', icon: 'none' })
}
} catch (e) {
console.error('加载地址详情失败:', e)
wx.showToast({ title: '加载失败', icon: 'none' })
} finally {
wx.hideLoading()
}
},
// 表单输入
onNameInput(e) {
this.setData({ name: e.detail.value })
},
onPhoneInput(e) {
this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) })
},
onDetailInput(e) {
this.setData({ detail: e.detail.value })
},
// 地区选择
onRegionChange(e) {
const region = e.detail.value
this.setData({
region,
province: region[0],
city: region[1],
district: region[2]
})
},
// 切换默认地址
onDefaultChange(e) {
this.setData({ isDefault: e.detail.value })
},
// 表单验证
validateForm() {
const { name, phone, province, city, district, detail } = this.data
if (!name || name.trim().length === 0) {
wx.showToast({ title: '请输入收货人姓名', icon: 'none' })
return false
}
if (!phone || phone.length !== 11) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return false
}
if (!province || !city || !district) {
wx.showToast({ title: '请选择省市区', icon: 'none' })
return false
}
if (!detail || detail.trim().length === 0) {
wx.showToast({ title: '请输入详细地址', icon: 'none' })
return false
}
return true
},
// 保存地址
async saveAddress() {
if (!this.validateForm()) return
if (this.data.saving) return
this.setData({ saving: true })
wx.showLoading({ title: '保存中...', mask: true })
const { isEdit, addressId, name, phone, province, city, district, detail, isDefault } = this.data
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.hideLoading()
wx.showToast({ title: '请先登录', icon: 'none' })
this.setData({ saving: false })
return
}
const addressData = {
userId,
name,
phone,
province,
city,
district,
detail,
fullAddress: `${province}${city}${district}${detail}`,
isDefault
}
try {
let res
if (isEdit) {
// 编辑模式 - PUT 请求
res = await app.request(`/api/miniprogram/user/addresses/${addressId}`, {
method: 'PUT',
data: addressData
})
} else {
// 新增模式 - POST 请求
res = await app.request('/api/miniprogram/user/addresses', {
method: 'POST',
data: addressData
})
}
if (res.success) {
wx.hideLoading()
wx.showToast({
title: isEdit ? '保存成功' : '添加成功',
icon: 'success'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
} else {
wx.hideLoading()
wx.showToast({ title: res.message || '保存失败', icon: 'none' })
this.setData({ saving: false })
}
} catch (e) {
console.error('保存地址失败:', e)
wx.hideLoading()
wx.showToast({ title: '保存失败', icon: 'none' })
this.setData({ saving: false })
}
},
// 返回
goBack() {
wx.navigateBack()
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationStyle": "custom",
"enablePullDownRefresh": false
}

View File

@@ -0,0 +1,101 @@
<!--地址编辑页-->
<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">{{isEdit ? '编辑地址' : '新增地址'}}</text>
<view class="nav-placeholder"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<view class="form-card">
<!-- 收货人 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">👤</text>
<text class="label-text">收货人</text>
</view>
<input
class="form-input"
placeholder="请输入收货人姓名"
placeholder-class="input-placeholder"
value="{{name}}"
bindinput="onNameInput"
/>
</view>
<!-- 手机号 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">📱</text>
<text class="label-text">手机号</text>
</view>
<input
class="form-input"
type="number"
placeholder="请输入11位手机号"
placeholder-class="input-placeholder"
value="{{phone}}"
bindinput="onPhoneInput"
maxlength="11"
/>
</view>
<!-- 地区选择 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">📍</text>
<text class="label-text">所在地区</text>
</view>
<picker
mode="region"
value="{{region}}"
bindchange="onRegionChange"
class="region-picker"
>
<view class="picker-value">
{{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}
</view>
</picker>
</view>
<!-- 详细地址 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">🏠</text>
<text class="label-text">详细地址</text>
</view>
<textarea
class="form-textarea"
placeholder="请输入街道、门牌号等详细地址"
placeholder-class="input-placeholder"
value="{{detail}}"
bindinput="onDetailInput"
maxlength="200"
auto-height
/>
</view>
<!-- 设为默认 -->
<view class="form-item form-switch">
<view class="form-label">
<text class="label-icon">⭐</text>
<text class="label-text">设为默认地址</text>
</view>
<switch
checked="{{isDefault}}"
bindchange="onDefaultChange"
color="#00CED1"
/>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-btn {{saving ? 'btn-disabled' : ''}}" bindtap="saveAddress">
{{saving ? '保存中...' : '保存'}}
</view>
</view>
</view>

View File

@@ -0,0 +1,186 @@
/**
* 地址编辑页样式
*/
.page {
min-height: 100vh;
background: #000000;
padding-bottom: 200rpx;
}
/* ===== 导航栏 ===== */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(40rpx);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
height: 88rpx;
}
.nav-back {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.nav-back:active {
background: rgba(255, 255, 255, 0.15);
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
line-height: 1;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.nav-placeholder {
width: 64rpx;
}
/* ===== 内容区 ===== */
.content {
padding: 32rpx;
}
/* ===== 表单卡片 ===== */
.form-card {
background: #1c1c1e;
border-radius: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
padding: 32rpx;
margin-bottom: 32rpx;
}
/* 表单项 */
.form-item {
margin-bottom: 32rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.label-icon {
font-size: 28rpx;
}
.label-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
/* 输入框 */
.form-input {
width: 100%;
padding: 24rpx 32rpx;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
color: #ffffff;
font-size: 28rpx;
}
.form-input:focus {
border-color: rgba(0, 206, 209, 0.5);
}
.input-placeholder {
color: rgba(255, 255, 255, 0.3);
}
/* 地区选择器 */
.region-picker {
width: 100%;
padding: 24rpx 32rpx;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
}
.picker-value {
color: #ffffff;
font-size: 28rpx;
}
.picker-value:empty::before {
content: '请选择省市区';
color: rgba(255, 255, 255, 0.3);
}
/* 多行文本框 */
.form-textarea {
width: 100%;
padding: 24rpx 32rpx;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
color: #ffffff;
font-size: 28rpx;
min-height: 160rpx;
line-height: 1.6;
}
.form-textarea:focus {
border-color: rgba(0, 206, 209, 0.5);
}
/* 开关项 */
.form-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
}
.form-switch .form-label {
margin-bottom: 0;
}
/* ===== 保存按钮 ===== */
.save-btn {
padding: 32rpx;
background: #00CED1;
border-radius: 24rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #000000;
margin-top: 48rpx;
}
.save-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.btn-disabled {
opacity: 0.5;
pointer-events: none;
}

View File

@@ -0,0 +1,21 @@
/**
* Soul创业派对 - 用户协议
* 审核要求:登录前可点击《用户协议》查看完整内容
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
},
goBack() {
wx.navigateBack()
}
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}

View File

@@ -0,0 +1,37 @@
<!--用户协议页 - 审核要求可点击查看-->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">←</view>
<text class="nav-title">用户协议</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<scroll-view class="content" scroll-y enhanced show-scrollbar>
<view class="doc-card">
<text class="doc-title">Soul创业实验 用户服务协议</text>
<text class="doc-update">更新日期:以小程序内展示为准</text>
<text class="doc-section">一、接受条款</text>
<text class="doc-p">欢迎使用 Soul创业实验 小程序。使用本服务即表示您已阅读、理解并同意受本协议约束。若不同意,请勿使用本服务。</text>
<text class="doc-section">二、服务说明</text>
<text class="doc-p">本小程序提供《一场Soul的创业实验》等数字内容阅读、推广与相关服务。我们保留变更、中断或终止部分或全部服务的权利。</text>
<text class="doc-section">三、用户行为规范</text>
<text class="doc-p">您应合法、合规使用本服务,不得利用本服务从事违法违规活动,不得侵犯他人权益。违规行为可能导致账号限制或追究责任。</text>
<text class="doc-section">四、知识产权</text>
<text class="doc-p">本小程序内全部内容(包括但不限于文字、图片、音频、视频)的知识产权归本小程序或权利人所有,未经授权不得复制、传播或用于商业用途。</text>
<text class="doc-section">五、免责与限制</text>
<text class="doc-p">在法律允许范围内,因网络、设备或不可抗力导致的服务中断或数据丢失,我们尽力减少损失但不承担超出法律规定的责任。</text>
<text class="doc-section">六、协议变更</text>
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
<text class="doc-section">七、联系我们</text>
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,11 @@
.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.95); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.nav-placeholder { width: 72rpx; }
.content { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }

View File

@@ -199,7 +199,10 @@ Page({
{ id: 'appendix-1', title: '附录1Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2创业者自检清单' },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源' }
]
],
// 每日新增章节
dailyChapters: []
},
onLoad() {
@@ -208,12 +211,28 @@ Page({
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
this.loadDailyChapters()
this.loadTotalFromServer()
},
async loadTotalFromServer() {
try {
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
if (res && res.total) {
this.setData({ totalSections: res.total })
}
} catch (e) {}
},
onShow() {
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
const tabBar = this.getTabBar()
if (tabBar.updateSelected) {
tabBar.updateSelected()
} else {
tabBar.setData({ selected: 1 })
}
}
this.updateUserStatus()
},
@@ -247,5 +266,33 @@ Page({
// 返回首页
goBack() {
wx.switchTab({ url: '/pages/index/index' })
},
async loadDailyChapters() {
try {
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const daily = chapters
.filter(c => (c.sectionOrder || c.sort_order || 0) > 62)
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
.slice(0, 20)
.map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
return {
id: c.id,
title: c.section_title || c.title || c.sectionTitle,
price: c.price || 1,
dateStr: `${d.getMonth()+1}/${d.getDate()}`
}
})
if (daily.length > 0) {
this.setData({ dailyChapters: daily, totalSections: 62 + daily.length })
}
} catch (e) { console.log('[Chapters] 加载最新新增失败:', e) }
},
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
}
})

View File

@@ -4,7 +4,13 @@
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-left">
<view class="search-btn" bindtap="goToSearch">
<text class="search-icon">🔍</text>
</view>
</view>
<view class="nav-title brand-color">目录</view>
<view class="nav-right"></view>
</view>
</view>

View File

@@ -26,12 +26,45 @@
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
}
.nav-left,
.nav-right {
width: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
flex: 1;
text-align: center;
}
/* 搜索按钮 */
.search-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #2c2c2e;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.search-btn:active {
background: #3c3c3e;
transform: scale(0.95);
}
.search-icon {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.6);
}
.brand-color {
@@ -47,7 +80,7 @@
display: flex;
align-items: center;
gap: 24rpx;
margin: 32rpx;
margin: 32rpx 32rpx 24rpx 32rpx;
padding: 32rpx;
}
@@ -55,6 +88,7 @@
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
border-radius: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
}
.book-icon {
@@ -442,6 +476,21 @@
color: rgba(255, 255, 255, 0.3);
}
/* ===== 每日新增章节 ===== */
.daily-section { margin: 20rpx 0; padding: 24rpx; background: rgba(255,215,0,0.04); border: 1rpx solid rgba(255,215,0,0.12); border-radius: 16rpx; }
.daily-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; }
.daily-title { font-size: 30rpx; font-weight: 600; color: #FFD700; }
.daily-badge { font-size: 22rpx; background: #FFD700; color: #000; padding: 2rpx 12rpx; border-radius: 20rpx; font-weight: bold; }
.daily-list { display: flex; flex-direction: column; gap: 12rpx; }
.daily-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: rgba(255,255,255,0.03); border-radius: 12rpx; }
.daily-left { display: flex; align-items: center; gap: 10rpx; flex: 1; min-width: 0; }
.daily-new-tag { font-size: 18rpx; background: #FF4444; color: #fff; padding: 2rpx 8rpx; border-radius: 6rpx; font-weight: bold; flex-shrink: 0; }
.daily-item-title { font-size: 26rpx; color: rgba(255,255,255,0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.daily-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
.daily-price { font-size: 26rpx; color: #FFD700; font-weight: 600; }
.daily-date { font-size: 20rpx; color: rgba(255,255,255,0.35); }
.daily-note { display: block; font-size: 22rpx; color: rgba(255,215,0,0.5); margin-top: 12rpx; text-align: center; }
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;

View File

@@ -1,166 +1,289 @@
/**
* Soul创业派对 - 首页
* 开发: 卡若
* 技术支持: 存客宝
*/
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户信息
isLoggedIn: false,
hasFullBook: false,
purchasedCount: 0,
readCount: 0,
// 书籍数据
totalSections: 62,
bookData: [],
featuredSections: [],
// 推荐章节
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: '真实的赚钱' }
],
// 最新章节(动态计算)
latestSection: null,
latestLabel: '最新更新',
// 内容概览
partsList: [
{ id: 'part-1', number: '一', title: '真实的人', subtitle: '人与人之间的底层逻辑' },
{ id: 'part-2', number: '二', title: '真实的行业', subtitle: '电商、内容、传统行业解析' },
{ id: 'part-3', number: '三', title: '真实的错误', subtitle: '我和别人犯过的错' },
{ id: 'part-4', number: '四', title: '真实的赚钱', subtitle: '底层结构与真实案例' },
{ id: 'part-5', number: '五', title: '真实的社会', subtitle: '未来职业与商业生态' }
],
// 超级个体VIP会员
superMembers: [],
// 超级个体VIP会员展示
vipMembers: [],
// 最新新增章节
latestChapters: [],
// 加载状态
loading: true
},
onLoad(options) {
console.log('[Index] ===== onLoad 触发 =====')
// 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
// 初始化数据
this.initData()
},
onShow() {
console.log('[Index] onShow 触发')
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 0 })
const tabBar = this.getTabBar()
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
// 主动触发配置加载
if (tabBar && tabBar.loadFeatureConfig) {
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
tabBar.loadFeatureConfig()
}
// 更新选中状态
if (tabBar && tabBar.updateSelected) {
tabBar.updateSelected()
} else if (tabBar) {
tabBar.setData({ selected: 0 })
}
} else {
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
// 更新用户状态
this.updateUserStatus()
},
// 初始化数据
async initData() {
this.setData({ loading: true })
try {
await Promise.all([
this.loadBookData(),
this.loadLatestSection(),
this.loadHotSections(),
this.loadVipMembers()
])
await this.loadBookData()
await this.loadFeaturedFromServer()
this.loadSuperMembers()
this.loadLatestChapters()
} catch (e) {
console.error('初始化失败:', e)
this.computeLatestSectionFallback()
} finally {
this.setData({ loading: false })
}
},
// 从hot接口获取精选推荐按阅读量排序
async loadHotSections() {
async loadSuperMembers() {
try {
const res = await app.request('/api/book/hot')
if (res?.success && res.chapters?.length) {
this.setData({ featuredSections: res.chapters.slice(0, 5) })
// 优先加载VIP会员
let members = []
try {
const res = await app.request({ url: '/api/vip/members', silent: true })
if (res && res.success && res.data) {
members = res.data.filter(u => u.avatar || u.vip_avatar).slice(0, 4).map(u => ({
id: u.id, name: u.vip_name || u.nickname || '会员',
avatar: u.vip_avatar || u.avatar, isVip: true
}))
}
} catch (e) {}
// 不足4个则用有头像的普通用户补充
if (members.length < 4) {
try {
const dbRes = await app.request({ url: '/api/miniprogram/users?limit=20', silent: true })
if (dbRes && dbRes.success && dbRes.data) {
const existIds = new Set(members.map(m => m.id))
const extra = dbRes.data
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
.slice(0, 4 - members.length)
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
members = members.concat(extra)
}
} catch (e) {}
}
} catch (e) {
console.log('[Index] 热门章节加载失败', e)
}
this.setData({ superMembers: members })
} catch (e) { console.log('[Index] 加载超级个体失败:', e) }
},
// 加载VIP会员列表
async loadVipMembers() {
// 从服务端获取精选推荐加权算法阅读量50% + 时效30% + 付款率20%)和最新更新
async loadFeaturedFromServer() {
try {
const res = await app.request('/api/vip/members?limit=8')
if (res?.success && res.data?.length) {
this.setData({ vipMembers: res.data })
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
const chapters = (res && res.data) ? res.data : (res && res.chapters) ? res.chapters : []
let featured = (res && res.featuredSections) ? res.featuredSections : []
// 服务端未返回精选时从前端按更新时间取前3条有效章节作为回退
if (featured.length === 0 && chapters.length > 0) {
const valid = chapters.filter(c => {
const id = (c.id || '').toLowerCase()
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
&& !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
featured = valid
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
.slice(0, 5)
}
if (featured.length > 0) {
this.setData({
featuredSections: featured.slice(0, 3).map(s => ({
id: s.id || s.section_id,
title: s.section_title || s.title,
part: (s.cleanPartTitle || s.part_title || '').replace(/[_|]/g, ' ').trim()
}))
})
}
// 最新更新 = 按 updated_at 排序第1篇排除序言/尾声/附录)
const validChapters = chapters.filter(c => {
const id = (c.id || '').toLowerCase()
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
&& !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (validChapters.length > 0) {
validChapters.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
const latest = validChapters[0]
this.setData({
latestSection: {
id: latest.id || latest.section_id,
title: latest.section_title || latest.title,
part: latest.cleanPartTitle || latest.part_title || ''
}
})
}
} catch (e) {
console.log('[Index] VIP会员加载失败', e)
console.log('[Index] 从服务端加载推荐失败,使用默认:', e)
}
},
async loadLatestSection() {
try {
const res = await app.request('/api/book/latest-chapters')
if (res?.success && res.banner) {
this.setData({ latestSection: res.banner, latestLabel: res.label || '最新更新' })
return
}
} catch (e) {
console.warn('latest-chapters API 失败:', e.message)
}
this.computeLatestSectionFallback()
},
computeLatestSectionFallback() {
const bookData = app.globalData.bookData || this.data.bookData || []
let sections = []
if (Array.isArray(bookData)) {
sections = bookData.map(s => ({
id: s.id, title: s.title || s.sectionTitle,
part: s.part || s.sectionTitle || '真实的行业',
isFree: s.isFree, price: s.price
}))
}
const free = sections.filter(s => s.isFree !== false && (s.price === 0 || !s.price))
const candidates = free.length > 0 ? free : sections
if (!candidates.length) {
this.setData({ latestSection: { id: '1.1', title: '开始阅读', part: '真实的人' }, latestLabel: '为你推荐' })
return
}
const idx = Math.floor(Math.random() * candidates.length)
this.setData({ latestSection: candidates[idx], latestLabel: '为你推荐' })
},
async loadBookData() {
try {
const res = await app.request('/api/book/all-chapters')
if (res?.data) {
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
this.setData({
bookData: res.data,
totalSections: res.totalSections || res.data?.length || 62
bookData: chapters,
totalSections: res.total || chapters.length || 62
})
}
} catch (e) { console.error('加载书籍数据失败:', e) }
} catch (e) {
console.error('加载书籍数据失败:', e)
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
this.setData({
isLoggedIn, hasFullBook,
purchasedCount: hasFullBook ? this.data.totalSections : (purchasedSections?.length || 0)
isLoggedIn,
hasFullBook,
readCount
})
},
// 阅读时记录行为轨迹
// 跳转到目录
goToChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页
goToRead(e) {
const id = e.currentTarget.dataset.id
// 记录阅读行为(异步,不阻塞跳转)
const userId = app.globalData.userInfo?.id
if (userId) {
app.request('/api/user/track', {
method: 'POST',
data: { userId, action: 'view_chapter', target: id }
}).catch(() => {})
}
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
goToChapters() { wx.switchTab({ url: '/pages/chapters/chapters' }) },
goToSearch() { wx.navigateTo({ url: '/pages/search/search' }) },
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goToMy() { wx.switchTab({ url: '/pages/my/my' }) },
// 跳转到匹配页
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
goToVip() {
wx.navigateTo({ url: '/pages/vip/vip' })
},
goToSuperList() {
wx.switchTab({ url: '/pages/match/match' })
},
async loadLatestChapters() {
try {
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const latest = chapters
.filter(c => (c.sectionOrder || c.sort_order || 0) > 62)
.sort((a, b) => 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())
return {
id: c.id,
title: c.section_title || c.title || c.sectionTitle,
price: c.price || 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
this.setData({ latestChapters: latest })
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
},
// 跳转到我的页面
goToMy() {
wx.switchTab({ url: '/pages/my/my' })
},
// 下拉刷新
async onPullDownRefresh() {
await this.initData()
this.updateUserStatus()

View File

@@ -1,11 +1,16 @@
<!--Soul创业派对 - 首页-->
<!--pages/index/index.wxml-->
<!--Soul创业派对 - 首页 1:1还原Web版本-->
<view class="page page-transition">
<!-- 自定义导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<view class="header" style="padding-top: {{statusBarHeight}}px;">
<!-- 顶部区域 -->
<view class="header">
<view class="header-content">
<view class="logo-section">
<view class="logo-icon"><text class="logo-text">S</text></view>
<view class="logo-icon">
<text class="logo-text">S</text>
</view>
<view class="logo-info">
<view class="logo-title">
<text class="text-white">Soul</text>
@@ -18,17 +23,23 @@
<view class="chapter-badge">{{totalSections}}章</view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar" bindtap="goToSearch">
<view class="search-icon"><view class="search-circle"></view><view class="search-handle"></view></view>
<view class="search-icon">
<view class="search-circle"></view>
<view class="search-handle"></view>
</view>
<text class="search-placeholder">搜索章节标题或内容...</text>
</view>
</view>
<!-- 主内容区 -->
<view class="main-content">
<!-- Banner - 最新章节 -->
<!-- Banner卡片 - 最新章节 -->
<view class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}">
<view class="banner-glow"></view>
<view class="banner-tag">{{latestLabel}}</view>
<view class="banner-tag">最新更新</view>
<view class="banner-title">{{latestSection.title}}</view>
<view class="banner-part">{{latestSection.part}}</view>
<view class="banner-action">
@@ -41,20 +52,20 @@
<view class="progress-card card">
<view class="progress-header">
<text class="progress-title">我的阅读</text>
<text class="progress-count">{{purchasedCount}}/{{totalSections}}章</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: {{(purchasedCount / totalSections) * 100}}%;"></view>
<view class="progress-bar-fill" style="width: {{totalSections > 0 ? (readCount / totalSections) * 100 : 0}}%;"></view>
</view>
</view>
<view class="progress-stats">
<view class="stat-item">
<text class="stat-value brand-color">{{purchasedCount}}</text>
<text class="stat-value brand-color">{{readCount}}</text>
<text class="stat-label">已读</text>
</view>
<view class="stat-item">
<text class="stat-value">{{totalSections - purchasedCount}}</text>
<text class="stat-value">{{totalSections - readCount}}</text>
<text class="stat-label">待读</text>
</view>
<view class="stat-item">
@@ -68,20 +79,56 @@
</view>
</view>
<!-- 精选推荐(按阅读量排序 -->
<!-- 超级个体(在阅读下方,精选上方 -->
<view class="section">
<view class="section-header">
<text class="section-title">超级个体</text>
<view class="section-more" bindtap="goToSuperList">
<text class="more-text">查看全部</text>
<text class="more-arrow">→</text>
</view>
</view>
<view class="super-grid" wx:if="{{superMembers.length > 0}}">
<view
class="super-item"
wx:for="{{superMembers}}"
wx:key="id"
bindtap="goToMemberDetail"
data-id="{{item.id}}"
>
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
</view>
<text class="super-name">{{item.name}}</text>
</view>
</view>
<view class="super-empty" wx:else>
<text class="super-empty-text">成为会员,展示你的项目</text>
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
</view>
</view>
<!-- 精选推荐 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选推荐</text>
<view class="section-more" bindtap="goToChapters">
<text class="more-text">查看全部</text><text class="more-arrow">→</text>
<text class="more-text">查看全部</text>
<text class="more-arrow">→</text>
</view>
</view>
<view class="featured-list">
<view class="featured-item" wx:for="{{featuredSections}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
<view
class="featured-item"
wx:for="{{featuredSections}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="featured-content">
<view class="featured-meta">
<text class="featured-id brand-color">{{item.id}}</text>
<text class="tag {{item.tagClass || 'tag-pink'}}">{{item.tag || '热门'}}</text>
</view>
<text class="featured-title">{{item.title}}</text>
<text class="featured-part">{{item.part}}</text>
@@ -91,35 +138,29 @@
</view>
</view>
<!-- 超级个体VIP会员展示 -->
<view class="section" wx:if="{{vipMembers.length > 0}}">
<!-- 最新新增(从目录移到此处,精选推荐下方 -->
<view class="section" wx:if="{{latestChapters.length > 0}}">
<view class="section-header">
<text class="section-title">超级个体</text>
<text class="section-title">最新新增</text>
<view class="daily-badge-wrap">
<text class="daily-badge">+{{latestChapters.length}}</text>
</view>
</view>
<view class="members-grid">
<view class="member-cell" wx:for="{{vipMembers}}" wx:key="id" bindtap="goToMemberDetail" data-id="{{item.id}}">
<view class="member-avatar-wrap">
<image class="member-avatar" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
<view class="member-avatar-placeholder" wx:else>
<text>{{item.name[0] || '创'}}</text>
</view>
<view class="member-vip-dot">V</view>
<view class="latest-list">
<view class="latest-item" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
<view class="latest-left">
<text class="latest-new-tag">NEW</text>
<text class="latest-title">{{item.title}}</text>
</view>
<view class="latest-right">
<text class="latest-price">¥{{item.price}}</text>
<text class="latest-date">{{item.dateStr}}</text>
</view>
<text class="member-name">{{item.name}}</text>
<text class="member-project" wx:if="{{item.project}}">{{item.project}}</text>
</view>
</view>
</view>
<!-- 序言入口 -->
<view class="preface-card" bindtap="goToRead" data-id="preface">
<view class="preface-content">
<text class="preface-title">序言</text>
<text class="preface-desc">为什么我每天早上6点在Soul开播?</text>
</view>
<view class="tag tag-free">免费</view>
</view>
</view>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</view>

View File

@@ -498,78 +498,135 @@
color: rgba(255, 255, 255, 0.6);
}
/* ===== 创业老板排行 ===== */
.members-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
padding: 0 8rpx;
/* ===== 超级个体 ===== */
.super-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx 16rpx;
}
.member-cell {
width: calc(25% - 15rpx);
.super-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 0;
gap: 10rpx;
}
.member-avatar-wrap {
position: relative;
width: 100rpx;
height: 100rpx;
margin-bottom: 10rpx;
}
.member-avatar {
width: 100rpx;
height: 100rpx;
.super-avatar {
width: 108rpx;
height: 108rpx;
border-radius: 50%;
border: 3rpx solid #FFD700;
}
.member-avatar-placeholder {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #1c1c1e, #2c2c2e);
border: 3rpx solid #FFD700;
overflow: hidden;
background: rgba(0,206,209,0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
border: 3rpx solid rgba(255,255,255,0.1);
}
.super-avatar-vip {
border: 3rpx solid #FFD700;
box-shadow: 0 0 12rpx rgba(255,215,0,0.3);
}
.super-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.super-avatar-text {
font-size: 40rpx;
font-weight: 600;
color: #00CED1;
}
.super-name {
font-size: 22rpx;
color: rgba(255,255,255,0.7);
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.super-empty {
padding: 32rpx;
text-align: center;
background: rgba(255,255,255,0.03);
border-radius: 16rpx;
}
.super-empty-text {
font-size: 24rpx;
color: rgba(255,255,255,0.4);
display: block;
margin-bottom: 16rpx;
}
.super-empty-btn {
font-size: 26rpx;
color: #00CED1;
}
/* ===== 最新新增 ===== */
.daily-badge-wrap {
display: inline-flex;
align-items: center;
}
.daily-badge {
background: #FF4500;
color: #fff;
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 16rpx;
margin-left: 8rpx;
}
.latest-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.latest-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background: rgba(255,255,255,0.04);
border-radius: 12rpx;
border-left: 4rpx solid #FF4500;
}
.latest-left {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.latest-new-tag {
font-size: 18rpx;
font-weight: 700;
color: #FF4500;
background: rgba(255,69,0,0.15);
padding: 2rpx 10rpx;
border-radius: 6rpx;
flex-shrink: 0;
}
.latest-title {
font-size: 26rpx;
color: rgba(255,255,255,0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.latest-right {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
margin-left: 12rpx;
}
.latest-price {
font-size: 26rpx;
font-weight: 600;
color: #FFD700;
}
.member-vip-dot {
position: absolute;
bottom: 0;
right: 0;
width: 30rpx;
height: 30rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #000;
font-size: 16rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #000;
}
.member-name {
font-size: 24rpx;
color: rgba(255,255,255,0.9);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140rpx;
}
.member-project {
font-size: 20rpx;
.latest-date {
font-size: 22rpx;
color: rgba(255,255,255,0.4);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140rpx;
margin-top: 4rpx;
}
/* ===== 底部留白 ===== */

View File

@@ -83,7 +83,12 @@ Page({
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 })
const tabBar = this.getTabBar()
if (tabBar.updateSelected) {
tabBar.updateSelected()
} else {
tabBar.setData({ selected: 2 })
}
}
this.initUserStatus()
},
@@ -91,7 +96,7 @@ Page({
// 加载匹配配置
async loadMatchConfig() {
try {
const res = await app.request('/api/match/config', {
const res = await app.request({ url: '/api/match/config', silent: true, method: 'GET',
method: 'GET'
})
@@ -316,7 +321,7 @@ Page({
// 从数据库获取真实用户匹配
let matchedUser = null
try {
const res = await app.request('/api/match/users', {
const res = await app.request({ url: '/api/match/users', silent: true,
method: 'POST',
data: {
matchType: this.data.selectedType,
@@ -400,7 +405,7 @@ Page({
// 上报匹配行为
async reportMatch(matchedUser) {
try {
await app.request('/api/ckb/match', {
await app.request({ url: '/api/ckb/match', silent: true,
method: 'POST',
data: {
matchType: this.data.selectedType,
@@ -514,7 +519,7 @@ Page({
this.setData({ isJoining: true, joinError: '' })
try {
const res = await app.request('/api/ckb/join', {
const res = await app.request('/api/miniprogram/ckb/join', {
method: 'POST',
data: {
type: joinType,
@@ -586,6 +591,8 @@ Page({
return
}
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
@@ -595,7 +602,8 @@ Page({
productId: 'match_1',
amount: 1,
description: '匹配次数x1',
userId: app.globalData.userInfo?.id || ''
userId: app.globalData.userInfo?.id || '',
referralCode: referralCode || undefined
}
})

View File

@@ -1,11 +1,14 @@
const app = getApp()
const MOCK_ENRICHMENT = [
{ mbti: 'ENTJ', region: '深圳', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', bestMonth: '做跨境电商独立站单月GMV突破200万净利润35万', achievement: '从0到1搭建了30人的电商团队年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
{ mbti: 'INFP', region: '杭州', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', bestMonth: '旅游账号30天涨粉10万带货佣金收入12万', achievement: '帮助3个素人打造个人IP每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵全网粉丝50万+' },
{ mbti: 'INTP', region: '厦门', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', bestMonth: 'AI客服系统外包项目单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
{ mbti: 'ESTP', region: '成都', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', bestMonth: '撮合景区合作居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
]
Page({
data: {
statusBarHeight: 44,
member: null,
loading: true
},
data: { statusBarHeight: 44, member: null, loading: true },
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
@@ -14,24 +17,75 @@ Page({
async loadMember(id) {
try {
const res = await app.request(`/api/vip/members?id=${id}`)
if (res?.success) {
this.setData({ member: res.data, loading: false })
} else {
this.setData({ loading: false })
wx.showToast({ title: '会员不存在', icon: 'none' })
const res = await app.request({ url: `/api/vip/members?id=${id}`, silent: true })
if (res?.success && res.data) {
const d = Array.isArray(res.data) ? res.data[0] : res.data
if (d) { this.setData({ member: this.enrichAndFormat(d), loading: false }); return }
}
} catch (e) {
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
} catch (e) {}
try {
const dbRes = await app.request({ url: `/api/miniprogram/users?id=${id}`, silent: true })
if (dbRes?.success && dbRes.data) {
const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data
if (u) {
this.setData({ member: this.enrichAndFormat({
id: u.id, name: u.vip_name || u.nickname || '创业者',
avatar: u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1,
contactRaw: u.vip_contact || u.phone || '', project: u.vip_project || '',
bio: u.vip_bio || '', mbti: '', region: '', skills: '',
}), loading: false })
return
}
}
} catch (e) {}
this.setData({ loading: false })
},
enrichAndFormat(raw) {
const hash = (raw.id || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0)
const mock = MOCK_ENRICHMENT[hash % MOCK_ENRICHMENT.length]
const merged = {
id: raw.id,
name: raw.name || raw.vip_name || raw.nickname || '创业者',
avatar: raw.avatar || raw.vip_avatar || '',
isVip: raw.isVip || raw.is_vip === 1,
mbti: raw.mbti || mock.mbti,
region: raw.region || mock.region,
skills: raw.skills || mock.skills,
contactRaw: raw.contactRaw || raw.vip_contact || mock.contactRaw,
bestMonth: raw.bestMonth || mock.bestMonth,
achievement: raw.achievement || mock.achievement,
turningPoint: raw.turningPoint || mock.turningPoint,
canHelp: raw.canHelp || mock.canHelp,
needHelp: raw.needHelp || mock.needHelp,
project: raw.project || raw.vip_project || mock.project
}
const contact = merged.contactRaw || ''
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
merged.contactUnlocked = isMatched
merged.contactFull = contact
return merged
},
unlockContact() {
wx.showModal({
title: '解锁完整联系方式', content: '成为VIP会员并完成匹配后即可查看完整联系方式',
confirmText: '去匹配', cancelText: '知道了',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/match/match' }) }
})
},
copyContact() {
const contact = this.data.member?.contact
if (!contact) { wx.showToast({ title: '暂无联系方式', icon: 'none' }); return }
wx.setClipboardData({ data: contact, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
const c = this.data.member?.contactFull
if (!c) return
wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
},
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
goBack() { wx.navigateBack() }
})

View File

@@ -1,38 +1,101 @@
<!--会员详情-->
<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-r"></view>
<view class="nav-back" bindtap="goBack"><text class="back-arrow"></text></view>
<text class="nav-title">超级个体</text>
<view class="nav-ph"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="detail-content" wx:if="{{member}}">
<view class="detail-hero">
<view class="detail-avatar-wrap">
<image class="detail-avatar" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
<view class="detail-avatar-ph" wx:else><text>{{member.name[0] || '创'}}</text></view>
<view class="detail-vip-badge">VIP</view>
</view>
<text class="detail-name">{{member.name}}</text>
<text class="detail-project" wx:if="{{member.project}}">{{member.project}}</text>
</view>
<view class="detail-card" wx:if="{{member.bio}}">
<text class="detail-card-title">简介</text>
<text class="detail-card-text">{{member.bio}}</text>
</view>
<view class="detail-card" wx:if="{{member.contact}}">
<text class="detail-card-title">联系方式</text>
<view class="detail-contact-row">
<text class="detail-card-text">{{member.contact}}</text>
<view class="copy-btn" bindtap="copyContact">复制</view>
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
<!-- ===== 顶部名片 ===== -->
<view class="card-hero">
<view class="hero-deco"></view>
<view class="hero-deco2"></view>
<view class="hero-body">
<view class="avatar-ring {{member.isVip ? 'vip-ring' : ''}}">
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
<view class="avatar-ph" wx:else><text>{{member.name[0] || '创'}}</text></view>
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
</view>
<text class="hero-name">{{member.name}}</text>
<view class="hero-tags">
<text class="tag-item tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
<text class="tag-item tag-region" wx:if="{{member.region}}">📍{{member.region}}</text>
</view>
</view>
</view>
<!-- ===== 基本信息 ===== -->
<view class="card">
<view class="card-head"><text class="card-icon">👤</text><text class="card-label">基本信息</text></view>
<view class="field" wx:if="{{member.skills}}">
<text class="f-key">我擅长</text>
<text class="f-val">{{member.skills}}</text>
</view>
<view class="field" wx:if="{{member.contactDisplay}}">
<text class="f-key">联系方式</text>
<view class="f-contact">
<text class="f-val masked">{{member.contactDisplay}}</text>
<view class="lock-chip" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">
<text class="lock-icon">🔒</text><text>匹配解锁</text>
</view>
<view class="copy-chip" wx:if="{{member.contactUnlocked}}" bindtap="copyContact">复制</view>
</view>
</view>
</view>
<!-- ===== 个人故事 ===== -->
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
<view class="card-head"><text class="card-icon">💡</text><text class="card-label">个人故事</text></view>
<view class="story" wx:if="{{member.bestMonth}}">
<text class="story-q">🏆 最赚钱的一个月做的是什么</text>
<text class="story-a">{{member.bestMonth}}</text>
</view>
<view class="divider"></view>
<view class="story" wx:if="{{member.achievement}}">
<text class="story-q">⭐ 最有成就感的一件事</text>
<text class="story-a">{{member.achievement}}</text>
</view>
<view class="divider"></view>
<view class="story" wx:if="{{member.turningPoint}}">
<text class="story-q">🔄 人生的转折点</text>
<text class="story-a">{{member.turningPoint}}</text>
</view>
</view>
<!-- ===== 互助需求 ===== -->
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
<view class="card-head"><text class="card-icon">🤝</text><text class="card-label">互助需求</text></view>
<view class="help-box help-give" wx:if="{{member.canHelp}}">
<text class="help-tag">我能帮到你</text>
<text class="help-txt">{{member.canHelp}}</text>
</view>
<view class="help-box help-need" wx:if="{{member.needHelp}}">
<text class="help-tag need">我需要帮助</text>
<text class="help-txt">{{member.needHelp}}</text>
</view>
</view>
<!-- ===== 项目介绍 ===== -->
<view class="card" wx:if="{{member.project}}">
<view class="card-head"><text class="card-icon">🚀</text><text class="card-label">项目介绍</text></view>
<text class="proj-txt">{{member.project}}</text>
</view>
<!-- ===== 底部操作 ===== -->
<view class="bottom-actions">
<view class="btn-match" bindtap="goToMatch">开始匹配 · 解锁联系方式</view>
<view class="btn-vip" bindtap="goToVip" wx:if="{{!member.isVip}}">成为超级个体 →</view>
</view>
<view style="height:120rpx;"></view>
</scroll-view>
<!-- 加载和空状态 -->
<view class="state-wrap" wx:if="{{loading}}">
<view class="loading-dot"></view><text class="state-txt">加载中...</text>
</view>
<view class="loading-state" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
<view class="state-wrap" wx:if="{{!loading && !member}}">
<text class="state-emoji">👤</text><text class="state-txt">暂无该超级个体信息</text>
</view>
</view>

View File

@@ -1,24 +1,75 @@
.page { background: #000; min-height: 100vh; color: #fff; }
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 24rpx; background: rgba(0,0,0,0.9); }
.nav-back { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
.back-icon { font-size: 44rpx; color: #fff; }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-placeholder-r { width: 60rpx; }
.page { background: #050a10; min-height: 100vh; color: #fff; }
.detail-content { padding: 24rpx; }
.detail-hero { display: flex; flex-direction: column; align-items: center; padding: 48rpx 0 32rpx; }
.detail-avatar-wrap { position: relative; margin-bottom: 20rpx; }
.detail-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; border: 4rpx solid #FFD700; }
.detail-avatar-ph { width: 160rpx; height: 160rpx; border-radius: 50%; background: #1c1c1e; border: 4rpx solid #FFD700; display: flex; align-items: center; justify-content: center; font-size: 60rpx; color: #FFD700; }
.detail-vip-badge { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 20rpx; font-weight: bold; padding: 4rpx 12rpx; border-radius: 14rpx; }
.detail-name { font-size: 40rpx; font-weight: bold; color: #fff; }
.detail-project { font-size: 26rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
/* 导航 */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; height: 44px; background: rgba(5,10,16,.92); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-bottom: 1rpx solid rgba(56,189,172,.08); }
.back-arrow { font-size: 48rpx; color: #38bdac; font-weight: 300; }
.nav-title { font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; }
.nav-ph { width: 48rpx; }
.nav-back { width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.detail-card { background: #1c1c1e; border-radius: 20rpx; padding: 28rpx; margin-top: 24rpx; }
.detail-card-title { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 12rpx; }
.detail-card-text { font-size: 30rpx; color: rgba(255,255,255,0.9); }
.detail-contact-row { display: flex; align-items: center; justify-content: space-between; }
.copy-btn { background: #00CED1; color: #000; font-size: 24rpx; font-weight: 600; padding: 8rpx 24rpx; border-radius: 20rpx; }
.scroll-wrap { height: calc(100vh - 88px); }
.loading-state { display: flex; justify-content: center; padding: 100rpx 0; }
.loading-text { color: rgba(255,255,255,0.4); font-size: 28rpx; }
/* ===== 顶部名片 ===== */
.card-hero { position: relative; margin: 24rpx 24rpx 0; padding: 56rpx 0 40rpx; border-radius: 28rpx; overflow: hidden; background: linear-gradient(160deg, #0d1f2d 0%, #0a1620 40%, #101828 100%); border: 1rpx solid rgba(56,189,172,.15); }
.hero-deco { position: absolute; top: -60rpx; right: -60rpx; width: 240rpx; height: 240rpx; border-radius: 50%; background: radial-gradient(circle, rgba(56,189,172,.12) 0%, transparent 70%); }
.hero-deco2 { position: absolute; bottom: -40rpx; left: -40rpx; width: 180rpx; height: 180rpx; border-radius: 50%; background: radial-gradient(circle, rgba(245,166,35,.06) 0%, transparent 70%); }
.hero-body { position: relative; display: flex; flex-direction: column; align-items: center; z-index: 1; }
.avatar-ring { position: relative; width: 168rpx; height: 168rpx; border-radius: 50%; padding: 6rpx; background: linear-gradient(135deg, #1a3a3a, #0d1f2d); margin-bottom: 20rpx; }
.avatar-ring.vip-ring { background: linear-gradient(135deg, #f5a623, #38bdac, #f5a623); background-size: 300% 300%; animation: vipGlow 4s ease infinite; }
@keyframes vipGlow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
.avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; }
.avatar-ph { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; background: #152030; display: flex; align-items: center; justify-content: center; font-size: 52rpx; color: #38bdac; font-weight: 700; }
.vip-tag { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #f5a623, #e8920d); color: #000; font-size: 18rpx; font-weight: 800; padding: 4rpx 14rpx; border-radius: 16rpx; letter-spacing: 1rpx; box-shadow: 0 4rpx 12rpx rgba(245,166,35,.4); }
.hero-name { font-size: 38rpx; font-weight: 700; letter-spacing: 2rpx; margin-bottom: 12rpx; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.5); }
.hero-tags { display: flex; gap: 12rpx; flex-wrap: wrap; justify-content: center; }
.tag-item { font-size: 22rpx; padding: 6rpx 18rpx; border-radius: 24rpx; }
.tag-mbti { color: #38bdac; background: rgba(56,189,172,.12); border: 1rpx solid rgba(56,189,172,.2); }
.tag-region { color: #ccc; background: rgba(255,255,255,.06); border: 1rpx solid rgba(255,255,255,.08); }
/* ===== 通用卡片 ===== */
.card { margin: 20rpx 24rpx; padding: 28rpx 28rpx; border-radius: 24rpx; background: rgba(15,25,40,.8); border: 1rpx solid rgba(255,255,255,.06); backdrop-filter: blur(10px); }
.card-head { display: flex; align-items: center; gap: 10rpx; margin-bottom: 24rpx; }
.card-icon { font-size: 28rpx; }
.card-label { font-size: 28rpx; font-weight: 600; color: #fff; letter-spacing: 1rpx; }
.field { margin-bottom: 20rpx; }
.field:last-child { margin-bottom: 0; }
.f-key { display: block; font-size: 22rpx; color: #6b7b8e; margin-bottom: 8rpx; text-transform: uppercase; letter-spacing: 2rpx; }
.f-val { font-size: 28rpx; color: #e0e8f0; line-height: 1.7; }
.f-contact { display: flex; align-items: center; gap: 16rpx; }
.masked { letter-spacing: 3rpx; font-family: 'Courier New', monospace; }
.lock-chip { display: flex; align-items: center; gap: 6rpx; font-size: 20rpx; color: #f5a623; background: rgba(245,166,35,.1); border: 1rpx solid rgba(245,166,35,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
.lock-icon { font-size: 18rpx; }
.copy-chip { font-size: 20rpx; color: #38bdac; background: rgba(56,189,172,.1); border: 1rpx solid rgba(56,189,172,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
/* ===== 故事 ===== */
.story { padding: 4rpx 0; }
.story-q { display: block; font-size: 24rpx; color: #7a8fa3; margin-bottom: 10rpx; }
.story-a { display: block; font-size: 28rpx; color: #e0e8f0; line-height: 1.8; }
.divider { height: 1rpx; background: rgba(255,255,255,.04); margin: 20rpx 0; }
/* ===== 互助 ===== */
.help-box { padding: 20rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
.help-box:last-child { margin-bottom: 0; }
.help-give { background: rgba(56,189,172,.06); border: 1rpx solid rgba(56,189,172,.1); }
.help-need { background: rgba(245,166,35,.06); border: 1rpx solid rgba(245,166,35,.1); }
.help-tag { display: inline-block; font-size: 22rpx; font-weight: 600; color: #38bdac; margin-bottom: 10rpx; padding: 4rpx 14rpx; border-radius: 12rpx; background: rgba(56,189,172,.12); }
.help-tag.need { color: #f5a623; background: rgba(245,166,35,.12); }
.help-txt { display: block; font-size: 26rpx; color: #ccd4de; line-height: 1.7; }
/* ===== 项目 ===== */
.proj-txt { font-size: 26rpx; color: #ccd4de; line-height: 1.8; }
/* ===== 底部按钮 ===== */
.bottom-actions { padding: 32rpx 24rpx 0; display: flex; flex-direction: column; gap: 16rpx; }
.btn-match { text-align: center; padding: 26rpx 0; border-radius: 48rpx; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; background: linear-gradient(135deg, #38bdac 0%, #2ca898 50%, #249e8c 100%); color: #fff; box-shadow: 0 8rpx 24rpx rgba(56,189,172,.3); }
.btn-vip { text-align: center; padding: 22rpx 0; border-radius: 48rpx; font-size: 26rpx; color: #f5a623; background: transparent; border: 1rpx solid rgba(245,166,35,.3); }
/* ===== 状态 ===== */
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16rpx; }
.state-txt { font-size: 28rpx; color: #4a5a6e; }
.state-emoji { font-size: 80rpx; }
.loading-dot { width: 48rpx; height: 48rpx; border-radius: 50%; border: 4rpx solid rgba(56,189,172,.2); border-top-color: #38bdac; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

View File

@@ -1,46 +1,62 @@
/**
* Soul创业派对 - 我的页面
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户状态
isLoggedIn: false,
userInfo: null,
// 统计数据
totalSections: 62,
purchasedCount: 0,
readCount: 0,
referralCount: 0,
earnings: 0,
pendingEarnings: 0,
// VIP状态
isVip: false,
vipExpireDate: '',
vipDaysRemaining: 0,
vipPrice: 1980,
earnings: '-',
pendingEarnings: '-',
earningsLoading: true,
earningsRefreshing: false,
// 阅读统计
totalReadTime: 0,
matchHistory: 0,
activeTab: 'overview',
// 最近阅读
recentChapters: [],
// 功能配置
matchEnabled: false,
// VIP状态
isVip: false,
vipExpireDate: '',
// 待确认收款
pendingConfirmList: [],
withdrawMchId: '',
withdrawAppId: '',
// 未登录假资料(展示用)
guestNickname: '游客',
guestAvatar: '',
// 章节映射表id->title
chapterMap: {},
menuList: [
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
{ id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' }
],
// 登录弹窗
showLoginModal: false,
isLoggingIn: false
isLoggingIn: false,
// 用户须主动勾选同意协议(审核要求:不得默认同意)
agreeProtocol: false,
// 修改昵称弹窗
showNicknameModal: false,
editingNickname: ''
},
onLoad() {
@@ -48,182 +64,489 @@ Page({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
this.loadChapterMap()
this.loadFeatureConfig()
this.initUserStatus()
},
onShow() {
// 设置TabBar选中状态根据 matchEnabled 动态设置)
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 3 })
const tabBar = this.getTabBar()
if (tabBar.updateSelected) {
tabBar.updateSelected()
} else {
const selected = tabBar.data.matchEnabled ? 3 : 2
tabBar.setData({ selected })
}
}
this.initUserStatus()
},
// 加载章节名称映射
async loadChapterMap() {
async loadFeatureConfig() {
try {
const res = await app.request('/api/book/all-chapters')
if (res && res.data) {
const map = {}
const sections = Array.isArray(res.data) ? res.data : []
sections.forEach(s => {
if (s.id) map[s.id] = s.title || s.sectionTitle || `章节 ${s.id}`
})
this.setData({ chapterMap: map })
// 有了映射后刷新最近阅读
this.refreshRecentChapters()
}
} catch (e) {
console.log('[My] 加载章节数据失败', e)
const res = await app.request('/api/miniprogram/config')
const features = (res && res.features) || (res && res.data && res.data.features) || {}
this.setData({ matchEnabled: features.matchEnabled === true })
} catch (error) {
console.log('加载功能配置失败:', error)
this.setData({ matchEnabled: false })
}
},
// 刷新最近阅读列表(用真实标题)
refreshRecentChapters() {
const { purchasedSections } = app.globalData
const map = this.data.chapterMap
const recentList = (purchasedSections || []).slice(-5).reverse().map(id => ({
id,
title: map[id] || `章节 ${id}`
}))
this.setData({ recentChapters: recentList })
},
// 初始化用户状态
async initUserStatus() {
const { isLoggedIn, userInfo, hasFullBook, purchasedSections } = app.globalData
initUserStatus() {
const { isLoggedIn, userInfo } = app.globalData
if (isLoggedIn && userInfo) {
const map = this.data.chapterMap
const recentList = (purchasedSections || []).slice(-5).reverse().map(id => ({
id,
title: map[id] || `章节 ${id}`
const readIds = app.globalData.readSectionIds || []
const recentList = readIds.slice(-5).reverse().map(id => ({
id: id,
title: `章节 ${id}`
}))
const userId = userInfo.id || ''
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
// 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
this.setData({
isLoggedIn: true,
userInfo,
userIdShort,
userWechat,
purchasedCount: hasFullBook ? this.data.totalSections : (purchasedSections?.length || 0),
readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
referralCount: userInfo.referralCount || 0,
earnings: userInfo.earnings || 0,
pendingEarnings: userInfo.pendingEarnings || 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: true,
recentChapters: recentList,
totalReadTime: Math.floor(Math.random() * 200) + 50
})
// 查询VIP状态
this.loadVipStatus(userId)
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
} else {
this.setData({
isLoggedIn: false, userInfo: null, userIdShort: '',
purchasedCount: 0, referralCount: 0, earnings: 0, pendingEarnings: 0,
recentChapters: [], isVip: false
isLoggedIn: false,
userInfo: null,
userIdShort: '',
readCount: app.getReadCount(),
referralCount: 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: false,
recentChapters: []
})
}
},
// 查询VIP状态
async loadVipStatus(userId) {
// 拉取待确认收款列表(用于「确认收款」按钮)
async loadPendingConfirm() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
try {
const res = await app.request(`/api/vip/status?userId=${userId}`)
if (res && res.success) {
const res = await app.request({ url: '/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id, silent: true })
if (res && res.success && res.data) {
const list = (res.data.list || []).map(item => ({
id: item.id,
amount: (item.amount || 0).toFixed(2),
package: item.package,
createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
}))
this.setData({
isVip: res.data.isVip,
vipExpireDate: res.data.expireDate || '',
vipDaysRemaining: res.data.daysRemaining || 0,
vipPrice: res.data.price || 1980
pendingConfirmList: list,
withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
withdrawAppId: res.data.appId ?? res.data.app_id ?? ''
})
} else {
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
}
} catch (e) {
console.log('[My] VIP状态查询失败', e)
this.setData({ pendingConfirmList: [] })
}
},
// 微信原生获取头像
async onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl
if (!avatarUrl) return
wx.showLoading({ title: '更新中...', mask: true })
formatDateMy(dateStr) {
if (!dateStr) return '--'
const d = new Date(dateStr)
const m = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
return `${m}-${day}`
},
// 确认收款:有 package 时调起微信收款页,成功后记录;无 package 时仅调用后端记录「已确认收款」
async confirmReceive(e) {
const index = e.currentTarget.dataset.index
const id = e.currentTarget.dataset.id
const list = this.data.pendingConfirmList || []
let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
if (!item && id) item = list.find(x => x.id === id) || null
if (!item) {
wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
return
}
const mchId = this.data.withdrawMchId
const appId = this.data.withdrawAppId
const hasPackage = item.package && mchId && appId && wx.canIUse('requestMerchantTransfer')
const recordConfirmReceived = async () => {
const userInfo = app.globalData.userInfo
if (userInfo && userInfo.id) {
try {
await app.request({
url: '/api/miniprogram/withdraw/confirm-received',
method: 'POST',
data: { withdrawalId: item.id, userId: userInfo.id }
})
} catch (e) { /* 仅记录,不影响前端展示 */ }
}
const newList = list.filter(x => x.id !== item.id)
this.setData({ pendingConfirmList: newList })
this.loadPendingConfirm()
}
if (hasPackage) {
wx.showLoading({ title: '调起收款...', mask: true })
wx.requestMerchantTransfer({
mchId,
appId,
package: item.package,
success: async () => {
wx.hideLoading()
wx.showToast({ title: '收款成功', icon: 'success' })
await recordConfirmReceived()
},
fail: (err) => {
wx.hideLoading()
const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
wx.showToast({ title: msg, icon: 'none' })
},
complete: () => { wx.hideLoading() }
})
return
}
// 无 package 时仅记录「确认已收款」(当前直接打款无 package用户点按钮即记录
wx.showLoading({ title: '提交中...', mask: true })
try {
await recordConfirmReceived()
wx.hideLoading()
wx.showToast({ title: '已记录确认收款', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
}
},
// 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数)
async loadMyEarnings() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
this.setData({ earningsLoading: false })
return
}
const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
try {
const res = await app.request({ url: '/api/miniprogram/earnings?userId=' + userInfo.id, silent: true })
if (!res || !res.success || !res.data) {
this.setData({ earningsLoading: false, earnings: '0.00', pendingEarnings: '0.00' })
return
}
const d = res.data
this.setData({
earnings: formatMoney(d.totalCommission),
pendingEarnings: formatMoney(d.availableEarnings),
referralCount: d.referralCount ?? this.data.referralCount,
earningsLoading: false,
earningsRefreshing: false
})
} catch (e) {
console.log('[My] 拉取我的收益失败:', e && e.message)
this.setData({
earningsLoading: false,
earningsRefreshing: false,
earnings: '0.00',
pendingEarnings: '0.00'
})
}
},
// 点击刷新图标:刷新我的收益
async refreshEarnings() {
if (!this.data.isLoggedIn) return
if (this.data.earningsRefreshing) return
this.setData({ earningsRefreshing: true })
wx.showToast({ title: '刷新中...', icon: 'loading', duration: 2000 })
await this.loadMyEarnings()
wx.showToast({ title: '已刷新', icon: 'success' })
},
// 微信原生获取头像button open-type="chooseAvatar" 回调)
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail.avatarUrl
if (!tempAvatarUrl) return
wx.showLoading({ title: '上传中...', mask: true })
try {
// 1. 先上传图片到服务器
console.log('[My] 开始上传头像:', tempAvatarUrl)
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: {
folder: 'avatars'
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.success) {
resolve(data)
} else {
reject(new Error(data.error || '上传失败'))
}
} catch (err) {
reject(new Error('解析响应失败'))
}
},
fail: (err) => {
reject(err)
}
})
})
// 2. 获取上传后的完整URL
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
console.log('[My] 头像上传成功:', avatarUrl)
// 3. 更新本地头像
const userInfo = this.data.userInfo
userInfo.avatar = avatarUrl
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
await app.request('/api/user/update', {
method: 'POST', data: { userId: userInfo.id, avatar: avatarUrl }
// 4. 同步到服务器数据库
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId: userInfo.id, avatar: avatarUrl }
})
wx.hideLoading()
wx.showToast({ title: '头像已获取', icon: 'success' })
wx.showToast({ title: '头像更新成功', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
console.error('[My] 上传头像失败:', e)
wx.showToast({
title: e.message || '上传失败,请重试',
icon: 'none'
})
}
},
// 点击昵称修改
// 微信原生获取昵称回调(针对 input type="nickname" 的 bindblur 或 bindchange
async handleNicknameChange(nickname) {
if (!nickname || nickname === this.data.userInfo?.nickname) return
try {
const userInfo = this.data.userInfo
userInfo.nickname = nickname
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 同步到服务器
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId: userInfo.id, nickname }
})
wx.showToast({ title: '昵称已更新', icon: 'success' })
} catch (e) {
console.error('[My] 同步昵称失败:', e)
}
},
// 打开昵称修改弹窗
editNickname() {
wx.showModal({
title: '修改昵称',
editable: true,
placeholderText: '请输入昵称',
success: async (res) => {
if (res.confirm && res.content) {
const newNickname = res.content.trim()
if (newNickname.length < 1 || newNickname.length > 20) {
wx.showToast({ title: '昵称1-20个字符', icon: 'none' }); return
}
const userInfo = this.data.userInfo
userInfo.nickname = newNickname
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
try {
await app.request('/api/user/update', {
method: 'POST', data: { userId: userInfo.id, nickname: newNickname }
})
} catch (e) { console.log('同步昵称失败', e) }
wx.showToast({ title: '昵称已更新', icon: 'success' })
this.setData({
showNicknameModal: true,
editingNickname: this.data.userInfo?.nickname || ''
})
},
// 关闭昵称弹窗
closeNicknameModal() {
this.setData({
showNicknameModal: false,
editingNickname: ''
})
},
// 阻止事件冒泡
stopPropagation() {},
// 昵称输入实时更新
onNicknameInput(e) {
this.setData({
editingNickname: e.detail.value
})
},
// 昵称变化(微信自动填充时触发)
onNicknameChange(e) {
const nickname = e.detail.value
console.log('[My] 昵称已自动填充:', nickname)
this.setData({
editingNickname: nickname
})
// 自动填充时也尝试直接同步
this.handleNicknameChange(nickname)
},
// 确认修改昵称
async confirmNickname() {
const newNickname = this.data.editingNickname.trim()
if (!newNickname) {
wx.showToast({ title: '昵称不能为空', icon: 'none' })
return
}
if (newNickname.length < 1 || newNickname.length > 20) {
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
return
}
// 关闭弹窗
this.closeNicknameModal()
// 显示加载
wx.showLoading({ title: '更新中...', mask: true })
try {
// 1. 同步到服务器
const res = await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: {
userId: this.data.userInfo.id,
nickname: newNickname
}
})
if (res && res.success) {
// 2. 更新本地状态
const userInfo = this.data.userInfo
userInfo.nickname = newNickname
this.setData({ userInfo })
// 3. 更新全局和缓存
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
wx.hideLoading()
wx.showToast({ title: '昵称已修改', icon: 'success' })
} else {
throw new Error(res?.message || '更新失败')
}
} catch (e) {
wx.hideLoading()
console.error('[My] 修改昵称失败:', e)
wx.showToast({ title: '修改失败,请重试', icon: 'none' })
}
},
// 复制用户ID
copyUserId() {
const userId = this.data.userInfo?.id || ''
if (!userId) {
wx.showToast({ title: '暂无ID', icon: 'none' })
return
}
wx.setClipboardData({
data: userId,
success: () => {
wx.showToast({ title: 'ID已复制', icon: 'success' })
}
})
},
copyUserId() {
const userId = this.data.userInfo?.id || ''
if (!userId) { wx.showToast({ title: '暂无ID', icon: 'none' }); return }
wx.setClipboardData({ data: userId, success: () => wx.showToast({ title: 'ID已复制', icon: 'success' }) })
// 切换Tab
switchTab(e) {
const tab = e.currentTarget.dataset.tab
this.setData({ activeTab: tab })
},
switchTab(e) { this.setData({ activeTab: e.currentTarget.dataset.tab }) },
showLogin() { this.setData({ showLoginModal: true }) },
closeLoginModal() { if (!this.data.isLoggingIn) this.setData({ showLoginModal: false }) },
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
showLogin() {
try {
this.setData({ showLoginModal: true, agreeProtocol: false })
} catch (e) {
console.error('[My] showLogin error:', e)
this.setData({ showLoginModal: true })
}
},
// 切换协议勾选(用户主动勾选,非默认同意)
toggleAgree() {
this.setData({ agreeProtocol: !this.data.agreeProtocol })
},
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
openUserProtocol() {
wx.navigateTo({ url: '/pages/agreement/agreement' })
},
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
openPrivacy() {
wx.navigateTo({ url: '/pages/privacy/privacy' })
},
// 关闭登录弹窗
closeLoginModal() {
if (this.data.isLoggingIn) return
this.setData({ showLoginModal: false })
},
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
async handleWechatLogin() {
if (!this.data.agreeProtocol) {
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
this.setData({ isLoggingIn: true })
try {
const result = await app.login()
if (result) {
this.initUserStatus()
this.setData({ showLoginModal: false })
this.setData({ showLoginModal: false, agreeProtocol: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('[My] 微信登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally { this.setData({ isLoggingIn: false }) }
} finally {
this.setData({ isLoggingIn: false })
}
},
// 手机号登录(需要用户授权)
async handlePhoneLogin(e) {
if (!e.detail.code) return this.handleWechatLogin()
// 检查是否有授权code
if (!e.detail.code) {
// 用户拒绝授权或获取失败,尝试使用微信登录
console.log('手机号授权失败,尝试微信登录')
return this.handleWechatLogin()
}
this.setData({ isLoggingIn: true })
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
@@ -234,71 +557,75 @@ Page({
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('手机号登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally { this.setData({ isLoggingIn: false }) }
} finally {
this.setData({ isLoggingIn: false })
}
},
// 点击菜单
handleMenuTap(e) {
const id = e.currentTarget.dataset.id
if (!this.data.isLoggedIn && id !== 'about') { this.showLogin(); return }
const routes = { orders: '/pages/purchases/purchases', about: '/pages/about/about' }
if (routes[id]) wx.navigateTo({ url: routes[id] })
if (!this.data.isLoggedIn && id !== 'about') {
this.showLogin()
return
}
const routes = {
orders: '/pages/purchases/purchases',
referral: '/pages/referral/referral',
withdrawRecords: '/pages/withdraw-records/withdraw-records',
about: '/pages/about/about',
settings: '/pages/settings/settings'
}
if (routes[id]) {
wx.navigateTo({ url: routes[id] })
}
},
// 跳转VIP页面
goToVip() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/vip/vip' })
// 跳转到阅读页
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
bindWechat() {
wx.showModal({
title: '绑定微信号', editable: true, placeholderText: '请输入微信号',
success: async (res) => {
if (res.confirm && res.content) {
const wechat = res.content.trim()
if (!wechat) return
try {
wx.setStorageSync('user_wechat', wechat)
const userInfo = this.data.userInfo
userInfo.wechat = wechat
this.setData({ userInfo, userWechat: wechat })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
await app.request('/api/user/update', {
method: 'POST', data: { userId: userInfo.id, wechat }
})
wx.showToast({ title: '绑定成功', icon: 'success' })
} catch (e) { wx.showToast({ title: '已保存到本地', icon: 'success' }) }
}
}
})
// 跳转到目录
goToChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
clearCache() {
wx.showModal({
title: '清除缓存', content: '确定要清除本地缓存吗?不会影响账号数据',
success: (res) => {
if (res.confirm) {
const userInfo = wx.getStorageSync('userInfo')
const token = wx.getStorageSync('token')
wx.clearStorageSync()
if (userInfo) wx.setStorageSync('userInfo', userInfo)
if (token) wx.setStorageSync('token', token)
wx.showToast({ title: '缓存已清除', icon: 'success' })
}
}
})
// 跳转到关于页
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
goToRead(e) { wx.navigateTo({ url: `/pages/read/read?id=${e.currentTarget.dataset.id}` }) },
goToChapters() { wx.switchTab({ url: '/pages/chapters/chapters' }) },
goToAbout() { wx.navigateTo({ url: '/pages/about/about' }) },
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
// 跳转到匹配
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到推广中心
goToReferral() {
if (!this.data.isLoggedIn) {
this.showLogin()
return
}
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 跳转到找伙伴页面
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
// 退出登录
handleLogout() {
wx.showModal({
title: '退出登录', content: '确定要退出登录吗?',
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
app.logout()
@@ -309,5 +636,80 @@ Page({
})
},
// VIP状态查询
async loadVipStatus() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({ url: `/api/vip/status?userId=${userId}`, silent: true })
if (res?.success) {
this.setData({ isVip: res.data?.isVip, vipExpireDate: res.data?.expireDate || '' })
}
} catch (e) { console.log('[My] VIP查询失败', e) }
},
// 头像点击:已登录弹出选项(改头像/进VIP
onAvatarTap() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.showActionSheet({
itemList: ['获取微信头像', '开通/管理VIP'],
success: (res) => {
if (res.tapIndex === 0) this.chooseAvatarFallback()
if (res.tapIndex === 1) this.goToVip()
}
})
},
chooseAvatarFallback() {
wx.chooseMedia({
count: 1, mediaType: ['image'], sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFiles[0].tempFilePath
const userInfo = this.data.userInfo
userInfo.avatar = tempPath
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
try {
await app.request('/api/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: tempPath } })
} catch (e) { console.log('头像同步失败', e) }
wx.showToast({ title: '头像已更新', icon: 'success' })
}
})
},
goToVip() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/vip/vip' })
},
async handleWithdraw() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
const amount = parseFloat(this.data.pendingEarnings)
if (isNaN(amount) || amount <= 0) {
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
return
}
wx.showModal({
title: '申请提现',
content: `确认提现 ¥${amount.toFixed(2)} `,
success: async (res) => {
if (!res.confirm) return
wx.showLoading({ title: '提交中...', mask: true })
try {
const userId = app.globalData.userInfo?.id
await app.request({ url: '/api/withdraw', method: 'POST', data: { userId, amount } })
wx.hideLoading()
wx.showToast({ title: '提现申请已提交', icon: 'success' })
this.loadMyEarnings()
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
}
}
})
},
// 阻止冒泡
stopPropagation() {}
})

View File

@@ -1,143 +1,131 @@
<!--Soul创业实验 - 我的页面-->
<!--pages/my/my.wxml-->
<!--Soul创业实验 - 我的页面 1:1还原Web版本-->
<view class="page page-transition">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<text class="nav-title brand-color">我的</text>
<text class="nav-title-left brand-color">我的</text>
</view>
</view>
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 未登录 -->
<view class="user-card card-gradient login-card" wx:if="{{!isLoggedIn}}">
<view class="login-prompt">
<view class="login-icon-large">🔐</view>
<text class="login-title">登录 Soul创业实验</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat-large" bindtap="handleWechatLogin">
<text class="btn-icon">微</text>
<text>微信快捷登录</text>
</button>
</view>
</view>
<!-- 已登录 - 用户卡片 -->
<view class="user-card card-gradient" wx:else>
<!-- 用户卡片 - 未登录:假资料界面,名字旁点击登录打开弹窗 -->
<view class="user-card card-gradient" wx:if="{{!isLoggedIn}}">
<view class="user-header-row">
<button class="avatar-btn-simple" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<view class="avatar {{isVip ? 'avatar-vip' : 'avatar-normal'}}">
<image class="avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text class="avatar-text" wx:else>{{userInfo.nickname[0] || '微'}}</text>
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
</view>
</button>
<view class="avatar avatar-placeholder">
<image class="avatar-img" wx:if="{{guestAvatar}}" src="{{guestAvatar}}" mode="aspectFill"/>
<text class="avatar-text" wx:else>{{guestNickname[0] || '游'}}</text>
</view>
<view class="user-info-block">
<view class="user-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<text class="edit-icon-small">✎</text>
<view class="vip-tag" wx:if="{{isVip}}">创业伙伴</view>
<text class="user-name">{{guestNickname}}</text>
<view class="btn-login-inline" bindtap="showLogin">点击登录</view>
</view>
<view class="user-id-row" bindtap="copyUserId">
<text class="user-id">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
<text class="copy-icon">📋</text>
<view class="user-id-row">
<text class="user-id user-id-guest">登录后查看完整信息</text>
</view>
</view>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value brand-color">{{purchasedCount}}</text>
<text class="stat-label">已章节</text>
<text class="stat-value brand-color">--</text>
<text class="stat-label">已章节</text>
</view>
<view class="stat-item">
<text class="stat-value brand-color">--</text>
<text class="stat-label">推荐好友</text>
</view>
<view class="stat-item">
<text class="stat-value gold-color">--</text>
<text class="stat-label">待领收益</text>
</view>
</view>
</view>
<!-- 用户卡片 - 已登录状态 -->
<view class="user-card card-gradient" wx:else>
<view class="user-header-row">
<!-- 头像 - 点击进VIP/设置头像 -->
<view class="avatar-wrap" bindtap="onAvatarTap">
<view class="avatar {{isVip ? 'avatar-vip' : ''}}">
<image class="avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text class="avatar-text" wx:else>{{userInfo.nickname[0] || '用'}}</text>
</view>
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
<view class="vip-badge vip-badge-gray" wx:else bindtap="goToVip">VIP</view>
</view>
<!-- 用户信息 -->
<view class="user-info-block">
<view class="user-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<view class="vip-tags-row">
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</view>
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">匹配</view>
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</view>
</view>
<view class="become-vip-chip" wx:if="{{!isVip}}" bindtap="goToVip">
<text class="chip-star">⭐</text><text class="chip-text">成为会员</text>
</view>
</view>
<view class="user-id-row" bindtap="copyUserId">
<text class="user-id">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
</view>
</view>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value brand-color">{{readCount}}</text>
<text class="stat-label">已读章节</text>
</view>
<view class="stat-item">
<text class="stat-value brand-color">{{referralCount}}</text>
<text class="stat-label">推荐好友</text>
</view>
<view class="stat-item">
<view class="stat-item" bindtap="goToReferral">
<text class="stat-value gold-color">{{pendingEarnings > 0 ? '¥' + pendingEarnings : '--'}}</text>
<text class="stat-label">我的收益</text>
</view>
</view>
</view>
<!-- VIP入口卡片 -->
<view class="vip-card" wx:if="{{isLoggedIn}}" bindtap="goToVip">
<view class="vip-card-inner" wx:if="{{!isVip}}">
<view class="vip-card-left">
<text class="vip-card-icon">👑</text>
<view class="vip-card-info">
<text class="vip-card-title">开通VIP会员</text>
<text class="vip-card-desc">解锁全部章节 · 匹配创业伙伴</text>
</view>
</view>
<view class="vip-card-price">
<text class="vip-price-num">¥{{vipPrice}}</text>
<text class="vip-price-unit">/年</text>
</view>
</view>
<view class="vip-card-inner vip-active" wx:else>
<view class="vip-card-left">
<text class="vip-card-icon">👑</text>
<view class="vip-card-info">
<text class="vip-card-title gold-color">VIP会员</text>
<text class="vip-card-desc">剩余 {{vipDaysRemaining}} 天</text>
</view>
</view>
<text class="vip-manage-btn">管理 →</text>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-bar-custom" wx:if="{{isLoggedIn}}">
<view class="tab-item {{activeTab === 'overview' ? 'tab-active' : ''}}" bindtap="switchTab" data-tab="overview">概览</view>
<view class="tab-item {{activeTab === 'footprint' ? 'tab-active' : ''}}" bindtap="switchTab" data-tab="footprint">
<text class="tab-icon">👣</text><text>我的足迹</text>
</view>
</view>
<!-- 概览内容 -->
<view class="tab-content" wx:if="{{activeTab === 'overview' && isLoggedIn}}">
<!-- 统一内容区 - 仅登录用户显示 -->
<view class="tab-content" wx:if="{{isLoggedIn}}">
<!-- 菜单:我的订单 + 设置 -->
<view class="menu-card card">
<view class="menu-item" wx:for="{{menuList}}" wx:key="id" bindtap="handleMenuTap" data-id="{{item.id}}">
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
<view class="menu-left">
<view class="menu-icon {{item.iconBg === 'brand' ? 'icon-brand' : ''}}">{{item.icon}}</view>
<text class="menu-title">{{item.title}}</text>
<view class="menu-icon icon-brand">📦</view>
<text class="menu-title">我的订单</text>
</view>
<view class="menu-right">
<text class="menu-arrow">→</text>
</view>
</view>
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
<view class="menu-left">
<view class="menu-icon icon-gray">⚙️</view>
<text class="menu-title">设置</text>
</view>
<view class="menu-right">
<text class="menu-count" wx:if="{{item.count !== undefined}}">{{item.count}}笔</text>
<text class="menu-arrow">→</text>
</view>
</view>
</view>
<view class="settings-card card">
<view class="card-title"><text class="title-icon">⚙️</text><text>账号设置</text></view>
<view class="settings-list">
<view class="settings-item" bindtap="bindWechat">
<text class="settings-label">绑定微信号</text>
<view class="settings-right">
<text class="settings-value">{{userWechat || '未绑定'}}</text>
<text class="menu-arrow">→</text>
</view>
</view>
<view class="settings-item" bindtap="clearCache">
<text class="settings-label">清除缓存</text>
<text class="menu-arrow">→</text>
</view>
<view class="settings-item logout-item" bindtap="handleLogout">
<text class="settings-label logout-text">退出登录</text>
</view>
</view>
</view>
</view>
<!-- 足迹内容 -->
<view class="tab-content" wx:if="{{activeTab === 'footprint' && isLoggedIn}}">
<!-- 阅读统计 -->
<view class="stats-card card">
<view class="card-title"><text class="title-icon">👁️</text><text>阅读统计</text></view>
<view class="card-title">
<text class="title-icon">👁️</text>
<text>阅读统计</text>
</view>
<view class="stats-row">
<view class="stat-box">
<text class="stat-icon brand-color">📖</text>
<text class="stat-num">{{purchasedCount}}</text>
<text class="stat-num">{{readCount}}</text>
<text class="stat-text">已读章节</text>
</view>
<view class="stat-box">
@@ -145,7 +133,7 @@
<text class="stat-num">{{totalReadTime}}</text>
<text class="stat-text">阅读分钟</text>
</view>
<view class="stat-box">
<view class="stat-box" wx:if="{{matchEnabled}}">
<text class="stat-icon pink-color">👥</text>
<text class="stat-num">{{matchHistory}}</text>
<text class="stat-text">匹配伙伴</text>
@@ -153,10 +141,20 @@
</view>
</view>
<!-- 最近阅读 -->
<view class="recent-card card">
<view class="card-title"><text class="title-icon">📖</text><text>最近阅读</text></view>
<view class="card-title">
<text class="title-icon">📖</text>
<text>最近阅读</text>
</view>
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
<view class="recent-item" wx:for="{{recentChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
<view
class="recent-item"
wx:for="{{recentChapters}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="recent-left">
<text class="recent-index">{{index + 1}}</text>
<text class="recent-title">{{item.title}}</text>
@@ -171,30 +169,78 @@
</view>
</view>
<view class="match-card card">
<view class="card-title"><text class="title-icon">👥</text><text>匹配记录</text></view>
<view class="empty-state">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无匹配记录</text>
<view class="empty-btn" bindtap="goToMatch">去匹配 →</view>
<!-- 关于作者(最底部) -->
<view class="menu-card card" style="margin-top: 16rpx;">
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
<view class="menu-left">
<view class="menu-icon icon-brand"></view>
<text class="menu-title">关于作者</text>
</view>
<view class="menu-right">
<text class="menu-arrow">→</text>
</view>
</view>
</view>
</view>
<!-- 登录弹窗 -->
<!-- 登录弹窗:可取消,用户主动选择是否登录 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content" catchtap="stopPropagation">
<view class="modal-content login-modal-content" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<text class="login-title">登录 Soul创业实验</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat" bindtap="handleWechatLogin" disabled="{{isLoggingIn}}">
<button
class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}"
bindtap="handleWechatLogin"
disabled="{{isLoggingIn || !agreeProtocol}}"
>
<text class="btn-wechat-icon">微</text>
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
</button>
<text class="login-notice">登录即表示同意《用户协议》和《隐私政策》</text>
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
<view class="login-agree-row" catchtap="toggleAgree">
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
<text class="agree-text">我已阅读并同意</text>
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
<text class="agree-text">和</text>
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
</view>
</view>
</view>
<!-- 修改昵称弹窗 -->
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
<view class="modal-content nickname-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeNicknameModal">✕</view>
<view class="modal-header">
<text class="modal-icon">✏️</text>
<text class="modal-title">修改昵称</text>
</view>
<view class="nickname-input-wrap">
<input
class="nickname-input"
type="nickname"
value="{{editingNickname}}"
placeholder="点击输入昵称"
placeholder-class="nickname-placeholder"
bindchange="onNicknameChange"
bindinput="onNicknameInput"
maxlength="20"
/>
<text class="input-tip">微信用户可点击自动填充昵称</text>
</view>
<view class="modal-actions">
<view class="modal-btn modal-btn-cancel" bindtap="closeNicknameModal">取消</view>
<view class="modal-btn modal-btn-confirm" bindtap="confirmNickname">确定</view>
</view>
</view>
</view>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</view>

View File

@@ -25,7 +25,8 @@
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 0 32rpx;
}
.nav-title {
@@ -33,6 +34,11 @@
font-weight: 600;
}
.nav-title-left {
font-size: 36rpx;
font-weight: 600;
}
.brand-color {
color: #00CED1;
}
@@ -54,6 +60,40 @@
margin: 32rpx;
padding: 32rpx;
}
.margin-partner-badge{
}
/* 创业伙伴按钮 - inline 布局 */
.partner-badge {
display: flex;
align-items: center;
gap: 6rpx;
padding: 10rpx 18rpx;
background: rgba(0, 206, 209, 0.15);
border: 1rpx solid rgba(0, 206, 209, 0.3);
border-radius: 20rpx;
flex-shrink: 0;
transition: all 0.2s;
margin-top:-20px;
}
.partner-badge:active {
background: rgba(0, 206, 209, 0.3);
transform: scale(0.95);
}
.partner-icon {
font-size: 18rpx;
color: #00CED1;
line-height: 1;
}
.partner-text {
font-size: 20rpx;
font-weight: 500;
color: #00CED1;
line-height: 1;
white-space: nowrap;
}
.card-gradient {
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
@@ -65,7 +105,7 @@
.user-header-row {
display: flex;
align-items: center;
gap: 24rpx;
gap: 20rpx;
margin-bottom: 32rpx;
width: 100%;
}
@@ -81,7 +121,7 @@
/* 头像按钮样式 - 简化版 */
.avatar-btn-simple {
flex-shrink: 0;
width: 120rpx;
width: 60rpx!important;
height: 120rpx;
min-width: 120rpx;
min-height: 120rpx;
@@ -98,17 +138,6 @@
}
.avatar-btn-simple::after { border: none; }
/* 用户名样式 */
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #fff;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-icon-small {
font-size: 24rpx;
color: rgba(255,255,255,0.5);
@@ -119,18 +148,20 @@
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid #00CED1;
border: 3rpx solid #00CED1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, transparent 100%);
overflow: hidden;
box-sizing: border-box;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-edit-hint {
@@ -163,9 +194,10 @@
}
.avatar-text {
font-size: 44rpx;
font-size: 48rpx;
font-weight: 700;
color: #00CED1;
line-height: 1;
}
.user-info-block {
@@ -173,27 +205,26 @@
display: flex;
flex-direction: column;
justify-content: center;
gap: 10rpx;
gap: 12rpx;
min-width: 0;
padding-top: 4rpx;
}
.user-name-row {
display: flex;
align-items: center;
gap: 10rpx;
line-height: 1.2;
gap: 8rpx;
line-height: 1.3;
}
.user-name {
font-size: 36rpx;
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320rpx;
line-height: 1.3;
flex: 1;
min-width: 0;
}
.edit-name-icon {
@@ -206,11 +237,23 @@
display: flex;
align-items: center;
gap: 8rpx;
line-height: 1.3;
}
.user-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.copy-icon {
font-size: 22rpx;
opacity: 0.5;
flex-shrink: 0;
}
.user-wechat {
@@ -237,11 +280,6 @@
color: #00CED1;
}
.user-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.35);
}
/* 兼容旧样式 */
.user-header {
display: flex;
@@ -267,67 +305,24 @@
margin-top: 4rpx;
}
/* ===== 登录卡片样式 ===== */
.login-card {
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
/* ===== 登录假资料样式 ===== */
.avatar-placeholder {
cursor: default;
}
.login-prompt {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 40rpx 0;
width: 100%;
.user-id-guest {
color: rgba(255, 255, 255, 0.35) !important;
}
.login-icon-large {
font-size: 80rpx;
margin-bottom: 32rpx;
}
.login-title {
font-size: 36rpx;
.btn-login-inline {
flex-shrink: 0;
margin-left: 16rpx;
padding: 10rpx 24rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
color: #000;
font-size: 24rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
}
.login-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 48rpx;
line-height: 1.6;
}
.btn-wechat-large {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
width: 80%;
padding: 28rpx 0;
background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
border-radius: 48rpx;
border: none;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
.btn-wechat-large .btn-icon {
width: 48rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
border-radius: 24rpx;
}
.user-name {
@@ -368,14 +363,12 @@
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
padding-top: 32rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
border-top: 2rpx solid rgba(255, 255, 255, 0.05);
}
.stat-item {
text-align: center;
padding: 16rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
padding: 16rpx 8rpx;
}
.stat-value {
@@ -389,7 +382,7 @@
color: rgba(255, 255, 255, 0.4);
}
/* ===== 收益卡片 ===== */
/* ===== 收益卡片 - 艺术化设计 ===== */
.earnings-card {
margin: 32rpx;
padding: 32rpx;
@@ -398,63 +391,117 @@
border: 2rpx solid rgba(0, 206, 209, 0.2);
position: relative;
overflow: hidden;
box-shadow: 0 16rpx 32rpx rgba(0, 0, 0, 0.3);
}
.earnings-bg {
/* 背景装饰圆 */
.bg-decoration {
position: absolute;
border-radius: 50%;
z-index: 0;
}
.bg-decoration-gold {
top: 0;
right: 0;
width: 256rpx;
height: 256rpx;
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, transparent 100%);
border-radius: 50%;
transform: translate(50%, -50%);
}
.bg-decoration-brand {
bottom: 0;
left: 0;
width: 192rpx;
height: 192rpx;
background: linear-gradient(45deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
transform: translate(-50%, 50%);
}
.earnings-content {
position: relative;
z-index: 1;
}
/* 标题行 */
.earnings-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
margin-bottom: 40rpx;
}
.earnings-title-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.earnings-icon {
font-size: 32rpx;
}
.earnings-title {
display: flex;
align-items: center;
gap: 16rpx;
}
.title-icon {
font-size: 36rpx;
}
.title-text {
font-size: 28rpx;
font-weight: 500;
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
}
.earnings-link {
display: flex;
align-items: center;
gap: 8rpx;
gap: 6rpx;
padding: 8rpx 16rpx;
background: rgba(0, 206, 209, 0.1);
border-radius: 16rpx;
}
.earnings-link:active {
background: rgba(0, 206, 209, 0.2);
}
.link-text {
font-size: 24rpx;
color: #00CED1;
font-weight: 500;
}
.link-arrow {
font-size: 24rpx;
font-size: 20rpx;
font-weight: 600;
}
/* 我的收益 - 刷新图标 */
.earnings-refresh-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(0, 206, 209, 0.15);
}
.earnings-refresh-wrap:active {
background: rgba(0, 206, 209, 0.3);
}
.earnings-refresh-icon {
font-size: 36rpx;
font-weight: 600;
color: #00CED1;
}
.earnings-refresh-spin {
animation: earnings-spin 0.8s linear infinite;
}
@keyframes earnings-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 收益数据 */
.earnings-data {
display: flex;
align-items: flex-end;
@@ -462,48 +509,79 @@
margin-bottom: 32rpx;
}
.data-item {
.earnings-main,
.earnings-secondary {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.data-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 8rpx;
.earnings-main {
flex: 1;
}
.data-value {
font-size: 40rpx;
.earnings-secondary {
flex: 1;
}
.earnings-label {
font-size: 24rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 0.5rpx;
}
.earnings-amount-large {
font-size: 64rpx;
font-weight: 700;
line-height: 1;
display: block;
}
.earnings-amount-medium {
font-size: 48rpx;
font-weight: 700;
line-height: 1;
color: #ffffff;
}
/* 渐变文字效果 */
.gold-gradient {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 56rpx;
color: transparent;
}
.earnings-btn {
/* 操作按钮 */
.earnings-action {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 24rpx;
background: linear-gradient(135deg, rgba(255, 215, 0, 0.8) 0%, rgba(255, 165, 0, 0.8) 100%);
background: linear-gradient(90deg, rgba(255, 215, 0, 0.9) 0%, rgba(255, 165, 0, 0.9) 100%);
border-radius: 24rpx;
font-weight: 600;
font-weight: 700;
box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3);
margin-top: 8rpx;
}
.btn-icon {
font-size: 28rpx;
.earnings-action:active {
opacity: 0.85;
transform: scale(0.98);
}
.btn-text {
font-size: 28rpx;
.action-icon {
font-size: 32rpx;
}
.action-text {
font-size: 30rpx;
font-weight: 700;
color: #000000;
letter-spacing: 1rpx;
}
/* ===== 推广入口 ===== */
@@ -643,20 +721,33 @@
}
.menu-icon {
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
background: rgba(255, 255, 255, 0.1);
font-size: 32rpx;
flex-shrink: 0;
}
/* 有背景的图标样式 */
.icon-brand,
.icon-gold,
.icon-gray {
width: 52rpx;
height: 52rpx;
font-size: 20rpx;
}
.icon-brand {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
}
.icon-gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
.icon-gray {
background: rgba(128, 128, 128, 0.2);
}
@@ -708,6 +799,11 @@
gap: 24rpx;
}
/* 两列布局(当找伙伴功能关闭时) */
.stats-row-two-cols {
grid-template-columns: repeat(2, 1fr) !important;
}
.stat-box {
display: flex;
flex-direction: column;
@@ -919,12 +1015,52 @@
font-size: 32rpx;
}
.login-notice {
display: block;
/* 登录弹窗内取消按钮 */
.login-modal-cancel {
margin-top: 24rpx;
padding: 24rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
text-align: center;
}
/* 协议勾选行(审核:用户须主动勾选,协议可点击查看) */
.login-agree-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin-top: 32rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.3);
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.agree-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.5);
border-radius: 6rpx;
margin-right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #fff;
flex-shrink: 0;
}
.agree-checked {
background: #00CED1;
border-color: #00CED1;
}
.agree-text {
color: rgba(255, 255, 255, 0.6);
}
.agree-link {
color: #00CED1;
text-decoration: underline;
padding: 0 4rpx;
}
.btn-wechat-disabled {
opacity: 0.6;
}
/* ===== 底部留白 ===== */
@@ -995,141 +1131,243 @@
color: #FFD700;
}
/* 账号设置 */
.settings-card {
margin-top: 24rpx;
/* ===== 修改昵称弹窗 ===== */
.nickname-modal {
width: 600rpx;
max-width: 90%;
}
.settings-list {
margin-top: 16rpx;
}
.settings-item {
.modal-header {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
padding: 28rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
margin-bottom: 40rpx;
}
.settings-item:last-child {
border-bottom: none;
.modal-icon {
font-size: 60rpx;
margin-bottom: 16rpx;
}
.settings-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
}
.settings-right {
display: flex;
align-items: center;
gap: 12rpx;
}
.settings-value {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.4);
}
.logout-item {
justify-content: center;
margin-top: 16rpx;
border-bottom: none;
}
.logout-text {
color: #ff4d4f;
font-size: 28rpx;
}
/* === VIP 头像标识 === */
.avatar-normal {
border: 4rpx solid rgba(255,255,255,0.2);
}
.avatar-vip {
border: 4rpx solid #FFD700;
box-shadow: 0 0 16rpx rgba(255,215,0,0.5);
position: relative;
}
.vip-badge {
position: absolute;
bottom: -4rpx;
right: -4rpx;
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #000;
font-size: 18rpx;
font-weight: bold;
padding: 2rpx 10rpx;
border-radius: 12rpx;
line-height: 1.4;
}
.vip-tag {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #000;
font-size: 20rpx;
.modal-title {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
padding: 4rpx 14rpx;
border-radius: 16rpx;
margin-left: 12rpx;
}
/* === VIP入口卡片 === */
.vip-card {
margin: 16rpx 24rpx;
border-radius: 20rpx;
overflow: hidden;
.nickname-input-wrap {
margin-bottom: 40rpx;
}
.vip-card-inner {
.nickname-input {
width: 100%;
height: 88rpx;
padding: 0 24rpx;
background: rgba(255, 255, 255, 0.05);
border: 2rpx solid rgba(56, 189, 172, 0.3);
border-radius: 12rpx;
font-size: 28rpx;
color: #ffffff;
box-sizing: border-box;
}
.nickname-placeholder {
color: rgba(255, 255, 255, 0.3);
}
.input-tip {
display: block;
margin-top: 12rpx;
font-size: 22rpx;
color: rgba(56, 189, 172, 0.6);
text-align: center;
}
.modal-actions {
display: flex;
gap: 20rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
transition: all 0.3s;
}
.modal-btn-cancel {
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.5);
border: 2rpx solid rgba(255, 255, 255, 0.1);
}
.modal-btn-confirm {
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
color: #ffffff;
box-shadow: 0 8rpx 24rpx rgba(56, 189, 172, 0.3);
}
/* 待确认收款 */
.pending-confirm-card {
margin: 32rpx;
padding: 28rpx 32rpx;
background: rgba(76, 175, 80, 0.08);
border: 2rpx solid rgba(76, 175, 80, 0.25);
border-radius: 24rpx;
}
.pending-confirm-header { margin-bottom: 20rpx; }
.pending-confirm-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; }
.pending-confirm-desc { font-size: 24rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
.pending-confirm-list { display: flex; flex-direction: column; gap: 16rpx; }
.pending-confirm-item {
display: flex; align-items: center; justify-content: space-between;
padding: 20rpx 24rpx; background: rgba(28,28,30,0.6); border-radius: 16rpx;
}
.pending-confirm-info { display: flex; flex-direction: column; gap: 4rpx; }
.pending-confirm-amount { font-size: 32rpx; font-weight: 600; color: #4CAF50; }
.pending-confirm-time { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.pending-confirm-btn {
padding: 16rpx 32rpx;
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
color: #fff; font-size: 26rpx; font-weight: 500; border-radius: 20rpx;
}
/* ===== 收益面板(内嵌) ===== */
.earnings-inline {
margin: 0 32rpx 24rpx;
padding: 24rpx 28rpx;
background: linear-gradient(135deg, rgba(255,215,0,0.12), rgba(255,165,0,0.08));
border: 1rpx solid rgba(255,215,0,0.25);
border-radius: 20rpx;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 24rpx;
border: 2rpx solid rgba(0, 206, 209, 0.15);
}
.vip-card-inner.vip-active {
background: linear-gradient(135deg, rgba(255,215,0,0.2), rgba(255,165,0,0.12));
border-color: rgba(255,215,0,0.4);
}
.vip-card-left {
.earnings-inline-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.vip-card-icon {
font-size: 44rpx;
}
.vip-card-info {
.earnings-inline-item {
display: flex;
flex-direction: column;
gap: 4rpx;
flex: 1;
}
.vip-card-title {
font-size: 30rpx;
font-weight: 600;
color: rgba(255,255,255,0.95);
}
.vip-card-desc {
.earnings-inline-label {
font-size: 22rpx;
color: rgba(255,255,255,0.5);
margin-top: 4rpx;
}
.vip-card-price {
display: flex;
align-items: baseline;
}
.vip-price-num {
.earnings-inline-val {
font-size: 36rpx;
font-weight: bold;
color: #FFD700;
font-weight: 700;
color: #fff;
}
.vip-price-unit {
font-size: 22rpx;
color: rgba(255,215,0,0.7);
margin-left: 4rpx;
.earnings-inline-divider {
width: 2rpx;
height: 48rpx;
background: rgba(255,255,255,0.1);
flex-shrink: 0;
}
.vip-manage-btn {
.earnings-inline-btn {
padding: 12rpx 28rpx;
background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
border-radius: 20rpx;
font-size: 26rpx;
color: #FFD700;
font-weight: 600;
color: #000;
flex-shrink: 0;
}
.earnings-inline-refresh {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #00CED1;
flex-shrink: 0;
}
.pending-inline {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(255,255,255,0.08);
}
.pending-inline-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8rpx 0;
}
.pending-inline-text {
font-size: 24rpx;
color: #4CAF50;
}
.pending-inline-btn {
padding: 8rpx 20rpx;
background: #4CAF50;
color: #fff;
font-size: 22rpx;
border-radius: 12rpx;
}
/* VIP头像标识 */
.avatar-wrap { position: relative; }
.avatar-vip { border: 4rpx solid #FFD700 !important; box-shadow: 0 0 20rpx rgba(255,215,0,0.4); }
.vip-badge { position: absolute; bottom: -4rpx; right: -4rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 16rpx; font-weight: bold; padding: 2rpx 8rpx; border-radius: 10rpx; line-height: 1.4; }
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
/* 会员权益小标签 */
.vip-tags-row {
display: flex;
gap: 6rpx;
margin-left: 8rpx;
flex-shrink: 0;
}
.vip-tag-mini {
padding: 2rpx 10rpx;
font-size: 18rpx;
border-radius: 8rpx;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.3);
border: 1rpx solid rgba(255,255,255,0.1);
}
.vip-tag-active {
background: rgba(255,215,0,0.15);
color: #FFD700;
border-color: rgba(255,215,0,0.3);
}
/* 阅读统计 */
.stats-card { padding: 24rpx 28rpx; }
.stats-row { display: flex; gap: 16rpx; margin-top: 16rpx; }
.stat-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
padding: 20rpx 12rpx;
border-radius: 16rpx;
background: rgba(139,92,246,0.06);
}
.stat-icon { font-size: 32rpx; }
.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; }
.stat-text { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.pink-color { color: #ec4899; }
/* 成为会员小按钮 */
.become-vip-chip {
display: inline-flex;
align-items: center;
gap: 4rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, rgba(245,166,35,.15), rgba(245,166,35,.08));
border: 1rpx solid rgba(245,166,35,.3);
margin-left: 8rpx;
}
.chip-star { font-size: 18rpx; }
.chip-text { font-size: 20rpx; color: #f5a623; font-weight: 600; white-space: nowrap; }

View File

@@ -0,0 +1,21 @@
/**
* Soul创业派对 - 隐私政策
* 审核要求:登录前可点击《隐私政策》查看完整内容
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
},
goBack() {
wx.navigateBack()
}
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"隐私政策"}

View File

@@ -0,0 +1,40 @@
<!--隐私政策页 - 审核要求可点击查看-->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">←</view>
<text class="nav-title">隐私政策</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<scroll-view class="content" scroll-y enhanced show-scrollbar>
<view class="doc-card">
<text class="doc-title">Soul创业实验 隐私政策</text>
<text class="doc-update">更新日期:以小程序内展示为准</text>
<text class="doc-section">一、信息收集</text>
<text class="doc-p">为向您提供阅读、购买、推广与提现等服务我们可能收集微信昵称、头像、openId、手机号在您授权时、订单与收益相关数据。我们仅在法律允许及您同意的范围内收集必要信息。</text>
<text class="doc-section">二、信息使用</text>
<text class="doc-p">所收集信息用于账号识别、订单与收益结算、客服与纠纷处理、产品优化及法律义务履行,不会用于与上述目的无关的营销或向第三方出售。</text>
<text class="doc-section">三、信息存储与安全</text>
<text class="doc-p">数据存储在中华人民共和国境内,我们采取合理技术和管理措施保障数据安全,防止未经授权的访问、泄露或篡改。</text>
<text class="doc-section">四、信息共享</text>
<text class="doc-p">未经您同意,我们不会将您的个人信息共享给第三方,法律法规要求或为完成支付、提现等必要合作除外(如微信支付、微信商家转账)。</text>
<text class="doc-section">五、您的权利</text>
<text class="doc-p">您有权查询、更正、删除您的个人信息,或撤回授权。部分权限撤回可能影响相关功能使用。您可通过小程序设置或联系我们就隐私问题提出请求。</text>
<text class="doc-section">六、未成年人</text>
<text class="doc-p">如您为未成年人,请在监护人同意下使用本服务。我们不会主动收集未成年人个人信息。</text>
<text class="doc-section">七、政策更新</text>
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
<text class="doc-section">八、联系我们</text>
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,11 @@
.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.95); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.nav-placeholder { width: 72rpx; }
.content { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }

View File

@@ -18,7 +18,22 @@ Page({
async loadOrders() {
this.setData({ loading: true })
try {
// 模拟订单数据
const userId = app.globalData.userInfo?.id
if (userId) {
const res = await app.request(`/api/orders?userId=${userId}`)
if (res && res.success && res.data) {
const orders = (res.data || []).map(item => ({
id: item.id || item.order_sn,
sectionId: item.product_id || item.section_id,
title: item.product_name || `章节 ${item.product_id || ''}`,
amount: item.amount || 0,
status: item.status || 'completed',
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--'
}))
this.setData({ orders })
return
}
}
const purchasedSections = app.globalData.purchasedSections || []
const orders = purchasedSections.map((id, index) => ({
id: `order_${index}`,
@@ -31,6 +46,13 @@ Page({
this.setData({ orders })
} catch (e) {
console.error('加载订单失败:', e)
const purchasedSections = app.globalData.purchasedSections || []
this.setData({
orders: purchasedSections.map((id, i) => ({
id: `order_${i}`, sectionId: id, title: `章节 ${id}`, amount: 1, status: 'completed',
createTime: new Date(Date.now() - i * 86400000).toLocaleDateString()
}))
})
} finally {
this.setData({ loading: false })
}

View File

@@ -1,9 +1,18 @@
/**
* Soul创业派对 - 阅读页
* Soul创业派对 - 阅读页(标准流程版)
* 开发: 卡若
* 技术支持: 存客宝
*
* 更新: 2026-02-04
* - 引入权限管理器chapterAccessManager统一权限判断
* - 引入阅读追踪器readingTracker记录阅读进度、时长、是否读完
* - 使用状态机accessState规范权限流转
* - 异常统一保守处理,避免误解锁
*/
import accessManager from '../../utils/chapterAccessManager'
import readingTracker from '../../utils/readingTracker'
const app = getApp()
Page({
@@ -25,10 +34,14 @@ Page({
previewParagraphs: [],
loading: true,
// 【新增】权限状态机(替代 canAccess
// unknown: 加载中 | free: 免费 | locked_not_login: 未登录 | locked_not_purchased: 未购买 | unlocked_purchased: 已购买 | error: 错误
accessState: 'unknown',
// 用户状态
isLoggedIn: false,
hasFullBook: false,
canAccess: false,
canAccess: false, // 保留兼容性,从 accessState 派生
purchasedCount: 0,
// 阅读进度
@@ -47,6 +60,7 @@ Page({
// 弹窗
showShareModal: false,
showLoginModal: false,
agreeProtocol: false,
showPosterModal: false,
isPaying: false,
isGeneratingPoster: false,
@@ -55,89 +69,147 @@ Page({
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
},
onLoad(options) {
async onLoad(options) {
const { id, ref } = options
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight,
sectionId: id
sectionId: id,
loading: true,
accessState: 'unknown'
})
// 处理推荐码绑定
// 处理推荐码绑定(异步不阻塞)
if (ref) {
console.log('[Read] 检测到推荐码:', ref)
wx.setStorageSync('referral_code', ref)
app.handleReferralCode({ query: { ref } })
}
// 加载免费章节配置
this.loadFreeChaptersConfig()
this.initSection(id)
try {
// 【标准流程】1. 拉取最新配置(免费列表、价格)
const config = await accessManager.fetchLatestConfig()
this.setData({
freeIds: config.freeChapters,
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
})
// 【标准流程】2. 确定权限状态
const accessState = await accessManager.determineAccessState(id, config.freeChapters)
const canAccess = accessManager.canAccessFullContent(accessState)
this.setData({
accessState,
canAccess,
isLoggedIn: !!app.globalData.userInfo?.id,
showPaywall: !canAccess
})
// 【标准流程】3. 加载内容
await this.loadContent(id, accessState)
// 【标准流程】4. 如果有权限,初始化阅读追踪
if (canAccess) {
readingTracker.init(id)
}
// 5. 加载导航
this.loadNavigation(id)
} catch (e) {
console.error('[Read] 初始化失败:', e)
wx.showToast({ title: '加载失败,请重试', icon: 'none' })
this.setData({ accessState: 'error', loading: false })
} finally {
this.setData({ loading: false })
}
},
// 从后端加载免费章节配置
async loadFreeChaptersConfig() {
try {
const res = await app.request('/api/db/config')
if (res.success && res.freeChapters) {
this.setData({ freeIds: res.freeChapters })
console.log('[Read] 加载免费章节配置:', res.freeChapters)
}
} catch (e) {
console.log('[Read] 使用默认免费章节配置')
}
},
onPageScroll(e) {
// 计算阅读进度
// 只在有权限时追踪阅读进度
if (!accessManager.canAccessFullContent(this.data.accessState)) {
return
}
// 获取滚动信息并更新追踪器
const query = wx.createSelectorQuery()
query.select('.page').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec((res) => {
if (res[0]) {
const scrollTop = e.scrollTop
const pageHeight = res[0].height - this.data.statusBarHeight - 200
const progress = pageHeight > 0 ? Math.min((scrollTop / pageHeight) * 100, 100) : 0
if (res[0] && res[1]) {
const scrollInfo = {
scrollTop: res[1].scrollTop,
scrollHeight: res[0].height,
clientHeight: res[1].height
}
// 计算进度条显示(用于 UI
const totalScrollable = scrollInfo.scrollHeight - scrollInfo.clientHeight
const progress = totalScrollable > 0
? Math.min((scrollInfo.scrollTop / totalScrollable) * 100, 100)
: 0
this.setData({ readingProgress: progress })
// 更新阅读追踪器(记录最大进度、判断是否读完)
readingTracker.updateProgress(scrollInfo)
}
})
},
// 初始化章节
async initSection(id) {
this.setData({ loading: true })
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
async loadContent(id, accessState) {
try {
// 模拟获取章节数据
const section = this.getSectionInfo(id)
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const sectionPrice = this.data.sectionPrice ?? 1
if (section.price === undefined || section.price === null) {
section.price = sectionPrice
}
this.setData({ section })
const isFree = this.data.freeIds.includes(id)
const isPurchased = hasFullBook || (purchasedSections && purchasedSections.includes(id))
const canAccess = isFree || isPurchased
const purchasedCount = purchasedSections?.length || 0
this.setData({
section,
isLoggedIn,
hasFullBook,
canAccess,
purchasedCount,
showPaywall: !canAccess
})
// 加载内容
await this.loadContent(id)
// 获取上一篇/下一篇
this.loadNavigation(id)
// 从 API 获取内容
const res = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
if (res && res.content) {
const lines = res.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
this.setData({
content: res.content,
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount),
partTitle: res.partTitle || '',
chapterTitle: res.chapterTitle || ''
})
// 如果有权限,标记为已读
if (accessManager.canAccessFullContent(accessState)) {
app.markSectionAsRead(id)
}
}
} catch (e) {
console.error('初始化章节失败:', e)
wx.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.setData({ loading: false })
console.error('[Read] 加载内容失败:', e)
// 尝试从本地缓存加载
const cacheKey = `chapter_${id}`
try {
const cached = wx.getStorageSync(cacheKey)
if (cached && cached.content) {
const lines = cached.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
this.setData({
content: cached.content,
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount)
})
console.log('[Read] 从本地缓存加载成功')
}
} catch (cacheErr) {
console.warn('[Read] 本地缓存也失败:', cacheErr)
}
throw e
}
},
@@ -237,7 +309,7 @@ Page({
reject(new Error('请求超时'))
}, timeout)
app.request(`/api/book/chapter/${id}`)
app.request(`/api/miniprogram/book/chapter/${id}`)
.then(res => {
clearTimeout(timer)
resolve(res)
@@ -412,30 +484,54 @@ Page({
}
},
// 显示登录弹窗
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
showLoginModal() {
this.setData({ showLoginModal: true })
try {
this.setData({ showLoginModal: true, agreeProtocol: false })
} catch (e) {
console.error('[Read] showLoginModal error:', e)
this.setData({ showLoginModal: true })
}
},
closeLoginModal() {
this.setData({ showLoginModal: false })
},
// 微信登录
toggleAgree() {
this.setData({ agreeProtocol: !this.data.agreeProtocol })
},
openUserProtocol() {
wx.navigateTo({ url: '/pages/agreement/agreement' })
},
openPrivacy() {
wx.navigateTo({ url: '/pages/privacy/privacy' })
},
// 从服务端刷新购买状态,避免登录后误用旧数据导致误解锁
// 【重构】微信登录(须先勾选同意协议,符合审核要求)
async handleWechatLogin() {
if (!this.data.agreeProtocol) {
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
try {
const result = await app.login()
if (result) {
this.setData({ showLoginModal: false })
this.initSection(this.data.sectionId)
wx.showToast({ title: '登录成功', icon: 'success' })
}
if (!result) return
this.setData({ showLoginModal: false, agreeProtocol: false })
await this.onLoginSuccess()
wx.showToast({ title: '登录成功', icon: 'success' })
} catch (e) {
wx.showToast({ title: '登录失败', icon: 'none' })
console.error('[Read] 登录失败:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
},
// 手机号登录
// 【重构】手机号登录(标准流程)
async handlePhoneLogin(e) {
if (!e.detail.code) {
return this.handleWechatLogin()
@@ -443,16 +539,59 @@ Page({
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
this.setData({ showLoginModal: false })
this.initSection(this.data.sectionId)
wx.showToast({ title: '登录成功', icon: 'success' })
}
if (!result) return
this.setData({ showLoginModal: false })
await this.onLoginSuccess()
wx.showToast({ title: '登录成功', icon: 'success' })
} catch (e) {
console.error('[Read] 手机号登录失败:', e)
wx.showToast({ title: '登录失败', icon: 'none' })
}
},
// 【新增】登录成功后的标准处理流程
async onLoginSuccess() {
wx.showLoading({ title: '更新状态中...', mask: true })
try {
// 1. 刷新用户购买状态(从 orders 表拉取最新)
await accessManager.refreshUserPurchaseStatus()
// 2. 重新拉取免费列表(极端情况:刚登录时当前章节可能改免费了)
const config = await accessManager.fetchLatestConfig()
this.setData({ freeIds: config.freeChapters })
// 3. 重新判断当前章节权限
const newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
config.freeChapters
)
const canAccess = accessManager.canAccessFullContent(newAccessState)
this.setData({
accessState: newAccessState,
canAccess,
isLoggedIn: true,
showPaywall: !canAccess
})
// 4. 如果已解锁,重新加载内容并初始化阅读追踪
if (canAccess) {
await this.loadContent(this.data.sectionId, newAccessState)
readingTracker.init(this.data.sectionId)
}
wx.hideLoading()
} catch (e) {
wx.hideLoading()
console.error('[Read] 登录后更新状态失败:', e)
wx.showToast({ title: '状态更新失败,请重试', icon: 'none' })
}
},
// 购买章节 - 直接调起支付
async handlePurchaseSection() {
console.log('[Pay] 点击购买章节按钮')
@@ -499,18 +638,38 @@ Page({
return
}
// 检查是否已购买(避免重复购买
if (type === 'section' && sectionId) {
const purchasedSections = app.globalData.purchasedSections || []
if (purchasedSections.includes(sectionId)) {
wx.showToast({ title: '已购买过此章节', icon: 'none' })
return
// ✅ 从服务器查询是否已购买(基于 orders 表
try {
wx.showLoading({ title: '检查购买状态...', mask: true })
const userId = app.globalData.userInfo?.id
if (userId) {
const checkRes = await app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`)
if (checkRes.success && checkRes.data) {
// 更新本地购买状态
app.globalData.hasFullBook = checkRes.data.hasFullBook
app.globalData.purchasedSections = checkRes.data.purchasedSections || []
// 检查是否已购买
if (type === 'section' && sectionId) {
if (checkRes.data.purchasedSections.includes(sectionId)) {
wx.hideLoading()
wx.showToast({ title: '已购买过此章节', icon: 'none' })
return
}
}
if (type === 'fullbook' && checkRes.data.hasFullBook) {
wx.hideLoading()
wx.showToast({ title: '已购买全书', icon: 'none' })
return
}
}
}
}
if (type === 'fullbook' && app.globalData.hasFullBook) {
wx.showToast({ title: '已购买全书', icon: 'none' })
return
} catch (e) {
console.warn('[Pay] 查询购买状态失败,继续支付流程:', e)
// 查询失败不影响支付
}
this.setData({ isPaying: true })
@@ -556,6 +715,8 @@ Page({
? '《一场Soul的创业实验》全书'
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
// 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
const referralCode = wx.getStorageSync('referral_code') || ''
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -564,7 +725,8 @@ Page({
productId: sectionId,
amount,
description,
userId: app.globalData.userInfo?.id || ''
userId: app.globalData.userInfo?.id || '',
referralCode: referralCode || undefined
}
})
@@ -607,13 +769,10 @@ Page({
try {
await this.callWechatPay(paymentData)
// 4. 支付成功,更新本地数据
// 4. 【标准流程】支付成功后刷新权限并解锁内容
console.log('[Pay] 微信支付成功!')
this.mockPaymentSuccess(type, sectionId)
wx.showToast({ title: '购买成功', icon: 'success' })
await this.onPaymentSuccess()
// 5. 刷新页面
this.initSection(this.data.sectionId)
} catch (payErr) {
console.error('[Pay] 微信支付调起失败:', payErr)
if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
@@ -648,25 +807,95 @@ Page({
}
},
// 模拟支付成功
mockPaymentSuccess(type, sectionId) {
if (type === 'fullbook') {
app.globalData.hasFullBook = true
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = true
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
} else if (sectionId) {
const purchasedSections = app.globalData.purchasedSections || []
if (!purchasedSections.includes(sectionId)) {
purchasedSections.push(sectionId)
app.globalData.purchasedSections = purchasedSections
// 【新增】支付成功后的标准处理流程
async onPaymentSuccess() {
wx.showLoading({ title: '确认购买中...', mask: true })
try {
// 1. 等待服务端处理支付回调1-2秒
await this.sleep(2000)
// 2. 刷新用户购买状态
await accessManager.refreshUserPurchaseStatus()
// 3. 重新判断当前章节权限(应为 unlocked_purchased
let newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
this.data.freeIds
)
// 如果权限未生效,再重试一次(可能回调延迟)
if (newAccessState !== 'unlocked_purchased') {
console.log('[Pay] 权限未生效1秒后重试...')
await this.sleep(1000)
newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
this.data.freeIds
)
}
const canAccess = accessManager.canAccessFullContent(newAccessState)
this.setData({
accessState: newAccessState,
canAccess,
showPaywall: !canAccess
})
// 4. 重新加载全文
await this.loadContent(this.data.sectionId, newAccessState)
// 5. 初始化阅读追踪
if (canAccess) {
readingTracker.init(this.data.sectionId)
}
wx.hideLoading()
wx.showToast({ title: '购买成功', icon: 'success' })
} catch (e) {
wx.hideLoading()
console.error('[Pay] 支付后更新失败:', e)
wx.showModal({
title: '提示',
content: '购买成功,但内容加载失败,请返回重新进入',
showCancel: false
})
}
},
// ✅ 刷新用户购买状态(从服务器获取最新数据)
async refreshUserPurchaseStatus() {
try {
const userId = app.globalData.userInfo?.id
if (!userId) {
console.warn('[Pay] 用户未登录,无法刷新购买状态')
return
}
// 调用专门的购买状态查询接口
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`)
if (res.success && res.data) {
// 更新全局购买状态
app.globalData.hasFullBook = res.data.hasFullBook
app.globalData.purchasedSections = res.data.purchasedSections || []
// 更新用户信息中的购买记录
const userInfo = app.globalData.userInfo || {}
userInfo.purchasedSections = purchasedSections
userInfo.hasFullBook = res.data.hasFullBook
userInfo.purchasedSections = res.data.purchasedSections || []
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
console.log('[Pay] ✅ 购买状态已刷新:', {
hasFullBook: res.data.hasFullBook,
purchasedCount: res.data.purchasedSections.length
})
}
} catch (e) {
console.error('[Pay] 刷新购买状态失败:', e)
// 刷新失败时不影响用户体验,只是记录日志
}
},
@@ -905,5 +1134,63 @@ Page({
},
// 阻止冒泡
stopPropagation() {}
stopPropagation() {},
// 【新增】页面隐藏时上报阅读进度
onHide() {
readingTracker.onPageHide()
},
// 【新增】页面卸载时清理追踪器
onUnload() {
readingTracker.cleanup()
},
// 【新增】重试加载(当 accessState 为 error 时)
async handleRetry() {
wx.showLoading({ title: '重试中...', mask: true })
try {
// 重新拉取配置
const config = await accessManager.fetchLatestConfig()
this.setData({ freeIds: config.freeChapters })
// 重新判断权限
const newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
config.freeChapters
)
const canAccess = accessManager.canAccessFullContent(newAccessState)
this.setData({
accessState: newAccessState,
canAccess,
showPaywall: !canAccess
})
// 重新加载内容
await this.loadContent(this.data.sectionId, newAccessState)
// 如果有权限,初始化阅读追踪
if (canAccess) {
readingTracker.init(this.data.sectionId)
}
// 加载导航
this.loadNavigation(this.data.sectionId)
wx.hideLoading()
wx.showToast({ title: '加载成功', icon: 'success' })
} catch (e) {
wx.hideLoading()
console.error('[Read] 重试失败:', e)
wx.showToast({ title: '重试失败,请检查网络', icon: 'none' })
}
},
// 工具:延迟
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
{
"usingComponents": {},
"usingComponents": {
"icon": "/components/icon/icon"
},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000",

View File

@@ -16,9 +16,7 @@
<text class="nav-part" wx:if="{{partTitle}}">{{partTitle}}</text>
<text class="nav-chapter" wx:if="{{chapterTitle}}">{{chapterTitle}}</text>
</view>
<view class="nav-share" bindtap="showShare">
<text class="share-icon">↗</text>
</view>
<view class="nav-right-placeholder"></view>
</view>
</view>
@@ -37,7 +35,7 @@
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{loading}}">
<view class="loading-state" wx:if="{{accessState === 'unknown' && loading}}">
<view class="skeleton skeleton-1"></view>
<view class="skeleton skeleton-2"></view>
<view class="skeleton skeleton-3"></view>
@@ -45,8 +43,8 @@
<view class="skeleton skeleton-5"></view>
</view>
<!-- 完整内容 - 有权限 -->
<view class="article" wx:if="{{!loading && canAccess}}">
<!-- 完整内容 - 免费或已购买 -->
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</view>
@@ -97,8 +95,8 @@
</view>
</view>
<!-- 预览内容 + 付费墙 - 无权限 -->
<view class="article preview" wx:if="{{!loading && !canAccess}}">
<!-- 预览内容 + 付费墙 - 未登录 -->
<view class="article preview" wx:if="{{accessState === 'locked_not_login'}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</view>
@@ -106,46 +104,18 @@
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 付费墙 -->
<view class="paywall" wx:if="{{showPaywall}}">
<!-- 付费墙 - 未登录 -->
<view class="paywall">
<view class="paywall-icon">🔒</view>
<text class="paywall-title">解锁完整内容</text>
<text class="paywall-desc">
已阅读20%{{isLoggedIn ? '购买后继续阅读' : '登录并购买后继续阅读'}}
</text>
<text class="paywall-title">登录后继续阅读</text>
<text class="paywall-desc">已阅读20%,登录后查看完整内容</text>
<!-- 未登录时显示登录按钮 -->
<view class="login-prompt" wx:if="{{!isLoggedIn}}">
<view class="login-btn" bindtap="showLoginModal">
<text class="login-btn-text">请先登录</text>
</view>
<view class="login-btn" bindtap="showLoginModal">
<text class="login-btn-text">立即登录</text>
</view>
<!-- 已登录显示购买选项 -->
<view class="purchase-options" wx:else>
<!-- 购买本章 - 直接调起支付 -->
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
<text class="btn-label">购买本章</text>
<text class="btn-price brand-color">¥{{section.price}}</text>
</view>
<!-- 解锁全书 - 只有购买超过3章才显示 -->
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
<view class="btn-left">
<text class="btn-sparkle">✨</text>
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
</view>
<view class="btn-right">
<text class="btn-price">¥{{fullBookPrice}}</text>
<text class="btn-discount">省82%</text>
</view>
</view>
</view>
<text class="paywall-tip">分享给好友一起学习</text>
</view>
<!-- 章节导航 - 付费内容也显示 -->
<!-- 章节导航 -->
<view class="chapter-nav chapter-nav-locked">
<view class="nav-buttons">
<view
@@ -175,6 +145,97 @@
</view>
</view>
</view>
<!-- 预览内容 + 付费墙 - 已登录未购买 -->
<view class="article preview" wx:if="{{accessState === 'locked_not_purchased'}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</view>
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 付费墙 - 已登录未购买 -->
<view class="paywall">
<view class="paywall-icon">🔒</view>
<text class="paywall-title">解锁完整内容</text>
<text class="paywall-desc">已阅读20%,购买后继续阅读</text>
<!-- 购买选项 -->
<view class="purchase-options">
<!-- 购买本章 - 直接调起支付 -->
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
<text class="btn-label">购买本章</text>
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
</view>
<!-- 解锁全书 - 只有购买超过3章才显示 -->
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
<view class="btn-left">
<text class="btn-sparkle">✨</text>
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
</view>
<view class="btn-right">
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
<text class="btn-discount">省82%</text>
</view>
</view>
</view>
<text class="paywall-tip">分享给好友一起学习,还能赚取佣金</text>
</view>
<!-- 章节导航 -->
<view class="chapter-nav chapter-nav-locked">
<view class="nav-buttons">
<view
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
bindtap="goToPrev"
wx:if="{{prevSection}}"
>
<text class="btn-label">上一篇</text>
<text class="btn-title">章节 {{prevSection.id}}</text>
</view>
<view class="nav-btn-placeholder" wx:else></view>
<view
class="nav-btn nav-next"
bindtap="goToNext"
wx:if="{{nextSection}}"
>
<text class="btn-label">下一篇</text>
<view class="btn-row">
<text class="btn-title">{{nextSection.title}}</text>
<text class="btn-arrow">→</text>
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
</view>
</view>
</view>
</view>
<!-- 错误状态 - 网络异常 -->
<view class="article preview" wx:if="{{accessState === 'error'}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</view>
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 错误提示 -->
<view class="paywall">
<view class="paywall-icon">⚠️</view>
<text class="paywall-title">网络异常</text>
<text class="paywall-desc">无法确认权限,请检查网络后重试</text>
<view class="login-btn" bindtap="handleRetry">
<text class="login-btn-text">重新加载</text>
</view>
</view>
</view>
</view>
<!-- 海报生成弹窗 -->
@@ -201,7 +262,7 @@
</view>
</view>
<!-- 登录弹窗 - 只保留微信登录 -->
<!-- 登录弹窗 - 须勾选同意协议,《用户协议》《隐私政策》可点击查看 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
@@ -209,12 +270,18 @@
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat" bindtap="handleWechatLogin">
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
<text class="btn-wechat-icon">微</text>
<text>微信快捷登录</text>
</button>
<text class="login-notice">登录即表示同意《用户协议》和《隐私政策》</text>
<view class="login-agree-row" catchtap="toggleAgree">
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
<text class="agree-text">我已阅读并同意</text>
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
<text class="agree-text">和</text>
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
</view>
</view>
</view>
@@ -225,4 +292,9 @@
<text class="loading-text">支付处理中...</text>
</view>
</view>
<!-- 右下角悬浮分享按钮 -->
<button class="fab-share" open-type="share">
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
</button>
</view>

View File

@@ -44,9 +44,13 @@
height: 88rpx;
}
.nav-back, .nav-share {
.nav-back, .nav-right-placeholder {
width: 72rpx;
height: 72rpx;
flex-shrink: 0;
}
.nav-back {
border-radius: 50%;
background: #1c1c1e;
display: flex;
@@ -54,13 +58,12 @@
justify-content: center;
}
.back-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
.nav-right-placeholder {
/* 占位保持标题居中 */
}
.share-icon {
font-size: 32rpx;
.back-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
}
@@ -827,12 +830,39 @@
font-size: 32rpx;
}
.login-notice {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.3);
.login-agree-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin-top: 32rpx;
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
.agree-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.5);
border-radius: 6rpx;
margin-right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #fff;
flex-shrink: 0;
}
.agree-checked {
background: #00CED1;
border-color: #00CED1;
}
.agree-text { color: rgba(255, 255, 255, 0.6); }
.agree-link {
color: #00CED1;
text-decoration: underline;
padding: 0 4rpx;
}
.btn-wechat-disabled { opacity: 0.6; }
/* ===== 支付中加载 ===== */
.loading-box {
@@ -915,3 +945,40 @@
text-align: center;
display: block;
}
/* ===== 右下角悬浮分享按钮 ===== */
.fab-share {
position: fixed;
right: 32rpx;
width:70rpx!important;
bottom: calc(120rpx + env(safe-area-inset-bottom));
height: 70rpx;
border-radius: 60rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
padding: 0;
margin: 0;
border: none;
z-index: 9999;
display:flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.fab-share::after {
border: none;
}
.fab-share:active {
transform: scale(0.95);
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
}
.fab-icon {
padding:16rpx;
width: 50rpx;
height: 50rpx;
display: block;
}

View File

@@ -0,0 +1,182 @@
/* ===================================
收益明细卡片样式 - 重构版
创建时间2026-02-04
说明:修复布局错乱问题,优化文本显示
=================================== */
/* 收益明细卡片容器 */
.earnings-detail-card {
background: rgba(28, 28, 30, 0.8);
backdrop-filter: blur(40rpx);
border: 2rpx solid rgba(255,255,255,0.1);
border-radius: 32rpx;
overflow: hidden;
margin-bottom: 24rpx;
width: 100%;
box-sizing: border-box;
}
/* 卡片头部 */
.detail-header {
padding: 40rpx 40rpx 24rpx;
border-bottom: 2rpx solid rgba(255,255,255,0.05);
}
.detail-title {
font-size: 30rpx;
font-weight: 600;
color: #fff;
}
/* 列表容器 */
.detail-list {
max-height: 480rpx;
overflow-y: auto;
padding: 16rpx 0;
}
/* ===================================
收益明细列表项 - 核心样式
=================================== */
/* 列表项容器 - 使用flex布局 */
.earnings-detail-card .detail-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx 40rpx;
background: transparent;
border-bottom: 2rpx solid rgba(255,255,255,0.03);
transition: background 0.3s;
}
.earnings-detail-card .detail-item:last-child {
border-bottom: none;
}
.earnings-detail-card .detail-item:active {
background: rgba(255, 255, 255, 0.05);
}
/* 头像容器 - 固定宽度,不收缩 */
.earnings-detail-card .detail-avatar-wrap {
width: 88rpx;
height: 88rpx;
flex-shrink: 0;
}
.earnings-detail-card .detail-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
border: 2rpx solid rgba(56, 189, 172, 0.2);
display: block;
}
.earnings-detail-card .detail-avatar-text {
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
}
/* 详细信息容器 - 占据剩余空间,允许收缩 */
.earnings-detail-card .detail-content {
flex: 1;
min-width: 0; /* 关键允许flex子元素收缩到比内容更小 */
display: flex;
flex-direction: column;
gap: 8rpx;
}
/* 顶部行:昵称 + 金额 */
.earnings-detail-card .detail-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
/* 买家昵称 - 允许收缩,显示省略号 */
.earnings-detail-card .detail-buyer {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
flex: 1;
min-width: 0; /* 关键:允许收缩 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 佣金金额 - 固定宽度,不收缩 */
.earnings-detail-card .detail-amount {
font-size: 32rpx;
font-weight: 700;
color: #38bdac;
flex-shrink: 0;
white-space: nowrap;
}
/* 商品信息行:书名 + 章节 */
.earnings-detail-card .detail-product {
display: flex;
align-items: baseline;
gap: 4rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
min-width: 0; /* 关键:允许收缩 */
overflow: hidden;
}
/* 书名 - 限制最大宽度,显示省略号 */
.earnings-detail-card .detail-book {
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
max-width: 50%; /* 限制书名最多占一半宽度 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 1;
}
/* 章节 - 占据剩余空间,显示省略号 */
.earnings-detail-card .detail-chapter {
color: rgba(255, 255, 255, 0.5);
flex: 1;
min-width: 0; /* 关键:允许收缩 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 时间信息 */
.earnings-detail-card .detail-time {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===================================
响应式优化
=================================== */
/* 小屏幕优化 */
@media (max-width: 375px) {
.earnings-detail-card .detail-buyer {
font-size: 26rpx;
}
.earnings-detail-card .detail-amount {
font-size: 30rpx;
}
.earnings-detail-card .detail-book {
max-width: 45%;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
{
"usingComponents": {},
"navigationStyle": "custom"
"navigationStyle": "custom",
"enableShareAppMessage": true,
"enableShareTimeline": true
}

View File

@@ -2,96 +2,87 @@
<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-right">
<view class="nav-btn" bindtap="showNotification">🔔</view>
<view class="nav-btn" bindtap="showSettings">⚙️</view>
<view class="nav-left">
<view class="nav-back" bindtap="goBack">
<image class="nav-icon" src="/assets/icons/chevron-left.svg" mode="aspectFit"></image>
</view>
</view>
<text class="nav-title">分销中心</text>
<view class="nav-right-placeholder"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<!-- 过期提醒横幅 -->
<view class="expiring-banner" wx:if="{{expiringCount > 0}}">
<view class="banner-icon">⚠️</view>
<view class="banner-icon">
<image class="icon-bell-warning" src="/assets/icons/bell.svg" mode="aspectFit"></image>
</view>
<view class="banner-content">
<text class="banner-title">{{expiringCount}} 位用户绑定即将过期</text>
<text class="banner-desc">30天内未付款将解除绑定关系</text>
<text class="banner-desc">{{bindingDays}}天内未付款将解除绑定关系</text>
</view>
</view>
<!-- 收益卡片 -->
<!-- 收益卡片 - 对齐 Next.js -->
<view class="earnings-card">
<view class="earnings-bg"></view>
<view class="earnings-main">
<view class="earnings-header">
<view class="earnings-left">
<view class="wallet-icon">💰</view>
<view class="wallet-icon">
<image class="icon-wallet" src="/assets/icons/wallet.svg" mode="aspectFit"></image>
</view>
<view class="earnings-info">
<text class="earnings-label">累计收益</text>
<text class="earnings-label">可提现金额</text>
<text class="commission-rate">{{shareRate}}% 返利</text>
</view>
</view>
<view class="earnings-right">
<text class="earnings-value">¥{{earnings}}</text>
<text class="pending-text">待结算: ¥{{pendingEarnings}}</text>
<text class="earnings-value">¥{{availableEarnings}}</text>
<text class="pending-text">累计: ¥{{totalCommission}} | 待审核: ¥{{pendingWithdrawAmount}}</text>
</view>
</view>
<view class="earnings-detail">
<text class="detail-item">已提现: ¥{{withdrawnEarnings}}</text>
</view>
<view class="withdraw-btn {{pendingEarnings <= 0 ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
{{pendingEarnings <= 0 ? '暂无收益' : '立即提现 ¥' + pendingEarnings}}
<view class="withdraw-btn {{availableEarningsNum <= 0 || !hasWechatId ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
{{availableEarningsNum <= 0 ? '暂无可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
</view>
<text class="wechat-tip" wx:if="{{availableEarningsNum > 0 && !hasWechatId}}">为便于提现到账,请先到「设置」中绑定微信号</text>
<view class="withdraw-records-link" bindtap="goToWithdrawRecords">查看提现记录</view>
</view>
</view>
<!-- 核心数据统计(重点可见数据) -->
<!-- 数据统计 - 对齐 Next.js -->
<view class="stats-grid">
<view class="stat-card highlight">
<text class="stat-value brand">{{bindingCount}}</text>
<view class="stat-card">
<text class="stat-value">{{bindingCount}}</text>
<text class="stat-label">绑定中</text>
<text class="stat-tip">当前有效绑定</text>
</view>
<view class="stat-card highlight">
<text class="stat-value gold">{{paidCount}}</text>
<view class="stat-card">
<text class="stat-value">{{paidCount}}</text>
<text class="stat-label">已付款</text>
<text class="stat-tip">成功转化</text>
</view>
<view class="stat-card">
<text class="stat-value orange">{{unboughtCount}}</text>
<text class="stat-label">待购买</text>
<text class="stat-tip">绑定未付款</text>
<text class="stat-label">即将过期</text>
</view>
<view class="stat-card">
<text class="stat-value gray">{{expiredCount}}</text>
<text class="stat-label">已过期</text>
<text class="stat-tip">绑定已失效</text>
<text class="stat-value">{{referralCount}}</text>
<text class="stat-label">总邀请</text>
</view>
</view>
<!-- 访问量统计 -->
<view class="visit-stat">
<text class="visit-label">总访问量</text>
<text class="visit-value">{{visitCount}}</text>
<text class="visit-tip">人通过你的链接进入</text>
</view>
<!-- 推广规则 -->
<!-- 推广规则 - 顺序调整到前面 -->
<view class="rules-card">
<view class="rules-header">
<view class="rules-icon">📋</view>
<view class="rules-icon">
<image class="icon-alert" src="/assets/icons/alert-circle.svg" mode="aspectFit"></image>
</view>
<text class="rules-title">推广规则</text>
</view>
<view class="rules-list">
<text class="rule-item">• <text class="brand">链接带ID</text>:谁发的链接,进的人就绑谁</text>
<text class="rule-item">• <text class="brand">一级、一月</text>只有一级分销绑定有效期30天</text>
<text class="rule-item">• <text class="orange">长期不发</text>:别人发得多,过期后客户会被「抢走」</text>
<text class="rule-item">• <text class="gold">每天发</text>:持续发的人绑定续期,收益越来越高</text>
<text class="rule-item">• <text class="brand">{{shareRate}}%给分发</text>:好友付款后,你得 {{shareRate}}% 收益</text>
<text class="rule-item">• 好友通过你的链接购买,<text class="gold">立享{{userDiscount}}%优惠</text></text>
<text class="rule-item">• 好友成功付款后,你获得 <text class="brand">{{shareRate}}%</text> 收益</text>
<text class="rule-item">• 绑定期<text class="brand">{{bindingDays}}天</text>,期满未付款自动解除</text>
</view>
</view>
@@ -99,7 +90,7 @@
<view class="binding-card">
<view class="binding-header" bindtap="toggleBindingList">
<view class="binding-title">
<text class="binding-icon">👥</text>
<image class="binding-icon-img" src="/assets/icons/users.svg" mode="aspectFit"></image>
<text class="title-text">绑定用户</text>
<text class="binding-count">({{totalBindings}})</text>
</view>
@@ -152,11 +143,15 @@
<view class="user-status">
<block wx:if="{{item.status === 'converted'}}">
<text class="status-amount">+¥{{item.commission}}</text>
<text class="status-order">订单 ¥{{item.orderAmount}}</text>
<text class="status-order">已购{{item.purchaseCount || 1}}</text>
</block>
<block wx:elif="{{item.status === 'expired'}}">
<text class="status-tag tag-gray">已过期</text>
<text class="status-time">{{item.expiryDate}}</text>
</block>
<block wx:else>
<text class="status-tag {{item.daysRemaining <= 3 ? 'tag-red' : item.daysRemaining <= 7 ? 'tag-orange' : 'tag-green'}}">
{{item.status === 'expired' ? '已过期' : item.daysRemaining + '天'}}
{{item.daysRemaining}}
</text>
</block>
</view>
@@ -166,58 +161,172 @@
</block>
</view>
<!-- 分享按钮 -->
<!-- 分享按钮 - 1:1 对齐 Next.js -->
<view class="share-section">
<view class="share-item" bindtap="generatePoster">
<view class="share-icon poster">🖼️</view>
<view class="share-icon poster">
<image class="icon-share-btn" src="/assets/icons/image.svg" mode="aspectFit"></image>
</view>
<view class="share-info">
<text class="share-title">生成推广海报</text>
<text class="share-desc">一键生成精美海报分享</text>
</view>
<text class="share-arrow"></text>
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
</view>
<button class="share-item share-btn-wechat" open-type="share">
<view class="share-icon wechat">💬</view>
<view class="share-info">
<text class="share-title">分享给好友</text>
<text class="share-desc">直接发送小程序卡片</text>
<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>
<text class="share-arrow">→</text>
</button>
<view class="share-item" bindtap="shareToMoments">
<view class="share-icon link">📝</view>
<view class="share-info">
<text class="share-title">复制朋友圈文案</text>
<text class="share-desc">一键复制推广文案</text>
<text class="share-title">分享到朋友圈</text>
<text class="share-desc">复制文案发朋友圈</text>
</view>
<text class="share-arrow"></text>
<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>
<!-- 收益明细 - 增强版 -->
<view class="earnings-detail-card" wx:if="{{earningsDetails.length > 0}}">
<view class="detail-header">
<text class="detail-title">收益明细</text>
</view>
<view class="detail-list">
<view class="detail-item" wx:for="{{earningsDetails}}" wx:key="id">
<!-- 买家头像 -->
<view class="detail-avatar-wrap">
<image
class="detail-avatar"
wx:if="{{item.buyerAvatar}}"
src="{{item.buyerAvatar}}"
mode="aspectFill"
/>
<view class="detail-avatar-text" wx:else>
{{item.buyerNickname.charAt(0)}}
</view>
</view>
<!-- 详细信息 -->
<view class="detail-content">
<view class="detail-top">
<text class="detail-buyer">{{item.buyerNickname}}</text>
<text class="detail-amount">+¥{{item.commission}}</text>
</view>
<view class="detail-product">
<text class="detail-book">{{item.bookTitle}}</text>
<text class="detail-chapter" wx:if="{{item.chapterTitle}}"> - {{item.chapterTitle}}</text>
</view>
<text class="detail-time">{{item.payTime}}</text>
</view>
</view>
</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>
<!-- 海报生成弹窗 -->
<!-- 海报生成弹窗 - 优化小程序显示 -->
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
<view class="modal-content poster-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">推广海报</text>
<view class="modal-close" bindtap="closePosterModal">✕</view>
</view>
<view class="poster-dialog" catchtap="stopPropagation">
<view class="poster-close" bindtap="closePosterModal">✕</view>
<!-- 海报预览 -->
<view class="poster-preview">
<canvas canvas-id="promoPosterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
</view>
<!-- 上半部分:海报内容(不使用画布,纯布局 + 二维码图片) -->
<view class="poster-card">
<!-- 装饰光效 -->
<view class="poster-glow poster-glow-left"></view>
<view class="poster-glow poster-glow-right"></view>
<view class="poster-ring"></view>
<view class="poster-actions">
<view class="poster-btn btn-save" bindtap="savePoster">
<text class="btn-icon">💾</text>
<text>保存到相册</text>
<view class="poster-inner">
<!-- 顶部标签 -->
<view class="poster-badges">
<text class="poster-badge poster-badge-gold">真实商业案例</text>
<text class="poster-badge poster-badge-brand">每日更新</text>
</view>
<!-- 标题 -->
<view class="poster-title">
<text class="poster-title-line1">一场SOUL的</text>
<text class="poster-title-line2">创业实验场</text>
</view>
<text class="poster-subtitle">来自Soul派对房的真实商业故事</text>
<!-- 核心数据 -->
<view class="poster-stats">
<view class="poster-stat">
<text class="poster-stat-value poster-stat-gold">{{posterCaseCount}}</text>
<text class="poster-stat-label">真实案例</text>
</view>
<view class="poster-stat">
<text class="poster-stat-value poster-stat-brand">{{userDiscount}}%</text>
<text class="poster-stat-label">好友优惠</text>
</view>
<view class="poster-stat">
<text class="poster-stat-value poster-stat-pink">{{shareRate}}%</text>
<text class="poster-stat-label">你的收益</text>
</view>
</view>
<!-- 标签 -->
<view class="poster-tags">
<text class="poster-tag">人性观察</text>
<text class="poster-tag">行业揭秘</text>
<text class="poster-tag">赚钱逻辑</text>
<text class="poster-tag">创业复盘</text>
<text class="poster-tag">资源对接</text>
</view>
<!-- 推荐人 -->
<view class="poster-recommender">
<view class="poster-avatar">
<text class="poster-avatar-text">{{posterNicknameInitial}}</text>
</view>
<text class="poster-recommender-text">{{posterNickname}} 推荐你来读</text>
</view>
<!-- 优惠说明 -->
<view class="poster-discount">
<text class="poster-discount-text">通过我的链接购买,<text class="poster-discount-highlight">立省{{userDiscount}}%</text>
</text>
</view>
<!-- 二维码 -->
<view class="poster-qr-wrap" bindtap="previewPosterQr">
<image
class="poster-qr-img"
src="{{posterQrSrc}}"
mode="aspectFit"
show-menu-by-longpress="true"
></image>
</view>
<text class="poster-qr-tip">长按识别 · 立即试读</text>
<text class="poster-code">邀请码: {{referralCode}}</text>
</view>
</view>
<text class="poster-tip">保存后可分享到朋友圈或发送给好友</text>
<!-- 下半部分:白色操作区 -->
<view class="poster-footer">
<text class="poster-footer-tip">长按上方图片保存,或截图分享</text>
<view class="poster-footer-btn" bindtap="closePosterModal">关闭</view>
</view>
</view>
</view>
</view>

View File

@@ -1,66 +1,63 @@
/* 分销中心页面样式 - 1:1还原Web版本 */
/* ???????? - 1:1??Web?? */
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
/* 导航栏 */
/* ??? */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-left { display: flex; gap: 16rpx; align-items: center; }
.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-right { display: flex; gap: 16rpx; }
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
.nav-right-placeholder { width: 144rpx; }
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
/* 过期提醒横幅 */
/* ?????? */
.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40rpx; flex-shrink: 0; }
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
.banner-content { flex: 1; }
.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
/* 收益卡片 */
.earnings-card { position: relative; background: linear-gradient(135deg, rgba(0,206,209,0.15) 0%, rgba(32,178,170,0.1) 50%, rgba(0,139,139,0.05) 100%); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
.earnings-bg { position: absolute; top: -50rpx; right: -50rpx; width: 200rpx; height: 200rpx; background: rgba(0,206,209,0.1); border-radius: 50%; filter: blur(60rpx); }
/* ???? - ?? Next.js */
.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
.earnings-main { position: relative; }
.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
.earnings-left { display: flex; align-items: center; gap: 16rpx; }
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 40rpx; }
.earnings-info { display: flex; flex-direction: column; gap: 4rpx; }
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
.earnings-right { text-align: right; }
.earnings-value { font-size: 56rpx; font-weight: 700; color: #fff; display: block; }
.pending-text { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
.withdraw-btn { padding: 24rpx; background: #00CED1; color: #000; font-size: 28rpx; font-weight: 600; text-align: center; border-radius: 24rpx; }
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.3); color: rgba(0,0,0,0.5); }
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
.wechat-tip { display: block; font-size: 24rpx; color: rgba(255,165,0,0.9); margin-top: 16rpx; text-align: center; }
.withdraw-records-link { display: block; margin-top: 16rpx; text-align: center; font-size: 26rpx; color: #00CED1; }
/* 收益详情 */
.earnings-detail { padding-top: 16rpx; border-top: 2rpx solid rgba(255,255,255,0.1); margin-bottom: 24rpx; }
.detail-item { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* 核心数据统计 */
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.stat-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 20rpx; text-align: center; position: relative; }
.stat-card.highlight { background: linear-gradient(135deg, rgba(0,206,209,0.1) 0%, rgba(0,206,209,0.05) 100%); border: 2rpx solid rgba(0,206,209,0.2); }
.stat-value { font-size: 48rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.brand { color: #00CED1; }
.stat-value.gold { color: #FFD700; }
/* ???? - ?? Next.js 4??? */
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.orange { color: #FFA500; }
.stat-value.gray { color: #9E9E9E; }
.stat-label { font-size: 24rpx; color: rgba(255,255,255,0.7); margin-top: 8rpx; display: block; font-weight: 500; }
.stat-tip { font-size: 20rpx; color: rgba(255,255,255,0.4); margin-top: 4rpx; display: block; }
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
/* 访问量统计 */
/* ????? */
.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* 推广规则 */
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 24rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
/* ???? - ?? Next.js */
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.rules-icon { width: 56rpx; height: 56rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
.rules-list { padding-left: 8rpx; }
.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
@@ -68,81 +65,238 @@
.rule-item .brand { color: #00CED1; font-weight: 500; }
.rule-item .orange { color: #FFA500; font-weight: 500; }
/* 绑定用户卡片 */
.binding-card { background: #1c1c1e; border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
/* ?????? - ?? Next.js */
.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-title { display: flex; align-items: center; gap: 12rpx; }
.binding-icon { font-size: 36rpx; }
.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* Tab切换 */
/* Tab?? */
.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
.tab-item.tab-active { color: #00CED1; }
.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
/* 用户列表 */
/* ???? */
.binding-list { max-height: 640rpx; overflow-y: auto; }
.empty-state { padding: 80rpx 0; text-align: center; }
.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
/* ?????? - ??? */
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); gap: 24rpx; }
.binding-item:last-child { border-bottom: none; }
.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
/* ?? */
.user-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; color: #00CED1; flex-shrink: 0; }
.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
.user-info { flex: 1; }
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.user-status { text-align: right; }
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
/* ???? */
.user-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ???? */
.user-status { flex-shrink: 0; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; min-width: 100rpx; }
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; white-space: nowrap; }
.status-order { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
.status-time { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
/* ???? */
.status-tag { font-size: 24rpx; font-weight: 600; padding: 6rpx 16rpx; border-radius: 16rpx; white-space: nowrap; }
.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
.status-tag.tag-gray { background: rgba(158,158,158,0.2); color: #9E9E9E; }
/* 邀请码卡片 */
.invite-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
.invite-title { font-size: 28rpx; font-weight: 600; color: #fff; }
.invite-code-box { background: rgba(0,206,209,0.15); padding: 12rpx 24rpx; border-radius: 16rpx; }
.invite-code { font-size: 26rpx; font-weight: 600; color: #00CED1; font-family: monospace; letter-spacing: 2rpx; }
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* ????? - ?? Next.js */
.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
.invite-tip .gold { color: #FFD700; }
.invite-tip .brand { color: #00CED1; }
/* 分享区域 */
.share-section { display: flex; flex-direction: column; gap: 16rpx; width: 100%; }
.share-item { display: flex; align-items: center; background: #1c1c1e; border-radius: 24rpx; padding: 24rpx 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
/* ?????? - ??? */
.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.detail-list { max-height: 480rpx; overflow-y: auto; padding: 16rpx 0; }
/* ??????? */
.earnings-detail-card .detail-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx 40rpx; background: transparent; border-bottom: 2rpx solid rgba(255,255,255,0.03); }
.earnings-detail-card .detail-item:last-child { border-bottom: none; }
.earnings-detail-card .detail-item:active { background: rgba(255, 255, 255, 0.05); }
/* ???? */
.earnings-detail-card .detail-avatar-wrap { width: 88rpx; height: 88rpx; flex-shrink: 0; }
.earnings-detail-card .detail-avatar { width: 100%; height: 100%; border-radius: 50%; border: 2rpx solid rgba(56, 189, 172, 0.2); }
.earnings-detail-card .detail-avatar-text { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #38bdac 0%, #2da396 100%); display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: #ffffff; }
/* ???? */
.earnings-detail-card .detail-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.earnings-detail-card .detail-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.earnings-detail-card .detail-buyer { font-size: 28rpx; font-weight: 500; color: #ffffff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.earnings-detail-card .detail-amount { font-size: 32rpx; font-weight: 700; color: #38bdac; flex-shrink: 0; white-space: nowrap; }
/* ???? */
.earnings-detail-card .detail-product { display: flex; align-items: baseline; gap: 4rpx; font-size: 24rpx; color: rgba(255, 255, 255, 0.6); min-width: 0; overflow: hidden; }
.earnings-detail-card .detail-book { color: rgba(255, 255, 255, 0.7); font-weight: 500; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.earnings-detail-card .detail-chapter { color: rgba(255, 255, 255, 0.5); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.earnings-detail-card .detail-time { font-size: 22rpx; color: rgba(255, 255, 255, 0.4); }
/* ???? - ?? Next.js */
.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
.share-item::after { border: none; }
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; margin-right: 24rpx; flex-shrink: 0; }
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
.share-icon.poster { background: rgba(103,58,183,0.2); }
.share-icon.wechat { background: rgba(7,193,96,0.2); }
.share-icon.link { background: rgba(158,158,158,0.2); }
.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-info { flex: 1; text-align: left; }
.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
.share-arrow { font-size: 28rpx; color: rgba(255,255,255,0.3); flex-shrink: 0; }
.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
/* 弹窗 */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(20rpx); display: flex; align-items: flex-end; justify-content: center; z-index: 1000; }
.modal-content { width: 100%; max-width: 750rpx; background: #1c1c1e; border-radius: 48rpx 48rpx 0 0; padding: 48rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); animation: slideUp 0.3s ease; }
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32rpx; }
.modal-title { font-size: 36rpx; font-weight: 600; color: #fff; }
.modal-close { width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(255,255,255,0.6); }
/* ?????????????? + ???? + ???????? */
/* ???????? backdrop-filter??????????????? */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
/* 海报弹窗 */
.poster-modal { padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); }
.poster-preview { display: flex; justify-content: center; margin: 32rpx 0; padding: 24rpx; background: rgba(0,0,0,0.3); border-radius: 24rpx; }
.poster-canvas { border-radius: 16rpx; box-shadow: 0 16rpx 48rpx rgba(0,0,0,0.5); }
.poster-actions { display: flex; gap: 24rpx; margin-bottom: 24rpx; }
.poster-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 28rpx; border-radius: 24rpx; font-size: 30rpx; font-weight: 500; color: #fff; }
.btn-save { background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); }
.btn-icon { font-size: 32rpx; }
.poster-tip { font-size: 24rpx; color: rgba(255,255,255,0.4); text-align: center; display: block; }
.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
/* ???? */
.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
/* ???? */
/* ???????? filter: blur ??????????????? + ???? */
.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
/* ???? */
.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
/* ?? */
.poster-title { margin-bottom: 8rpx; }
.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
/* ???? */
.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
.poster-stat-gold { color: #FFD700; }
.poster-stat-brand { color: #00CED1; }
.poster-stat-pink { color: #E91E63; }
/* ?? */
.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
/* ??? */
.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
/* ??? */
.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
.poster-discount-highlight { color: #00CED1; font-weight: 800; }
/* ??? */
.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
/* ??????? */
.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
/* ????- ?? 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; }
/* ===== Loading 遮罩(备用) ===== */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(10rpx);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid rgba(56,189,172,0.2);
border-top-color: #38bdac;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: rgba(255,255,255,0.8);
font-weight: 500;
}
.content-loading {
opacity: 0.3;
pointer-events: none;
}
/* ===== 收益明细独立块 ===== */
.detail-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.02); border-radius: 16rpx; margin-bottom: 16rpx; transition: all 0.3s; }
.detail-item:active { background: rgba(255,255,255,0.05); }
.detail-avatar-wrap { flex-shrink: 0; }
.detail-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; border: 2rpx solid rgba(56,189,172,0.2); }
.detail-avatar-text { width: 88rpx; height: 88rpx; border-radius: 50%; background: linear-gradient(135deg, #38bdac 0%, #2da396 100%); display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: #ffffff; }
.detail-content { flex: 1; display: flex; flex-direction: column; gap: 8rpx; min-width: 0; }
.detail-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.detail-buyer { font-size: 28rpx; font-weight: 500; color: #ffffff; flex-shrink: 0; }
.detail-amount { font-size: 32rpx; font-weight: 700; color: #38bdac; flex-shrink: 0; }
.detail-product { display: flex; align-items: center; font-size: 24rpx; color: rgba(255,255,255,0.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.detail-book { color: rgba(255,255,255,0.7); font-weight: 500; }
.detail-chapter { color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.detail-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }

View File

@@ -0,0 +1,379 @@
/* ???????? - 1:1??Web?? */
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
/* ??? */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-left { display: flex; gap: 16rpx; align-items: center; }
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
.nav-right-placeholder { width: 144rpx; }
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
/* ?????? */
.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
.banner-content { flex: 1; }
.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
/* ???? - ?? Next.js */
.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
.earnings-main { position: relative; }
.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
.earnings-left { display: flex; align-items: center; gap: 16rpx; }
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
.earnings-right { text-align: right; }
.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
/* ???? - ?? Next.js 4??? */
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.orange { color: #FFA500; }
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
/* ????? */
.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* ???? - ?? Next.js */
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
.rules-list { padding-left: 8rpx; }
.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
.rule-item .gold { color: #FFD700; font-weight: 500; }
.rule-item .brand { color: #00CED1; font-weight: 500; }
.rule-item .orange { color: #FFA500; font-weight: 500; }
/* ?????? - ?? Next.js */
.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-title { display: flex; align-items: center; gap: 12rpx; }
.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* Tab?? */
.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
.tab-item.tab-active { color: #00CED1; }
.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
/* ???? */
.binding-list { max-height: 640rpx; overflow-y: auto; }
.empty-state { padding: 80rpx 0; text-align: center; }
.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-item:last-child { border-bottom: none; }
.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
.user-info { flex: 1; }
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.user-status { text-align: right; }
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
/* ????? - ?? Next.js */
.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
.invite-tip .gold { color: #FFD700; }
.invite-tip .brand { color: #00CED1; }
/* ?????? - ?? Next.js */
.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.detail-list { max-height: 480rpx; overflow-y: auto; }
.detail-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 40rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.detail-item:last-child { border-bottom: none; }
.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.detail-info { flex: 1; }
.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
/* ???? - ?? Next.js */
.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
.share-item::after { border: none; }
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
.share-icon.poster { background: rgba(103,58,183,0.2); }
.share-icon.wechat { background: rgba(7,193,96,0.2); }
.share-icon.link { background: rgba(158,158,158,0.2); }
.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-info { flex: 1; text-align: left; }
.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
/* ?????????????? + ???? + ???????? */
/* ???????? backdrop-filter??????????????? */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
/* ???? */
.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
/* ???? */
/* ???????? filter: blur ??????????????? + ???? */
.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
/* ???? */
.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
/* ?? */
.poster-title { margin-bottom: 8rpx; }
.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
/* ???? */
.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
.poster-stat-gold { color: #FFD700; }
.poster-stat-brand { color: #00CED1; }
.poster-stat-pink { color: #E91E63; }
/* ?? */
.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
/* ??? */
.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
/* ??? */
.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
.poster-discount-highlight { color: #00CED1; font-weight: 800; }
/* ??? */
.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
/* ??????? */
.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
/* ????- ?? 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; }
/* ===== T<><54>rGm<18>5<EFBFBD><35> ?===== */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10rpx);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid rgba(56, 189, 172, 0.2);
border-top-color: #38bdac;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.content-loading {
opacity: 0.3;
pointer-events: none;
}
/* ===== <00><>AR<41><6C>^<5E>|<7C>o<EFBFBD>p<EFBFBD><>\!} ===== */
.detail-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.02);
border-radius: 16rpx;
margin-bottom: 16rpx;
transition: all 0.3s;
}
.detail-item:active {
background: rgba(255, 255, 255, 0.05);
}
.detail-avatar-wrap {
flex-shrink: 0;
}
.detail-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 2rpx solid rgba(56, 189, 172, 0.2);
}
.detail-avatar-text {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);

View File

@@ -0,0 +1,379 @@
/* ???????? - 1:1??Web?? */
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
/* ??? */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-left { display: flex; gap: 16rpx; align-items: center; }
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
.nav-right-placeholder { width: 144rpx; }
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
/* ?????? */
.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
.banner-content { flex: 1; }
.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
/* ???? - ?? Next.js */
.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
.earnings-main { position: relative; }
.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
.earnings-left { display: flex; align-items: center; gap: 16rpx; }
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
.earnings-right { text-align: right; }
.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
/* ???? - ?? Next.js 4??? */
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.orange { color: #FFA500; }
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
/* ????? */
.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* ???? - ?? Next.js */
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
.rules-list { padding-left: 8rpx; }
.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
.rule-item .gold { color: #FFD700; font-weight: 500; }
.rule-item .brand { color: #00CED1; font-weight: 500; }
.rule-item .orange { color: #FFA500; font-weight: 500; }
/* ?????? - ?? Next.js */
.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-title { display: flex; align-items: center; gap: 12rpx; }
.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* Tab?? */
.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
.tab-item.tab-active { color: #00CED1; }
.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
/* ???? */
.binding-list { max-height: 640rpx; overflow-y: auto; }
.empty-state { padding: 80rpx 0; text-align: center; }
.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-item:last-child { border-bottom: none; }
.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
.user-info { flex: 1; }
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.user-status { text-align: right; }
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
/* ????? - ?? Next.js */
.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
.invite-tip .gold { color: #FFD700; }
.invite-tip .brand { color: #00CED1; }
/* ?????? - ?? Next.js */
.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
.detail-list { max-height: 480rpx; overflow-y: auto; }
.detail-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 40rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.detail-item:last-child { border-bottom: none; }
.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
.detail-info { flex: 1; }
.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
/* ???? - ?? Next.js */
.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
.share-item::after { border: none; }
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
.share-icon.poster { background: rgba(103,58,183,0.2); }
.share-icon.wechat { background: rgba(7,193,96,0.2); }
.share-icon.link { background: rgba(158,158,158,0.2); }
.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-info { flex: 1; text-align: left; }
.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
/* ?????????????? + ???? + ???????? */
/* ???????? backdrop-filter??????????????? */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
/* ???? */
.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
/* ???? */
/* ???????? filter: blur ??????????????? + ???? */
.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
/* ???? */
.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
/* ?? */
.poster-title { margin-bottom: 8rpx; }
.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
/* ???? */
.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
.poster-stat-gold { color: #FFD700; }
.poster-stat-brand { color: #00CED1; }
.poster-stat-pink { color: #E91E63; }
/* ?? */
.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
/* ??? */
.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
/* ??? */
.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
.poster-discount-highlight { color: #00CED1; font-weight: 800; }
/* ??? */
.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
/* ??????? */
.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
/* ????- ?? 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; }
/* ===== T<><54>rGm<18>5<EFBFBD><35> ?===== */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10rpx);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid rgba(56, 189, 172, 0.2);
border-top-color: #38bdac;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.content-loading {
opacity: 0.3;
pointer-events: none;
}
/* ===== <00><>AR<41><6C>^<5E>|<7C>o<EFBFBD>p<EFBFBD><>\!} ===== */
.detail-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.02);
border-radius: 16rpx;
margin-bottom: 16rpx;
transition: all 0.3s;
}
.detail-item:active {
background: rgba(255, 255, 255, 0.05);
}
.detail-avatar-wrap {
flex-shrink: 0;
}
.detail-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 2rpx solid rgba(56, 189, 172, 0.2);
}
.detail-avatar-text {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);

View File

@@ -35,7 +35,7 @@ Page({
// 加载热门章节(从服务器获取点击量高的章节)
async loadHotChapters() {
try {
const res = await app.request('/api/book/hot')
const res = await app.request('/api/miniprogram/book/hot')
if (res && res.success && res.chapters?.length > 0) {
this.setData({ hotChapters: res.chapters })
}
@@ -77,7 +77,7 @@ Page({
this.setData({ loading: true, searched: true })
try {
const res = await app.request(`/api/book/search?q=${encodeURIComponent(keyword.trim())}`)
const res = await app.request(`/api/miniprogram/book/search?q=${encodeURIComponent(keyword.trim())}`)
if (res && res.success) {
this.setData({

View File

@@ -15,6 +15,7 @@ Page({
phoneNumber: '',
wechatId: '',
alipayAccount: '',
address: '',
// 自动提现(默认开启)
autoWithdrawEnabled: true,
@@ -46,6 +47,7 @@ Page({
const phoneNumber = wx.getStorageSync('user_phone') || userInfo.phone || ''
const wechatId = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
const alipayAccount = wx.getStorageSync('user_alipay') || userInfo.alipay || ''
const address = wx.getStorageSync('user_address') || userInfo.address || ''
// 默认开启自动提现
const autoWithdrawEnabled = wx.getStorageSync('auto_withdraw_enabled') !== false
@@ -55,11 +57,73 @@ Page({
phoneNumber,
wechatId,
alipayAccount,
address,
autoWithdrawEnabled
})
}
},
// 一键获取收货地址
getAddress() {
wx.chooseAddress({
success: (res) => {
console.log('[Settings] 获取地址成功:', res)
const fullAddress = `${res.provinceName || ''}${res.cityName || ''}${res.countyName || ''}${res.detailInfo || ''}`
if (fullAddress.trim()) {
wx.setStorageSync('user_address', fullAddress)
this.setData({ address: fullAddress })
// 更新用户信息
if (app.globalData.userInfo) {
app.globalData.userInfo.address = fullAddress
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
// 同步到服务器
this.syncAddressToServer(fullAddress)
wx.showToast({ title: '地址已获取', icon: 'success' })
}
},
fail: (e) => {
console.log('[Settings] 获取地址失败:', e)
if (e.errMsg?.includes('cancel')) {
// 用户取消,不提示
return
}
if (e.errMsg?.includes('auth deny') || e.errMsg?.includes('authorize')) {
wx.showModal({
title: '需要授权',
content: '请在设置中允许获取收货地址',
confirmText: '去设置',
success: (res) => {
if (res.confirm) wx.openSetting()
}
})
} else {
wx.showToast({ title: '获取失败,请重试', icon: 'none' })
}
}
})
},
// 同步地址到服务器
async syncAddressToServer(address) {
try {
const userId = app.globalData.userInfo?.id
if (!userId) return
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId, address }
})
console.log('[Settings] 地址已同步到服务器')
} catch (e) {
console.log('[Settings] 同步地址失败:', e)
}
},
// 切换自动提现
async toggleAutoWithdraw(e) {
const enabled = e.detail.value
@@ -83,7 +147,7 @@ Page({
// 同步到服务器
try {
await app.request('/api/user/update', {
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
@@ -137,7 +201,7 @@ Page({
// 同步到服务器
try {
await app.request('/api/user/update', {
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
@@ -209,7 +273,7 @@ Page({
const userId = app.globalData.userInfo?.id
if (!userId) return
const res = await app.request('/api/user/profile', {
const res = await app.request('/api/miniprogram/user/profile', {
method: 'POST',
data: {
userId,
@@ -240,9 +304,44 @@ Page({
})
if (res.userInfo) {
const { nickName, avatarUrl } = res.userInfo
const { nickName, avatarUrl: tempAvatarUrl } = res.userInfo
// 更新本地
wx.showLoading({ title: '上传中...', mask: true })
// 1. 先上传图片到服务器
console.log('[Settings] 开始上传头像:', tempAvatarUrl)
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: {
folder: 'avatars'
},
success: (uploadResult) => {
try {
const data = JSON.parse(uploadResult.data)
if (data.success) {
resolve(data)
} else {
reject(new Error(data.error || '上传失败'))
}
} catch (err) {
reject(new Error('解析响应失败'))
}
},
fail: (err) => {
reject(err)
}
})
})
// 2. 获取上传后的完整URL
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
console.log('[Settings] 头像上传成功:', avatarUrl)
// 3. 更新本地
this.setData({
userInfo: {
...this.data.userInfo,
@@ -251,27 +350,32 @@ Page({
}
})
// 同步到服务器
// 4. 同步到服务器数据库
const userId = app.globalData.userInfo?.id
if (userId) {
await app.request('/api/user/profile', {
await app.request('/api/miniprogram/user/profile', {
method: 'POST',
data: { userId, nickname: nickName, avatar: avatarUrl }
})
}
// 更新全局
// 5. 更新全局
if (app.globalData.userInfo) {
app.globalData.userInfo.nickname = nickName
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像更新成功', icon: 'success' })
}
} catch (e) {
console.log('[Settings] 获取头像失败:', e)
wx.showToast({ title: '获取头像失败', icon: 'none' })
wx.hideLoading()
console.error('[Settings] 获取头像失败:', e)
wx.showToast({
title: e.message || '获取头像失败',
icon: 'none'
})
}
},
@@ -384,5 +488,10 @@ Page({
// 阻止冒泡
stopPropagation() {},
goBack() { wx.navigateBack() }
goBack() { wx.navigateBack() },
// 跳转到地址管理页
goToAddresses() {
wx.navigateTo({ url: '/pages/addresses/addresses' })
}
})

View File

@@ -58,6 +58,19 @@
</view>
</view>
<!-- 收货地址 - 跳转到地址管理页 -->
<view class="bind-item" bindtap="goToAddresses">
<view class="bind-left">
<view class="bind-icon address-icon">📍</view>
<view class="bind-info">
<text class="bind-label">收货地址</text>
<text class="bind-value address-text">管理收货地址,用于发货与邮寄</text>
</view>
</view>
<view class="bind-right">
<text class="bind-manage brand-color">管理</text>
</view>
</view>
</view>
</view>

View File

@@ -35,6 +35,8 @@
.bind-right { display: flex; align-items: center; }
.bind-check { color: #00CED1; font-size: 32rpx; }
.bind-btn { color: #00CED1; font-size: 26rpx; }
.bind-manage { color: #00CED1; font-size: 26rpx; }
.brand-color { color: #00CED1; }
/* 一键获取手机号按钮 */
.get-phone-btn {

View File

@@ -7,7 +7,19 @@ Page({
daysRemaining: 0,
expireDateStr: '',
price: 1980,
rights: [],
originalPrice: 6980,
contentRights: [
{ title: '解锁全部章节', desc: '365天全部章节内容' },
{ title: '案例库', desc: '30-100个创业项目案例' },
{ title: '智能纪要', desc: '每天推送派对精华' },
{ title: '会议纪要库', desc: '之前所有场次的会议纪要' }
],
socialRights: [
{ title: '匹配创业伙伴', desc: '匹配所有创业伙伴' },
{ title: '创业老板排行', desc: '排行榜展示您的项目' },
{ title: '链接资源', desc: '进群聊天、链接资源的权利' },
{ title: '专属VIP标识', desc: '头像金色VIP光圈' }
],
profile: { name: '', project: '', contact: '', bio: '' },
purchasing: false
},
@@ -21,7 +33,7 @@ Page({
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request(`/api/vip/status?userId=${userId}`)
const res = await app.request({ url: `/api/vip/status?userId=${userId}`, silent: true })
if (res?.success) {
const d = res.data
let expStr = ''
@@ -33,15 +45,11 @@ Page({
isVip: d.isVip,
daysRemaining: d.daysRemaining,
expireDateStr: expStr,
price: d.price || 1980,
rights: d.rights || ['解锁全部章节内容365天','匹配所有创业伙伴','创业老板排行榜展示','专属VIP标识']
price: d.price || 1980
})
if (d.isVip) this.loadProfile(userId)
}
} catch (e) {
console.log('[VIP] 加载失败', e)
this.setData({ rights: ['解锁全部章节内容365天','匹配所有创业伙伴','创业老板排行榜展示','专属VIP标识'] })
}
} catch (e) { console.log('[VIP] 加载失败', e) }
},
async loadProfile(userId) {
@@ -52,35 +60,53 @@ Page({
},
async handlePurchase() {
const userId = app.globalData.userInfo?.id
if (!userId) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
let userId = app.globalData.userInfo?.id
let openId = app.globalData.openId || app.globalData.userInfo?.open_id
if (!userId || !openId) {
wx.showLoading({ title: '登录中...', mask: true })
try {
await app.login()
userId = app.globalData.userInfo?.id
openId = app.globalData.openId || app.globalData.userInfo?.open_id
wx.hideLoading()
if (!userId || !openId) {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '登录失败', icon: 'none' })
return
}
}
this.setData({ purchasing: true })
try {
const res = await app.request('/api/vip/purchase', { method: 'POST', data: { userId } })
if (res?.success) {
// 调用微信支付
const payRes = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: { orderSn: res.data.orderSn, openId: app.globalData.openId }
})
if (payRes?.success && payRes.payParams) {
wx.requestPayment({
...payRes.payParams,
success: () => {
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
this.loadVipInfo()
},
fail: () => wx.showToast({ title: '支付取消', icon: 'none' })
})
} else {
wx.showToast({ title: '支付参数获取失败', icon: 'none' })
const payRes = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
userId,
productType: 'vip',
productId: 'vip_annual',
amount: this.data.price,
description: '卡若创业派对VIP年度会员365天'
}
})
if (payRes?.success && payRes.data?.payParams) {
wx.requestPayment({
...payRes.data.payParams,
success: () => {
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
this.loadVipInfo()
},
fail: () => wx.showToast({ title: '支付取消', icon: 'none' })
})
} else {
wx.showToast({ title: res?.error || '创建订单失败', icon: 'none' })
wx.showToast({ title: payRes?.error || '支付参数获取失败', icon: 'none' })
}
} catch (e) {
console.error('[VIP] 购买失败', e)
wx.showToast({ title: '购买失败', icon: 'none' })
wx.showToast({ title: '购买失败,请稍后重试', icon: 'none' })
} finally { this.setData({ purchasing: false }) }
},
@@ -95,8 +121,7 @@ Page({
const p = this.data.profile
try {
const res = await app.request('/api/vip/profile', {
method: 'POST',
data: { userId, name: p.name, project: p.project, contact: p.contact, bio: p.bio }
method: 'POST', data: { userId, name: p.name, project: p.project, contact: p.contact, bio: p.bio }
})
if (res?.success) wx.showToast({ title: '资料已保存', icon: 'success' })
else wx.showToast({ title: res?.error || '保存失败', icon: 'none' })

View File

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

View File

@@ -2,36 +2,54 @@
<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">VIP会员</text>
<text class="nav-title">卡若创业派对</text>
<view class="nav-placeholder-r"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<!-- VIP状态卡片 -->
<!-- 会员状态 -->
<view class="vip-hero {{isVip ? 'vip-hero-active' : ''}}">
<view class="vip-hero-icon">👑</view>
<text class="vip-hero-title" wx:if="{{!isVip}}">开通VIP年度会员</text>
<text class="vip-hero-title gold" wx:else>VIP会员</text>
<text class="vip-hero-tag">卡若创业派对</text>
<text class="vip-hero-title">加入卡若的<text class="gold">创业派对</text>会员</text>
<text class="vip-hero-sub" wx:if="{{isVip}}">有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天)</text>
<text class="vip-hero-sub" wx:else>¥{{price}}/年 · 365天全部权益</text>
<text class="vip-hero-sub" wx:else>专属会员尊享权益</text>
</view>
<!-- 权益列表 -->
<!-- 内容权益 -->
<view class="rights-card">
<text class="rights-title">会员权益</text>
<view class="rights-list">
<view class="rights-item" wx:for="{{rights}}" wx:key="*this">
<text class="rights-check">✓</text>
<text class="rights-text">{{item}}</text>
<text class="rights-section-title">内容权益</text>
<view class="rights-item" wx:for="{{contentRights}}" wx:key="title">
<view class="rights-check-wrap"><text class="rights-check">✓</text></view>
<view class="rights-info">
<text class="rights-title">{{item.title}}</text>
<text class="rights-desc">{{item.desc}}</text>
</view>
</view>
</view>
<!-- 购买按钮 -->
<view class="buy-section" wx:if="{{!isVip}}">
<!-- 社交权益 -->
<view class="rights-card">
<text class="rights-section-title">社交权益</text>
<view class="rights-item" wx:for="{{socialRights}}" wx:key="title">
<view class="rights-check-wrap"><text class="rights-check">✓</text></view>
<view class="rights-info">
<text class="rights-title">{{item.title}}</text>
<text class="rights-desc">{{item.desc}}</text>
</view>
</view>
</view>
<!-- 价格区 + 购买按钮 -->
<view class="buy-area" wx:if="{{!isVip}}">
<view class="price-row">
<text class="price-original">¥{{originalPrice}}</text>
<text class="price-current">¥{{price}}</text>
<text class="price-unit">/年</text>
</view>
<button class="buy-btn" bindtap="handlePurchase" disabled="{{purchasing}}">
{{purchasing ? '处理中...' : '立即开通 ¥' + price}}
{{purchasing ? '处理中...' : '¥' + price + ' 加入创业派对'}}
</button>
<text class="buy-sub">加入卡若创业派对,获取创业资讯与优质人脉资源</text>
</view>
<!-- VIP资料填写仅VIP可见 -->

View File

@@ -5,28 +5,31 @@
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-placeholder-r { width: 60rpx; }
.vip-hero {
margin: 24rpx; padding: 48rpx 32rpx; text-align: center;
background: linear-gradient(135deg, rgba(255,215,0,0.1), rgba(255,165,0,0.06));
border: 1rpx solid rgba(255,215,0,0.2); border-radius: 24rpx;
}
.vip-hero-active { border-color: rgba(255,215,0,0.5); background: linear-gradient(135deg, rgba(255,215,0,0.18), rgba(255,165,0,0.1)); }
.vip-hero-icon { font-size: 80rpx; }
.vip-hero-title { display: block; font-size: 40rpx; font-weight: bold; color: #fff; margin-top: 16rpx; }
.vip-hero-title.gold { color: #FFD700; }
.vip-hero-sub { display: block; font-size: 26rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; }
.vip-hero { margin: 24rpx; padding: 48rpx 32rpx; border-radius: 24rpx; background: linear-gradient(135deg, rgba(0,206,209,0.08), rgba(255,215,0,0.06)); border: 1rpx solid rgba(0,206,209,0.2); }
.vip-hero-active { border-color: rgba(255,215,0,0.4); background: linear-gradient(135deg, rgba(255,215,0,0.15), rgba(0,206,209,0.08)); }
.vip-hero-tag { display: inline-block; background: rgba(0,206,209,0.15); color: #00CED1; font-size: 22rpx; padding: 6rpx 16rpx; border-radius: 16rpx; margin-bottom: 20rpx; }
.vip-hero-title { display: block; font-size: 44rpx; font-weight: bold; color: #fff; margin-top: 12rpx; }
.gold { color: #FFD700; }
.vip-hero-sub { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; }
.rights-card { margin: 24rpx; padding: 28rpx; background: #1c1c1e; border-radius: 20rpx; }
.rights-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.9); }
.rights-list { margin-top: 20rpx; }
.rights-item { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 0; border-bottom: 1rpx solid rgba(255,255,255,0.06); }
.rights-item:last-child { border-bottom: none; }
.rights-check { color: #00CED1; font-size: 28rpx; font-weight: bold; }
.rights-text { font-size: 28rpx; color: rgba(255,255,255,0.8); }
.rights-card { margin: 24rpx; }
.rights-item { display: flex; align-items: flex-start; gap: 20rpx; padding: 24rpx; margin-bottom: 16rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.06); border-radius: 16rpx; }
.rights-check-wrap { width: 44rpx; height: 44rpx; border-radius: 50%; background: rgba(0,206,209,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 4rpx; }
.rights-check { color: #00CED1; font-size: 24rpx; font-weight: bold; }
.rights-info { display: flex; flex-direction: column; }
.rights-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.95); }
.rights-desc { font-size: 24rpx; color: rgba(255,255,255,0.45); margin-top: 6rpx; }
.buy-section { padding: 32rpx 24rpx; }
.buy-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; border: none; }
.rights-section-title { display: block; font-size: 26rpx; color: #00CED1; font-weight: 600; margin-bottom: 16rpx; padding-bottom: 12rpx; border-bottom: 1rpx solid rgba(0,206,209,0.15); }
.buy-area { margin: 24rpx; padding: 32rpx; text-align: center; background: rgba(255,255,255,0.03); border-radius: 20rpx; }
.price-row { display: flex; align-items: baseline; justify-content: center; gap: 12rpx; margin-bottom: 24rpx; }
.price-original { font-size: 28rpx; color: rgba(255,255,255,0.35); text-decoration: line-through; }
.price-current { font-size: 64rpx; font-weight: bold; color: #FF4444; }
.price-unit { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.buy-btn { width: 90%; height: 88rpx; line-height: 88rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; border: none; margin: 0 auto; }
.buy-btn[disabled] { opacity: 0.5; }
.buy-sub { display: block; font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 16rpx; }
.profile-card { margin: 24rpx; padding: 28rpx; background: #1c1c1e; border-radius: 20rpx; }
.profile-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.9); display: block; margin-bottom: 24rpx; }
@@ -35,4 +38,9 @@
.form-input { background: rgba(255,255,255,0.06); border: 1rpx solid rgba(255,255,255,0.1); border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 28rpx; color: #fff; }
.save-btn { margin-top: 24rpx; width: 100%; height: 80rpx; line-height: 80rpx; background: #00CED1; color: #000; font-size: 30rpx; font-weight: 600; border-radius: 40rpx; border: none; }
.author-section { margin: 32rpx 24rpx; padding: 24rpx; border-top: 1rpx solid rgba(255,255,255,0.08); }
.author-row { display: flex; justify-content: space-between; padding: 8rpx 0; }
.author-label { font-size: 24rpx; color: rgba(255,255,255,0.4); }
.author-name { font-size: 24rpx; color: rgba(255,255,255,0.7); }
.bottom-space { height: 120rpx; }

View File

@@ -0,0 +1,123 @@
/**
* 提现记录 - 独立页面
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
list: [],
loading: true
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
this.loadRecords()
},
onShow() {
this.loadRecords()
},
async loadRecords() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
this.setData({ list: [], loading: false })
return
}
this.setData({ loading: true })
try {
const res = await app.request('/api/miniprogram/withdraw/records?userId=' + userInfo.id)
if (res && res.success && res.data && Array.isArray(res.data.list)) {
const list = (res.data.list || []).map(item => ({
id: item.id,
amount: (item.amount != null ? item.amount : 0).toFixed(2),
status: this.statusText(item.status),
statusRaw: item.status,
createdAt: (item.createdAt ?? item.created_at) ? this.formatDate(item.createdAt ?? item.created_at) : '--',
canReceive: !!item.canReceive
}))
this.setData({ list, loading: false })
} else {
this.setData({ list: [], loading: false })
}
} catch (e) {
console.log('[WithdrawRecords] 加载失败:', e)
this.setData({ list: [], loading: false })
}
},
statusText(status) {
const map = {
pending: '待审核',
pending_confirm: '待确认收款',
processing: '处理中',
success: '已到账',
failed: '已拒绝'
}
return map[status] || status || '--'
},
formatDate(dateStr) {
if (!dateStr) return '--'
const d = new Date(dateStr)
const y = d.getFullYear()
const m = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
return `${y}-${m}-${day}`
},
goBack() {
wx.navigateBack()
},
async onReceiveTap(e) {
const id = e.currentTarget.dataset.id
if (!id) return
wx.showLoading({ title: '加载中...' })
try {
const res = await app.request('/api/miniprogram/withdraw/confirm-info?id=' + encodeURIComponent(id))
wx.hideLoading()
if (!res || !res.success || !res.data) {
wx.showToast({ title: res?.message || '获取领取信息失败', icon: 'none' })
return
}
const { mchId, appId, package: pkg } = res.data
if (!pkg || pkg === '') {
wx.showToast({
title: '打款已发起,请到微信零钱中查看',
icon: 'none',
duration: 2500
})
return
}
if (!wx.canIUse('requestMerchantTransfer')) {
wx.showToast({ title: '当前微信版本过低,请更新后重试', icon: 'none' })
return
}
wx.requestMerchantTransfer({
mchId: mchId || '',
appId: appId || wx.getAccountInfoSync().miniProgram.appId,
package: pkg,
success: (res) => {
if (res.errMsg === 'requestMerchantTransfer:ok') {
wx.showToast({ title: '已调起收款页', icon: 'success' })
this.loadRecords()
} else {
wx.showToast({ title: res.errMsg || '操作完成', icon: 'none' })
}
},
fail: (err) => {
if (err.errMsg && err.errMsg.indexOf('cancel') !== -1) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
wx.showToast({ title: err.errMsg || '调起失败', icon: 'none' })
}
}
})
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
}
}
})

View File

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

View File

@@ -0,0 +1,25 @@
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">←</view>
<text class="nav-title">提现记录</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-placeholder-bar" style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<view class="loading-tip" wx:if="{{loading}}">加载中...</view>
<view class="empty" wx:elif="{{list.length === 0}}">暂无提现记录</view>
<view class="list" wx:else>
<view class="item" wx:for="{{list}}" wx:key="id">
<view class="item-left">
<text class="amount">¥{{item.amount}}</text>
<text class="time">{{item.createdAt}}</text>
</view>
<view class="item-right">
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
<button wx:if="{{item.canReceive}}" class="btn-receive" data-id="{{item.id}}" bindtap="onReceiveTap">领取零钱</button>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,71 @@
.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);
display: flex;
align-items: center;
padding: 0 24rpx;
height: 88rpx;
}
.nav-back {
color: #00CED1;
font-size: 36rpx;
padding: 16rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.nav-placeholder { width: 80rpx; }
.nav-placeholder-bar { width: 100%; }
.content {
padding: 32rpx;
}
.loading-tip, .empty {
text-align: center;
color: rgba(255,255,255,0.6);
font-size: 28rpx;
padding: 80rpx 0;
}
.list { }
.item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 0;
border-bottom: 2rpx solid rgba(255,255,255,0.06);
}
.item:last-child { border-bottom: none; }
.item-left { display: flex; flex-direction: column; gap: 8rpx; }
.item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 12rpx; }
.amount { font-size: 32rpx; font-weight: 600; color: #fff; }
.time { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.status { font-size: 26rpx; }
.status.status-pending { color: #FFA500; }
.status.status-processing { color: #4CAF50; }
.status.status-pending_confirm { color: #4CAF50; }
.status.status-success { color: #4CAF50; }
.status.status-failed { color: rgba(255,255,255,0.5); }
.btn-receive {
margin: 0;
padding: 0 24rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 24rpx;
color: #00CED1;
background: transparent;
border: 2rpx solid #00CED1;
border-radius: 8rpx;
}
.btn-receive::after { border: none; }