优化个人中心页面,调整导航栏布局以避让右上角胶囊,增强用户体验。更新匹配功能逻辑,增加未开放提示,确保用户在使用时获得明确反馈。

This commit is contained in:
Alex-larget
2026-03-06 12:12:13 +08:00
parent 7e3d36d67f
commit 3b193fb5a8
72 changed files with 1970 additions and 2987 deletions

View File

@@ -54,7 +54,11 @@ Page({
partCount: 0,
// 加载状态
loading: true
loading: true,
// 链接卡若 - 留资弹窗
showLeadModal: false,
leadPhone: ''
},
onLoad(options) {
@@ -124,8 +128,8 @@ Page({
// 不再过滤无头像用户,无头像时用首字母展示
members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({
id: u.id,
name: u.vipName || u.vip_name || u.nickname || '会员',
avatar: u.vipAvatar || u.vip_avatar || u.avatar || '',
name: u.nickname || u.vipName || u.vip_name || '会员', // 超级个体:用户资料优先,随「我的」修改实时生效
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) {
@@ -301,6 +305,113 @@ Page({
wx.navigateTo({ url: '/pages/about/about' })
},
async onLinkKaruo() {
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再链接卡若',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
}
})
return
}
const userId = app.globalData.userInfo.id
const leadKey = 'karuo_lead_' + userId
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
if (!phone && !wechatId) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || '').trim()
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
}
} catch (e) {}
}
if (phone || wechatId) {
const hasLead = wx.getStorageSync(leadKey)
if (hasLead) {
wx.showToast({ title: '已提交联系方式,卡若会尽快联系你', icon: 'none' })
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync(leadKey, true)
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
}
return
}
this.setData({ showLeadModal: true, leadPhone: '' })
},
closeLeadModal() {
this.setData({ showLeadModal: false, leadPhone: '' })
},
onLeadPhoneInput(e) {
this.setData({ leadPhone: (e.detail.value || '').trim() })
},
async submitLead() {
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
if (!phone) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (phone.length < 11) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
const leadKey = userId ? ('karuo_lead_' + userId) : ''
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId,
phone,
name: (app.globalData.userInfo?.nickname || '').trim() || undefined
}
})
wx.hideLoading()
this.setData({ showLeadModal: false, leadPhone: '' })
if (res && res.success) {
if (leadKey) wx.setStorageSync(leadKey, true)
wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
}
},
goToSuperList() {
wx.switchTab({ url: '/pages/match/match' })
},
@@ -316,8 +427,22 @@ Page({
if (candidates.length === 0) {
candidates = chapters.filter(exclude)
}
// 解析「第X场」用于倒序最新场次大放在最上方
const sessionNum = (c) => {
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
const m = title.match(/第\s*(\d+)\s*场/) || title.match(/第(\d+)场/)
if (m) return parseInt(m[1], 10)
const id = c.id != null ? String(c.id) : ''
if (/^\d+$/.test(id)) return parseInt(id, 10)
return 0
}
const latest = candidates
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
.sort((a, b) => {
const na = sessionNum(a)
const nb = sessionNum(b)
if (na !== nb) return nb - na // 场次倒序:最新在上
return 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())

View File

@@ -17,7 +17,7 @@
</view>
</view>
<view class="header-right">
<view class="contact-btn" bindtap="goToAbout">
<view class="contact-btn" bindtap="onLinkKaruo">
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
<text class="contact-text">点击链接卡若</text>
</view>
@@ -186,4 +186,17 @@
<!-- 底部留白 -->
<view class="bottom-space"></view>
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时) -->
<view class="lead-mask" wx:if="{{showLeadModal}}" catchtap="closeLeadModal">
<view class="lead-box" catchtap="">
<text class="lead-title">留下联系方式</text>
<text class="lead-desc">方便卡若与您联系</text>
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
<view class="lead-actions">
<button class="lead-btn lead-btn-cancel" bindtap="closeLeadModal">取消</button>
<button class="lead-btn lead-btn-submit" bindtap="submitLead">提交</button>
</view>
</view>
</view>
</view>

View File

@@ -842,3 +842,73 @@
.bottom-space {
height: 40rpx;
}
/* ===== 链接卡若 - 留资弹窗 ===== */
.lead-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
}
.lead-box {
width: 100%;
max-width: 560rpx;
background: #1C1C1E;
border-radius: 24rpx;
padding: 48rpx 40rpx;
border: 2rpx solid rgba(56, 189, 172, 0.3);
}
.lead-title {
display: block;
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 12rpx;
}
.lead-desc {
display: block;
font-size: 26rpx;
color: #A0AEC0;
margin-bottom: 32rpx;
}
.lead-input {
width: 100%;
height: 88rpx;
background: #0a1628;
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 0 24rpx;
box-sizing: border-box;
font-size: 30rpx;
color: #ffffff;
margin-bottom: 32rpx;
}
.lead-actions {
display: flex;
gap: 24rpx;
}
.lead-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
}
.lead-btn-cancel {
background: rgba(255, 255, 255, 0.1);
color: #A0AEC0;
}
.lead-btn-submit {
background: #38bdac;
color: #ffffff;
}

