diff --git a/.cursor/agent/小程序开发工程师/evolution/2026-03-03.md b/.cursor/agent/小程序开发工程师/evolution/2026-03-03.md new file mode 100644 index 00000000..fa6a9b29 --- /dev/null +++ b/.cursor/agent/小程序开发工程师/evolution/2026-03-03.md @@ -0,0 +1,27 @@ +# 2026-03-03 经验 + +## 我的页面卡片区边距优化 + +### 问题/场景 + +用户反馈「我的」页面的卡片区域左右边距过大,内容区在水平方向上显得较窄,未充分利用屏幕横向空间。 + +### 解决方案 + +将卡片区域及上下关联区块的左右边距统一缩小: + +| 区块 | 修改前 | 修改后 | +|------|--------|--------| +| `.main-content` | 32rpx | 16rpx | +| `.header-block` | 40rpx | 16rpx | +| `.guest-block` | 48rpx | 16rpx | +| `.card` padding | 40rpx | 32rpx | +| `.card` margin-bottom | 32rpx | 24rpx | + +### 提炼规则 + +个人中心、设置类页面(如「我的」)的卡片区左右边距宜紧凑,**推荐 16rpx**;卡片内边距 32rpx、卡片间距 24rpx,以充分利用横向空间。 + +### 适用 + +- `miniprogram/pages/my/` 及类似个人中心、设置页 diff --git a/.cursor/agent/小程序开发工程师/evolution/索引.md b/.cursor/agent/小程序开发工程师/evolution/索引.md index eaf1ff56..6bdf6290 100644 --- a/.cursor/agent/小程序开发工程师/evolution/索引.md +++ b/.cursor/agent/小程序开发工程师/evolution/索引.md @@ -3,3 +3,4 @@ | 日期 | 摘要 | 文件 | |------|------|------| | 2026-02-28 | input 边距口诀、match 资源对接弹窗修正 | [2026-02-28.md](./2026-02-28.md) | +| 2026-03-03 | 我的页面卡片区边距优化,16rpx 推荐值 | [2026-03-03.md](./2026-03-03.md) | diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md index f8697f96..a4bf4ab5 100644 --- a/.cursor/agent/开发助理/经验清单.md +++ b/.cursor/agent/开发助理/经验清单.md @@ -24,6 +24,7 @@ |------|------|------|------------|------| | 2026-02-27 | 小程序、团队 | 最佳实践 | SKILL-小程序开发 §6、SKILL-管理端开发 §4.1 | 输入框 padding 用 view/div 包裹 | | 2026-02-28 | 小程序、管理端 | 最佳实践 | miniprogram §6、admin §4.1 | input 边距口诀「外边包 view、内部 width 100%」;match 弹窗已修正 | +| 2026-03-03 | 小程序 | 最佳实践 | miniprogram §8 | 我的页面卡片区边距 16rpx,个人中心类页面布局规范 | --- @@ -34,4 +35,4 @@ --- -**最后更新**:2026-02-28 +**最后更新**:2026-03-03 diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md index 9874bcd4..59665cf4 100644 --- a/.cursor/agent/开发助理/项目索引/小程序.md +++ b/.cursor/agent/开发助理/项目索引/小程序.md @@ -20,9 +20,10 @@ | 2026-02-27 | 吸收经验:输入框 padding 用 view 包裹,已升级 SKILL-小程序开发 §6 | 已完成 | | 2026-02-28 | stitch_soul 需求评审:首页/目录/导师/会员/资料五类页面,待需求与接口确定后分阶段实现 | 待续 | | 2026-02-28 | 吸收经验:input 边距口诀「外边包 view、内部 width 100%」写入 Skill §6;match 资源对接弹窗已按规范修正 | 已完成 | +| 2026-03-03 | 吸收经验:我的页面卡片区边距优化,16rpx 为个人中心类页面推荐值,已升级 SKILL §8 | 已完成 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-02-28 +**最后更新**:2026-03-03 diff --git a/.cursor/skills/miniprogram-dev/SKILL.md b/.cursor/skills/miniprogram-dev/SKILL.md index 393567ec..a4eea193 100644 --- a/.cursor/skills/miniprogram-dev/SKILL.md +++ b/.cursor/skills/miniprogram-dev/SKILL.md @@ -68,11 +68,20 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑 --- -## 7. 何时使用本 Skill +## 7. 布局与边距(个人中心/设置类页面) + +- **卡片区左右边距**:宜紧凑,**推荐 16rpx**,避免内容区过窄。 +- **卡片**:内边距 32rpx,卡片间距 24rpx。 +- **适用**:`pages/my/`、设置页等个人中心类页面。 + +--- + +## 8. 何时使用本 Skill - 在 **miniprogram/** 下新增或修改页面、组件、utils 时。 - 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。 - 做阅读、支付、推荐、提现等与 soul-api 对接的功能时。 - 做表单、input/textarea 样式时(遵循 §6,用 view 包裹,padding 写在 view 上)。 +- 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。 遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。 diff --git a/miniprogram/app.js b/miniprogram/app.js index aacfcca8..553d1ce2 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -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) } }, diff --git a/miniprogram/assets/icons/book-arrow-teal.svg b/miniprogram/assets/icons/book-arrow-teal.svg new file mode 100644 index 00000000..6a861630 --- /dev/null +++ b/miniprogram/assets/icons/book-arrow-teal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/book-arrow.svg b/miniprogram/assets/icons/book-arrow.svg new file mode 100644 index 00000000..2b1320d0 --- /dev/null +++ b/miniprogram/assets/icons/book-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/book-open-teal.svg b/miniprogram/assets/icons/book-open-teal.svg new file mode 100644 index 00000000..482f4093 --- /dev/null +++ b/miniprogram/assets/icons/book-open-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/clock-teal.svg b/miniprogram/assets/icons/clock-teal.svg new file mode 100644 index 00000000..96a928e3 --- /dev/null +++ b/miniprogram/assets/icons/clock-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/clock.svg b/miniprogram/assets/icons/clock.svg new file mode 100644 index 00000000..547f24a7 --- /dev/null +++ b/miniprogram/assets/icons/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/eye-off.svg b/miniprogram/assets/icons/eye-off.svg new file mode 100644 index 00000000..05b73282 --- /dev/null +++ b/miniprogram/assets/icons/eye-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/eye-teal.svg b/miniprogram/assets/icons/eye-teal.svg new file mode 100644 index 00000000..cdd62e60 --- /dev/null +++ b/miniprogram/assets/icons/eye-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/eye.svg b/miniprogram/assets/icons/eye.svg new file mode 100644 index 00000000..10ff508d --- /dev/null +++ b/miniprogram/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/folder-teal.svg b/miniprogram/assets/icons/folder-teal.svg new file mode 100644 index 00000000..28003510 --- /dev/null +++ b/miniprogram/assets/icons/folder-teal.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/folder.svg b/miniprogram/assets/icons/folder.svg new file mode 100644 index 00000000..5e5b7042 --- /dev/null +++ b/miniprogram/assets/icons/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/info-blue.svg b/miniprogram/assets/icons/info-blue.svg new file mode 100644 index 00000000..0f5bb1d2 --- /dev/null +++ b/miniprogram/assets/icons/info-blue.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/info.svg b/miniprogram/assets/icons/info.svg new file mode 100644 index 00000000..11d0f224 --- /dev/null +++ b/miniprogram/assets/icons/info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/settings-gray.svg b/miniprogram/assets/icons/settings-gray.svg new file mode 100644 index 00000000..d7098fc1 --- /dev/null +++ b/miniprogram/assets/icons/settings-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/users-teal.svg b/miniprogram/assets/icons/users-teal.svg new file mode 100644 index 00000000..ed97706f --- /dev/null +++ b/miniprogram/assets/icons/users-teal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/images/author-avatar.png b/miniprogram/assets/images/author-avatar.png new file mode 100644 index 00000000..c02ae73f Binary files /dev/null and b/miniprogram/assets/images/author-avatar.png differ diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js index ff164288..2dc3a222 100644 --- a/miniprogram/pages/about/about.js +++ b/miniprogram/pages/about/about.js @@ -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] 加载书籍统计失败,使用默认值') diff --git a/miniprogram/pages/about/about.wxml b/miniprogram/pages/about/about.wxml index 598e9464..3ddb3660 100644 --- a/miniprogram/pages/about/about.wxml +++ b/miniprogram/pages/about/about.wxml @@ -8,9 +8,13 @@ + 加载中... - - {{author.avatar}} + + + + {{author.avatar}} + {{author.name}} {{author.title}} {{author.bio}} @@ -33,7 +37,7 @@ - + 📚 {{bookInfo.title}} @@ -58,7 +62,7 @@ - + 联系作者 🎉 diff --git a/miniprogram/pages/about/about.wxss b/miniprogram/pages/about/about.wxss index 337aa041..06ad5a00 100644 --- a/miniprogram/pages/about/about.wxss +++ b/miniprogram/pages/about/about.wxss @@ -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; } diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js index 9106b80a..154ffa44 100644 --- a/miniprogram/pages/chapters/chapters.js +++ b/miniprogram/pages/chapters/chapters.js @@ -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' }) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 35b27f0c..9fb60ee6 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -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() }, diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 53bc16a7..04cd2438 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -4,7 +4,7 @@ - + @@ -12,17 +12,15 @@ S - - Soul创业派对 - - + - 联系作者 - - + Soul创业派对 来自派对房的真实故事 + + + 点击链接卡若 + {{totalSections}}章 @@ -38,8 +36,8 @@ - - + @@ -81,14 +86,10 @@ - + 超级个体 - - 查看全部 - - @@ -124,14 +125,10 @@ - + 精选推荐 - - 查看全部 - - 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 || '' diff --git a/miniprogram/pages/member-detail/member-detail.wxml b/miniprogram/pages/member-detail/member-detail.wxml index 553c52bb..0b1730c5 100644 --- a/miniprogram/pages/member-detail/member-detail.wxml +++ b/miniprogram/pages/member-detail/member-detail.wxml @@ -6,10 +6,7 @@ 个人资料 - - - - + @@ -18,21 +15,23 @@ - - - {{member.name[0] || '创'}} + + + + {{member.name[0] || '创'}} + VIP {{member.name}} - + {{member.mbti}} 📍{{member.region}} - - + + 👤 基本信息 @@ -55,26 +54,30 @@ 我擅长 {{member.skills}} - + 联系方式 - {{member.contactDisplay || '未填写'}} - 🔒 - 📋 + {{member.contactDisplay || member.contactRaw}} + + + + 📋 - + 微信号 - {{member.wechatDisplay || '未填写'}} - 🔒 - 📋 + {{member.wechatDisplay || member.wechatRaw}} + + + + 📋 - + 💡 @@ -85,12 +88,12 @@ 🏆最赚钱的一个月做的是什么 {{member.bestMonth}} - + 最有成就感的一件事 {{member.achievement}} - + 🔄人生的转折点 {{member.turningPoint}} @@ -98,7 +101,7 @@ - + 🤝 diff --git a/miniprogram/pages/member-detail/member-detail.wxss b/miniprogram/pages/member-detail/member-detail.wxss index 1151f00f..c6f547b2 100644 --- a/miniprogram/pages/member-detail/member-detail.wxss +++ b/miniprogram/pages/member-detail/member-detail.wxss @@ -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; } diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index c333a623..6fad4513 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -1,8 +1,11 @@ - + - + 我的 + + + @@ -16,77 +19,69 @@ - - - - - - - {{userInfo.nickname ? userInfo.nickname[0] : '?'}} + + + + + + + + {{userInfo.nickname ? userInfo.nickname[0] : '?'}} + + VIP + VIP - VIP - VIP - - - {{userInfo.nickname || '点击设置昵称'}} - - 会员 - 匹配 - 排行 + + + {{userInfo.nickname || '点击设置昵称'}} + {{isVip ? '会员中心' : '成为会员'}} + + + 会员 + 匹配 + 排行 + + 微信号: {{userWechat || userIdShort || '--'}} - {{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}} - 会员到期时间:{{vipExpireDate}} - - - - - 💰 - 分享收益 - - - - {{referralCount}} - 推荐好友 - - - {{earnings === '-' ? '--' : earnings}} - 我的收益 - - - {{pendingEarnings === '-' ? '--' : pendingEarnings}} - 可提现金额 - - - - - 👁️ + 阅读统计 - 📖 + {{readCount}} 已读章节 - + {{totalReadTime}} 阅读分钟 - 👥 + {{matchHistory}} 匹配伙伴 @@ -96,7 +91,7 @@ - 📖 + 最近阅读 @@ -121,22 +116,29 @@ - + - 📦 + 我的订单 - + 关于作者 + + + + 设置 + + + diff --git a/miniprogram/pages/my/my.wxss b/miniprogram/pages/my/my.wxss index 91026a52..125731fb 100644 --- a/miniprogram/pages/my/my.wxss +++ b/miniprogram/pages/my/my.wxss @@ -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; } diff --git a/miniprogram/pages/profile-show/profile-show.js b/miniprogram/pages/profile-show/profile-show.js index cc107f64..81e7b14d 100644 --- a/miniprogram/pages/profile-show/profile-show.js +++ b/miniprogram/pages/profile-show/profile-show.js @@ -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, diff --git a/miniprogram/pages/profile-show/profile-show.wxml b/miniprogram/pages/profile-show/profile-show.wxml index cbec4d7c..7cb81542 100644 --- a/miniprogram/pages/profile-show/profile-show.wxml +++ b/miniprogram/pages/profile-show/profile-show.wxml @@ -63,7 +63,7 @@ 复制 - + 点击右上角 ⋯ 编辑完善资料 diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index feabf631..77c8aa60 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -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) { diff --git a/miniprogram/pages/vip/vip.js b/miniprogram/pages/vip/vip.js index c356a57b..65c30361 100644 --- a/miniprogram/pages/vip/vip.js +++ b/miniprogram/pages/vip/vip.js @@ -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() { diff --git a/miniprogram/pages/vip/vip.wxml b/miniprogram/pages/vip/vip.wxml index ff304a30..8c1d8a74 100644 --- a/miniprogram/pages/vip/vip.wxml +++ b/miniprogram/pages/vip/vip.wxml @@ -8,14 +8,10 @@ - + - VIP PREMIUM - - 加入卡若的 - 创业派对 - 会员 - + 加入卡若的 + 创业派对 会员 有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天) 一次加入 尊享终身陪伴与成长 @@ -54,47 +50,6 @@ {{purchasing ? "处理中..." : "¥" + price + "/年 加入创业派对"}} - - - - 会员资料(展示在创业老板排行) - - 头像 - - - - + - 上传头像 - - - - - 姓名 - - - - - - 项目名称 - - - - - - 联系方式 - - - - - - 一句话简介 - - - - - - - - + \ No newline at end of file diff --git a/miniprogram/pages/vip/vip.wxss b/miniprogram/pages/vip/vip.wxss index d8258be0..36c20143 100644 --- a/miniprogram/pages/vip/vip.wxss +++ b/miniprogram/pages/vip/vip.wxss @@ -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; } diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json index fa7f85a9..46fd5fc8 100644 --- a/miniprogram/project.private.config.json +++ b/miniprogram/project.private.config.json @@ -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", diff --git a/miniprogram/utils/chapterAccessManager.js b/miniprogram/utils/chapterAccessManager.js index 8b66c587..70a2db90 100644 --- a/miniprogram/utils/chapterAccessManager.js +++ b/miniprogram/utils/chapterAccessManager.js @@ -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} 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 } diff --git a/soul-admin/src/App.tsx b/soul-admin/src/App.tsx index 6d6e6b32..7562bb5a 100644 --- a/soul-admin/src/App.tsx +++ b/soul-admin/src/App.tsx @@ -7,8 +7,8 @@ import { UsersPage } from './pages/users/UsersPage' import { DistributionPage } from './pages/distribution/DistributionPage' import { WithdrawalsPage } from './pages/withdrawals/WithdrawalsPage' import { ContentPage } from './pages/content/ContentPage' -import { ChaptersPage } from './pages/chapters/ChaptersPage' import { ReferralSettingsPage } from './pages/referral-settings/ReferralSettingsPage' +import { AuthorSettingsPage } from './pages/author-settings/AuthorSettingsPage' import { SettingsPage } from './pages/settings/SettingsPage' import { PaymentPage } from './pages/payment/PaymentPage' import { SitePage } from './pages/site/SitePage' @@ -18,6 +18,7 @@ import { MatchRecordsPage } from './pages/match-records/MatchRecordsPage' import { VipRolesPage } from './pages/vip-roles/VipRolesPage' import { MentorsPage } from './pages/mentors/MentorsPage' import { MentorConsultationsPage } from './pages/mentor-consultations/MentorConsultationsPage' +import { AdminUsersPage } from './pages/admin-users/AdminUsersPage' import { ApiDocPage } from './pages/api-doc/ApiDocPage' import { NotFoundPage } from './pages/not-found/NotFoundPage' @@ -33,11 +34,12 @@ function App() { } /> } /> } /> - } /> } /> + } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/soul-admin/src/components/ui/dialog.tsx b/soul-admin/src/components/ui/dialog.tsx index 763985ee..91ad8fa8 100644 --- a/soul-admin/src/components/ui/dialog.tsx +++ b/soul-admin/src/components/ui/dialog.tsx @@ -31,6 +31,7 @@ const DialogContent = React.forwardRef< { setMounted(true) }, []) + useEffect(() => { + const inMore = moreMenuItems.some((item) => location.pathname === item.href) + if (inMore) setMoreExpanded(true) + }, [location.pathname]) useEffect(() => { if (!mounted) return @@ -89,8 +103,8 @@ export function AdminLayout() {

Soul创业派对

-