更新小程序开发文档,新增2026-03-03的最佳实践记录,优化个人中心类页面的卡片区边距规范,确保一致性与可用性。调整相关页面以反映最新设计稿,提升用户体验与功能一致性。
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
5
miniprogram/assets/icons/book-arrow-teal.svg
Normal 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 |
5
miniprogram/assets/icons/book-arrow.svg
Normal 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 |
4
miniprogram/assets/icons/book-open-teal.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#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 |
4
miniprogram/assets/icons/clock-teal.svg
Normal 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 |
4
miniprogram/assets/icons/clock.svg
Normal 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 |
6
miniprogram/assets/icons/eye-off.svg
Normal 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 |
4
miniprogram/assets/icons/eye-teal.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.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 |
4
miniprogram/assets/icons/eye.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.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 |
3
miniprogram/assets/icons/folder-teal.svg
Normal 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 |
3
miniprogram/assets/icons/folder.svg
Normal 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 |
5
miniprogram/assets/icons/info-blue.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="#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 |
5
miniprogram/assets/icons/info.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="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 |
4
miniprogram/assets/icons/settings-gray.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="#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 |
6
miniprogram/assets/icons/users-teal.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="#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 |
BIN
miniprogram/assets/images/author-avatar.png
Normal file
|
After Width: | Height: | Size: 503 KiB |
@@ -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] 加载书籍统计失败,使用默认值')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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. dailyChapters(sort_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' })
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体/会员详情页
|
||||
* 接口:优先 /api/miniprogram/vip/members?id=xx(VIP),回退 /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 || ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||