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:
2026-02-24 14:52:25 +08:00
33 changed files with 1967 additions and 1810 deletions

View File

@@ -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'
}
}
})

View File

@@ -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'
}
}
})

View File

@@ -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'
}
}
})

View File

@@ -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: '附录1Soul派对房精选对话' },
{ 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'
}
}
})

View File

@@ -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;

View File

@@ -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'
}
}
})

View File

@@ -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>

View File

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

View File

@@ -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() {}
})

View File

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

View File

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

View File

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

View File

@@ -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() {}
})

View File

@@ -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>

View File

@@ -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; }

View File

@@ -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'
}
}
})

View File

@@ -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() }
})

View File

@@ -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/idoptions:', 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
})
// 先拉取章节获取 idmid 时必需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) {

View File

@@ -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>

View File

@@ -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()
},

View File

@@ -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}}"

View File

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

View File

@@ -45,7 +45,7 @@ Page({
if (isLoggedIn && userInfo) {
// 从本地存储或用户信息中获取绑定数据
const phoneNumber = wx.getStorageSync('user_phone') || userInfo.phone || ''
const wechatId = wx.getStorageSync('user_wechat') || userInfo.wechatId || userInfo.wechat || ''
const wechatId = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
const alipayAccount = wx.getStorageSync('user_alipay') || userInfo.alipay || ''
const address = wx.getStorageSync('user_address') || userInfo.address || ''
// 默认开启自动提现
@@ -63,8 +63,67 @@ Page({
}
},
// 收货地址已改为「地址管理」页goToAddresses
// 一键获取收货地址
getAddress() {
wx.chooseAddress({
success: (res) => {
console.log('[Settings] 获取地址成功:', res)
const fullAddress = `${res.provinceName || ''}${res.cityName || ''}${res.countyName || ''}${res.detailInfo || ''}`
if (fullAddress.trim()) {
wx.setStorageSync('user_address', fullAddress)
this.setData({ address: fullAddress })
// 更新用户信息
if (app.globalData.userInfo) {
app.globalData.userInfo.address = fullAddress
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
// 同步到服务器
this.syncAddressToServer(fullAddress)
wx.showToast({ title: '地址已获取', icon: 'success' })
}
},
fail: (e) => {
console.log('[Settings] 获取地址失败:', e)
if (e.errMsg?.includes('cancel')) {
// 用户取消,不提示
return
}
if (e.errMsg?.includes('auth deny') || e.errMsg?.includes('authorize')) {
wx.showModal({
title: '需要授权',
content: '请在设置中允许获取收货地址',
confirmText: '去设置',
success: (res) => {
if (res.confirm) wx.openSetting()
}
})
} else {
wx.showToast({ title: '获取失败,请重试', icon: 'none' })
}
}
})
},
// 同步地址到服务器
async syncAddressToServer(address) {
try {
const userId = app.globalData.userInfo?.id
if (!userId) return
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId, address }
})
console.log('[Settings] 地址已同步到服务器')
} catch (e) {
console.log('[Settings] 同步地址失败:', e)
}
},
// 切换自动提现
async toggleAutoWithdraw(e) {
const enabled = e.detail.value
@@ -434,13 +493,5 @@ Page({
// 跳转到地址管理页
goToAddresses() {
wx.navigateTo({ url: '/pages/addresses/addresses' })
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 设置',
path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
}
}
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -119,13 +119,5 @@ Page({
wx.hideLoading()
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
}
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 提现记录',
path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
}
}
})