更新小程序开发文档,新增2026-03-03的最佳实践记录,优化个人中心类页面的卡片区边距规范,确保一致性与可用性。调整相关页面以反映最新设计稿,提升用户体验与功能一致性。

This commit is contained in:
Alex-larget
2026-03-04 19:06:06 +08:00
parent 7064f82126
commit 5a5f0087d2
66 changed files with 2555 additions and 1059 deletions

View File

@@ -8,7 +8,7 @@ const { parseScene } = require('./utils/scene.js')
App({
globalData: {
// API基础地址 - 连接真实后端
baseUrl: 'https://soulapi.quwanzhi.com',
baseUrl: 'https://soulapi.quwanzhi.com',
// baseUrl: 'https://souldev.quwanzhi.com',
// baseUrl: 'http://localhost:8080',
@@ -56,7 +56,10 @@ App({
navBarHeight: 88,
// TabBar相关
currentTab: 0
currentTab: 0,
// 更新检测:上次检测时间戳,避免频繁请求
lastUpdateCheck: 0
},
onLaunch(options) {
@@ -77,9 +80,10 @@ App({
this.handleReferralCode(options)
},
// 小程序显示时也检查分享参数
// 小程序显示时:处理分享参数、检测更新(从后台切回时)
onShow(options) {
this.handleReferralCode(options)
this.checkUpdate()
},
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref与 utils/scene 解析闭环
@@ -238,21 +242,30 @@ App({
}
},
// 检查更新
/**
* 小程序更新检测(基于 wx.getUpdateManager
* - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求)
*/
checkUpdate() {
if (wx.canIUse('getUpdateManager')) {
try {
if (!wx.canIUse('getUpdateManager')) return
const now = Date.now()
const lastCheck = this.globalData.lastUpdateCheck || 0
if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测
this.globalData.lastUpdateCheck = now
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('发现新版本')
console.log('[App] 发现新版本,正在下载...')
}
})
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启应用?',
content: '新版本已准备好,重启后即可使用',
confirmText: '立即重启',
cancelText: '稍后',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
@@ -260,13 +273,15 @@ App({
}
})
})
updateManager.onUpdateFailed(() => {
wx.showToast({
title: '更新失败,请稍后重试',
icon: 'none'
icon: 'none',
duration: 2500
})
})
} catch (e) {
console.warn('[App] checkUpdate failed:', e)
}
},

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8l4 4-4 4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8v8M9 11l3-3 3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="12 6 12 12 16 14" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.576 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<path d="M14.084 14.158a3 3 0 1 1-4.242-4.242" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-4.444" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<line x1="2" y1="2" x2="22" y2="22" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="16" x2="12" y2="12" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="8" x2="12.01" y2="8" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="16" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="8" x2="12.01" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="7" r="4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

View File

@@ -7,23 +7,15 @@ const app = getApp()
Page({
data: {
statusBarHeight: 44,
authorLoading: true,
author: {
name: '卡若',
avatar: 'K',
title: 'Soul派对房主理人 · 私域运营专家',
bio: '每天早上6点到9点在Soul派对房分享真实的创业故事。专注私域运营与项目变现用"云阿米巴"模式帮助创业者构建可持续的商业体系。本书记录了62个真实商业案例涵盖电商、内容、传统行业等多个领域。',
stats: [
{ label: '商业案例', value: '62' },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
],
// 联系方式已移至后台配置
contact: null,
highlights: [
'5年私域运营经验',
'帮助100+品牌从0到1增长',
'连续创业者,擅长商业模式设计'
]
avatarImg: '/assets/images/author-avatar.png',
title: '',
bio: '',
stats: [],
highlights: []
},
bookInfo: {
title: '一场Soul的创业实验',
@@ -44,22 +36,68 @@ Page({
this.setData({
statusBarHeight: app.globalData.statusBarHeight
})
this.loadAuthor()
this.loadBookStats()
},
async loadAuthor() {
this.setData({ authorLoading: true })
try {
const res = await app.request({ url: '/api/miniprogram/about/author', silent: true })
if (res?.success && res.data) {
const d = res.data
let avatarImg = d.avatarImg || ''
if (avatarImg && !avatarImg.startsWith('http')) {
const base = (app.globalData.baseUrl || '').replace(/\/$/, '')
avatarImg = base ? base + (avatarImg.startsWith('/') ? avatarImg : '/' + avatarImg) : avatarImg
}
this.setData({
author: {
name: d.name || '卡若',
avatar: d.avatar || 'K',
avatarImg: avatarImg || '/assets/images/author-avatar.png',
title: d.title || '',
bio: d.bio || '',
stats: Array.isArray(d.stats) ? d.stats : [
{ label: '商业案例', value: '62' },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
],
highlights: Array.isArray(d.highlights) ? d.highlights : []
},
authorLoading: false
})
} else {
this.setData({ authorLoading: false })
}
} catch (e) {
console.log('[About] 加载作者配置失败,使用默认')
this.setData({ authorLoading: false })
}
},
// 加载书籍统计
// 加载书籍统计(合并到作者统计第一项「商业案例」)
async loadBookStats() {
try {
const res = await app.request('/api/miniprogram/book/stats')
if (res && res.success) {
this.setData({
'bookInfo.totalChapters': res.data?.totalChapters || 62,
'author.stats': [
{ label: '商业案例', value: String(res.data?.totalChapters || 62) },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
]
})
const res = await app.request({ url: '/api/miniprogram/book/stats', silent: true })
if (res?.success && res.data) {
const total = res.data?.totalChapters || 62
this.setData({ 'bookInfo.totalChapters': total })
const stats = this.data.author?.stats || []
const idx = stats.findIndex((s) => s && (s.label === '商业案例' || s.label === '章节'))
if (idx >= 0 && stats[idx]) {
const next = [...stats]
next[idx] = { ...stats[idx], value: String(total) }
this.setData({ 'author.stats': next })
} else if (stats.length === 0) {
this.setData({
'author.stats': [
{ label: '商业案例', value: String(total) },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
]
})
}
}
} catch (e) {
console.log('[About] 加载书籍统计失败,使用默认值')

View File

@@ -8,9 +8,13 @@
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<view wx:if="{{authorLoading}}" class="loading-row">加载中...</view>
<!-- 作者信息卡片 -->
<view class="author-card">
<view class="author-avatar">{{author.avatar}}</view>
<view class="author-card" wx:if="{{!authorLoading}}">
<view class="author-avatar-wrap">
<image wx:if="{{author.avatarImg}}" class="author-avatar-img" src="{{author.avatarImg}}" mode="aspectFill"/>
<view wx:else class="author-avatar">{{author.avatar}}</view>
</view>
<text class="author-name">{{author.name}}</text>
<text class="author-title">{{author.title}}</text>
<text class="author-bio">{{author.bio}}</text>
@@ -33,7 +37,7 @@
</view>
<!-- 书籍信息 -->
<view class="book-info-card" wx:if="{{bookInfo}}">
<view class="book-info-card" wx:if="{{bookInfo && !authorLoading}}">
<text class="card-title">📚 {{bookInfo.title}}</text>
<view class="book-stats">
<view class="book-stat">
@@ -58,7 +62,7 @@
</view>
<!-- 联系方式 - 引导到Soul派对房 -->
<view class="contact-card">
<view class="contact-card" wx:if="{{!authorLoading}}">
<text class="card-title">联系作者</text>
<view class="contact-item">
<text class="contact-icon">🎉</text>

View File

@@ -4,8 +4,11 @@
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.nav-placeholder { width: 72rpx; }
.content { padding: 32rpx; }
.loading-row { text-align: center; color: rgba(255,255,255,0.6); font-size: 28rpx; padding: 48rpx 0; }
.author-card { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; padding: 48rpx; text-align: center; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
.author-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; margin: 0 auto 24rpx; font-size: 64rpx; color: #fff; font-weight: 700; border: 4rpx solid rgba(0,206,209,0.3); }
.author-avatar-wrap { width: 160rpx; height: 160rpx; margin: 0 auto 24rpx; overflow: hidden; border-radius: 50%; border: 4rpx solid rgba(0,206,209,0.3); flex-shrink: 0; }
.author-avatar-img { width: 100%; height: 100%; display: block; }
.author-avatar { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; font-size: 64rpx; color: #fff; font-weight: 700; }
.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 8rpx; }
.author-title { font-size: 26rpx; color: #00CED1; display: block; margin-bottom: 24rpx; }
.author-bio { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.8; display: block; margin-bottom: 32rpx; }

View File

@@ -212,29 +212,31 @@ Page({
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
this.loadBookDataFromServer()
this.loadDailyChapters()
this.loadTotalFromServer()
this.loadChaptersOnce()
},
async loadTotalFromServer() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
if (res && (res.total || (res.data && res.data.length))) {
this.setData({ totalSections: res.total || (res.data || []).length })
}
} catch (e) {}
// 固定模块(序言、尾声、附录)不参与中间篇章
_isFixedPart(pt) {
if (!pt) return false
const p = String(pt).toLowerCase().replace(/[_\s|]/g, '')
return p.includes('序言') || p.includes('尾声') || p.includes('附录')
},
// stitch_soul P0-8从服务端加载目录按 part 聚合,带 isNew、免费/¥1
async loadBookDataFromServer() {
// 一次请求拉取全量目录,同时更新 totalSections / bookData / dailyChapters
async loadChaptersOnce() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const rows = (res && res.data) || (res && res.chapters) || []
if (rows.length === 0) return
// 1. totalSections
const totalSections = res.total ?? rows.length
// 2. bookData过滤序言/尾声/附录,中间篇章按 part 聚合)
const filtered = rows.filter(r => !this._isFixedPart(r.partTitle || r.part_title))
const partMap = new Map()
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
rows.forEach((r, idx) => {
filtered.forEach((r) => {
const pid = r.partId || r.part_id || 'part-1'
const cid = r.chapterId || r.chapter_id || 'chapter-1'
if (!partMap.has(pid)) {
@@ -270,9 +272,28 @@ Page({
chapters: Array.from(p.chapters.values())
}))
const firstPart = bookData[0] && bookData[0].id
// 3. dailyChapterssort_order > 62 的新增章节按更新时间取前20
const baseSort = 62
const daily = rows
.filter(r => (r.sectionOrder ?? r.sort_order ?? 0) > baseSort)
.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,
mid: c.mid ?? c.MID ?? 0,
title: c.section_title || c.title || c.sectionTitle,
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
this.setData({
bookData,
totalSections: rows.length,
totalSections,
dailyChapters: daily,
expandedPart: firstPart || this.data.expandedPart
})
} catch (e) { console.log('[Chapters] 加载目录失败:', e) }
@@ -324,30 +345,6 @@ Page({
wx.switchTab({ url: '/pages/index/index' })
},
async loadDailyChapters() {
try {
const res = await app.request({ url: '/api/miniprogram/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,
mid: c.mid ?? c.MID ?? 0,
title: c.section_title || c.title || c.sectionTitle,
price: c.price || 1,
dateStr: `${d.getMonth()+1}/${d.getDate()}`
}
})
if (daily.length > 0) {
this.setData({ dailyChapters: daily, totalSections: 62 + daily.length })
}
} catch (e) { console.log('[Chapters] 加载最新新增失败:', e) }
},
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })

View File

@@ -104,19 +104,13 @@ Page({
this.updateUserStatus()
},
// 初始化数据
async initData() {
this.setData({ loading: true })
try {
await this.loadBookData()
await this.loadFeaturedFromServer()
this.loadSuperMembers()
this.loadLatestChapters()
} catch (e) {
console.error('初始化失败:', e)
} finally {
this.setData({ loading: false })
}
// 初始化数据:首次进页面并行异步加载,加快首屏展示
initData() {
this.setData({ loading: false })
this.loadBookData()
this.loadFeaturedFromServer()
this.loadSuperMembers()
this.loadLatestChapters()
},
async loadSuperMembers() {
@@ -358,9 +352,14 @@ Page({
wx.switchTab({ url: '/pages/my/my' })
},
// 下拉刷新
// 下拉刷新(等待各异步加载完成后再结束)
async onPullDownRefresh() {
await this.initData()
await Promise.all([
this.loadBookData(),
this.loadFeaturedFromServer(),
this.loadSuperMembers(),
this.loadLatestChapters()
])
this.updateUserStatus()
wx.stopPullDownRefresh()
},

View File

@@ -4,7 +4,7 @@
<!-- 自定义导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 顶部区域 -->
<!-- 顶部区域按设计稿S 图标 + 标题副标题 | 点击链接卡若 + 章数) -->
<view class="header">
<view class="header-content">
<view class="logo-section">
@@ -12,17 +12,15 @@
<text class="logo-text">S</text>
</view>
<view class="logo-info">
<view class="logo-title-row">
<text class="logo-title-text">Soul创业派对</text>
<view class="contact-btn" bindtap="goToAbout">
<text class="contact-icon">+</text>
<text class="contact-text">联系作者</text>
</view>
</view>
<text class="logo-title-text">Soul创业派对</text>
<text class="logo-subtitle">来自派对房的真实故事</text>
</view>
</view>
<view class="header-right">
<view class="contact-btn" bindtap="goToAbout">
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
<text class="contact-text">点击链接卡若</text>
</view>
<view class="chapter-badge">{{totalSections}}章</view>
</view>
</view>
@@ -38,8 +36,8 @@
<!-- 主内容区 -->
<view class="main-content">
<!-- Banner卡片 - 最新章节 -->
<view class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
<!-- Banner卡片 - 最新章节(异步加载) -->
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
<view class="banner-glow"></view>
<view class="banner-tag">最新更新</view>
<view class="banner-title">{{latestSection.title}}</view>
@@ -49,6 +47,13 @@
<view class="banner-arrow">→</view>
</view>
</view>
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
<view class="banner-glow"></view>
<view class="banner-tag">最新更新</view>
<view class="banner-title">加载中...</view>
<view class="banner-part"></view>
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 阅读进度卡 -->
<view class="progress-card card">
@@ -81,14 +86,10 @@
</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 wx:if="{{superMembersLoading}}" class="super-loading">
@@ -124,14 +125,10 @@
</view>
</view>
<!-- 精选推荐(带 tag -->
<!-- 精选推荐(带 tag,已去掉「查看全部」 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选推荐</text>
<view class="section-more" bindtap="goToChapters">
<text class="more-text">查看全部</text>
<text class="more-arrow"></text>
</view>
</view>
<view class="featured-list">
<view

View File

@@ -55,13 +55,6 @@
flex-direction: column;
}
.logo-title-row {
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.logo-title-text {
font-size: 36rpx;
font-weight: 700;
@@ -71,22 +64,25 @@
.contact-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
background: rgba(0, 206, 209, 0.1);
border: 2rpx solid rgba(0, 206, 209, 0.2);
border-radius: 32rpx;
font-size: 20rpx;
font-weight: 600;
color: #00CED1;
gap: 12rpx;
padding: 8rpx 20rpx 8rpx 12rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 40rpx;
font-size: 24rpx;
font-weight: 500;
color: #ffffff;
}
.contact-icon {
font-size: 20rpx;
.contact-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.contact-text {
font-size: 20rpx;
font-size: 24rpx;
}
.logo-title {
@@ -105,7 +101,8 @@
.logo-subtitle {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
margin-top: 4rpx;
margin-top: 8rpx;
display: block;
}
.header-right {
@@ -176,6 +173,10 @@
margin-bottom: 24rpx;
}
.banner-skeleton .banner-title {
color: rgba(255, 255, 255, 0.5);
}
.banner-glow {
position: absolute;
top: 0;

View File

@@ -1,12 +1,13 @@
/**
* Soul创业派对 - 超级个体/会员详情页
* 接口:优先 /api/miniprogram/vip/members?id=xxVIP回退 /api/miniprogram/users?id=xx任意用户
* 字段映射name/vipName, avatar/vipAvatar, contact/vipContact/phone, wechatId, project/vipProject/projectIntro,
* mbti, region, industry, position, businessScale, skills,
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
* helpOffer→canHelp, helpNeed→needHelp
*/
const app = getApp()
const MOCK_ENRICHMENT = [
{ mbti: 'ENTJ', region: '深圳', industry: '跨境电商', position: '创始人', businessScale: '年GMV 3000万+', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', wechatRaw: 'wxid_entj001', bestMonth: '做跨境电商独立站单月GMV突破200万净利润35万', achievement: '从0到1搭建了30人的电商团队年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
{ mbti: 'INFP', region: '杭州', industry: '新媒体/电商', position: '创始人', businessScale: '年GMV 5000万+', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', wechatRaw: 'wxid_****abc', bestMonth: '旅游账号30天涨粉10万带货佣金收入12万', achievement: '帮助3个素人打造个人IP每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵全网粉丝50万+' },
{ mbti: 'INTP', region: '厦门', industry: 'SaaS/技术', position: '技术合伙人', businessScale: '天使轮200万', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', wechatRaw: 'wxid_intp003', bestMonth: 'AI客服系统外包项目单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
{ mbti: 'ESTP', region: '成都', industry: '资源对接', position: '联合创始人', businessScale: '50+城市', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', wechatRaw: 'wxid_estp004', bestMonth: '撮合景区合作居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
]
Page({
data: { statusBarHeight: 44, member: null, loading: true },
@@ -32,7 +33,7 @@ Page({
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 === 1,
avatar: u.vipAvatar || u.vip_avatar || u.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 || '',
@@ -51,29 +52,34 @@ Page({
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]
// 将空值、「未填写」、纯空格均视为未填写(用于隐藏对应项)
_emptyIfPlaceholder(v) {
if (v == null || v === undefined) return ''
const s = String(v).trim()
return (s === '' || s === '未填写') ? '' : s
},
enrichAndFormat(raw) {
const e = (v) => this._emptyIfPlaceholder(v)
const merged = {
id: raw.id,
name: raw.name || raw.vipName || raw.vip_name || raw.nickname || '创业者',
avatar: raw.avatar || raw.vipAvatar || raw.vip_avatar || '',
isVip: raw.isVip || raw.is_vip === 1,
mbti: raw.mbti || mock.mbti,
region: raw.region || mock.region,
industry: raw.industry || mock.industry,
position: raw.position || mock.position,
businessScale: raw.businessScale || raw.business_scale || mock.businessScale,
skills: raw.skills || mock.skills,
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || mock.contactRaw,
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || mock.wechatRaw,
bestMonth: raw.bestMonth || raw.storyBestMonth || raw.story_best_month || mock.bestMonth,
achievement: raw.achievement || raw.storyAchievement || raw.story_achievement || mock.achievement,
turningPoint: raw.turningPoint || raw.storyTurning || raw.story_turning || mock.turningPoint,
canHelp: raw.canHelp || raw.helpOffer || raw.help_offer || mock.canHelp,
needHelp: raw.needHelp || raw.helpNeed || raw.help_need || mock.needHelp,
project: raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro || mock.project
isVip: !!(raw.isVip || raw.is_vip),
mbti: e(raw.mbti),
region: e(raw.region),
industry: e(raw.industry),
position: e(raw.position),
businessScale: e(raw.businessScale || raw.business_scale),
skills: e(raw.skills),
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || '',
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || '',
bestMonth: e(raw.bestMonth || raw.storyBestMonth || raw.story_best_month),
achievement: e(raw.achievement || raw.storyAchievement || raw.story_achievement),
turningPoint: e(raw.turningPoint || raw.storyTurning || raw.story_turning),
canHelp: e(raw.canHelp || raw.helpOffer || raw.help_offer),
needHelp: e(raw.needHelp || raw.helpNeed || raw.help_need),
project: e(raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro)
}
const contact = merged.contactRaw || ''

View File

@@ -6,10 +6,7 @@
<text class="nav-icon"></text>
</view>
<text class="nav-title">个人资料</text>
<view class="nav-right">
<view class="nav-icon-wrap"><text class="nav-icon">⋯</text></view>
<view class="nav-icon-wrap"><text class="nav-icon-dot">●</text></view>
</view>
<view class="nav-placeholder"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
@@ -18,21 +15,23 @@
<view class="card-profile">
<view class="profile-deco"></view>
<view class="profile-body">
<view class="avatar-wrap {{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="avatar-outer">
<view class="avatar-wrap {{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>
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
</view>
<text class="profile-name">{{member.name}}</text>
<view class="profile-tags">
<view class="profile-tags" wx:if="{{member.mbti || member.region}}">
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
<text class="tag tag-region" wx:if="{{member.region}}"><text class="pin-icon">📍</text>{{member.region}}</text>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="card">
<!-- 基本信息(未填写行已隐藏) -->
<view class="card" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.contactRaw || member.contactDisplay || member.wechatRaw || member.wechatDisplay}}">
<view class="card-head">
<text class="card-icon">👤</text>
<text class="card-label">基本信息</text>
@@ -55,26 +54,30 @@
<text class="f-key">我擅长</text>
<text class="f-val">{{member.skills}}</text>
</view>
<view class="field" wx:if="{{member.contactDisplay || member.contactRaw}}">
<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 || '未填写'}}</text>
<view class="icon-copy" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">🔒</view>
<view class="icon-copy" wx:else bindtap="copyContact">📋</view>
<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">
<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>
</view>
</view>
<view class="field" wx:if="{{member.wechatDisplay || member.wechatRaw}}">
<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 || '未填写'}}</text>
<view class="icon-copy" wx:if="{{!member.wechatUnlocked}}" bindtap="unlockContact">🔒</view>
<view class="icon-copy" wx:else bindtap="copyWechat">📋</view>
<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">
<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>
</view>
</view>
</view>
<!-- 个人故事 -->
<!-- 个人故事(未填写行已隐藏) -->
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
<view class="card-head">
<text class="card-icon bulb">💡</text>
@@ -85,12 +88,12 @@
<view class="story-head"><text class="story-icon">🏆</text><text class="story-q">最赚钱的一个月做的是什么</text></view>
<text class="story-a">{{member.bestMonth}}</text>
</view>
<view class="divider" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
<view class="divider" wx:if="{{member.bestMonth}}"></view>
<view class="story" wx:if="{{member.achievement}}">
<view class="story-head"><text class="story-icon">⭐</text><text class="story-q">最有成就感的一件事</text></view>
<text class="story-a">{{member.achievement}}</text>
</view>
<view class="divider" wx:if="{{member.achievement && member.turningPoint}}"></view>
<view class="divider" wx:if="{{member.achievement}}"></view>
<view class="story" wx:if="{{member.turningPoint}}">
<view class="story-head"><text class="story-icon turn">🔄</text><text class="story-q">人生的转折点</text></view>
<text class="story-a">{{member.turningPoint}}</text>
@@ -98,7 +101,7 @@
</view>
</view>
<!-- 互助需求 -->
<!-- 互助需求(未填写行已隐藏) -->
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
<view class="card-head">
<text class="card-icon">🤝</text>

View File

@@ -35,12 +35,16 @@
pointer-events: none;
}
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
.avatar-wrap {
.avatar-outer {
position: relative;
width: 176rpx; height: 176rpx;
margin-bottom: 32rpx;
}
.avatar-wrap {
position: relative;
width: 100%; height: 100%;
border-radius: 50%;
overflow: hidden;
margin-bottom: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
}
@@ -62,10 +66,12 @@
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
}
.vip-tag {
position: absolute; bottom: 4rpx; right: 4rpx;
position: absolute; bottom: -4rpx; right: -4rpx;
background: linear-gradient(135deg, #F59E0B, #e8920d);
color: #000; font-size: 20rpx; font-weight: 800;
padding: 6rpx 16rpx; border-radius: 20rpx;
padding: 6rpx 14rpx; border-radius: 16rpx;
z-index: 2;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
}
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
@@ -96,6 +102,8 @@
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
.f-row { display: flex; align-items: center; gap: 16rpx; }
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
.icon-eye-off { display: flex; align-items: center; justify-content: center; }
.icon-eye-off .icon-img { width: 40rpx; height: 40rpx; }
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }

View File

@@ -1,8 +1,11 @@
<!-- 我的页 - professional_profile_with_earnings_vip 1:1 还原 -->
<!-- 我的页 - 设计稿 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>
</view>
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
@@ -16,77 +19,69 @@
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
</view>
<!-- 已登录:用户 -->
<view class="header-block" wx:else>
<view class="user-row">
<view class="avatar-wrap" bindtap="onAvatarTap">
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
<!-- 已登录:用户卡片(设计稿布局) -->
<view class="profile-card" wx:else>
<view class="profile-card-inner">
<view class="profile-top-row">
<view class="avatar-wrap" bindtap="onAvatarTap">
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
</view>
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
<view class="vip-badge vip-badge-gray" wx:else bindtap="goToVip">VIP</view>
</view>
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
<view class="vip-badge vip-badge-gray" wx:else bindtap="goToVip">VIP</view>
</view>
<view class="user-meta">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<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="goToVip">排行</text>
<view class="profile-meta">
<view class="profile-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
</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="goToVip">排行</text>
</view>
<text class="user-wechat" bindtap="copyUserId">微信号: {{userWechat || userIdShort || '--'}}</text>
</view>
<text class="user-id" bindtap="copyUserId">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
<text class="vip-expire" wx:if="{{isVip && vipExpireDate}}">会员到期时间:{{vipExpireDate}}</text>
</view>
<view class="user-actions">
<view class="action-btn" bindtap="handleMenuTap" data-id="settings"><text class="action-icon">⚙️</text></view>
<view class="action-btn" catchtap="goToProfileEdit"><text class="action-icon">✎</text></view>
<view class="profile-stats-row">
<view class="profile-stat">
<text class="profile-stat-val">{{readCount}}</text>
<text class="profile-stat-label">已读章节</text>
</view>
<view class="profile-stat">
<text class="profile-stat-val">{{referralCount}}</text>
<text class="profile-stat-label">推荐好友</text>
</view>
<view class="profile-stat">
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
<text class="profile-stat-label">我的收益</text>
</view>
</view>
</view>
</view>
<!-- 已登录:内容区 -->
<view class="main-content" wx:if="{{isLoggedIn}}">
<!-- 分享收益 -->
<view class="card earnings-card">
<view class="card-header">
<text class="card-icon">💰</text>
<text class="card-title">分享收益</text>
</view>
<view class="earnings-grid">
<view class="earnings-col">
<text class="earnings-val primary">{{referralCount}}</text>
<text class="earnings-label">推荐好友</text>
</view>
<view class="earnings-col">
<text class="earnings-val primary">{{earnings === '-' ? '--' : earnings}}</text>
<text class="earnings-label">我的收益</text>
</view>
<view class="earnings-col" bindtap="handleWithdraw">
<text class="earnings-val primary">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
<text class="earnings-label">可提现金额</text>
</view>
</view>
</view>
<!-- 阅读统计 -->
<view class="card stats-card">
<view class="card-header">
<text class="card-icon">👁️</text>
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
<text class="card-title">阅读统计</text>
</view>
<view class="stats-grid">
<view class="stat-box">
<text class="stat-icon">📖</text>
<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">
<text class="stat-icon">⏱</text>
<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">
<text class="stat-icon">👥</text>
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
<text class="stat-num">{{matchHistory}}</text>
<text class="stat-label">匹配伙伴</text>
</view>
@@ -96,7 +91,7 @@
<!-- 最近阅读 -->
<view class="card recent-card">
<view class="card-header">
<text class="card-icon">📖</text>
<image class="card-icon-img" src="/assets/icons/book-arrow-teal.svg" mode="aspectFit"/>
<text class="card-title">最近阅读</text>
</view>
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
@@ -121,22 +116,29 @@
</view>
</view>
<!-- 我的订单 + 关于作者 -->
<!-- 我的订单 + 关于作者 + 设置 -->
<view class="card menu-card">
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
<view class="menu-left">
<view class="menu-icon-wrap icon-teal"><text class="menu-icon">📦</text></view>
<view class="menu-icon-wrap icon-teal"><image class="menu-icon-img" src="/assets/icons/folder-teal.svg" mode="aspectFit"/></view>
<text class="menu-text">我的订单</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
<view class="menu-left">
<view class="menu-icon-wrap icon-blue"><text class="menu-icon"></text></view>
<view class="menu-icon-wrap icon-blue"><image class="menu-icon-img" src="/assets/icons/info-blue.svg" mode="aspectFit"/></view>
<text class="menu-text">关于作者</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
<view class="menu-left">
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
<text class="menu-text">设置</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>

View File

@@ -5,7 +5,6 @@
/* 真机适配:底部留足 TabBar + 安全区,避免「我的订单」被遮挡 */
.page {
min-height: 100vh;
background: #121212;
padding-bottom: calc(220rpx + env(safe-area-inset-bottom, 0px));
}
@@ -16,16 +15,18 @@
background: rgba(18,18,18,0.9); backdrop-filter: blur(8rpx);
display: flex; align-items: center;
min-height: 44px;
padding: 0 200rpx 0 32rpx; /* 右侧 200rpx 避让真机右上角胶囊 */
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; }
.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-icon { width: 44rpx; height: 44rpx; opacity: 0.7; }
.nav-placeholder { width: 100%; }
/* ===== 未登录 ===== */
.guest-block {
display: flex; flex-direction: column; align-items: center;
padding: 64rpx 48rpx;
padding: 64rpx 16rpx;
}
.guest-avatar { width: 144rpx; height: 144rpx; border-radius: 50%; background: #1A1A1A; border: 4rpx solid #374151; overflow: hidden; margin-bottom: 24rpx; }
.guest-avatar-img { width: 100%; height: 100%; display: block; }
@@ -33,9 +34,13 @@
.guest-name { font-size: 36rpx; font-weight: bold; color: #E5E7EB; margin-bottom: 24rpx; }
.guest-login-btn { padding: 20rpx 48rpx; background: #4FD1C5; color: #000; font-size: 28rpx; font-weight: 600; border-radius: 24rpx; }
/* ===== 用户(设计稿 header ===== */
.header-block { padding: 32rpx 40rpx 48rpx; }
.user-row { display: flex; align-items: center; gap: 32rpx; }
/* ===== 用户卡片(设计稿 1:1 ===== */
.profile-card { padding: 24rpx 16rpx 32rpx; }
.profile-card-inner {
background: #1A1A1A; border-radius: 24rpx; padding: 32rpx;
border: 1rpx solid rgba(75,85,99,0.5);
}
.profile-top-row { display: flex; align-items: flex-start; gap: 32rpx; }
.avatar-wrap { position: relative; flex-shrink: 0; }
.avatar-inner {
width: 130rpx; height: 130rpx; border-radius: 50%; overflow: hidden;
@@ -52,33 +57,43 @@
padding: 4rpx 12rpx; border-radius: 8rpx;
}
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
.user-actions { display: flex; flex-direction: column; gap: 24rpx; flex-shrink: 0; margin-left: auto; align-items: flex-end; }
.action-btn { width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; }
.action-icon { font-size: 36rpx; color: #4FD1C5; opacity: 0.9; }
.user-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
.profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
.user-name {
font-size: 44rpx; font-weight: bold; color: #fff;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
}
.become-member-btn {
padding: 12rpx 28rpx; border: 2rpx solid #C8A146; color: #C8A146;
font-size: 24rpx; font-weight: 500; border-radius: 40rpx; white-space: nowrap; flex-shrink: 0;
}
.become-member-vip { border-color: rgba(200,161,70,0.5); color: rgba(200,161,70,0.8); }
.vip-tags { display: flex; gap: 12rpx; flex-shrink: 0; }
.vip-tag {
font-size: 20rpx; padding: 6rpx 12rpx; border-radius: 8rpx;
border: 1rpx solid #374151; background: rgba(255,255,255,0.05); color: #9CA3AF;
}
.vip-tag-active { border-color: #C8A146; background: rgba(200,161,70,0.1); color: #C8A146; }
.user-id { display: block; font-size: 28rpx; color: #6B7280; }
.vip-expire { display: block; font-size: 22rpx; color: #6B7280; margin-top: 4rpx; }
.user-wechat { font-size: 26rpx; color: #6B7280; }
.profile-stats-row {
display: flex; justify-content: space-around; margin-top: 32rpx;
padding-top: 24rpx; border-top: 1rpx solid #374151;
}
.profile-stat { text-align: center; }
.profile-stat-val { display: block; font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
.profile-stat-label { display: block; font-size: 22rpx; color: #6B7280; margin-top: 8rpx; }
/* ===== 主内容区 ===== */
.main-content { padding: 0 32rpx 48rpx; }
.main-content { }
/* 卡片通用 */
.card {
background: #1A1A1A; border-radius: 24rpx; padding: 40rpx;
margin-bottom: 32rpx; border: 1rpx solid rgba(75,85,99,0.5);
background: #1A1A1A; border-radius: 24rpx; padding: 32rpx;
margin-bottom: 24rpx; border: 1rpx solid rgba(75,85,99,0.5);
}
.card-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 32rpx; }
.card-icon { font-size: 40rpx; }
.card-icon-img { width: 40rpx; height: 40rpx; flex-shrink: 0; }
.card-title { font-size: 32rpx; font-weight: bold; color: #fff; }
/* 分享收益 */
@@ -92,16 +107,20 @@
.earnings-val.primary { color: #4FD1C5; }
.earnings-label { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
/* 阅读统计 */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24rpx; }
.stat-box {
background: #252525; border-radius: 20rpx; padding: 20rpx;
display: flex; flex-direction: column; align-items: center; justify-content: center;
aspect-ratio: 2/1;
/* 阅读统计 - 统一高度避免真机错位 */
.stats-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24rpx;
align-items: stretch;
}
.stat-icon { font-size: 40rpx; margin-bottom: 8rpx; color: #4FD1C5; }
.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; }
.stat-label { font-size: 20rpx; color: #6B7280; margin-top: 4rpx; }
.stat-box {
background: #252525; border-radius: 20rpx; padding: 24rpx;
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 140rpx;
}
.stat-icon { font-size: 40rpx; margin-bottom: 8rpx; color: #4FD1C5; flex-shrink: 0; }
.stat-icon-img { width: 44rpx; height: 44rpx; margin-bottom: 8rpx; flex-shrink: 0; display: block; }
.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; line-height: 1.2; }
.stat-label { font-size: 20rpx; color: #6B7280; margin-top: 4rpx; line-height: 1.2; }
/* 最近阅读 */
.recent-list { display: flex; flex-direction: column; gap: 24rpx; }
@@ -131,9 +150,15 @@
}
.menu-icon-wrap .menu-icon { font-size: 32rpx; }
.icon-teal { background: rgba(79,209,197,0.2); }
.icon-teal .menu-icon { color: #4FD1C5; }
.icon-teal .menu-icon,
.icon-teal .menu-icon-img { color: #4FD1C5; }
.icon-teal .menu-icon-img { width: 32rpx; height: 32rpx; }
.icon-blue { background: rgba(59,130,246,0.2); }
.icon-blue .menu-icon { color: #3B82F6; }
.icon-blue .menu-icon,
.icon-blue .menu-icon-img { color: #3B82F6; }
.icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; }
.icon-gray { background: rgba(156,163,175,0.15); }
.icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; }
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }

View File

@@ -33,11 +33,22 @@ Page({
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userInfo.id}`, silent: true })
if (res?.success && res.data) {
const d = res.data
const e = (v) => (v == null || v === undefined ? '' : (String(v).trim() === '' || String(v).trim() === '未填写' ? '' : String(v).trim()))
const phone = d.phone || ''
const wechat = d.wechatId || wx.getStorageSync('user_wechat') || ''
this.setData({
profile: {
...d,
industry: e(d.industry),
position: e(d.position),
businessScale: e(d.businessScale || d.business_scale),
skills: e(d.skills),
storyBestMonth: e(d.storyBestMonth || d.story_best_month),
storyAchievement: e(d.storyAchievement || d.story_achievement),
storyTurning: e(d.storyTurning || d.story_turning),
helpOffer: e(d.helpOffer || d.help_offer),
helpNeed: e(d.helpNeed || d.help_need),
projectIntro: e(d.projectIntro || d.project_intro),
phoneMask: phone ? phone.slice(0, 3) + '****' + phone.slice(-2) : '',
wechatMask: wechat ? (wechat.length > 8 ? wechat.slice(0, 4) + '****' + wechat.slice(-3) : wechat) : '',
phone,

View File

@@ -63,7 +63,7 @@
<text class="field-hint" wx:if="{{profile.wechat}}">复制</text>
</view>
</view>
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.phone && !profile.wechat && !profile.skills}}">
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.phone && !profile.wechat}}">
点击右上角 ⋯ 编辑完善资料
</view>
</view>

View File

@@ -65,9 +65,6 @@ Page({
showPosterModal: false,
isPaying: false,
isGeneratingPoster: false,
// 免费章节
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
// 章节 mid扫码/海报分享用,便于分享 path 带 mid
sectionMid: null
@@ -124,12 +121,13 @@ Page({
try {
const config = await accessManager.fetchLatestConfig()
this.setData({
freeIds: config.freeChapters,
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
})
const accessState = await accessManager.determineAccessState(id, config.freeChapters)
// 统一:先拉章节数据,用 isFree/price===0 判断免费
const chapterRes = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
const accessState = await accessManager.determineAccessState(id, chapterRes)
const canAccess = accessManager.canAccessFullContent(accessState)
this.setData({
@@ -139,8 +137,8 @@ Page({
showPaywall: !canAccess
})
// 【标准流程】3. 加载内容
await this.loadContent(id, accessState)
// 加载内容(复用已拉取的章节数据,避免二次请求)
await this.loadContent(id, accessState, chapterRes)
// 【标准流程】4. 如果有权限,初始化阅读追踪
if (canAccess) {
@@ -192,18 +190,22 @@ Page({
},
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
async loadContent(id, accessState) {
// prefetchedChapter若已有章节数据含 content则复用避免二次请求
async loadContent(id, accessState, prefetchedChapter) {
try {
const section = this.getSectionInfo(id)
const sectionPrice = this.data.sectionPrice ?? 1
if (section.price === undefined || section.price === null) {
section.price = sectionPrice
let res = prefetchedChapter
if (!res || !res.content) {
res = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true })
}
const section = {
id: res.id || id,
title: res.sectionTitle || res.title || this.getSectionTitle(id),
isFree: res.isFree === true || (res.price !== undefined && res.price === 0),
price: res.price ?? sectionPrice
}
this.setData({ section })
// 从 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)
@@ -584,14 +586,14 @@ Page({
// 1. 刷新用户购买状态(从 orders 表拉取最新)
await accessManager.refreshUserPurchaseStatus()
// 2. 重新拉取免费列表(极端情况:刚登录时当前章节可能改免费了)
const config = await accessManager.fetchLatestConfig()
this.setData({ freeIds: config.freeChapters })
// 3. 重新判断当前章节权限
// 2. 重新拉取章节数据,用 isFree/price 判断免费
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
silent: true
})
const newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
config.freeChapters
chapterRes
)
const canAccess = accessManager.canAccessFullContent(newAccessState)
@@ -602,9 +604,9 @@ Page({
showPaywall: !canAccess
})
// 4. 如果已解锁,重新加载内容并初始化阅读追踪
// 3. 如果已解锁,重新加载内容并初始化阅读追踪
if (canAccess) {
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionId, newAccessState, chapterRes)
readingTracker.init(this.data.sectionId)
}
@@ -855,19 +857,22 @@ Page({
// 2. 刷新用户购买状态
await accessManager.refreshUserPurchaseStatus()
// 3. 重新判断当前章节权限(应为 unlocked_purchased
// 3. 重新拉取章节并判断权限(应为 unlocked_purchased
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
silent: true
})
let newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
this.data.freeIds
chapterRes
)
// 如果权限未生效,再重试一次(可能回调延迟)
if (newAccessState !== 'unlocked_purchased') {
console.log('[Pay] 权限未生效1秒后重试...')
await this.sleep(1000)
newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
this.data.freeIds
chapterRes
)
}
@@ -880,7 +885,7 @@ Page({
})
// 4. 重新加载全文
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionId, newAccessState, chapterRes)
// 5. 初始化阅读追踪
if (canAccess) {
@@ -1188,14 +1193,20 @@ Page({
wx.showLoading({ title: '重试中...', mask: true })
try {
// 重新拉取配置
const config = await accessManager.fetchLatestConfig()
this.setData({ freeIds: config.freeChapters })
this.setData({
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
})
// 重新判断权限
// 重新拉取章节,用 isFree/price 判断免费
const chapterRes = await app.request({
url: `/api/miniprogram/book/chapter/${this.data.sectionId}`,
silent: true
})
const newAccessState = await accessManager.determineAccessState(
this.data.sectionId,
config.freeChapters
chapterRes
)
const canAccess = accessManager.canAccessFullContent(newAccessState)
@@ -1205,8 +1216,7 @@ Page({
showPaywall: !canAccess
})
// 重新加载内容
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionId, newAccessState, chapterRes)
// 如果有权限,初始化阅读追踪
if (canAccess) {

View File

@@ -21,7 +21,6 @@ Page({
{ title: '链接资源', desc: '深度私域资源池', icon: '🔗' },
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: '✓' }
],
profile: { vipName: '', vipProject: '', vipContact: '', vipAvatar: '', vipBio: '' },
purchasing: false
},
@@ -49,62 +48,10 @@ Page({
expireDateStr: expStr,
price: d.price || 1980
})
if (d.isVip) this.loadProfile(userId)
}
} catch (e) { console.log('[VIP] 加载失败', e) }
},
async loadProfile(userId) {
try {
const res = await app.request(`/api/miniprogram/vip/profile?userId=${userId}`)
if (res?.success) {
const p = res.data
// 头像若为相对路径则补全
if (p.vipAvatar && !p.vipAvatar.startsWith('http')) {
p.vipAvatar = app.globalData.baseUrl.replace(/\/$/, '') + (p.vipAvatar.startsWith('/') ? p.vipAvatar : '/' + p.vipAvatar)
}
this.setData({ profile: p })
}
} catch (e) { console.log('[VIP] 资料加载失败', e) }
},
async onChooseVipAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFiles[0].tempFilePath
wx.showLoading({ title: '上传中...', mask: true })
try {
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempPath,
name: 'file',
formData: { folder: 'avatars' },
success: (r) => {
try {
const d = JSON.parse(r.data)
d.success ? resolve(d) : reject(new Error(d.error || '上传失败'))
} catch { reject(new Error('解析失败')) }
},
fail: reject
})
})
const path = uploadRes.url || uploadRes.data?.url || ''
const avatarUrl = path.startsWith('http') ? path : (app.globalData.baseUrl.replace(/\/$/, '') + (path.startsWith('/') ? path : '/' + path))
this.setData({ 'profile.vipAvatar': avatarUrl })
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
}
}
})
},
async handlePurchase() {
let userId = app.globalData.userInfo?.id
let openId = app.globalData.openId || app.globalData.userInfo?.open_id
@@ -156,24 +103,6 @@ Page({
} finally { this.setData({ purchasing: false }) }
},
onVipNameInput(e) { this.setData({ 'profile.vipName': e.detail.value }) },
onVipProjectInput(e) { this.setData({ 'profile.vipProject': e.detail.value }) },
onVipContactInput(e) { this.setData({ 'profile.vipContact': e.detail.value }) },
onVipBioInput(e) { this.setData({ 'profile.vipBio': e.detail.value }) },
async saveProfile() {
const userId = app.globalData.userInfo?.id
if (!userId) return
const p = this.data.profile
try {
const res = await app.request('/api/miniprogram/vip/profile', {
method: 'POST', data: { userId, vipName: p.vipName, vipProject: p.vipProject, vipContact: p.vipContact, vipAvatar: p.vipAvatar, vipBio: p.vipBio }
})
if (res?.success) wx.showToast({ title: '资料已保存', icon: 'success' })
else wx.showToast({ title: res?.error || '保存失败', icon: 'none' })
} catch (e) { wx.showToast({ title: '保存失败', icon: 'none' }) }
},
goBack() { wx.navigateBack() },
onShareAppMessage() {

View File

@@ -8,14 +8,10 @@
<view class="nav-placeholder-r"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 会员宣传区 - 设计稿 premium 卡片 -->
<!-- 会员宣传区(已去掉 VIP PREMIUM 标签) -->
<view class="vip-hero {{isVip ? 'vip-hero-active' : ''}}">
<text class="vip-hero-tag">VIP PREMIUM</text>
<text class="vip-hero-title">
加入卡若的
<text class="gold">创业派对</text>
会员
</text>
<view class="vip-hero-title">加入卡若的</view>
<view class="vip-hero-title gold">创业派对 会员</view>
<text class="vip-hero-sub" wx:if="{{isVip}}">有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天)</text>
<text class="vip-hero-sub" wx:else>一次加入 尊享终身陪伴与成长</text>
</view>
@@ -54,47 +50,6 @@
{{purchasing ? "处理中..." : "¥" + price + "/年 加入创业派对"}}
</view>
</view>
<view class="bottom-spacer" wx:if="{{!isVip}}"></view>
<!-- VIP资料填写仅VIP可见 -->
<view class="profile-card" wx:if="{{isVip}}">
<text class="profile-title">会员资料(展示在创业老板排行)</text>
<view class="avatar-row">
<view class="avatar-label">头像</view>
<view class="avatar-slot" bindtap="onChooseVipAvatar">
<image wx:if="{{profile.vipAvatar}}" class="avatar-img" src="{{profile.vipAvatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">
<text class="avatar-add">+</text>
<text class="avatar-hint">上传头像</text>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">姓名</text>
<view class="form-input">
<input type="nickname" placeholder="您的真实姓名" placeholder-class="form-placeholder" value="{{profile.vipName}}" bindinput="onVipNameInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<view class="form-input">
<input placeholder="您的项目/公司名称" placeholder-class="form-placeholder" value="{{profile.vipProject}}" bindinput="onVipProjectInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">联系方式</text>
<view class="form-input">
<input placeholder="微信号或手机号" placeholder-class="form-placeholder" value="{{profile.vipContact}}" bindinput="onVipContactInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">一句话简介</text>
<view class="form-input">
<input placeholder="简要描述您的业务" placeholder-class="form-placeholder" value="{{profile.vipBio}}" bindinput="onVipBioInput" />
</view>
</view>
<view class="save-btn-wrap">
<button class="save-btn" bindtap="saveProfile">保存资料</button>
</view>
</view>
<view class="bottom-space"></view>
</view>

View File

@@ -8,7 +8,7 @@
.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; }
.vip-hero-title { display: block; font-size: 44rpx; font-weight: bold; color: #fff; }
.gold { color: #FFD700; }
.vip-hero-sub { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; }

View File

@@ -24,12 +24,47 @@
"miniprogram": {
"list": [
{
"name": "pages/about/about",
"pathName": "pages/about/about",
"name": "pages/vip/vip",
"pathName": "pages/vip/vip",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "pages/member-detail/member-detail",
"pathName": "pages/member-detail/member-detail",
"query": "id=ogpTW5QBRQNUOm4-zvg8it2XySrI",
"launchMode": "default",
"scene": null
},
{
"name": "pages/vip/vip",
"pathName": "pages/vip/vip",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/member-detail/member-detail",
"pathName": "pages/member-detail/member-detail",
"query": "id=ogpTW5cteGWqwOsRzh_CCIdKWGSE",
"launchMode": "default",
"scene": null
},
{
"name": "pages/about/about",
"pathName": "pages/about/about",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/about/about",
"pathName": "pages/about/about",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/vip/vip",
"pathName": "pages/vip/vip",

View File

@@ -18,45 +18,44 @@ class ChapterAccessManager {
}
/**
* 拉取最新配置(免费章节列表、价格等
* 拉取最新配置(价格等,免费章节数据为准
*/
async fetchLatestConfig() {
try {
const res = await app.request({ url: '/api/miniprogram/config', silent: true, timeout: 3000 })
if (res.success && res.freeChapters) {
if (res.success && res.prices) {
return {
freeChapters: res.freeChapters,
prices: res.prices || { section: 1, fullbook: 9.9 }
}
}
} catch (e) {
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
}
// 默认配置
return {
freeChapters: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
prices: { section: 1, fullbook: 9.9 }
}
}
/**
* 判断章节是否免费
* 判断章节是否免费(统一:章节 isFree 或 price===0
*/
isFreeChapter(sectionId, freeList) {
return freeList.includes(sectionId)
isFreeFromChapterData(chapterData) {
if (!chapterData) return false
if (chapterData.isFree === true) return true
if (chapterData.price !== undefined && chapterData.price === 0) return true
return false
}
/**
* 【核心方法】确定章节权限状态
* 【核心方法】确定章节权限状态(统一以章节数据 isFree/price 为准)
* @param {string} sectionId - 章节ID
* @param {Array} freeList - 免费章节列表
* @param {object} chapterData - 章节接口返回 { isFree, price, ... },免费 = isFree 或 price===0
* @returns {Promise<string>} accessState
*/
async determineAccessState(sectionId, freeList) {
async determineAccessState(sectionId, chapterData) {
try {
// 1. 检查是否免费
if (this.isFreeChapter(sectionId, freeList)) {
// 1. 检查是否免费(统一:章节 isFree 或 price===0
if (this.isFreeFromChapterData(chapterData)) {
console.log('[AccessManager] 免费章节:', sectionId)
return this.accessStates.FREE
}