View File

@@ -14,7 +14,7 @@ const app = getApp()
let MATCH_TYPES = [
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '立即咨询', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: '🎮', matchFromDB: true, showJoinAfterMatch: true }
]
@@ -108,8 +108,15 @@ Page({
})
if (res.success && res.data) {
// 更新全局配置
MATCH_TYPES = res.data.matchTypes || MATCH_TYPES
// 更新全局配置,导师顾问类型强制显示「导师顾问」
let types = res.data.matchTypes || MATCH_TYPES
types = types.map(t => {
if (t.id === 'mentor') {
return { ...t, label: '导师顾问', matchLabel: '导师顾问' }
}
return t
})
MATCH_TYPES = types
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
const matchPrice = res.data.matchPrice || 1
@@ -459,7 +466,7 @@ Page({
// 生成模拟匹配数据
generateMockMatch() {
const nicknames = ['创业先锋', '资源整合者', '私域专家', '商业导师', '连续创业者']
const nicknames = ['创业先锋', '资源整合者', '私域专家', '导师顾问', '连续创业者']
const concepts = [
'专注私域流量运营5年帮助100+品牌实现从0到1的增长。',
'连续创业者,擅长商业模式设计和资源整合。',

View File

@@ -1,7 +1,7 @@
/**
* Soul创业派对 - 超级个体/会员详情页
* 接口:优先 /api/miniprogram/vip/members?id=xxVIP回退 /api/miniprogram/users?id=xx任意用户
* 字段映射name/vipName, avatar/vipAvatar, contact/vipContact/phone, wechatId, project/vipProject/projectIntro,
* 头像/昵称统一用用户资料nickname/avatar优先随「我的」修改实时生效
* mbti, region, industry, position, businessScale, skills,
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
* helpOffer→canHelp, helpNeed→needHelp
@@ -32,8 +32,8 @@ Page({
const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data
if (u) {
this.setData({ member: this.enrichAndFormat({
id: u.id, name: u.vipName || u.vip_name || u.nickname || '创业者',
avatar: u.vipAvatar || u.vip_avatar || u.avatar || '', isVip: !!(u.is_vip),
id: u.id, name: u.nickname || u.vipName || u.vip_name || '创业者',
avatar: u.avatar || u.vipAvatar || u.vip_avatar || '', isVip: !!(u.is_vip),
contactRaw: u.vipContact || u.vip_contact || u.phone || '',
wechatId: u.wechatId || u.wechat_id,
project: u.vipProject || u.vip_project || u.projectIntro || u.project_intro || '',
@@ -63,7 +63,7 @@ Page({
const e = (v) => this._emptyIfPlaceholder(v)
const merged = {
id: raw.id,
name: raw.name || raw.vipName || raw.vip_name || raw.nickname || '创业者',
name: raw.nickname || raw.name || raw.vipName || raw.vip_name || '创业者',
avatar: raw.avatar || raw.vipAvatar || raw.vip_avatar || '',
isVip: !!(raw.isVip || raw.is_vip),
mbti: e(raw.mbti),
@@ -85,20 +85,84 @@ Page({
const contact = merged.contactRaw || ''
const wechat = merged.wechatRaw || ''
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
const unlockData = this._getUnlockData(merged.id)
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
merged.contactUnlocked = isMatched
merged.contactUnlocked = isMatched || unlockData.contact
merged.contactFull = contact
merged.wechatDisplay = wechat ? (wechat.slice(0, 4) + '****' + (wechat.length > 8 ? wechat.slice(-3) : '')) : ''
merged.wechatUnlocked = isMatched
merged.wechatUnlocked = isMatched || unlockData.wechat
merged.wechatFull = wechat
return merged
},
unlockContact() {
_getUnlockData(memberId) {
const userId = app.globalData.userInfo?.id
if (!userId || !memberId) return { contact: false, wechat: false }
try {
const raw = wx.getStorageSync('member_unlocks_' + userId)
if (Array.isArray(raw) && raw.includes(memberId)) {
return { contact: true, wechat: true }
}
const data = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
const member = data[memberId]
return {
contact: !!(member && member.contact),
wechat: !!(member && member.wechat)
}
} catch (e) { return { contact: false, wechat: false } }
},
_addUnlock(memberId, field) {
const userId = app.globalData.userInfo?.id
if (!userId || !memberId || !field) return
let obj = wx.getStorageSync('member_unlocks_' + userId)
if (Array.isArray(obj)) {
obj = obj.reduce((o, id) => { o[id] = { contact: true, wechat: true }; return o }, {})
}
obj = obj && typeof obj === 'object' ? obj : {}
if (!obj[memberId]) obj[memberId] = {}
obj[memberId][field] = true
wx.setStorageSync('member_unlocks_' + userId, obj)
},
_hasUsedFreeForMember(memberId) {
const d = this._getUnlockData(memberId)
return d.contact || d.wechat
},
unlockField(e) {
const field = e.currentTarget.dataset.field
if (!field) return
const member = this.data.member
if (!member?.id || (field !== 'contact' && field !== 'wechat')) return
const isLoggedIn = app.globalData.isLoggedIn
if (!isLoggedIn) {
wx.showModal({
title: '需要登录',
content: '请先登录后再解锁超级个体联系方式',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const d = this._getUnlockData(member.id)
if (d[field]) return
const isVip = app.globalData.hasFullBook
const usedFree = this._hasUsedFreeForMember(member.id)
if (isVip || !usedFree) {
this._addUnlock(member.id, field)
const m = this.enrichAndFormat(member)
this.setData({ member: m })
wx.showToast({ title: field === 'contact' ? '已解锁联系方式' : '已解锁微信号', icon: 'success' })
return
}
wx.showModal({
title: '解锁完整联系方式', content: '成为VIP会员并完成匹配后即可查看完整联系方式',
confirmText: '去匹配', cancelText: '知道了',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/match/match' }) }
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
content: '您的免费解锁次数已用完开通VIP会员¥1980/年)可无限解锁',
confirmText: '去开通',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
})
},

View File

@@ -57,8 +57,8 @@
<view class="field" wx:if="{{member.contactRaw || member.contactDisplay}}">
<text class="f-key">联系方式</text>
<view class="f-row">
<text class="f-val mono">{{member.contactDisplay || member.contactRaw}}</text>
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockContact">
<text class="f-val mono">{{member.contactUnlocked ? member.contactFull : (member.contactDisplay || member.contactRaw)}}</text>
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockField" data-field="contact">
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
</view>
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact">📋</view>
@@ -67,8 +67,8 @@
<view class="field" wx:if="{{member.wechatRaw || member.wechatDisplay}}">
<text class="f-key">微信号</text>
<view class="f-row">
<text class="f-val mono">{{member.wechatDisplay || member.wechatRaw}}</text>
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockContact">
<text class="f-val mono">{{member.wechatUnlocked ? member.wechatFull : (member.wechatDisplay || member.wechatRaw)}}</text>
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockField" data-field="wechat">
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
</view>
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat">📋</view>

View File

@@ -621,7 +621,7 @@ Page({
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到推广中心
// 跳转到推广中心(需登录)
goToReferral() {
if (!this.data.isLoggedIn) {
this.showLogin()
@@ -630,7 +630,7 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 跳转到找伙伴页面
// 跳转到找伙伴
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
@@ -650,14 +650,20 @@ Page({
})
},
// VIP状态查询
// VIP状态查询hasFullBook 优先,兼容模拟支付等本地已置 VIP 的情况)
async loadVipStatus() {
if (app.globalData.hasFullBook) {
this.setData({ isVip: true, vipExpireDate: this.data.vipExpireDate || '' })
}
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true })
if (res?.success) {
this.setData({ isVip: res.data?.isVip, vipExpireDate: res.data?.expireDate || '' })
this.setData({
isVip: res.data?.isVip || app.globalData.hasFullBook,
vipExpireDate: res.data?.expireDate || this.data.vipExpireDate || ''
})
}
} catch (e) { console.log('[My] VIP查询失败', e) }
},

View File

@@ -1,11 +1,11 @@
<!-- 我的页 - 设计稿 1:1 还原 -->
<view class="page">
<!-- 顶部导航:标题 + 右侧设置齿轮 -->
<!-- 顶部导航:左侧资料编辑 + 标题 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<text class="nav-title">我的</text>
<view class="nav-settings" bindtap="handleMenuTap" data-id="settings">
<image class="nav-settings-icon" src="/assets/icons/settings-gray.svg" mode="aspectFit"/>
<view class="nav-settings" bindtap="goToProfileEdit">
<image class="nav-settings-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
</view>
<text class="nav-title">我的</text>
</view>
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
@@ -38,22 +38,22 @@
</view>
<view class="vip-tags">
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">匹配</text>
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToMatch">匹配</text>
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</text>
</view>
<text class="user-wechat" bindtap="copyUserId">微信号: {{userWechat || userIdShort || '--'}}</text>
</view>
</view>
<view class="profile-stats-row">
<view class="profile-stat">
<view class="profile-stat" bindtap="goToChapters">
<text class="profile-stat-val">{{readCount}}</text>
<text class="profile-stat-label">已读章节</text>
</view>
<view class="profile-stat">
<view class="profile-stat" bindtap="goToReferral">
<text class="profile-stat-val">{{referralCount}}</text>
<text class="profile-stat-label">推荐好友</text>
</view>
<view class="profile-stat">
<view class="profile-stat" bindtap="goToReferral">
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
<text class="profile-stat-label">我的收益</text>
</view>
@@ -70,17 +70,17 @@
<text class="card-title">阅读统计</text>
</view>
<view class="stats-grid">
<view class="stat-box">
<view class="stat-box" bindtap="goToChapters">
<image class="stat-icon-img" src="/assets/icons/book-open-teal.svg" mode="aspectFit"/>
<text class="stat-num">{{readCount}}</text>
<text class="stat-label">已读章节</text>
</view>
<view class="stat-box">
<view class="stat-box" bindtap="goToChapters">
<image class="stat-icon-img" src="/assets/icons/clock-teal.svg" mode="aspectFit"/>
<text class="stat-num">{{totalReadTime}}</text>
<text class="stat-label">阅读分钟</text>
</view>
<view class="stat-box">
<view class="stat-box" bindtap="goToMatch">
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
<text class="stat-num">{{matchHistory}}</text>
<text class="stat-label">匹配伙伴</text>
@@ -208,7 +208,9 @@
<text class="modal-title">修改昵称</text>
</view>
<view class="nickname-input-wrap">
<input class="nickname-input" type="nickname" value="{{editingNickname}}" placeholder="点击输入昵称" placeholder-class="nickname-placeholder" bindchange="onNicknameChange" bindinput="onNicknameInput" maxlength="20"/>
<view class="nickname-input-inner">
<input class="nickname-input" type="nickname" value="{{editingNickname}}" placeholder="点击输入昵称" placeholder-class="nickname-placeholder" bindchange="onNicknameChange" bindinput="onNicknameInput" maxlength="20"/>
</view>
<text class="input-tip">微信用户可点击自动填充昵称</text>
</view>
<view class="modal-actions">

View File

@@ -9,7 +9,7 @@
padding-bottom: calc(220rpx + env(safe-area-inset-bottom, 0px));
}
/* ===== 导航栏(避让右上角系统胶囊) ===== */
/* ===== 导航栏(左侧设置 + 标题居中,右侧避让胶囊) ===== */
.nav-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: rgba(18,18,18,0.9); backdrop-filter: blur(8rpx);
@@ -18,8 +18,8 @@
padding: 0 120rpx 0 32rpx; /* 右侧避让胶囊 */
border-bottom: 1rpx solid rgba(255,255,255,0.05);
}
.nav-title { font-size: 40rpx; font-weight: bold; color: #4FD1C5; flex: 1; }
.nav-settings { width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; }
.nav-settings { width: 64rpx; height: 64rpx; flex-shrink: 0; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; }
.nav-title { font-size: 40rpx; font-weight: bold; color: #4FD1C5; flex: 1; text-align: center; }
.nav-settings-icon { width: 44rpx; height: 44rpx; opacity: 0.7; }
.nav-placeholder { width: 100%; }
@@ -217,7 +217,11 @@
.nickname-modal .modal-icon { font-size: 48rpx; }
.nickname-modal .modal-title { font-size: 36rpx; font-weight: bold; }
.nickname-input-wrap { margin-bottom: 32rpx; }
.nickname-input { width: 100%; padding: 24rpx; background: #262626; border-radius: 20rpx; font-size: 28rpx; color: #fff; box-sizing: border-box; }
.nickname-input-inner {
padding: 24rpx 32rpx; background: #262626; border-radius: 20rpx;
}
.nickname-input { width: 100%; font-size: 28rpx; color: #fff; background: transparent; box-sizing: border-box; }
.nickname-placeholder { color: #9CA3AF; }
.input-tip { display: block; font-size: 22rpx; color: #9CA3AF; margin-top: 12rpx; }
.modal-actions { display: flex; gap: 24rpx; }
.modal-btn { flex: 1; height: 80rpx; line-height: 80rpx; text-align: center; border-radius: 20rpx; font-size: 28rpx; }

View File

@@ -18,8 +18,10 @@
<!-- 头像 -->
<view class="avatar-section">
<view class="avatar-wrap" bindtap="chooseAvatar">
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
<view class="avatar-inner">
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
</view>
<view class="avatar-camera">📷</view>
</view>
<text class="avatar-change">更换头像</text>

View File

@@ -31,18 +31,22 @@
.avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 48rpx; }
.avatar-wrap {
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%; overflow: hidden;
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%;
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
}
.avatar-inner {
width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
}
.avatar-img { width: 100%; height: 100%; display: block; }
.avatar-placeholder {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
font-size: 72rpx; font-weight: bold; color: #5EEAD4; background: rgba(94,234,212,0.2);
}
.avatar-camera {
position: absolute; bottom: 0; right: 0;
position: absolute; bottom: -8rpx; right: -8rpx;
width: 56rpx; height: 56rpx; background: #5EEAD4; color: #000;
border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx;
border: 4rpx solid #050B14; box-sizing: border-box;
}
.avatar-change { font-size: 28rpx; color: #5EEAD4; font-weight: 500; margin-top: 16rpx; }

View File

@@ -81,6 +81,8 @@ Page({
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
console.log("页面:",mid);
// mid 有值但无 id 时,从 bookData 或 API 解析 id
if (mid && !id) {
const bookData = app.globalData.bookData || []
@@ -126,7 +128,7 @@ Page({
})
// 统一:先拉章节数据,用 isFree/price===0 判断免费
const chapterRes = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
const chapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
const accessState = await accessManager.determineAccessState(id, chapterRes)
const canAccess = accessManager.canAccessFullContent(accessState)
@@ -196,7 +198,7 @@ Page({
const sectionPrice = this.data.sectionPrice ?? 1
let res = prefetchedChapter
if (!res || !res.content) {
res = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
res = await app.request({ url: this._getChapterUrl({ id }), silent: true })
}
const section = {
id: res.id || id,
@@ -295,6 +297,17 @@ Page({
return titles[id] || `章节 ${id}`
},
// 根据 id/mid 构造章节接口路径(优先使用 mid
_getChapterUrl(params = {}) {
const { id, mid } = params
const finalMid = (mid !== undefined && mid !== null) ? mid : this.data.sectionMid
if (finalMid) {
return `/api/miniprogram/book/chapter/by-mid/${finalMid}`
}
const finalId = id || this.data.sectionId
return `/api/miniprogram/book/chapter/${finalId}`
},
// 加载内容 - 三级降级方案API → 本地缓存 → 备用API
async loadContent(id) {
const cacheKey = `chapter_${id}`
@@ -344,7 +357,7 @@ Page({
reject(new Error('请求超时'))
}, timeout)
app.request(`/api/miniprogram/book/chapter/${id}`)
app.request(this._getChapterUrl({ id }))
.then(res => {
clearTimeout(timer)
resolve(res)
@@ -360,14 +373,24 @@ Page({
setChapterContent(res) {
const lines = res.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
const sectionPrice = this.data.sectionPrice ?? 1
const sectionTitle = (res.sectionTitle || res.title || '').trim()
this.setData({
// 文章详情标题:只使用后端提供的 sectionTitle不再拼接其他本地标题信息
section: {
id: res.id || this.data.sectionId,
title: sectionTitle,
isFree: res.isFree === true || (res.price !== undefined && res.price === 0),
price: res.price ?? sectionPrice
},
content: res.content,
previewContent: lines.slice(0, previewCount).join('\n'),
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount),
partTitle: res.partTitle || '',
chapterTitle: res.chapterTitle || ''
// 导航栏、分享等使用的文章标题,同样统一为 sectionTitle
chapterTitle: sectionTitle
})
},
@@ -504,11 +527,16 @@ Page({
}
},
// 分享到朋友圈:带文章标题,过长时截断(朋友圈卡片标题显示有限)
onShareTimeline() {
const { section, sectionId, sectionMid } = this.data
const { section, sectionId, sectionMid, chapterTitle } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
return { title: section?.title ? `📚 ${section.title}` : 'Soul创业派对 - 真实商业故事', query: ref ? `${q}&ref=${ref}` : q }
const articleTitle = (section?.title || chapterTitle || '').trim()
const title = articleTitle
? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle)
: 'Soul创业派对 - 真实商业故事'
return { title, query: ref ? `${q}&ref=${ref}` : q }
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
@@ -588,7 +616,7 @@ Page({
// 2. 重新拉取章节数据,用 isFree/price 判断免费
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
url: this._getChapterUrl({}),
silent: true
})
const newAccessState = await accessManager.determineAccessState(
@@ -859,7 +887,7 @@ Page({
// 3. 重新拉取章节并判断权限(应为 unlocked_purchased
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
url: this._getChapterUrl({}),
silent: true
})
let newAccessState = await accessManager.determineAccessState(
@@ -1201,7 +1229,7 @@ Page({
// 重新拉取章节,用 isFree/price 判断免费
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
url: this._getChapterUrl({}),
silent: true
})
const newAccessState = await accessManager.determineAccessState(

View File

@@ -13,8 +13,7 @@
<text class="back-arrow">←</text>
</view>
<view class="nav-info">
<text class="nav-part" wx:if="{{partTitle}}">{{partTitle}}</text>
<text class="nav-chapter" wx:if="{{chapterTitle}}">{{chapterTitle}}</text>
<text class="nav-chapter" wx:if="{{section.title || chapterTitle}}">{{section.title || chapterTitle}}</text>
</view>
<view class="nav-right-placeholder"></view>
</view>

View File

@@ -64,67 +64,6 @@ Page({
}
},
// 一键获取收货地址
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

View File

@@ -1,3 +1,4 @@
import accessManager from '../../utils/chapterAccessManager'
const app = getApp()
Page({
@@ -88,9 +89,9 @@ Page({
if (payRes?.success && payRes.data?.payParams) {
wx.requestPayment({
...payRes.data.payParams,
success: () => {
success: async () => {
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
this.loadVipInfo()
await this._onVipPaymentSuccess()
},
fail: () => wx.showToast({ title: '支付取消', icon: 'none' })
})
@@ -103,6 +104,28 @@ Page({
} finally { this.setData({ purchasing: false }) }
},
async _onVipPaymentSuccess() {
wx.showLoading({ title: '同步权益中...', mask: true })
try {
await new Promise(r => setTimeout(r, 1500))
await accessManager.refreshUserPurchaseStatus()
await this.loadVipInfo()
app.globalData.hasFullBook = true
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = true
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
const pages = getCurrentPages()
pages.forEach(p => {
if (typeof p.initUserStatus === 'function') p.initUserStatus()
else if (typeof p.updateUserStatus === 'function') p.updateUserStatus()
})
} catch (e) {
console.error('[VIP] 支付后同步失败:', e)
}
wx.hideLoading()
},
goBack() { wx.navigateBack() },
onShareAppMessage() {