Merge branch 'yongpxu-soul' of https://github.com/fnvtk/Mycontent into yongpxu-soul
# Conflicts: # miniprogram/app.js resolved by origin/yongpxu-soul(远端) version # miniprogram/app.json resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/addresses/addresses.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/addresses/edit.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/agreement/agreement.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/chapters/chapters.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/index/index.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/index/index.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/match/match.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/my/my.wxss resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/privacy/privacy.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/purchases/purchases.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/read/read.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/read/read.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.wxml resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/referral/referral.wxss resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/settings/settings.js resolved by origin/yongpxu-soul(远端) version # miniprogram/pages/withdraw-records/withdraw-records.js resolved by origin/yongpxu-soul(远端) version # miniprogram/project.config.json resolved by origin/yongpxu-soul(远端) version # miniprogram/project.private.config.json resolved by origin/yongpxu-soul(远端) version # miniprogram/utils/chapterAccessManager.js resolved by origin/yongpxu-soul(远端) version
This commit is contained in:
@@ -119,13 +119,5 @@ Page({
|
||||
// 返回
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 收货地址',
|
||||
path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -197,13 +197,5 @@ Page({
|
||||
// 返回
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 编辑地址',
|
||||
path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,13 +17,5 @@ Page({
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 用户协议',
|
||||
path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,66 +6,203 @@
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
|
||||
|
||||
function buildNestedBookData(list) {
|
||||
const parts = {}
|
||||
const appendices = []
|
||||
let epilogueMid = 0
|
||||
let prefaceMid = 0
|
||||
list.forEach(ch => {
|
||||
if (ch.id === 'preface') {
|
||||
prefaceMid = ch.mid || 0
|
||||
return
|
||||
}
|
||||
const section = {
|
||||
id: ch.id,
|
||||
mid: ch.mid || 0,
|
||||
title: ch.sectionTitle || ch.chapterTitle || ch.id,
|
||||
isFree: !!ch.isFree,
|
||||
price: ch.price != null ? Number(ch.price) : 1
|
||||
}
|
||||
if (ch.id === 'epilogue') {
|
||||
epilogueMid = ch.mid || 0
|
||||
return
|
||||
}
|
||||
if ((ch.id || '').startsWith('appendix')) {
|
||||
appendices.push({ id: ch.id, mid: ch.mid || 0, title: ch.sectionTitle || ch.chapterTitle || ch.id })
|
||||
return
|
||||
}
|
||||
if (!ch.partId || ch.id === 'preface') return
|
||||
const pid = ch.partId
|
||||
const cid = ch.chapterId || 'chapter-' + (ch.id || '').split('.')[0]
|
||||
if (!parts[pid]) {
|
||||
parts[pid] = { id: pid, number: PART_NUMBERS[pid] || pid, title: ch.partTitle || pid, subtitle: ch.chapterTitle || '', chapters: {} }
|
||||
}
|
||||
if (!parts[pid].chapters[cid]) {
|
||||
parts[pid].chapters[cid] = { id: cid, title: ch.chapterTitle || cid, sections: [] }
|
||||
}
|
||||
parts[pid].chapters[cid].sections.push(section)
|
||||
})
|
||||
const bookData = Object.values(parts)
|
||||
.sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
.map(p => ({
|
||||
...p,
|
||||
chapters: Object.values(p.chapters).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
}))
|
||||
return { bookData, appendixList: appendices, epilogueMid, prefaceMid }
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
purchasedSections: [],
|
||||
|
||||
// 书籍数据 - 完整真实标题
|
||||
totalSections: 62,
|
||||
bookData: [],
|
||||
expandedPart: null,
|
||||
appendixList: [],
|
||||
epilogueMid: 0,
|
||||
prefaceMid: 0
|
||||
bookData: [
|
||||
{
|
||||
id: 'part-1',
|
||||
number: '一',
|
||||
title: '真实的人',
|
||||
subtitle: '人与人之间的底层逻辑',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
title: '第1章|人与人之间的底层逻辑',
|
||||
sections: [
|
||||
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', isFree: true, price: 1 },
|
||||
{ id: '1.2', title: '老墨:资源整合高手的社交方法', isFree: false, price: 1 },
|
||||
{ id: '1.3', title: '笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统', isFree: false, price: 1 },
|
||||
{ id: '1.4', title: '人性的三角结构:利益、情感、价值观', isFree: false, price: 1 },
|
||||
{ id: '1.5', title: '沟通差的问题:为什么你说的别人听不懂', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-2',
|
||||
title: '第2章|人性困境案例',
|
||||
sections: [
|
||||
{ id: '2.1', title: '相亲故事:你以为找的是人,实际是在找模式', isFree: false, price: 1 },
|
||||
{ id: '2.2', title: '找工作迷茫者:为什么简历解决不了人生', isFree: false, price: 1 },
|
||||
{ id: '2.3', title: '撸运费险:小钱困住大脑的真实心理', isFree: false, price: 1 },
|
||||
{ id: '2.4', title: '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', isFree: false, price: 1 },
|
||||
{ id: '2.5', title: '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-2',
|
||||
number: '二',
|
||||
title: '真实的行业',
|
||||
subtitle: '电商、内容、传统行业解析',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-3',
|
||||
title: '第3章|电商篇',
|
||||
sections: [
|
||||
{ id: '3.1', title: '3000万流水如何跑出来(退税模式解析)', isFree: false, price: 1 },
|
||||
{ id: '3.2', title: '供应链之王 vs 打工人:利润不在前端', isFree: false, price: 1 },
|
||||
{ id: '3.3', title: '社区团购的底层逻辑', isFree: false, price: 1 },
|
||||
{ id: '3.4', title: '跨境电商与退税套利', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-4',
|
||||
title: '第4章|内容商业篇',
|
||||
sections: [
|
||||
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', isFree: false, price: 1 },
|
||||
{ id: '4.2', title: '做号工厂:如何让一个号变成一个机器', isFree: false, price: 1 },
|
||||
{ id: '4.3', title: '情绪内容为什么比专业内容更赚钱', isFree: false, price: 1 },
|
||||
{ id: '4.4', title: '猫与宠物号:为什么宠物赛道永不过时', isFree: false, price: 1 },
|
||||
{ id: '4.5', title: '直播间里的三种人:演员、技术工、系统流', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-5',
|
||||
title: '第5章|传统行业篇',
|
||||
sections: [
|
||||
{ id: '5.1', title: '拍卖行抱朴:一天240万的摇号生意', isFree: false, price: 1 },
|
||||
{ id: '5.2', title: '土地拍卖:招拍挂背后的游戏规则', isFree: false, price: 1 },
|
||||
{ id: '5.3', title: '地摊经济数字化:一个月900块的餐车生意', isFree: false, price: 1 },
|
||||
{ id: '5.4', title: '不良资产拍卖:我错过的一个亿佣金', isFree: false, price: 1 },
|
||||
{ id: '5.5', title: '桶装水李总:跟物业合作的轻资产模式', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-3',
|
||||
number: '三',
|
||||
title: '真实的错误',
|
||||
subtitle: '我和别人犯过的错',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-6',
|
||||
title: '第6章|我人生错过的4件大钱',
|
||||
sections: [
|
||||
{ id: '6.1', title: '电商财税窗口:2016年的千万级机会', isFree: false, price: 1 },
|
||||
{ id: '6.2', title: '供应链金融:我不懂的杠杆游戏', isFree: false, price: 1 },
|
||||
{ id: '6.3', title: '内容红利:2019年我为什么没做抖音', isFree: false, price: 1 },
|
||||
{ id: '6.4', title: '数据资产化:我还在观望的未来机会', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-7',
|
||||
title: '第7章|别人犯的错误',
|
||||
sections: [
|
||||
{ id: '7.1', title: '投资房年轻人的迷茫:资金 vs 能力', isFree: false, price: 1 },
|
||||
{ id: '7.2', title: '信息差骗局:永远有人靠卖学习赚钱', isFree: false, price: 1 },
|
||||
{ id: '7.3', title: '在Soul找恋爱但想赚钱的人', isFree: false, price: 1 },
|
||||
{ id: '7.4', title: '创业者的三种死法:冲动、轻信、没结构', isFree: false, price: 1 },
|
||||
{ id: '7.5', title: '人情生意的终点:关系越多亏得越多', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-4',
|
||||
number: '四',
|
||||
title: '真实的赚钱',
|
||||
subtitle: '底层结构与真实案例',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-8',
|
||||
title: '第8章|底层结构',
|
||||
sections: [
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', isFree: false, price: 1 },
|
||||
{ id: '8.2', title: '价格杠杆:供应链与信息差', isFree: false, price: 1 },
|
||||
{ id: '8.3', title: '时间杠杆:自动化 + AI', isFree: false, price: 1 },
|
||||
{ id: '8.4', title: '情绪杠杆:咨询、婚恋、生意场', isFree: false, price: 1 },
|
||||
{ id: '8.5', title: '社交杠杆:认识谁比你会什么更重要', isFree: false, price: 1 },
|
||||
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-9',
|
||||
title: '第9章|我在Soul上亲访的赚钱案例',
|
||||
sections: [
|
||||
{ id: '9.1', title: '游戏账号私域:账号即资产', isFree: false, price: 1 },
|
||||
{ id: '9.2', title: '健康包模式:高复购、高毛利', isFree: false, price: 1 },
|
||||
{ id: '9.3', title: '药物私域:长期关系赛道', isFree: false, price: 1 },
|
||||
{ id: '9.4', title: '残疾机构合作:退税 × AI × 人力成本', isFree: false, price: 1 },
|
||||
{ id: '9.5', title: '私域银行:粉丝即小股东', isFree: false, price: 1 },
|
||||
{ id: '9.6', title: 'Soul派对房:陌生人成交的最快场景', isFree: false, price: 1 },
|
||||
{ id: '9.7', title: '飞书中台:从聊天到成交的流程化体系', isFree: false, price: 1 },
|
||||
{ id: '9.8', title: '餐饮女孩:6万营收、1万利润的死撑生意', isFree: false, price: 1 },
|
||||
{ id: '9.9', title: '电竞生态:从陪玩到签约到酒店的完整链条', isFree: false, price: 1 },
|
||||
{ id: '9.10', title: '淘客大佬:损耗30%的白色通道', isFree: false, price: 1 },
|
||||
{ id: '9.11', title: '蔬菜供应链:农户才是最赚钱的人', isFree: false, price: 1 },
|
||||
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', isFree: false, price: 1 },
|
||||
{ id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', isFree: false, price: 1 },
|
||||
{ id: '9.14', title: '大健康私域:一个月150万的70后', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-5',
|
||||
number: '五',
|
||||
title: '真实的社会',
|
||||
subtitle: '未来职业与商业生态',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-10',
|
||||
title: '第10章|未来职业的变化趋势',
|
||||
sections: [
|
||||
{ id: '10.1', title: 'AI时代:哪些工作会消失,哪些会崛起', isFree: false, price: 1 },
|
||||
{ id: '10.2', title: '一人公司:为什么越来越多人选择单干', isFree: false, price: 1 },
|
||||
{ id: '10.3', title: '为什么链接能力会成为第一价值', isFree: false, price: 1 },
|
||||
{ id: '10.4', title: '新型公司:Soul-飞书-线下的三位一体', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-11',
|
||||
title: '第11章|中国社会商业生态的未来',
|
||||
sections: [
|
||||
{ id: '11.1', title: '私域经济:为什么流量越来越贵', isFree: false, price: 1 },
|
||||
{ id: '11.2', title: '银发经济与孤独经济:两个被忽视的万亿市场', isFree: false, price: 1 },
|
||||
{ id: '11.3', title: '流量红利的终局', isFree: false, price: 1 },
|
||||
{ id: '11.4', title: '大模型 + 供应链的组合拳', isFree: false, price: 1 },
|
||||
{ id: '11.5', title: '社会分层的最终逻辑', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// 展开状态
|
||||
expandedPart: 'part-1',
|
||||
|
||||
// 附录
|
||||
appendixList: [
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
||||
],
|
||||
|
||||
// 每日新增章节
|
||||
dailyChapters: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -74,44 +211,21 @@ Page({
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadAndEnrichBookData()
|
||||
this.loadDailyChapters()
|
||||
this.loadTotalFromServer()
|
||||
},
|
||||
|
||||
async loadAndEnrichBookData() {
|
||||
async loadTotalFromServer() {
|
||||
try {
|
||||
let list = app.globalData.bookData || []
|
||||
if (!list.length) {
|
||||
const res = await app.request('/api/miniprogram/book/all-chapters')
|
||||
if (res?.data) {
|
||||
list = res.data
|
||||
app.globalData.bookData = list
|
||||
}
|
||||
const res = await app.request({ url: '/api/book/all-chapters', silent: true })
|
||||
if (res && res.total) {
|
||||
this.setData({ totalSections: res.total })
|
||||
}
|
||||
if (!list.length) {
|
||||
this.setData({ bookData: [], appendixList: [] })
|
||||
return
|
||||
}
|
||||
const { bookData, appendixList, epilogueMid, prefaceMid } = buildNestedBookData(list)
|
||||
const firstPartId = bookData[0]?.id || null
|
||||
this.setData({
|
||||
bookData,
|
||||
appendixList,
|
||||
epilogueMid,
|
||||
prefaceMid,
|
||||
totalSections: list.length,
|
||||
expandedPart: firstPartId || this.data.expandedPart
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[Chapters] 加载目录失败:', e)
|
||||
this.setData({ bookData: [], appendixList: [] })
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.updateUserStatus()
|
||||
if (!app.globalData.bookData?.length) {
|
||||
this.loadAndEnrichBookData()
|
||||
}
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
@@ -120,6 +234,7 @@ Page({
|
||||
tabBar.setData({ selected: 1 })
|
||||
}
|
||||
}
|
||||
this.updateUserStatus()
|
||||
},
|
||||
|
||||
// 更新用户状态
|
||||
@@ -136,11 +251,10 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到阅读页
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
|
||||
},
|
||||
|
||||
// 检查是否已购买
|
||||
@@ -154,16 +268,31 @@ Page({
|
||||
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' })
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 目录',
|
||||
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -476,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;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -20,13 +19,37 @@ Page({
|
||||
hasFullBook: false,
|
||||
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: [],
|
||||
prefaceMid: 0,
|
||||
|
||||
// 内容概览
|
||||
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: [],
|
||||
|
||||
// 最新新增章节
|
||||
latestChapters: [],
|
||||
|
||||
// 加载状态
|
||||
loading: true
|
||||
},
|
||||
|
||||
@@ -39,9 +62,10 @@ Page({
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
|
||||
// 处理分享参数与扫码 scene(推荐码绑定)
|
||||
if (options && (options.ref || options.scene)) {
|
||||
app.handleReferralCode(options)
|
||||
// 处理分享参数(推荐码绑定)
|
||||
if (options && options.ref) {
|
||||
console.log('[Index] 检测到推荐码:', options.ref)
|
||||
app.handleReferralCode({ query: options })
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
@@ -79,9 +103,11 @@ Page({
|
||||
// 初始化数据
|
||||
async initData() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
await this.loadBookData()
|
||||
await this.loadFeaturedFromServer()
|
||||
this.loadSuperMembers()
|
||||
this.loadLatestChapters()
|
||||
} catch (e) {
|
||||
console.error('初始化失败:', e)
|
||||
} finally {
|
||||
@@ -89,82 +115,100 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async loadBookData() {
|
||||
async loadSuperMembers() {
|
||||
try {
|
||||
const [chaptersRes, hotRes] = await Promise.all([
|
||||
app.request('/api/miniprogram/book/all-chapters'),
|
||||
app.request('/api/miniprogram/book/hot')
|
||||
])
|
||||
const list = chaptersRes?.data || []
|
||||
const hotList = hotRes?.data || []
|
||||
app.globalData.bookData = list
|
||||
// 优先加载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) {}
|
||||
}
|
||||
this.setData({ superMembers: members })
|
||||
} catch (e) { console.log('[Index] 加载超级个体失败:', e) }
|
||||
},
|
||||
|
||||
const toSection = (ch) => ({
|
||||
id: ch.id,
|
||||
mid: ch.mid || 0,
|
||||
title: ch.sectionTitle || ch.chapterTitle || ch.id,
|
||||
part: ch.partTitle || ''
|
||||
// 从服务端获取精选推荐(加权算法:阅读量50% + 时效30% + 付款率20%)和最新更新
|
||||
async loadFeaturedFromServer() {
|
||||
try {
|
||||
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('附录')
|
||||
})
|
||||
|
||||
let featuredSections = []
|
||||
if (hotList.length >= 3) {
|
||||
const freeCh = list.find(c => c.isFree || c.id === '1.1' || c.id === 'preface')
|
||||
const picks = []
|
||||
if (freeCh) picks.push({ ...toSection(freeCh), tag: '免费', tagClass: 'tag-free' })
|
||||
hotList.slice(0, 3 - picks.length).forEach((ch, i) => {
|
||||
if (!picks.find(p => p.id === ch.id)) {
|
||||
picks.push({ ...toSection(ch), tag: i === 0 ? '热门' : '推荐', tagClass: i === 0 ? 'tag-pink' : 'tag-purple' })
|
||||
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 || ''
|
||||
}
|
||||
})
|
||||
featuredSections = picks.slice(0, 3)
|
||||
}
|
||||
if (featuredSections.length < 3 && list.length > 0) {
|
||||
const fallback = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id)).slice(0, 3)
|
||||
featuredSections = fallback.map((ch, i) => ({
|
||||
...toSection(ch),
|
||||
tag: ch.isFree ? '免费' : (i === 0 ? '热门' : '推荐'),
|
||||
tagClass: ch.isFree ? 'tag-free' : (i === 0 ? 'tag-pink' : 'tag-purple')
|
||||
}))
|
||||
} catch (e) {
|
||||
console.log('[Index] 从服务端加载推荐失败,使用默认:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadBookData() {
|
||||
try {
|
||||
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: chapters,
|
||||
totalSections: res.total || chapters.length || 62
|
||||
})
|
||||
}
|
||||
|
||||
const partMap = {}
|
||||
list.forEach(ch => {
|
||||
if (!ch.partId || ch.id === 'preface' || ch.id === 'epilogue' || (ch.id || '').startsWith('appendix')) return
|
||||
if (!partMap[ch.partId]) {
|
||||
partMap[ch.partId] = { id: ch.partId, number: PART_NUMBERS[ch.partId] || ch.partId, title: ch.partTitle || ch.partId, subtitle: ch.chapterTitle || '' }
|
||||
}
|
||||
})
|
||||
const partsList = Object.values(partMap).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
|
||||
const paidCandidates = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id) && !(c.id || '').startsWith('appendix') && c.partId)
|
||||
const { hasFullBook, purchasedSections } = app.globalData
|
||||
let candidates = paidCandidates
|
||||
if (!hasFullBook && purchasedSections?.length) {
|
||||
const unpurchased = paidCandidates.filter(c => !purchasedSections.includes(c.id))
|
||||
if (unpurchased.length > 0) candidates = unpurchased
|
||||
}
|
||||
const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest'
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0)
|
||||
const selectedCh = candidates[seed % Math.max(candidates.length, 1)]
|
||||
const latestSection = selectedCh ? { ...toSection(selectedCh), mid: selectedCh.mid || 0 } : null
|
||||
const latestLabel = candidates.length === paidCandidates.length ? '推荐阅读' : '为你推荐'
|
||||
|
||||
const prefaceCh = list.find(c => c.id === 'preface')
|
||||
const prefaceMid = prefaceCh?.mid || 0
|
||||
|
||||
this.setData({
|
||||
bookData: list,
|
||||
totalSections: list.length || 62,
|
||||
featuredSections,
|
||||
partsList,
|
||||
latestSection,
|
||||
latestLabel,
|
||||
prefaceMid
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
this.setData({ featuredSections: [], partsList: [], latestSection: null })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -189,11 +233,10 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
// 跳转到阅读页
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
|
||||
},
|
||||
|
||||
// 跳转到匹配页
|
||||
@@ -201,6 +244,40 @@ Page({
|
||||
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' })
|
||||
@@ -211,13 +288,5 @@ Page({
|
||||
await this.initData()
|
||||
this.updateUserStatus()
|
||||
wx.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- Banner卡片 - 最新章节 -->
|
||||
<view wx:if="{{latestSection}}" class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<view class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
@@ -79,6 +79,36 @@
|
||||
</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">
|
||||
@@ -95,12 +125,10 @@
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="tag {{item.tagClass}}">{{item.tag}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
<text class="featured-part">{{item.part}}</text>
|
||||
@@ -110,35 +138,26 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容概览 -->
|
||||
<view class="section">
|
||||
<text class="section-title">内容概览</text>
|
||||
<view class="parts-list">
|
||||
<view
|
||||
class="part-item"
|
||||
wx:for="{{partsList}}"
|
||||
wx:key="id"
|
||||
bindtap="goToChapters"
|
||||
>
|
||||
<view class="part-icon">
|
||||
<text class="part-number">{{item.number}}</text>
|
||||
</view>
|
||||
<view class="part-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="part-arrow">→</view>
|
||||
<!-- 最新新增(从目录移到此处,精选推荐下方) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 序言入口 -->
|
||||
<view class="preface-card" bindtap="goToRead" data-id="preface" data-mid="{{prefaceMid}}">
|
||||
<view class="preface-content">
|
||||
<text class="preface-title">序言</text>
|
||||
<text class="preface-desc">为什么我每天早上6点在Soul开播?</text>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tag tag-free">免费</view>
|
||||
</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;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
|
||||
@@ -38,7 +38,6 @@ Page({
|
||||
todayMatchCount: 0,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchesRemaining: FREE_MATCH_LIMIT,
|
||||
showQuotaExhausted: false,
|
||||
needPayToMatch: false,
|
||||
|
||||
// 匹配状态
|
||||
@@ -67,53 +66,19 @@ Page({
|
||||
// 解锁弹窗
|
||||
showUnlockModal: false,
|
||||
|
||||
// 手机号绑定弹窗(一键加好友前校验)
|
||||
showBindPhoneModal: false,
|
||||
pendingAddWechatAfterBind: false,
|
||||
bindPhoneInput: '',
|
||||
showMatchPhoneManualInput: false,
|
||||
|
||||
// 登录弹窗(未登录时点击匹配弹出)
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false,
|
||||
agreeProtocol: false,
|
||||
|
||||
// 匹配价格(可配置)
|
||||
matchPrice: 1,
|
||||
extraMatches: 0,
|
||||
// 好友优惠展示(与 read 页一致)
|
||||
userDiscount: 5,
|
||||
hasReferralDiscount: false,
|
||||
showDiscountHint: false,
|
||||
displayMatchPrice: 1
|
||||
extraMatches: 0
|
||||
},
|
||||
|
||||
onLoad(options = {}) {
|
||||
// ref:支持 query.ref 或 scene 中的 ref=xxx(分享进入时)
|
||||
let ref = options.ref
|
||||
if (!ref && options.scene) {
|
||||
const sceneStr = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim()
|
||||
const parts = sceneStr.split(/[&_]/)
|
||||
for (const part of parts) {
|
||||
const eq = part.indexOf('=')
|
||||
if (eq > 0) {
|
||||
const k = part.slice(0, eq)
|
||||
const v = part.slice(eq + 1)
|
||||
if (k === 'ref' && v) { ref = v; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ref) {
|
||||
wx.setStorageSync('referral_code', ref)
|
||||
app.handleReferralCode({ query: { ref } })
|
||||
}
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
_ref: ref
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
this.loadMatchConfig()
|
||||
this.loadStoredContact()
|
||||
this.refreshMatchCountAndStatus()
|
||||
this.loadTodayMatchCount()
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -125,53 +90,43 @@ Page({
|
||||
tabBar.setData({ selected: 2 })
|
||||
}
|
||||
}
|
||||
this.loadStoredContact()
|
||||
this.refreshMatchCountAndStatus()
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 加载匹配配置(含 userDiscount 用于好友优惠展示)
|
||||
// 加载匹配配置
|
||||
async loadMatchConfig() {
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [matchRes, configRes] = await Promise.all([
|
||||
app.request('/api/miniprogram/match/config', { method: 'GET' }),
|
||||
app.request('/api/miniprogram/config', { method: 'GET' })
|
||||
])
|
||||
|
||||
const matchPrice = matchRes?.success && matchRes?.data ? (matchRes.data.matchPrice || 1) : 1
|
||||
const userDiscount = configRes?.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displayMatchPrice = hasReferralDiscount
|
||||
? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: matchPrice
|
||||
|
||||
if (matchRes?.success && matchRes?.data) {
|
||||
MATCH_TYPES = matchRes.data.matchTypes || MATCH_TYPES
|
||||
FREE_MATCH_LIMIT = matchRes.data.freeMatchLimit || FREE_MATCH_LIMIT
|
||||
}
|
||||
|
||||
this.setData({
|
||||
matchTypes: MATCH_TYPES,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displayMatchPrice
|
||||
const res = await app.request({ url: '/api/match/config', silent: true, method: 'GET',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
console.log('[Match] 加载匹配配置成功:', { matchPrice, userDiscount, hasReferralDiscount, displayMatchPrice })
|
||||
if (res.success && res.data) {
|
||||
// 更新全局配置
|
||||
MATCH_TYPES = res.data.matchTypes || MATCH_TYPES
|
||||
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
|
||||
const matchPrice = res.data.matchPrice || 1
|
||||
|
||||
this.setData({
|
||||
matchTypes: MATCH_TYPES,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchPrice: matchPrice
|
||||
})
|
||||
|
||||
console.log('[Match] 加载匹配配置成功:', {
|
||||
types: MATCH_TYPES.length,
|
||||
freeLimit: FREE_MATCH_LIMIT,
|
||||
price: matchPrice
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载本地存储的联系方式(含用户资料的手机号、微信号)
|
||||
// 加载本地存储的联系方式
|
||||
loadStoredContact() {
|
||||
const ui = app.globalData.userInfo || {}
|
||||
const phone = wx.getStorageSync('user_phone') || ui.phone || ''
|
||||
const wechat = wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || ''
|
||||
const phone = wx.getStorageSync('user_phone') || ''
|
||||
const wechat = wx.getStorageSync('user_wechat') || ''
|
||||
this.setData({
|
||||
phoneNumber: phone,
|
||||
wechatId: wechat,
|
||||
@@ -179,55 +134,49 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威)
|
||||
async refreshMatchCountAndStatus() {
|
||||
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`)
|
||||
if (res.success && res.data) {
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
// 根据 hasReferrer 更新优惠展示(与 read 页一致)
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref || res.data.hasReferrer)
|
||||
const matchPrice = this.data.matchPrice ?? 1
|
||||
const userDiscount = this.data.userDiscount ?? 5
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displayMatchPrice = hasReferralDiscount
|
||||
? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: matchPrice
|
||||
this.setData({ hasReferralDiscount, displayMatchPrice })
|
||||
// 加载今日匹配次数
|
||||
loadTodayMatchCount() {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const stored = wx.getStorageSync('match_count_data')
|
||||
if (stored) {
|
||||
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
|
||||
if (data.date === today) {
|
||||
this.setData({ todayMatchCount: data.count })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 拉取 matchQuota 失败:', e)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载匹配次数失败:', e)
|
||||
}
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 初始化用户状态(matchQuota 服务端纯计算:订单+match_records)
|
||||
// 保存今日匹配次数
|
||||
saveTodayMatchCount(count) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
wx.setStorageSync('match_count_data', { date: today, count })
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
const quota = app.globalData.matchQuota
|
||||
|
||||
// 今日剩余次数、今日已用:来自服务端 matchQuota(未登录无法计算,不能显示已用完)
|
||||
const remainToday = quota?.remainToday ?? 0
|
||||
const matchesUsedToday = quota?.matchesUsedToday ?? 0
|
||||
const purchasedRemain = quota?.purchasedRemain ?? 0
|
||||
const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT)
|
||||
// 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」
|
||||
const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
|
||||
const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
|
||||
|
||||
|
||||
// 获取额外购买的匹配次数
|
||||
const extraMatches = wx.getStorageSync('extra_match_count') || 0
|
||||
|
||||
// 总匹配次数 = 每日免费(3) + 额外购买次数
|
||||
// 全书用户无限制
|
||||
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches
|
||||
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount)
|
||||
const needPayToMatch = !hasFullBook && matchesRemaining <= 0
|
||||
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
hasPurchased: true,
|
||||
todayMatchCount: matchesUsedToday,
|
||||
hasPurchased: true, // 所有用户都可以使用匹配功能
|
||||
totalMatchesAllowed,
|
||||
matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)),
|
||||
matchesRemaining,
|
||||
needPayToMatch,
|
||||
showQuotaExhausted,
|
||||
extraMatches: purchasedRemain
|
||||
extraMatches
|
||||
})
|
||||
},
|
||||
|
||||
@@ -243,16 +192,25 @@ Page({
|
||||
|
||||
// 点击匹配按钮
|
||||
handleMatchClick() {
|
||||
// 检测是否登录,未登录则弹出登录弹窗
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
return
|
||||
}
|
||||
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// 资源对接类型需要购买章节才能使用
|
||||
// 资源对接类型需要登录+购买章节才能使用
|
||||
if (currentType && currentType.id === 'investor') {
|
||||
// 检查是否登录
|
||||
if (!this.data.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再使用资源对接功能',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否购买过章节
|
||||
const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook
|
||||
if (!hasPurchased) {
|
||||
@@ -262,7 +220,7 @@ Page({
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
wx.switchTab({ url: '/pages/catalog/catalog' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -277,8 +235,7 @@ Page({
|
||||
const hasWechat = !!this.data.wechatId
|
||||
|
||||
if (!hasPhone && !hasWechat) {
|
||||
// 没有绑定联系方式,先显示绑定提示(仍尝试加载已有资料填充)
|
||||
this.loadStoredContact()
|
||||
// 没有绑定联系方式,先显示绑定提示
|
||||
this.setData({
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
@@ -295,12 +252,12 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
// 创业合伙类型 - 超过匹配次数时直接弹出付费弹窗
|
||||
if (this.data.showQuotaExhausted || this.data.needPayToMatch) {
|
||||
// 创业合伙类型 - 真正的匹配功能
|
||||
if (this.data.needPayToMatch) {
|
||||
this.setData({ showUnlockModal: true })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.startMatch()
|
||||
},
|
||||
|
||||
@@ -322,8 +279,6 @@ Page({
|
||||
const delay = Math.random() * 2000 + 1000
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
// 打开弹窗前调取用户资料填充手机号、微信号
|
||||
this.loadStoredContact()
|
||||
this.setData({
|
||||
isMatching: false,
|
||||
showJoinModal: true,
|
||||
@@ -352,7 +307,6 @@ Page({
|
||||
|
||||
// 开始匹配 - 只匹配数据库中的真实用户
|
||||
async startMatch() {
|
||||
this.loadStoredContact()
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
@@ -364,28 +318,20 @@ Page({
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 1000)
|
||||
|
||||
// 从数据库获取真实用户匹配(后端会校验剩余次数)
|
||||
// 从数据库获取真实用户匹配
|
||||
let matchedUser = null
|
||||
let quotaExceeded = false
|
||||
try {
|
||||
const ui = app.globalData.userInfo || {}
|
||||
const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim()
|
||||
const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim()
|
||||
const res = await app.request('/api/miniprogram/match/users', {
|
||||
const res = await app.request({ url: '/api/match/users', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
phone,
|
||||
wechatId
|
||||
userId: app.globalData.userInfo?.id || ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (res.success && res.data) {
|
||||
matchedUser = res.data
|
||||
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
|
||||
} else if (res.code === 'QUOTA_EXCEEDED') {
|
||||
quotaExceeded = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 数据库匹配失败:', e)
|
||||
@@ -395,14 +341,7 @@ Page({
|
||||
const delay = Math.random() * 2000 + 2000
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
|
||||
// 次数用尽(后端校验)- 直接弹出付费弹窗
|
||||
if (quotaExceeded) {
|
||||
this.setData({ isMatching: false, showUnlockModal: true })
|
||||
this.refreshMatchCountAndStatus()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果没有匹配到用户,提示用户
|
||||
if (!matchedUser) {
|
||||
this.setData({ isMatching: false })
|
||||
@@ -414,17 +353,23 @@ Page({
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配成功:从服务端刷新配额(后端已写入 match_records)
|
||||
|
||||
// 增加今日匹配次数
|
||||
const newCount = this.data.todayMatchCount + 1
|
||||
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
|
||||
|
||||
this.setData({
|
||||
isMatching: false,
|
||||
currentMatch: matchedUser,
|
||||
needPayToMatch: false
|
||||
todayMatchCount: newCount,
|
||||
matchesRemaining,
|
||||
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
|
||||
})
|
||||
this.refreshMatchCountAndStatus()
|
||||
|
||||
this.saveTodayMatchCount(newCount)
|
||||
|
||||
// 上报匹配行为到存客宝
|
||||
this.reportMatch(matchedUser)
|
||||
|
||||
}, delay)
|
||||
},
|
||||
|
||||
@@ -460,7 +405,7 @@ Page({
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
try {
|
||||
await app.request('/api/miniprogram/ckb/match', {
|
||||
await app.request({ url: '/api/ckb/match', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
@@ -490,78 +435,26 @@ Page({
|
||||
this.setData({ currentMatch: null })
|
||||
},
|
||||
|
||||
// 添加微信好友(先校验手机号绑定)
|
||||
// 添加微信好友
|
||||
handleAddWechat() {
|
||||
if (!this.data.currentMatch) return
|
||||
|
||||
// 未登录需先登录
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再添加好友',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是否已绑定手机号(本地缓存或用户资料)
|
||||
const hasPhone = !!(
|
||||
wx.getStorageSync('user_phone') ||
|
||||
app.globalData.userInfo?.phone
|
||||
)
|
||||
|
||||
if (!hasPhone) {
|
||||
this.setData({
|
||||
showBindPhoneModal: true,
|
||||
pendingAddWechatAfterBind: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.doCopyWechat()
|
||||
},
|
||||
|
||||
// 执行复制联系方式(优先微信号,无则复制手机号)
|
||||
doCopyWechat() {
|
||||
if (!this.data.currentMatch) return
|
||||
const wechat = (this.data.currentMatch.wechat || this.data.currentMatch.wechatId || '').trim()
|
||||
const phone = (this.data.currentMatch.phone || '').trim()
|
||||
const toCopy = wechat || phone
|
||||
if (!toCopy) {
|
||||
wx.showModal({
|
||||
title: '暂无可复制',
|
||||
content: '该用户未提供微信号或手机号,请通过其他方式联系',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
return
|
||||
}
|
||||
const label = wechat ? '微信号' : '手机号'
|
||||
wx.setClipboardData({
|
||||
data: toCopy,
|
||||
data: this.data.currentMatch.wechat,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: wechat ? '微信号已复制' : '手机号已复制',
|
||||
content: wechat
|
||||
? `${label}:${toCopy}\n\n请打开微信添加好友,备注"创业合作"即可`
|
||||
: `${label}:${toCopy}\n\n可通过微信搜索该手机号添加好友`,
|
||||
title: '微信号已复制',
|
||||
content: `微信号:${this.data.currentMatch.wechat}\n\n请打开微信添加好友,备注"创业合作"即可`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换联系方式类型(同步刷新用户资料填充)
|
||||
// 切换联系方式类型
|
||||
switchContactType(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.loadStoredContact()
|
||||
this.setData({ contactType: type, joinError: '' })
|
||||
},
|
||||
|
||||
@@ -672,150 +565,6 @@ Page({
|
||||
this.setData({ showJoinModal: false, joinError: '' })
|
||||
},
|
||||
|
||||
// 关闭手机绑定弹窗
|
||||
closeBindPhoneModal() {
|
||||
this.setData({
|
||||
showBindPhoneModal: false,
|
||||
pendingAddWechatAfterBind: false,
|
||||
bindPhoneInput: '',
|
||||
showMatchPhoneManualInput: false
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
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 handleMatchWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
// 登录成功后必须拉取 matchQuota,否则无法正确显示剩余次数
|
||||
await this.refreshMatchCountAndStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Match] 微信登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 一键获取手机号(匹配页加好友前绑定)
|
||||
async onMatchGetPhoneNumber(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
wx.showToast({ title: '授权失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const code = e.detail.code
|
||||
if (!code) {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
return
|
||||
}
|
||||
try {
|
||||
wx.showLoading({ title: '获取中...', mask: true })
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const res = await app.request('/api/miniprogram/phone', {
|
||||
method: 'POST',
|
||||
data: { code, userId }
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res.success && res.phoneNumber) {
|
||||
await this.saveMatchPhoneAndContinue(res.phoneNumber)
|
||||
} else {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
}
|
||||
} catch (err) {
|
||||
wx.hideLoading()
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换为手动输入
|
||||
onMatchShowManualInput() {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
},
|
||||
|
||||
// 手动输入手机号
|
||||
onMatchPhoneInput(e) {
|
||||
this.setData({
|
||||
bindPhoneInput: e.detail.value.replace(/\D/g, '').slice(0, 11)
|
||||
})
|
||||
},
|
||||
|
||||
// 确认手动绑定手机号
|
||||
async confirmMatchPhoneBind() {
|
||||
const { bindPhoneInput } = this.data
|
||||
if (!bindPhoneInput || bindPhoneInput.length !== 11) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(bindPhoneInput)) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this.saveMatchPhoneAndContinue(bindPhoneInput)
|
||||
},
|
||||
|
||||
// 保存手机号到本地+服务器,并继续加好友
|
||||
async saveMatchPhoneAndContinue(phone) {
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.phone = phone
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
this.setData({
|
||||
phoneNumber: phone,
|
||||
userPhone: phone,
|
||||
bindPhoneInput: ''
|
||||
})
|
||||
this.loadStoredContact()
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, phone }
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 同步手机号到服务器失败:', e)
|
||||
}
|
||||
const pending = this.data.pendingAddWechatAfterBind
|
||||
this.closeBindPhoneModal()
|
||||
if (pending) {
|
||||
wx.showToast({ title: '绑定成功', icon: 'success' })
|
||||
setTimeout(() => this.doCopyWechat(), 500)
|
||||
}
|
||||
},
|
||||
|
||||
// 显示解锁弹窗
|
||||
showUnlockModal() {
|
||||
this.setData({ showUnlockModal: true })
|
||||
@@ -826,24 +575,7 @@ Page({
|
||||
this.setData({ showUnlockModal: false })
|
||||
},
|
||||
|
||||
// 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试)
|
||||
async pollOrderAndRefresh(orderSn) {
|
||||
const maxAttempts = 12
|
||||
const interval = 800
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
|
||||
if (r?.data?.status === 'paid') {
|
||||
await this.refreshMatchCountAndStatus()
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval))
|
||||
}
|
||||
await this.refreshMatchCountAndStatus()
|
||||
},
|
||||
|
||||
// 购买匹配次数(与购买章节逻辑一致,写入订单)
|
||||
// 购买匹配次数
|
||||
async buyMatchCount() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
|
||||
@@ -859,17 +591,16 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const matchPrice = this.data.matchPrice || 1
|
||||
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
|
||||
const referralCode = wx.getStorageSync('referral_code') || ''
|
||||
// 调用支付接口购买匹配次数(productType: match,订单类型:购买匹配次数)
|
||||
// 调用支付接口购买匹配次数
|
||||
const res = await app.request('/api/miniprogram/pay', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
openId,
|
||||
productType: 'match',
|
||||
productId: 'match_1',
|
||||
amount: matchPrice,
|
||||
amount: 1,
|
||||
description: '匹配次数x1',
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
referralCode: referralCode || undefined
|
||||
@@ -877,7 +608,6 @@ Page({
|
||||
})
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
const orderSn = res.data.orderSn
|
||||
// 调用微信支付
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
@@ -886,9 +616,13 @@ Page({
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
// 支付成功,增加匹配次数
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
// 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机)
|
||||
this.pollOrderAndRefresh(orderSn)
|
||||
this.initUserStatus()
|
||||
} else {
|
||||
throw new Error(res.error || '创建订单失败')
|
||||
}
|
||||
@@ -896,13 +630,14 @@ Page({
|
||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消', icon: 'none' })
|
||||
} else {
|
||||
// 测试模式(无支付环境时本地模拟)
|
||||
// 测试模式
|
||||
wx.showModal({
|
||||
title: '支付服务暂不可用',
|
||||
content: '是否使用测试模式购买?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
}
|
||||
@@ -924,13 +659,5 @@ Page({
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 找伙伴',
|
||||
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
|
||||
}
|
||||
}
|
||||
preventBubble() {}
|
||||
})
|
||||
|
||||
@@ -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); } }
|
||||
|
||||
@@ -29,26 +29,17 @@ Page({
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
|
||||
// Tab切换
|
||||
activeTab: 'overview', // overview | footprint
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
|
||||
// 功能配置
|
||||
matchEnabled: false, // 找伙伴功能开关
|
||||
matchEnabled: false,
|
||||
|
||||
// 菜单列表
|
||||
menuList: [
|
||||
{ id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'gray' },
|
||||
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
|
||||
{ id: 'referral', title: '推广中心', icon: '🎁', iconBg: 'gold', badge: '90%佣金' },
|
||||
{ id: 'withdrawRecords', title: '提现记录', icon: '📋', iconBg: 'gray' },
|
||||
{ id: 'about', title: '关于作者', icon: 'ℹ️', iconBg: 'brand' },
|
||||
{ id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' }
|
||||
],
|
||||
|
||||
// 待确认收款(用户确认模式)
|
||||
// VIP状态
|
||||
isVip: false,
|
||||
vipExpireDate: '',
|
||||
|
||||
// 待确认收款
|
||||
pendingConfirmList: [],
|
||||
withdrawMchId: '',
|
||||
withdrawAppId: '',
|
||||
@@ -65,11 +56,7 @@ Page({
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: '',
|
||||
|
||||
// 扫一扫结果弹窗
|
||||
showScanResultModal: false,
|
||||
scanResult: ''
|
||||
editingNickname: ''
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -95,48 +82,17 @@ Page({
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 加载功能配置
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/config',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (res && res.features) {
|
||||
this.setData({
|
||||
matchEnabled: res.features.matchEnabled === true
|
||||
})
|
||||
}
|
||||
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 })
|
||||
}
|
||||
},
|
||||
|
||||
// 登录后刷新购买状态(与 match/read 一致,避免其他页面用旧数据)
|
||||
async refreshPurchaseStatus() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
|
||||
if (res.success && res.data) {
|
||||
app.globalData.hasFullBook = res.data.hasFullBook || false
|
||||
app.globalData.purchasedSections = res.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = res.data.hasFullBook
|
||||
userInfo.purchasedSections = res.data.purchasedSections
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[My] 刷新购买状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
@@ -144,8 +100,7 @@ Page({
|
||||
if (isLoggedIn && userInfo) {
|
||||
const readIds = app.globalData.readSectionIds || []
|
||||
const recentList = readIds.slice(-5).reverse().map(id => ({
|
||||
id,
|
||||
mid: app.getSectionMid(id),
|
||||
id: id,
|
||||
title: `章节 ${id}`
|
||||
}))
|
||||
|
||||
@@ -169,6 +124,7 @@ Page({
|
||||
})
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
} else {
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
@@ -189,7 +145,7 @@ Page({
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id)
|
||||
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,
|
||||
@@ -291,7 +247,7 @@ Page({
|
||||
}
|
||||
const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/earnings?userId=' + userInfo.id)
|
||||
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
|
||||
@@ -566,7 +522,6 @@ Page({
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
await this.refreshPurchaseStatus()
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
@@ -595,7 +550,6 @@ Page({
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (result) {
|
||||
await this.refreshPurchaseStatus()
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
@@ -610,29 +564,15 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转编辑资料页
|
||||
goEditProfile() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 点击菜单
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (id === 'scan') {
|
||||
this.doScanCode()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
@@ -640,52 +580,16 @@ Page({
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
|
||||
if (routes[id]) {
|
||||
wx.navigateTo({ url: routes[id] })
|
||||
}
|
||||
},
|
||||
|
||||
// 扫一扫:调起扫码,展示解析值
|
||||
doScanCode() {
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
const result = res.result || ''
|
||||
this.setData({
|
||||
showScanResultModal: true,
|
||||
scanResult: result
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '扫码失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭扫码结果弹窗
|
||||
closeScanResultModal() {
|
||||
this.setData({ showScanResultModal: false, scanResult: '' })
|
||||
},
|
||||
|
||||
// 复制扫码结果
|
||||
copyScanResult() {
|
||||
const text = this.data.scanResult || ''
|
||||
if (!text) return
|
||||
wx.setClipboardData({
|
||||
data: text,
|
||||
success: () => wx.showToast({ title: '已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到阅读页
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
@@ -732,14 +636,80 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
// 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) }
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的',
|
||||
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
|
||||
// 头像点击:已登录弹出选项(改头像/进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() {}
|
||||
})
|
||||
|
||||
@@ -47,37 +47,33 @@
|
||||
<!-- 用户卡片 - 已登录状态 -->
|
||||
<view class="user-card card-gradient" wx:else>
|
||||
<view class="user-header-row">
|
||||
<!-- 头像 - 点击选择头像 -->
|
||||
<button class="avatar-btn-simple" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar">
|
||||
<!-- 头像 - 点击进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>
|
||||
</button>
|
||||
<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">ID: {{userIdShort}}</text>
|
||||
<text class="user-id">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 创业伙伴按钮 - inline 在同一行 -->
|
||||
<view class="margin-partner-badge">
|
||||
<view class="partner-badge">
|
||||
<text class="partner-icon">⭐</text>
|
||||
<text class="partner-text">创业伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑资料入口 -->
|
||||
<view class="edit-profile-entry" bindtap="goEditProfile">
|
||||
<text class="edit-profile-icon">✏️</text>
|
||||
<text class="edit-profile-text">编辑资料</text>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
@@ -89,85 +85,44 @@
|
||||
<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>
|
||||
<text class="stat-label">我的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待确认收款(用户确认模式)- 有数据时显示 -->
|
||||
<view class="pending-confirm-card" wx:if="{{isLoggedIn && pendingConfirmList.length > 0}}">
|
||||
<view class="pending-confirm-header">
|
||||
<text class="pending-confirm-title">待确认收款</text>
|
||||
<text class="pending-confirm-desc" wx:if="{{pendingConfirmList.length > 0}}">审核已通过,点击下方按钮完成收款</text>
|
||||
<text class="pending-confirm-desc" wx:else>暂无待确认的提现,审核通过后会出现在这里</text>
|
||||
</view>
|
||||
<view class="pending-confirm-list" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<view class="pending-confirm-item" wx:for="{{pendingConfirmList}}" wx:key="id">
|
||||
<view class="pending-confirm-info">
|
||||
<text class="pending-confirm-amount">¥{{item.amount}}</text>
|
||||
<text class="pending-confirm-time">{{item.createdAt}}</text>
|
||||
</view>
|
||||
<view class="pending-confirm-btn" bindtap="confirmReceive" data-index="{{index}}">确认收款</view>
|
||||
</view>
|
||||
</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.iconBg === 'gold' ? 'icon-gold' : item.iconBg === 'gray' ? 'icon-gray' : ''}}">
|
||||
{{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-badge gold-color" wx:if="{{item.badge}}">{{item.badge}}</text>
|
||||
<text class="menu-arrow">→</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>
|
||||
<!-- 根据 matchEnabled 显示 2 列或 3 列布局 -->
|
||||
<view class="stats-row {{matchEnabled ? '' : 'stats-row-two-cols'}}">
|
||||
<view class="stats-row">
|
||||
<view class="stat-box">
|
||||
<text class="stat-icon brand-color">📖</text>
|
||||
<text class="stat-num">{{readCount}}</text>
|
||||
@@ -199,7 +154,6 @@
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
@@ -215,16 +169,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 匹配记录 - 根据配置显示 -->
|
||||
<view class="match-card card" wx:if="{{matchEnabled}}">
|
||||
<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>
|
||||
@@ -287,21 +241,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 扫一扫结果弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showScanResultModal}}" bindtap="closeScanResultModal">
|
||||
<view class="modal-content scan-result-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeScanResultModal">✕</view>
|
||||
<view class="scan-result-header">
|
||||
<text class="scan-result-title">扫码解析结果</text>
|
||||
</view>
|
||||
<scroll-view class="scan-result-body" scroll-y><text class="scan-result-text">{{scanResult}}</text></scroll-view>
|
||||
<view class="scan-result-actions">
|
||||
<view class="scan-result-btn" bindtap="copyScanResult">复制</view>
|
||||
<view class="scan-result-btn primary" bindtap="closeScanResultModal">关闭</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
|
||||
@@ -95,39 +95,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 编辑资料入口 - 深色圆角按钮,与卡片统一 */
|
||||
.edit-profile-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
min-height: 88rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.3);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.edit-profile-entry:active {
|
||||
background: rgba(0, 206, 209, 0.12);
|
||||
}
|
||||
|
||||
.edit-profile-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-profile-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #00CED1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-gradient {
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
@@ -1270,27 +1237,137 @@
|
||||
color: #fff; font-size: 26rpx; font-weight: 500; border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* ===== 扫一扫结果弹窗 ===== */
|
||||
.scan-result-modal .modal-close { top: 24rpx; right: 24rpx; }
|
||||
.scan-result-header { margin-bottom: 24rpx; }
|
||||
.scan-result-title { font-size: 32rpx; font-weight: 600; color: #fff; }
|
||||
.scan-result-body {
|
||||
max-height: 320rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
.scan-result-text { font-size: 26rpx; color: rgba(255,255,255,0.9); line-height: 1.5; }
|
||||
.scan-result-actions { display: flex; gap: 24rpx; }
|
||||
.scan-result-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255,255,255,0.9);
|
||||
background: rgba(255,255,255,0.1);
|
||||
/* ===== 收益面板(内嵌) ===== */
|
||||
.earnings-inline {
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.15);
|
||||
}
|
||||
.scan-result-btn.primary { background: #00CED1; color: #000; }
|
||||
.earnings-inline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.earnings-inline-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
flex: 1;
|
||||
}
|
||||
.earnings-inline-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.earnings-inline-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.earnings-inline-divider {
|
||||
width: 2rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255,255,255,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.earnings-inline-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
|
||||
border-radius: 20rpx;
|
||||
font-size: 26rpx;
|
||||
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; }
|
||||
|
||||
@@ -17,13 +17,5 @@ Page({
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 隐私政策',
|
||||
path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,31 +15,29 @@ Page({
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
async loadOrders() {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
let purchasedSections = app.globalData.purchasedSections || []
|
||||
let sectionMidMap = app.globalData.sectionMidMap || {}
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
|
||||
if (res?.success && res.data) {
|
||||
purchasedSections = res.data.purchasedSections || []
|
||||
sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.purchasedSections = purchasedSections
|
||||
app.globalData.sectionMidMap = sectionMidMap
|
||||
}
|
||||
} catch (_) { /* 使用缓存 */ }
|
||||
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}`,
|
||||
sectionId: id,
|
||||
mid: sectionMidMap[id] || 0,
|
||||
title: `章节 ${id}`,
|
||||
amount: 1,
|
||||
status: 'completed',
|
||||
@@ -48,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 })
|
||||
}
|
||||
@@ -55,18 +60,8 @@ Page({
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
|
||||
},
|
||||
|
||||
goBack() { wx.navigateBack() },
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的订单',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
}
|
||||
goBack() { wx.navigateBack() }
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
import readingTracker from '../../utils/readingTracker'
|
||||
const { buildScene, parseScene } = require('../../utils/scene.js')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
@@ -57,12 +56,6 @@ Page({
|
||||
sectionPrice: 1,
|
||||
fullBookPrice: 9.9,
|
||||
totalSections: 62,
|
||||
// 好友优惠展示
|
||||
userDiscount: 5,
|
||||
hasReferralDiscount: false,
|
||||
showDiscountHint: false,
|
||||
displaySectionPrice: 1,
|
||||
displayFullBookPrice: 9.9,
|
||||
|
||||
// 弹窗
|
||||
showShareModal: false,
|
||||
@@ -73,41 +66,21 @@ Page({
|
||||
isGeneratingPoster: false,
|
||||
|
||||
// 免费章节
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
|
||||
|
||||
// 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用)
|
||||
shareImagePath: ''
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
// 官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与海报生成 buildScene 闭环
|
||||
const sceneStr = (options && options.scene) || ''
|
||||
const parsed = parseScene(sceneStr)
|
||||
const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0)
|
||||
const id = options.id || parsed.id || app.globalData.initialSectionId
|
||||
const ref = options.ref || parsed.ref
|
||||
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
|
||||
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
|
||||
|
||||
console.log('[Read] onLoad:', { options, sceneRaw: sceneStr || undefined, parsed, mid, id, ref })
|
||||
console.log('[Read] onLoad options:', options)
|
||||
|
||||
if (!mid && !id) {
|
||||
console.warn('[Read] 未获取到章节 mid/id,options:', options)
|
||||
wx.showToast({ title: '章节参数缺失', icon: 'none' })
|
||||
this.setData({ accessState: 'error', loading: false })
|
||||
return
|
||||
}
|
||||
|
||||
const { id, ref } = options
|
||||
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight,
|
||||
sectionId: '', // 加载后填充
|
||||
sectionMid: mid || null,
|
||||
sectionId: id,
|
||||
loading: true,
|
||||
accessState: 'unknown'
|
||||
})
|
||||
|
||||
// 处理推荐码绑定(异步不阻塞)
|
||||
if (ref) {
|
||||
console.log('[Read] 检测到推荐码:', ref)
|
||||
wx.setStorageSync('referral_code', ref)
|
||||
@@ -115,66 +88,35 @@ Page({
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
// 有推荐人 = ref/ referral_code 或 用户信息中有推荐人绑定
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || ref || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const showDiscountHint = userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
// 【标准流程】1. 拉取最新配置(免费列表、价格)
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9
|
||||
})
|
||||
|
||||
// 先拉取章节获取 id(mid 时必需;id 时可直接用)
|
||||
let resolvedId = id
|
||||
let prefetchedChapter = null
|
||||
if (mid && !id) {
|
||||
const chRes = await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
|
||||
if (chRes && chRes.id) {
|
||||
resolvedId = chRes.id
|
||||
prefetchedChapter = chRes
|
||||
}
|
||||
}
|
||||
this.setData({ sectionId: resolvedId })
|
||||
|
||||
const accessState = await accessManager.determineAccessState(resolvedId, config.freeChapters)
|
||||
// 【标准流程】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,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? 0
|
||||
showPaywall: !canAccess
|
||||
})
|
||||
|
||||
await this.loadContent(mid, resolvedId, accessState, prefetchedChapter)
|
||||
// 【标准流程】3. 加载内容
|
||||
await this.loadContent(id, accessState)
|
||||
|
||||
// 【标准流程】4. 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
readingTracker.init(resolvedId)
|
||||
readingTracker.init(id)
|
||||
}
|
||||
|
||||
this.loadNavigation(resolvedId)
|
||||
// 5. 加载导航
|
||||
this.loadNavigation(id)
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Read] 初始化失败:', e)
|
||||
@@ -217,9 +159,8 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 【重构】加载章节内容。mid 优先用 by-mid 接口,id 用旧接口;prefetched 避免重复请求
|
||||
async loadContent(mid, id, accessState, prefetched) {
|
||||
console.log('[Read] loadContent 请求参数:', { mid, id, accessState: accessState, prefetched: !!prefetched })
|
||||
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
|
||||
async loadContent(id, accessState) {
|
||||
try {
|
||||
const section = this.getSectionInfo(id)
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
@@ -227,43 +168,26 @@ Page({
|
||||
section.price = sectionPrice
|
||||
}
|
||||
this.setData({ section })
|
||||
|
||||
let res = prefetched
|
||||
if (!res) {
|
||||
res = mid
|
||||
? await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
|
||||
: await app.request(`/api/miniprogram/book/chapter/${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)
|
||||
const updates = {
|
||||
|
||||
this.setData({
|
||||
content: res.content,
|
||||
contentParagraphs: lines,
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
}
|
||||
if (res.mid) updates.sectionMid = res.mid
|
||||
this.setData(updates)
|
||||
})
|
||||
|
||||
// 如果有权限,标记为已读
|
||||
if (accessManager.canAccessFullContent(accessState)) {
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
// 始终用接口返回的 price/isFree 更新 section(不写死 1 元)
|
||||
const section = this.data.section || {}
|
||||
if (res.price !== undefined && res.price !== null) section.price = Number(res.price)
|
||||
if (res.isFree !== undefined) section.isFree = !!res.isFree
|
||||
// 0元即免费:接口返回 price 为 0 或 isFree 为 true 时,不展示付费墙
|
||||
const isFreeByPrice = res.price === 0 || res.price === '0' || Number(res.price) === 0
|
||||
const isFreeByFlag = res.isFree === true
|
||||
if (isFreeByPrice || isFreeByFlag) {
|
||||
this.setData({ section, showPaywall: false, canAccess: true, accessState: 'free' })
|
||||
app.markSectionAsRead(id)
|
||||
} else {
|
||||
this.setData({ section })
|
||||
}
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败:', e)
|
||||
@@ -307,12 +231,12 @@ Page({
|
||||
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
|
||||
}
|
||||
|
||||
// 普通章节:price 不写死,由 loadContent 从 config/接口 填充
|
||||
// 普通章节
|
||||
return {
|
||||
id: id,
|
||||
title: this.getSectionTitle(id),
|
||||
isFree: id === '1.1',
|
||||
price: undefined
|
||||
price: 1
|
||||
}
|
||||
},
|
||||
|
||||
@@ -335,6 +259,48 @@ Page({
|
||||
}
|
||||
return titles[id] || `章节 ${id}`
|
||||
},
|
||||
|
||||
// 加载内容 - 三级降级方案:API → 本地缓存 → 备用API
|
||||
async loadContent(id) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
|
||||
// 1. 优先从API获取
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 5000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
// 成功后缓存到本地
|
||||
wx.setStorageSync(cacheKey, res)
|
||||
console.log('[Read] 从API加载成功:', id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] API加载失败,尝试本地缓存:', e.message)
|
||||
}
|
||||
|
||||
// 2. API失败,尝试从本地缓存读取
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
this.setChapterContent(cached)
|
||||
console.log('[Read] 从本地缓存加载成功:', id)
|
||||
// 后台静默刷新
|
||||
this.silentRefresh(id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 本地缓存读取失败')
|
||||
}
|
||||
|
||||
// 3. 都失败,显示加载中并持续重试
|
||||
this.setData({
|
||||
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
|
||||
previewParagraphs: ['章节内容加载中...']
|
||||
})
|
||||
|
||||
// 延迟重试(最多3次)
|
||||
this.retryLoadContent(id, 3)
|
||||
},
|
||||
|
||||
// 带超时的章节请求
|
||||
fetchChapterWithTimeout(id, timeout = 5000) {
|
||||
@@ -397,11 +363,9 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setData({ section: this.getSectionInfo(id) })
|
||||
this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -412,7 +376,7 @@ Page({
|
||||
},
|
||||
|
||||
|
||||
// 加载导航(prevSection/nextSection 含 mid 时用于跳转,否则用 id)
|
||||
// 加载导航
|
||||
loadNavigation(id) {
|
||||
const sectionOrder = [
|
||||
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
|
||||
@@ -428,19 +392,14 @@ Page({
|
||||
'11.1', '11.2', '11.3', '11.4', '11.5',
|
||||
'epilogue'
|
||||
]
|
||||
const bookData = app.globalData.bookData || []
|
||||
const idToMid = {}
|
||||
bookData.forEach(ch => {
|
||||
if (ch.id && ch.mid) idToMid[ch.id] = ch.mid
|
||||
})
|
||||
|
||||
|
||||
const currentIndex = sectionOrder.indexOf(id)
|
||||
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
|
||||
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
|
||||
|
||||
|
||||
this.setData({
|
||||
prevSection: prevId ? { id: prevId, mid: idToMid[prevId], title: this.getSectionTitle(prevId) } : null,
|
||||
nextSection: nextId ? { id: nextId, mid: idToMid[nextId], title: this.getSectionTitle(nextId) } : null
|
||||
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
|
||||
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
|
||||
})
|
||||
},
|
||||
|
||||
@@ -460,6 +419,21 @@ Page({
|
||||
this.setData({ showShareModal: false })
|
||||
},
|
||||
|
||||
// 复制链接
|
||||
copyLink() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => {
|
||||
wx.showToast({ title: '链接已复制', icon: 'success' })
|
||||
this.setData({ showShareModal: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 复制分享文案(朋友圈风格)
|
||||
copyShareText() {
|
||||
const { section } = this.data
|
||||
@@ -480,91 +454,33 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用
|
||||
drawShareCard() {
|
||||
const { section, sectionId, contentParagraphs } = this.data
|
||||
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
|
||||
const raw = (contentParagraphs && contentParagraphs.length)
|
||||
? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim()
|
||||
: ''
|
||||
const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事')
|
||||
const ctx = wx.createCanvasContext('shareCardCanvas', this)
|
||||
const w = 500
|
||||
const h = 400
|
||||
// 白底
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
// 顶部:平台名
|
||||
ctx.setFillStyle('#333333')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36)
|
||||
// 深色内容区(模拟参考图效果)
|
||||
const boxX = 24
|
||||
const boxY = 52
|
||||
const boxW = w - 48
|
||||
const boxH = 300
|
||||
ctx.setFillStyle('#2c2c2e')
|
||||
ctx.fillRect(boxX, boxY, boxW, boxH)
|
||||
// 文章标题(白字)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(15)
|
||||
const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15)
|
||||
let y = boxY + 28
|
||||
titleLines.slice(0, 2).forEach(line => {
|
||||
ctx.fillText(line, boxX + 16, y)
|
||||
y += 22
|
||||
})
|
||||
y += 8
|
||||
// 正文摘要(浅灰)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.88)')
|
||||
ctx.setFontSize(12)
|
||||
const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12)
|
||||
excerptLines.slice(0, 8).forEach(line => {
|
||||
ctx.fillText(line, boxX + 16, y)
|
||||
y += 20
|
||||
})
|
||||
// 底部:小程序标识
|
||||
ctx.setFillStyle('#999999')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('小程序', 24, h - 16)
|
||||
ctx.draw(false, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'shareCardCanvas',
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
this.setData({ shareImagePath: res.tempFilePath })
|
||||
}
|
||||
}, this)
|
||||
})
|
||||
},
|
||||
|
||||
// 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
|
||||
getShareConfig() {
|
||||
const { section, sectionId, sectionMid, shareImagePath } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const shareTitle = section?.title
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
|
||||
|
||||
// 分享标题优化
|
||||
const shareTitle = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 Soul创业派对 - 真实商业故事'
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
|
||||
|
||||
return {
|
||||
title: shareTitle,
|
||||
path,
|
||||
imageUrl: shareImagePath || undefined
|
||||
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
|
||||
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
|
||||
}
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return this.getShareConfig()
|
||||
},
|
||||
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
const { section, sectionId, sectionMid } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
|
||||
return {
|
||||
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
|
||||
query: ref ? `${q}&ref=${ref}` : q
|
||||
query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -643,34 +559,9 @@ Page({
|
||||
// 1. 刷新用户购买状态(从 orders 表拉取最新)
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
|
||||
// 2. 重新拉取免费列表、价格与用户推荐人状态
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
})
|
||||
// 2. 重新拉取免费列表(极端情况:刚登录时当前章节可能改免费了)
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({ freeIds: config.freeChapters })
|
||||
|
||||
// 3. 重新判断当前章节权限
|
||||
const newAccessState = await accessManager.determineAccessState(
|
||||
@@ -688,7 +579,7 @@ Page({
|
||||
|
||||
// 4. 如果已解锁,重新加载内容并初始化阅读追踪
|
||||
if (canAccess) {
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
readingTracker.init(this.data.sectionId)
|
||||
}
|
||||
|
||||
@@ -713,7 +604,7 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const price = this.data.section?.price ?? this.data.sectionPrice ?? 1
|
||||
const price = this.data.section?.price || 1
|
||||
console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price })
|
||||
wx.hideLoading()
|
||||
await this.processPayment('section', this.data.sectionId, price)
|
||||
@@ -759,7 +650,6 @@ Page({
|
||||
// 更新本地购买状态
|
||||
app.globalData.hasFullBook = checkRes.data.hasFullBook
|
||||
app.globalData.purchasedSections = checkRes.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = checkRes.data.sectionMidMap || {}
|
||||
|
||||
// 检查是否已购买
|
||||
if (type === 'section' && sectionId) {
|
||||
@@ -844,8 +734,7 @@ Page({
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
paymentData = res.data.payParams
|
||||
paymentData._orderSn = res.data.orderSn
|
||||
console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
|
||||
console.log('[Pay] 获取支付参数成功:', paymentData)
|
||||
} else {
|
||||
throw new Error(res.error || res.message || '创建订单失败')
|
||||
}
|
||||
@@ -878,12 +767,11 @@ Page({
|
||||
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
|
||||
|
||||
try {
|
||||
const orderSn = paymentData._orderSn
|
||||
await this.callWechatPay(paymentData)
|
||||
|
||||
// 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
|
||||
// 4. 【标准流程】支付成功后刷新权限并解锁内容
|
||||
console.log('[Pay] 微信支付成功!')
|
||||
await this.onPaymentSuccess(orderSn)
|
||||
await this.onPaymentSuccess()
|
||||
|
||||
} catch (payErr) {
|
||||
console.error('[Pay] 微信支付调起失败:', payErr)
|
||||
@@ -919,34 +807,13 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 轮询订单状态,确认 paid 后刷新权限并解锁
|
||||
async pollOrderUntilPaid(orderSn) {
|
||||
const maxAttempts = 15
|
||||
const interval = 800
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
|
||||
if (r?.data?.status === 'paid') return true
|
||||
} catch (_) {}
|
||||
if (i < maxAttempts - 1) await this.sleep(interval)
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 【新增】支付成功后的标准处理流程
|
||||
async onPaymentSuccess(orderSn) {
|
||||
async onPaymentSuccess() {
|
||||
wx.showLoading({ title: '确认购买中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify)
|
||||
if (orderSn) {
|
||||
const paid = await this.pollOrderUntilPaid(orderSn)
|
||||
if (!paid) {
|
||||
console.warn('[Pay] 轮询超时,仍尝试刷新')
|
||||
}
|
||||
} else {
|
||||
await this.sleep(1500)
|
||||
}
|
||||
// 1. 等待服务端处理支付回调(1-2秒)
|
||||
await this.sleep(2000)
|
||||
|
||||
// 2. 刷新用户购买状态
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
@@ -976,7 +843,7 @@ Page({
|
||||
})
|
||||
|
||||
// 4. 重新加载全文
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
|
||||
// 5. 初始化阅读追踪
|
||||
if (canAccess) {
|
||||
@@ -1013,10 +880,7 @@ Page({
|
||||
// 更新全局购买状态
|
||||
app.globalData.hasFullBook = res.data.hasFullBook
|
||||
app.globalData.purchasedSections = res.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
|
||||
|
||||
// 更新用户信息中的购买记录
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = res.data.hasFullBook
|
||||
@@ -1026,8 +890,7 @@ Page({
|
||||
|
||||
console.log('[Pay] ✅ 购买状态已刷新:', {
|
||||
hasFullBook: res.data.hasFullBook,
|
||||
purchasedCount: res.data.purchasedSections.length,
|
||||
matchCount: res.data.matchCount
|
||||
purchasedCount: res.data.purchasedSections.length
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1051,19 +914,17 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到上一篇
|
||||
goToPrev() {
|
||||
const s = this.data.prevSection
|
||||
if (s) {
|
||||
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
|
||||
wx.redirectTo({ url: `/pages/read/read?${q}` })
|
||||
if (this.data.prevSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到下一篇
|
||||
goToNext() {
|
||||
const s = this.data.nextSection
|
||||
if (s) {
|
||||
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
|
||||
wx.redirectTo({ url: `/pages/read/read?${q}` })
|
||||
if (this.data.nextSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1072,120 +933,134 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
|
||||
// 生成海报
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
const safeParagraphs = contentParagraphs || []
|
||||
|
||||
// 与 utils/scene 闭环:生成 scene 用 buildScene,扫码后用 parseScene 解析
|
||||
let qrcodeTempPath = null
|
||||
try {
|
||||
const refVal = userId ? String(userId).slice(0, 12) : ''
|
||||
const scene = buildScene({
|
||||
mid: sectionMid || undefined,
|
||||
id: sectionMid ? undefined : (sectionId || ''),
|
||||
ref: refVal || undefined
|
||||
})
|
||||
const baseUrl = app.globalData.baseUrl || ''
|
||||
const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
|
||||
qrcodeTempPath = await new Promise((resolve) => {
|
||||
wx.downloadFile({
|
||||
url,
|
||||
success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null),
|
||||
fail: () => resolve(null)
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数)
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
|
||||
const qrRes = await app.request('/api/miniprogram/qrcode', {
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
// 内容摘要
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
y += 30
|
||||
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
|
||||
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
// 底部区域背景
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
// 绘制小程序码或占位符
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeImage) {
|
||||
// 下载base64图片并绘制
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
|
||||
const doDraw = () => {
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
y += 30
|
||||
const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150)
|
||||
const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事'
|
||||
const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeTempPath) {
|
||||
ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70)
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
drawQRCode().then(() => {
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(doDraw, 400)
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
@@ -1223,21 +1098,11 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册:画布 300x450 兼容 iOS,导出 2 倍 600x900 提升清晰度(宽高比 2:3 不变)
|
||||
// 保存海报到相册
|
||||
savePoster() {
|
||||
const width = 300
|
||||
const height = 450
|
||||
const exportScale = 2
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
destWidth: width * exportScale,
|
||||
destHeight: height * exportScale,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
if (!res.tempFilePath) {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
@@ -1245,25 +1110,25 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
|
||||
if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (sres) => {
|
||||
if (sres.confirm) wx.openSetting()
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[savePoster] canvasToTempFilePath fail:', err)
|
||||
wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
},
|
||||
@@ -1286,33 +1151,9 @@ Page({
|
||||
wx.showLoading({ title: '重试中...', mask: true })
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [config, purchaseRes] = await Promise.all([
|
||||
accessManager.fetchLatestConfig(),
|
||||
userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
|
||||
])
|
||||
const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
|
||||
const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
|
||||
const userDiscount = config.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displaySectionPrice = hasReferralDiscount
|
||||
? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: sectionPrice
|
||||
const displayFullBookPrice = hasReferralDiscount
|
||||
? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: fullBookPrice
|
||||
this.setData({
|
||||
freeIds: config.freeChapters,
|
||||
sectionPrice,
|
||||
fullBookPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displaySectionPrice,
|
||||
displayFullBookPrice,
|
||||
purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
|
||||
})
|
||||
// 重新拉取配置
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
this.setData({ freeIds: config.freeChapters })
|
||||
|
||||
// 重新判断权限
|
||||
const newAccessState = await accessManager.determineAccessState(
|
||||
@@ -1328,7 +1169,7 @@ Page({
|
||||
})
|
||||
|
||||
// 重新加载内容
|
||||
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
|
||||
await this.loadContent(this.data.sectionId, newAccessState)
|
||||
|
||||
// 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
|
||||
@@ -166,16 +166,7 @@
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<view class="btn-price-row" wx:if="{{hasReferralDiscount}}">
|
||||
<text class="btn-original-price">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price brand-color">¥{{displaySectionPrice}}</text>
|
||||
<text class="btn-discount-tag">省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<view class="btn-price-row" wx:elif="{{showDiscountHint}}">
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-discount-tag">好友链接立省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<text wx:else class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
@@ -185,9 +176,8 @@
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-original-price" wx:if="{{hasReferralDiscount}}">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-price">¥{{hasReferralDiscount ? displayFullBookPrice : (fullBookPrice || 9.9)}}</text>
|
||||
<text class="btn-discount">{{hasReferralDiscount ? '省' + userDiscount + '%' : '省82%'}}</text>
|
||||
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -256,7 +246,7 @@
|
||||
<view class="modal-close" bindtap="closePosterModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报预览:画布 300x450 避免 iOS transform 裁切;保存时导出 2 倍分辨率 -->
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
@@ -307,7 +297,4 @@
|
||||
<button class="fab-share" open-type="share">
|
||||
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</button>
|
||||
|
||||
<!-- 分享卡片用 canvas(离屏绘制,用于生成分享图) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
</view>
|
||||
|
||||
@@ -32,8 +32,7 @@ Page({
|
||||
pendingEarnings: 0, // 待结算收益(保留兼容)
|
||||
shareRate: 90, // 分成比例(90%),从 referral/data 或 config 获取
|
||||
minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取
|
||||
withdrawFee: 5, // 提现手续费%,从 referral/data 获取
|
||||
bindingDays: 30, // 绑定期天数,从 referral/data 获取
|
||||
bindingDays: 30, // 绑定期天数,从 referral/data 获取
|
||||
userDiscount: 5, // 好友购买优惠%,从 referral/data 获取
|
||||
hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置)
|
||||
|
||||
@@ -60,7 +59,6 @@ Page({
|
||||
showPosterModal: false,
|
||||
isGeneratingPoster: false,
|
||||
posterQrSrc: '',
|
||||
posterQrFilePath: '',
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
@@ -200,7 +198,6 @@ Page({
|
||||
pendingEarnings: formatMoney(realData?.pendingEarnings || 0),
|
||||
shareRate: realData?.shareRate ?? 90,
|
||||
minWithdrawAmount: minWithdrawAmount,
|
||||
withdrawFee: realData?.withdrawFee ?? 5,
|
||||
bindingDays: realData?.bindingDays ?? 30,
|
||||
userDiscount: realData?.userDiscount ?? 5,
|
||||
|
||||
@@ -286,6 +283,53 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
|
||||
shareToWechat() {
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
// 与 Next.js 完全相同的文案
|
||||
const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》
|
||||
|
||||
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事,55个真实案例,讲透创业的底层逻辑。
|
||||
|
||||
👉 点击阅读: ${referralLink}
|
||||
|
||||
#创业 #商业思维 #Soul派对`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '朋友圈文案已复制!',
|
||||
content: '打开微信 → 发朋友圈 → 粘贴即可',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
|
||||
handleMoreShare() {
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
// 与 Next.js 完全相同的文案
|
||||
const shareText = `我正在读《一场SOUL的创业实验场》,每天6-9点的真实商业故事,推荐给你!${referralLink}`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '分享文案已复制',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 生成推广海报 - 1:1 对齐 Next.js 设计
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...', mask: true })
|
||||
@@ -308,18 +352,15 @@ Page({
|
||||
},
|
||||
})
|
||||
|
||||
// 接口返回 { success, image: "data:image/png;base64,...", scene }
|
||||
const imageData = res?.image || res?.data?.image
|
||||
if (!res || !res.success || !imageData) {
|
||||
if (!res || !res.success || !res.image) {
|
||||
console.error('[Poster] 生成小程序码失败:', res)
|
||||
throw new Error(res?.error || res?.message || '生成小程序码失败')
|
||||
throw new Error(res?.error || '生成小程序码失败')
|
||||
}
|
||||
|
||||
// 小程序 image 组件支持 base64 格式,直接使用;同时写入本地供预览用
|
||||
const base64Str = String(imageData).trim()
|
||||
// 后端返回的是 data:image/png;base64,... 需要先写入本地临时文件,再作为 <image> 的 src
|
||||
const base64Data = String(res.image).replace(/^data:image\/\w+;base64,/, '')
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/poster_qrcode_${Date.now()}.png`
|
||||
const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
fs.writeFile({
|
||||
@@ -334,10 +375,10 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
// 优先用 base64 直接显示(兼容性更好);预览时用本地路径
|
||||
console.log('[Poster] 小程序码已保存到本地:', filePath)
|
||||
|
||||
this.setData({
|
||||
posterQrSrc: base64Str,
|
||||
posterQrFilePath: filePath,
|
||||
posterQrSrc: filePath,
|
||||
posterReferralLink: '', // 小程序版本不再使用 H5 链接
|
||||
posterNickname: nickname,
|
||||
posterNicknameInitial: (nickname || '用').charAt(0),
|
||||
@@ -348,7 +389,7 @@ Page({
|
||||
console.error('[Poster] 生成二维码失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterQrFilePath: '', posterReferralLink: '' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -529,15 +570,56 @@ Page({
|
||||
|
||||
// 预览二维码
|
||||
previewPosterQr() {
|
||||
const { posterQrSrc, posterQrFilePath } = this.data
|
||||
const url = posterQrFilePath || posterQrSrc
|
||||
if (!url) return
|
||||
wx.previewImage({ urls: [url] })
|
||||
const { posterQrSrc } = this.data
|
||||
if (!posterQrSrc) return
|
||||
wx.previewImage({ urls: [posterQrSrc] })
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
// 分享到朋友圈 - 随机文案
|
||||
shareToMoments() {
|
||||
// 10条随机文案,基于书的内容
|
||||
const shareTexts = [
|
||||
`🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
|
||||
|
||||
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
|
||||
|
||||
`📚 一个70后大健康私域,一个月150万流水是怎么做到的?\n\n答案在《Soul创业派对》第9章,全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
|
||||
|
||||
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》,创业者必读!\n\n#云阿米巴 #商业思维 #创业`,
|
||||
|
||||
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》,每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
|
||||
|
||||
`🚀 AI工具推广:一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会,你发现了吗?\n\n#AI #副业 #商业机会`,
|
||||
|
||||
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案!\n\n#美业 #轻创业 #月入十万`,
|
||||
|
||||
`🌟 3000万流水是怎么跑出来的?\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑,值得反复看\n\n#电商 #创业 #商业系统`,
|
||||
|
||||
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句,都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`,
|
||||
|
||||
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案!\n\n#职业规划 #创业 #未来`
|
||||
]
|
||||
|
||||
// 随机选择一条文案
|
||||
const randomIndex = Math.floor(Math.random() * shareTexts.length)
|
||||
const shareText = shareTexts[randomIndex]
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请打开微信朋友圈,粘贴分享文案,配合推广海报一起发布效果更佳!\n\n再次点击可获取新的随机文案',
|
||||
showCancel: false,
|
||||
confirmText: '去发朋友圈'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
const availableEarnings = this.data.availableEarningsNum || 0
|
||||
@@ -548,10 +630,7 @@ Page({
|
||||
wx.showToast({ title: '暂无可提现收益', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (availableEarnings < minWithdrawAmount) {
|
||||
wx.showToast({ title: `满${minWithdrawAmount}元可提现`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 任意金额可提现,不再设最低限额
|
||||
|
||||
// 未绑定微信号时引导去设置
|
||||
if (!hasWechatId) {
|
||||
@@ -567,16 +646,9 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const withdrawFee = this.data.withdrawFee ?? 5
|
||||
const actualAmount = withdrawFee > 0
|
||||
? Math.round(availableEarnings * (1 - withdrawFee / 100) * 100) / 100
|
||||
: availableEarnings
|
||||
const feeText = withdrawFee > 0
|
||||
? `\n扣除 ${withdrawFee}% 手续费后,实际到账 ¥${actualAmount.toFixed(2)}`
|
||||
: ''
|
||||
wx.showModal({
|
||||
title: '确认提现',
|
||||
content: `申请提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱${feeText}`,
|
||||
content: `将提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱`,
|
||||
confirmText: '立即提现',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
@@ -805,16 +877,27 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 分享 - 带自己的邀请码(与 app.getMyReferralCode 一致)
|
||||
// 分享 - 带推荐码
|
||||
onShareAppMessage() {
|
||||
const app = getApp()
|
||||
const ref = app.getMyReferralCode() || this.data.referralCode
|
||||
console.log('[Referral] 分享给好友,推荐码:', this.data.referralCode)
|
||||
return {
|
||||
title: 'Soul创业派对 - 来自派对房的真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
path: `/pages/index/index?ref=${this.data.referralCode}`
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
// 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
console.log('[Referral] 分享到朋友圈,推荐码:', this.data.referralCode)
|
||||
return {
|
||||
title: `Soul创业派对 - 62个真实商业案例`,
|
||||
query: `ref=${this.data.referralCode}`
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<image class="nav-icon" src="/assets/icons/chevron-left.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="nav-btn" bindtap="showNotification">
|
||||
<image class="nav-icon" src="/assets/icons/bell.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<text class="nav-title">分销中心</text>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
@@ -46,10 +43,9 @@
|
||||
<text class="pending-text">累计: ¥{{totalCommission}} | 待审核: ¥{{pendingWithdrawAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="withdraw-btn {{availableEarningsNum < minWithdrawAmount || !hasWechatId ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
|
||||
{{availableEarningsNum < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
|
||||
<view class="withdraw-btn {{availableEarningsNum <= 0 || !hasWechatId ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
|
||||
{{availableEarningsNum <= 0 ? '暂无可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
|
||||
</view>
|
||||
<text class="withdraw-fee-tip" wx:if="{{withdrawFee > 0}}">提现将扣除 {{withdrawFee}}% 手续费</text>
|
||||
<text class="wechat-tip" wx:if="{{availableEarningsNum > 0 && !hasWechatId}}">为便于提现到账,请先到「设置」中绑定微信号</text>
|
||||
<view class="withdraw-records-link" bindtap="goToWithdrawRecords">查看提现记录</view>
|
||||
</view>
|
||||
@@ -177,6 +173,28 @@
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<view class="share-item" bindtap="shareToWechat">
|
||||
<view class="share-icon wechat">
|
||||
<image class="icon-share-btn" src="/assets/icons/message-circle.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="share-info">
|
||||
<text class="share-title">分享到朋友圈</text>
|
||||
<text class="share-desc">复制文案发朋友圈</text>
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<view class="share-item" bindtap="handleMoreShare">
|
||||
<view class="share-icon link">
|
||||
<image class="icon-share-btn" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="share-info">
|
||||
<text class="share-title">更多分享方式</text>
|
||||
<text class="share-desc">使用系统分享功能</text>
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益明细 - 增强版 -->
|
||||
@@ -215,7 +233,14 @@
|
||||
</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>
|
||||
|
||||
<!-- 海报生成弹窗 - 优化小程序显示 -->
|
||||
@@ -284,7 +309,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<view class="poster-qr-wrap">
|
||||
<view class="poster-qr-wrap" bindtap="previewPosterQr">
|
||||
<image
|
||||
class="poster-qr-img"
|
||||
src="{{posterQrSrc}}"
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
|
||||
.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; }
|
||||
.withdraw-fee-tip { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; text-align: center; }
|
||||
.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; }
|
||||
|
||||
@@ -237,85 +236,12 @@
|
||||
|
||||
| ||||