miniprogram: 用永平版本替换(含超级个体、会员详情、提现等)
- 来源: 一场soul的创业实验-永平/soul/miniprogram - 新增: addresses/agreement/privacy/withdraw-records 等页面 - 新增: components/icon, utils/chapterAccessManager, readingTracker - 删除: 上传脚本、部署说明等冗余文件 - 同步永平最新结构和功能 Co-authored-by: Cursor <cursoragent@cursor.com>
18
miniprogram/.gitignore
vendored
@@ -1,10 +1,14 @@
|
||||
# 小程序上传密钥(敏感信息,请勿上传)
|
||||
private.key
|
||||
private.*.key
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# 预览二维码
|
||||
preview.jpg
|
||||
|
||||
# 微信开发者工具生成的文件
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
|
||||
@@ -10,6 +10,9 @@ App({
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
|
||||
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
||||
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
|
||||
|
||||
// 微信支付配置
|
||||
mchId: '1318592501', // 商户号
|
||||
@@ -27,6 +30,9 @@ App({
|
||||
purchasedSections: [],
|
||||
hasFullBook: false,
|
||||
|
||||
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
|
||||
readSectionIds: [],
|
||||
|
||||
// 推荐绑定
|
||||
pendingReferralCode: null, // 待绑定的推荐码
|
||||
|
||||
@@ -49,6 +55,7 @@ App({
|
||||
},
|
||||
|
||||
onLaunch(options) {
|
||||
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
||||
// 获取系统信息
|
||||
this.getSystemInfo()
|
||||
|
||||
@@ -80,21 +87,16 @@ App({
|
||||
|
||||
// 立即记录访问(不需要登录,用于统计"通过链接进的人数")
|
||||
this.recordReferralVisit(refCode)
|
||||
|
||||
// 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺)
|
||||
this.globalData.pendingReferralCode = refCode
|
||||
wx.setStorageSync('pendingReferralCode', refCode)
|
||||
// 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
|
||||
wx.setStorageSync('referral_code', refCode)
|
||||
|
||||
// 检查是否已经绑定过
|
||||
const boundRef = wx.getStorageSync('boundReferralCode')
|
||||
if (boundRef && boundRef !== refCode) {
|
||||
console.log('[App] 已绑定过其他推荐码,不更换绑定关系')
|
||||
// 但仍然记录访问,不return
|
||||
} else {
|
||||
// 保存待绑定的推荐码
|
||||
this.globalData.pendingReferralCode = refCode
|
||||
wx.setStorageSync('pendingReferralCode', refCode)
|
||||
|
||||
// 如果已登录,立即绑定
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
|
||||
this.bindReferralCode(refCode)
|
||||
}
|
||||
// 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
|
||||
this.bindReferralCode(refCode)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -106,7 +108,7 @@ App({
|
||||
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
|
||||
const userId = this.globalData.userInfo?.id || ''
|
||||
|
||||
await this.request('/api/referral/visit', {
|
||||
await this.request('/api/miniprogram/referral/visit', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
referralCode: refCode,
|
||||
@@ -114,7 +116,8 @@ App({
|
||||
visitorId: userId,
|
||||
source: 'miniprogram',
|
||||
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
|
||||
}
|
||||
},
|
||||
silent: true
|
||||
})
|
||||
console.log('[App] 记录推荐访问成功')
|
||||
} catch (e) {
|
||||
@@ -129,26 +132,21 @@ App({
|
||||
const userId = this.globalData.userInfo?.id
|
||||
if (!userId || !refCode) return
|
||||
|
||||
// 检查是否已绑定
|
||||
const boundRef = wx.getStorageSync('boundReferralCode')
|
||||
if (boundRef) {
|
||||
console.log('[App] 已绑定推荐码,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
|
||||
|
||||
// 调用API绑定推荐关系
|
||||
const res = await this.request('/api/referral/bind', {
|
||||
const res = await this.request('/api/miniprogram/referral/bind', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
referralCode: refCode
|
||||
}
|
||||
},
|
||||
silent: true
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
console.log('[App] 推荐码绑定成功')
|
||||
// 仅记录当前已绑定的推荐码,用于展示/调试;是否允许更换由后端根据30天规则判断
|
||||
wx.setStorageSync('boundReferralCode', refCode)
|
||||
this.globalData.pendingReferralCode = null
|
||||
wx.removeStorageSync('pendingReferralCode')
|
||||
@@ -203,9 +201,10 @@ App({
|
||||
|
||||
// 从服务器获取最新数据
|
||||
const res = await this.request('/api/book/all-chapters')
|
||||
if (res && res.data) {
|
||||
this.globalData.bookData = res.data
|
||||
wx.setStorageSync('bookData', res.data)
|
||||
if (res && (res.data || res.chapters)) {
|
||||
const chapters = res.data || res.chapters || []
|
||||
this.globalData.bookData = chapters
|
||||
wx.setStorageSync('bookData', chapters)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
@@ -244,11 +243,41 @@ App({
|
||||
}
|
||||
},
|
||||
|
||||
// 统一请求方法
|
||||
request(url, options = {}) {
|
||||
/**
|
||||
* 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段)
|
||||
*/
|
||||
_getApiErrorMsg(data, defaultMsg = '请求失败') {
|
||||
if (!data || typeof data !== 'object') return defaultMsg
|
||||
const msg = data.message || data.error
|
||||
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
|
||||
},
|
||||
|
||||
/**
|
||||
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
|
||||
* @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
|
||||
* @param {object} options - { method, data, header, silent }
|
||||
* @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计)
|
||||
*/
|
||||
request(urlOrOptions, options = {}) {
|
||||
let url
|
||||
if (typeof urlOrOptions === 'string') {
|
||||
url = urlOrOptions
|
||||
} else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) {
|
||||
url = urlOrOptions.url
|
||||
options = { ...urlOrOptions, url: undefined }
|
||||
} else {
|
||||
url = ''
|
||||
}
|
||||
const silent = !!options.silent
|
||||
const showError = (msg) => {
|
||||
if (!silent && msg) {
|
||||
wx.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
|
||||
wx.request({
|
||||
url: this.globalData.baseUrl + url,
|
||||
method: options.method || 'GET',
|
||||
@@ -259,38 +288,50 @@ App({
|
||||
...options.header
|
||||
},
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data)
|
||||
} else if (res.statusCode === 401) {
|
||||
// 未授权,清除登录状态
|
||||
this.logout()
|
||||
reject(new Error('未授权'))
|
||||
} else {
|
||||
reject(new Error(res.data?.message || '请求失败'))
|
||||
// 业务失败:success === false,soul-api 用 message 或 error 返回原因
|
||||
if (data && data.success === false) {
|
||||
const msg = this._getApiErrorMsg(data, '操作失败')
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 401) {
|
||||
this.logout()
|
||||
showError('未授权,请重新登录')
|
||||
reject(new Error('未授权'))
|
||||
return
|
||||
}
|
||||
// 4xx/5xx:优先用返回体的 message/error
|
||||
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 登录方法 - 获取openId用于支付
|
||||
// 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
|
||||
async login() {
|
||||
try {
|
||||
// 获取微信登录code
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
|
||||
console.log('[App] 获取登录code成功')
|
||||
|
||||
if (!loginRes || !loginRes.code) {
|
||||
console.warn('[App] wx.login 未返回 code')
|
||||
wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
try {
|
||||
// 发送code到服务器获取openId
|
||||
const res = await this.request('/api/miniprogram/login', {
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code }
|
||||
@@ -362,6 +403,20 @@ App({
|
||||
if (res.success && res.data?.openId) {
|
||||
this.globalData.openId = res.data.openId
|
||||
wx.setStorageSync('openId', res.data.openId)
|
||||
// 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
|
||||
if (res.data.user) {
|
||||
this.globalData.userInfo = res.data.user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
||||
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
||||
wx.setStorageSync('userInfo', res.data.user)
|
||||
wx.setStorageSync('token', res.data.token || '')
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
}
|
||||
return res.data.openId
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -378,13 +433,19 @@ App({
|
||||
return null
|
||||
},
|
||||
|
||||
// 手机号登录
|
||||
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
|
||||
async loginWithPhone(phoneCode) {
|
||||
try {
|
||||
// 尝试API登录
|
||||
const res = await this.request('/api/wechat/phone-login', {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes.code) {
|
||||
wx.showToast({ title: '获取登录态失败', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
const res = await this.request('/api/miniprogram/phone-login', {
|
||||
method: 'POST',
|
||||
data: { code: phoneCode }
|
||||
data: { code: loginRes.code, phoneCode }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
@@ -430,6 +491,21 @@ App({
|
||||
return this.globalData.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
// 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计)
|
||||
markSectionAsRead(sectionId) {
|
||||
if (!sectionId) return
|
||||
const list = this.globalData.readSectionIds || []
|
||||
if (list.includes(sectionId)) return
|
||||
list.push(sectionId)
|
||||
this.globalData.readSectionIds = list
|
||||
wx.setStorageSync('readSectionIds', list)
|
||||
},
|
||||
|
||||
// 已读章节数(用于首页展示)
|
||||
getReadCount() {
|
||||
return (this.globalData.readSectionIds || []).length
|
||||
},
|
||||
|
||||
// 获取章节总数
|
||||
getTotalSections() {
|
||||
return this.globalData.totalSections
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
"pages/my/my",
|
||||
"pages/read/read",
|
||||
"pages/about/about",
|
||||
"pages/agreement/agreement",
|
||||
"pages/privacy/privacy",
|
||||
"pages/referral/referral",
|
||||
"pages/purchases/purchases",
|
||||
"pages/settings/settings",
|
||||
"pages/search/search",
|
||||
"pages/addresses/addresses",
|
||||
"pages/addresses/edit",
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail"
|
||||
],
|
||||
|
||||
@@ -4,10 +4,48 @@
|
||||
* 开发: 卡若
|
||||
*/
|
||||
|
||||
/* ===== CSS 变量系统 ===== */
|
||||
page {
|
||||
/* 品牌色 */
|
||||
--app-brand: #00CED1;
|
||||
--app-brand-light: rgba(0, 206, 209, 0.1);
|
||||
--app-brand-dark: #20B2AA;
|
||||
|
||||
/* 背景色 */
|
||||
--app-bg-primary: #000000;
|
||||
--app-bg-secondary: #1c1c1e;
|
||||
--app-bg-tertiary: #2c2c2e;
|
||||
|
||||
/* 文字色 */
|
||||
--app-text-primary: #ffffff;
|
||||
--app-text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--app-text-tertiary: rgba(255, 255, 255, 0.4);
|
||||
|
||||
/* 分隔线 */
|
||||
--app-separator: rgba(255, 255, 255, 0.05);
|
||||
|
||||
/* iOS 系统色 */
|
||||
--ios-indigo: #5856D6;
|
||||
--ios-green: #30d158;
|
||||
--ios-red: #FF3B30;
|
||||
--ios-orange: #FF9500;
|
||||
--ios-yellow: #FFD700;
|
||||
|
||||
/* 金色 */
|
||||
--gold: #FFD700;
|
||||
--gold-light: #FFA500;
|
||||
|
||||
/* 粉色 */
|
||||
--pink: #E91E63;
|
||||
|
||||
/* 紫色 */
|
||||
--purple: #7B61FF;
|
||||
}
|
||||
|
||||
/* ===== 页面基础样式 ===== */
|
||||
page {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
background-color: var(--app-bg-primary);
|
||||
color: var(--app-text-primary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
|
||||
5
miniprogram/assets/icons/alert-circle.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 459 B |
4
miniprogram/assets/icons/arrow-right.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m12 5 7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 303 B |
4
miniprogram/assets/icons/bell.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
4
miniprogram/assets/icons/book-open.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
4
miniprogram/assets/icons/book.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 375 B |
3
miniprogram/assets/icons/chevron-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m15 18-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 195 B |
6
miniprogram/assets/icons/gift.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="8" width="18" height="4" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 8v13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 646 B |
4
miniprogram/assets/icons/home.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
5
miniprogram/assets/icons/image.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="9" r="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 485 B |
8
miniprogram/assets/icons/list.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="8" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 844 B |
3
miniprogram/assets/icons/message-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 304 B |
18
miniprogram/assets/icons/partners.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 两个人的头:完全分开,中间留空隙 -->
|
||||
<!-- 左侧头部 -->
|
||||
<circle cx="16" cy="18" r="7" fill="white" />
|
||||
<!-- 右侧头部 -->
|
||||
<circle cx="32" cy="18" r="7" fill="white" />
|
||||
|
||||
<!-- 左侧身体:单独一块,和右侧之间留出明显空隙 -->
|
||||
<path
|
||||
d="M10 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H10v-4z"
|
||||
fill="white"
|
||||
/>
|
||||
<!-- 右侧身体:单独一块,和左侧之间留出明显空隙 -->
|
||||
<path
|
||||
d="M26 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H26v-4z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
4
miniprogram/assets/icons/settings.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 865 B |
7
miniprogram/assets/icons/share.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="5" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="6" cy="12" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="18" cy="19" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
6
miniprogram/assets/icons/sparkles.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20 2v4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 4h-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="20" r="2" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
4
miniprogram/assets/icons/user.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
6
miniprogram/assets/icons/users.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 593 B |
4
miniprogram/assets/icons/wallet.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 429 B |
175
miniprogram/components/icon/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Icon 图标组件
|
||||
|
||||
SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。
|
||||
|
||||
**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签)
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 在页面 JSON 中引入组件
|
||||
|
||||
```json
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在 WXML 中使用
|
||||
|
||||
```xml
|
||||
<!-- 基础用法 -->
|
||||
<icon name="share" size="48" color="#00CED1"></icon>
|
||||
|
||||
<!-- 分享图标 -->
|
||||
<icon name="share" size="40" color="#ffffff"></icon>
|
||||
|
||||
<!-- 箭头图标 -->
|
||||
<icon name="arrow-up-right" size="32" color="#00CED1"></icon>
|
||||
|
||||
<!-- 搜索图标 -->
|
||||
<icon name="search" size="44" color="#ffffff"></icon>
|
||||
|
||||
<!-- 返回图标 -->
|
||||
<icon name="chevron-left" size="48" color="#ffffff"></icon>
|
||||
|
||||
<!-- 心形图标 -->
|
||||
<icon name="heart" size="40" color="#E91E63"></icon>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 属性说明
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|-----|------|--------|-----|
|
||||
| name | String | 'share' | 图标名称 |
|
||||
| size | Number | 48 | 图标大小(rpx) |
|
||||
| color | String | 'currentColor' | 图标颜色 |
|
||||
| customClass | String | '' | 自定义类名 |
|
||||
| customStyle | String | '' | 自定义样式 |
|
||||
|
||||
---
|
||||
|
||||
## 可用图标
|
||||
|
||||
| 图标名称 | 说明 | 对应 lucide-react |
|
||||
|---------|------|-------------------|
|
||||
| `share` | 分享 | `<Share2>` |
|
||||
| `arrow-up-right` | 右上箭头 | `<ArrowUpRight>` |
|
||||
| `chevron-left` | 左箭头 | `<ChevronLeft>` |
|
||||
| `search` | 搜索 | `<Search>` |
|
||||
| `heart` | 心形 | `<Heart>` |
|
||||
|
||||
---
|
||||
|
||||
## 添加新图标
|
||||
|
||||
在 `icon.js` 的 `getSvgPath` 方法中添加新图标:
|
||||
|
||||
```javascript
|
||||
getSvgPath(name) {
|
||||
const svgMap = {
|
||||
'new-icon': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><!-- SVG path 数据 --></svg>',
|
||||
// ... 其他图标
|
||||
}
|
||||
return svgMap[name] || ''
|
||||
}
|
||||
```
|
||||
|
||||
**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。
|
||||
**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。
|
||||
|
||||
---
|
||||
|
||||
## 样式定制
|
||||
|
||||
### 1. 使用 customClass
|
||||
|
||||
```xml
|
||||
<icon name="share" size="48" color="#00CED1" customClass="my-icon-class"></icon>
|
||||
```
|
||||
|
||||
```css
|
||||
.my-icon-class {
|
||||
opacity: 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用 customStyle
|
||||
|
||||
```xml
|
||||
<icon name="share" size="48" color="#ffffff" customStyle="opacity: 0.8; margin-right: 10rpx;"></icon>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 为什么使用 Base64 + image?
|
||||
|
||||
1. **矢量图标**:任意缩放不失真
|
||||
2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色
|
||||
3. **轻量级**:无需加载字体文件或外部图片
|
||||
4. **兼容性**:小程序不支持直接使用 SVG 标签,image 组件支持 Base64 SVG
|
||||
|
||||
### 为什么不用字体图标?
|
||||
|
||||
小程序对字体文件有限制,Base64 编码字体文件会增加包体积,SVG 图标更轻量。
|
||||
|
||||
### 与 lucide-react 的对应关系
|
||||
|
||||
- **lucide-react**: React 组件库,使用 SVG
|
||||
- **本组件**: 小程序自定义组件,也使用 SVG
|
||||
- **SVG path 数据**: 完全相同,从 lucide 官网复制
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
### 悬浮分享按钮
|
||||
|
||||
```xml
|
||||
<button class="fab-share" open-type="share">
|
||||
<icon name="share" size="48" color="#ffffff"></icon>
|
||||
</button>
|
||||
```
|
||||
|
||||
```css
|
||||
.fab-share {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 扩展图标库
|
||||
|
||||
可以继续添加更多 lucide-react 图标:
|
||||
|
||||
- `star` - 星星
|
||||
- `wallet` - 钱包
|
||||
- `gift` - 礼物
|
||||
- `info` - 信息
|
||||
- `settings` - 设置
|
||||
- `user` - 用户
|
||||
- `book-open` - 打开的书
|
||||
- `eye` - 眼睛
|
||||
- `clock` - 时钟
|
||||
- `users` - 用户组
|
||||
|
||||
---
|
||||
|
||||
**图标组件创建完成!** 🎉
|
||||
83
miniprogram/components/icon/icon.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// components/icon/icon.js
|
||||
Component({
|
||||
properties: {
|
||||
// 图标名称
|
||||
name: {
|
||||
type: String,
|
||||
value: 'share',
|
||||
observer: 'updateIcon'
|
||||
},
|
||||
// 图标大小(rpx)
|
||||
size: {
|
||||
type: Number,
|
||||
value: 48
|
||||
},
|
||||
// 图标颜色
|
||||
color: {
|
||||
type: String,
|
||||
value: '#ffffff',
|
||||
observer: 'updateIcon'
|
||||
},
|
||||
// 自定义类名
|
||||
customClass: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 自定义样式
|
||||
customStyle: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
svgData: ''
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.updateIcon()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// SVG 图标数据映射
|
||||
getSvgPath(name) {
|
||||
const svgMap = {
|
||||
'share': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
|
||||
|
||||
'arrow-up-right': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>',
|
||||
|
||||
'chevron-left': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
|
||||
|
||||
'search': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
||||
|
||||
'heart': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
|
||||
}
|
||||
|
||||
return svgMap[name] || ''
|
||||
},
|
||||
|
||||
// 更新图标
|
||||
updateIcon() {
|
||||
const { name, color } = this.data
|
||||
let svgString = this.getSvgPath(name)
|
||||
|
||||
if (svgString) {
|
||||
// 替换颜色占位符
|
||||
svgString = svgString.replace(/COLOR/g, color)
|
||||
|
||||
// 转换为 Base64 Data URL
|
||||
const svgData = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`
|
||||
|
||||
this.setData({
|
||||
svgData: svgData
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
svgData: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
4
miniprogram/components/icon/icon.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
5
miniprogram/components/icon/icon.wxml
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- components/icon/icon.wxml -->
|
||||
<view class="icon icon-{{name}} {{customClass}}" style="width: {{size}}rpx; height: {{size}}rpx; {{customStyle}}">
|
||||
<image wx:if="{{svgData}}" class="icon-image" src="{{svgData}}" mode="aspectFit" style="width: {{size}}rpx; height: {{size}}rpx;" />
|
||||
<text wx:else class="icon-text">{{name}}</text>
|
||||
</view>
|
||||
18
miniprogram/components/icon/icon.wxss
Normal file
@@ -0,0 +1,18 @@
|
||||
/* components/icon/icon.wxss */
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 24rpx;
|
||||
color: currentColor;
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
/**
|
||||
* Soul创业实验 - 自定义TabBar组件
|
||||
* 实现中间突出的"找伙伴"按钮
|
||||
* 根据后台配置动态显示/隐藏"找伙伴"按钮
|
||||
*/
|
||||
|
||||
console.log('[TabBar] ===== 组件文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
console.log('[TabBar] App 对象:', app)
|
||||
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
color: '#8e8e93',
|
||||
selectedColor: '#00CED1',
|
||||
matchEnabled: false, // 找伙伴功能开关,默认关闭
|
||||
list: [
|
||||
{
|
||||
pagePath: '/pages/index/index',
|
||||
@@ -23,20 +29,34 @@ Component({
|
||||
pagePath: '/pages/match/match',
|
||||
text: '找伙伴',
|
||||
iconType: 'match',
|
||||
isSpecial: true,
|
||||
hidden: true // 默认隐藏,等配置加载后根据后台设置显示
|
||||
isSpecial: true
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/my/my',
|
||||
text: '我的',
|
||||
iconType: 'user'
|
||||
}
|
||||
],
|
||||
matchEnabled: false // 找伙伴功能开关(默认隐藏,等待后台配置加载)
|
||||
]
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
console.log('[TabBar] Component attached 生命周期触发')
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
ready() {
|
||||
console.log('[TabBar] Component ready 生命周期触发')
|
||||
// 如果 attached 中没有成功加载,在 ready 中再次尝试
|
||||
if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) {
|
||||
console.log('[TabBar] 在 ready 中重新加载配置')
|
||||
this.loadFeatureConfig()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 页面加载时也调用(兼容性更好)
|
||||
attached() {
|
||||
// 初始化时获取当前页面
|
||||
console.log('[TabBar] attached() 方法触发')
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
@@ -44,31 +64,82 @@ Component({
|
||||
// 加载功能配置
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const app = getApp()
|
||||
const res = await app.request('/api/db/config')
|
||||
console.log('[TabBar] 开始加载功能配置...')
|
||||
console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
|
||||
|
||||
if (res.success && res.features) {
|
||||
const matchEnabled = res.features.matchEnabled === true
|
||||
this.setData({ matchEnabled })
|
||||
|
||||
// 更新list,隐藏或显示找伙伴
|
||||
const list = this.data.list.map(item => {
|
||||
if (item.iconType === 'match') {
|
||||
return { ...item, hidden: !matchEnabled }
|
||||
}
|
||||
return item
|
||||
})
|
||||
this.setData({ list })
|
||||
|
||||
console.log('[TabBar] 功能配置加载成功,找伙伴功能:', matchEnabled ? '开启' : '关闭')
|
||||
// app.request 的第一个参数是 url 字符串,第二个参数是 options 对象
|
||||
const res = await app.request('/api/miniprogram/config', {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
|
||||
// 兼容两种返回格式
|
||||
let matchEnabled = false
|
||||
|
||||
if (res && res.success && res.features) {
|
||||
console.log('[TabBar] features配置:', JSON.stringify(res.features))
|
||||
matchEnabled = res.features.matchEnabled === true
|
||||
console.log('[TabBar] matchEnabled值:', matchEnabled)
|
||||
} else if (res && res.configs && res.configs.feature_config) {
|
||||
// 备用格式:从 configs.feature_config 读取
|
||||
console.log('[TabBar] 使用备用格式,从configs读取')
|
||||
matchEnabled = res.configs.feature_config.matchEnabled === true
|
||||
console.log('[TabBar] matchEnabled值:', matchEnabled)
|
||||
} else {
|
||||
console.log('[TabBar] ⚠️ 未找到features配置,使用默认值false')
|
||||
console.log('[TabBar] res对象keys:', Object.keys(res || {}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[TabBar] 加载功能配置失败:', e)
|
||||
// 失败时默认隐藏找伙伴(与Web版保持一致)
|
||||
this.setData({ matchEnabled: false })
|
||||
|
||||
this.setData({ matchEnabled }, () => {
|
||||
console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled)
|
||||
// 配置加载完成后,根据当前路由设置选中状态
|
||||
this.updateSelected()
|
||||
})
|
||||
|
||||
// 如果当前在找伙伴页面,但功能已关闭,跳转到首页
|
||||
if (!matchEnabled) {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
if (currentPage && currentPage.route === 'pages/match/match') {
|
||||
console.log('[TabBar] 找伙伴功能已关闭,从match页面跳转到首页')
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[TabBar] ❌ 加载功能配置失败:', error)
|
||||
console.log('[TabBar] 错误详情:', error.message || error)
|
||||
// 默认关闭找伙伴功能
|
||||
this.setData({ matchEnabled: false }, () => {
|
||||
this.updateSelected()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 根据当前路由更新选中状态
|
||||
updateSelected() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length === 0) return
|
||||
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const route = currentPage.route
|
||||
|
||||
let selected = 0
|
||||
const { matchEnabled } = this.data
|
||||
|
||||
// 根据路由匹配对应的索引
|
||||
if (route === 'pages/index/index') {
|
||||
selected = 0
|
||||
} else if (route === 'pages/chapters/chapters') {
|
||||
selected = 1
|
||||
} else if (route === 'pages/match/match') {
|
||||
selected = 2
|
||||
} else if (route === 'pages/my/my') {
|
||||
selected = matchEnabled ? 3 : 2
|
||||
}
|
||||
|
||||
this.setData({ selected })
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const data = e.currentTarget.dataset
|
||||
const url = data.path
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<!--custom-tab-bar/index.wxml-->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-bar {{matchEnabled ? 'tab-bar-four' : 'tab-bar-three'}}">
|
||||
<view class="tab-bar-border"></view>
|
||||
|
||||
<!-- 首页 -->
|
||||
<view class="tab-bar-item" data-path="{{list[0].pagePath}}" data-index="0" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<!-- 首页图标 -->
|
||||
<view class="icon {{selected === 0 ? 'icon-active' : ''}}">
|
||||
<view class="icon-home">
|
||||
<view class="home-roof"></view>
|
||||
<view class="home-body"></view>
|
||||
</view>
|
||||
</view>
|
||||
<image class="tab-icon {{selected === 0 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/home.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 0 ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 0 ? selectedColor : color}}">{{list[0].text}}</view>
|
||||
</view>
|
||||
@@ -19,38 +16,32 @@
|
||||
<!-- 目录 -->
|
||||
<view class="tab-bar-item" data-path="{{list[1].pagePath}}" data-index="1" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<view class="icon {{selected === 1 ? 'icon-active' : ''}}">
|
||||
<view class="icon-list">
|
||||
<view class="list-line"></view>
|
||||
<view class="list-line"></view>
|
||||
<view class="list-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
<image class="tab-icon {{selected === 1 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/list.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 1 ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 找伙伴 - 中间突出按钮(可通过后台隐藏) -->
|
||||
<view wx:if="{{matchEnabled}}" class="tab-bar-item special-item" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
|
||||
<!-- 找伙伴 - 中间突出按钮(根据配置显示) -->
|
||||
<view class="tab-bar-item special-item" wx:if="{{matchEnabled}}" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
|
||||
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
|
||||
<view class="icon-users">
|
||||
<view class="user-circle user-1"></view>
|
||||
<view class="user-circle user-2"></view>
|
||||
</view>
|
||||
<image class="special-icon"
|
||||
src="/assets/icons/partners.svg"
|
||||
mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text special-text" style="color: {{selected === 2 ? selectedColor : color}}">{{list[2].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的 -->
|
||||
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="3" bindtap="switchTab">
|
||||
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="{{matchEnabled ? 3 : 2}}" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<view class="icon {{selected === 3 ? 'icon-active' : ''}}">
|
||||
<view class="icon-user">
|
||||
<view class="user-head"></view>
|
||||
<view class="user-body"></view>
|
||||
</view>
|
||||
</view>
|
||||
<image class="tab-icon {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/user.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 3 ? selectedColor : color}}">{{list[3].text}}</view>
|
||||
<view class="tab-bar-text" style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}">{{list[3].text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 三个tab布局(找伙伴功能关闭时) */
|
||||
.tab-bar-three .tab-bar-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 四个tab布局(找伙伴功能开启时) */
|
||||
.tab-bar-four .tab-bar-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-bar-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -58,105 +68,18 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ===== 首页图标 ===== */
|
||||
.icon-home {
|
||||
position: relative;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
/* ===== SVG 图标样式 ===== */
|
||||
.tab-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: block;
|
||||
filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%);
|
||||
}
|
||||
|
||||
.home-roof {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 18rpx solid transparent;
|
||||
border-right: 18rpx solid transparent;
|
||||
border-bottom: 14rpx solid #8e8e93;
|
||||
.tab-icon.icon-active {
|
||||
filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
|
||||
}
|
||||
|
||||
.home-body {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28rpx;
|
||||
height: 18rpx;
|
||||
background: #8e8e93;
|
||||
border-radius: 0 0 4rpx 4rpx;
|
||||
}
|
||||
|
||||
.icon-active .home-roof {
|
||||
border-bottom-color: #00CED1;
|
||||
}
|
||||
|
||||
.icon-active .home-body {
|
||||
background: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 目录图标 ===== */
|
||||
.icon-list {
|
||||
width: 36rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.list-line {
|
||||
width: 100%;
|
||||
height: 6rpx;
|
||||
background: #8e8e93;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
.list-line:nth-child(2) {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.list-line:nth-child(3) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.icon-active .list-line {
|
||||
background: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 我的图标 ===== */
|
||||
.icon-user {
|
||||
position: relative;
|
||||
width: 36rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.user-head {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background: #8e8e93;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28rpx;
|
||||
height: 18rpx;
|
||||
background: #8e8e93;
|
||||
border-radius: 14rpx 14rpx 0 0;
|
||||
}
|
||||
|
||||
.icon-active .user-head,
|
||||
.icon-active .user-body {
|
||||
background: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 找伙伴 - 中间特殊按钮 ===== */
|
||||
.special-item {
|
||||
@@ -189,39 +112,10 @@
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ===== 找伙伴图标 (双人) ===== */
|
||||
.icon-users {
|
||||
position: relative;
|
||||
width: 56rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.user-circle {
|
||||
position: absolute;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.user-circle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -12rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 22rpx;
|
||||
height: 14rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 11rpx 11rpx 0 0;
|
||||
}
|
||||
|
||||
.user-1 {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.user-2 {
|
||||
top: 0;
|
||||
right: 0;
|
||||
/* ===== 找伙伴特殊按钮图标 ===== */
|
||||
.special-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: block;
|
||||
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
123
miniprogram/pages/addresses/addresses.js
Normal 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()
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/addresses/addresses.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
66
miniprogram/pages/addresses/addresses.wxml
Normal 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>
|
||||
217
miniprogram/pages/addresses/addresses.wxss
Normal 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;
|
||||
}
|
||||
201
miniprogram/pages/addresses/edit.js
Normal 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()
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/addresses/edit.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
101
miniprogram/pages/addresses/edit.wxml
Normal 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>
|
||||
186
miniprogram/pages/addresses/edit.wxss
Normal 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;
|
||||
}
|
||||
21
miniprogram/pages/agreement/agreement.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户协议
|
||||
* 审核要求:登录前可点击《用户协议》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
1
miniprogram/pages/agreement/agreement.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}
|
||||
37
miniprogram/pages/agreement/agreement.wxml
Normal 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>
|
||||
11
miniprogram/pages/agreement/agreement.wxss
Normal 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; }
|
||||
@@ -199,7 +199,10 @@ Page({
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
||||
{ 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' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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() }
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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); } }
|
||||
|
||||
@@ -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() {}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
21
miniprogram/pages/privacy/privacy.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Soul创业派对 - 隐私政策
|
||||
* 审核要求:登录前可点击《隐私政策》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
1
miniprogram/pages/privacy/privacy.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"隐私政策"}
|
||||
40
miniprogram/pages/privacy/privacy.wxml
Normal 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>
|
||||
11
miniprogram/pages/privacy/privacy.wxss
Normal 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; }
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
1055
miniprogram/pages/read/read.js.backup
Normal file
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
182
miniprogram/pages/referral/earnings-detail-styles.wxss
Normal 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%;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
| ||||