重构小程序图标组件,替换传统 emoji 为 SVG 图标,提升视觉一致性和可维护性。更新多个页面以使用新图标组件,优化用户界面体验。同时,调整了数据加载逻辑,确保更高效的状态管理和用户交互。

This commit is contained in:
Alex-larget
2026-03-18 16:00:57 +08:00
parent 46f94a9c81
commit c55e54efbd
62 changed files with 2033 additions and 1270 deletions

View File

@@ -13,8 +13,8 @@ const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
App({
globalData: {
// API 基础地址:开发时修改下面一行切换环境
baseUrl: "https://soulapi.quwanzhi.com",
// baseUrl: 'http://localhost:8080', // 开发
// baseUrl: "https://soulapi.quwanzhi.com",
baseUrl: 'http://localhost:8080', // 开发
// baseUrl: 'https://souldev.quwanzhi.com', // 测试
// 小程序配置 - 真实AppID
appId: DEFAULT_APP_ID,
@@ -30,7 +30,7 @@ App({
openId: null, // 微信openId支付必需
isLoggedIn: false,
// 书籍数据
// 书籍数据bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters
bookData: null,
totalSections: 62,
@@ -76,7 +76,10 @@ App({
// 审核模式:后端 /api/miniprogram/config 返回 auditMode=true 时隐藏所有支付相关UI
auditMode: false,
// 客服/微信mp_config 返回 supportWechat
supportWechat: ''
supportWechat: '',
// config 统一缓存5min减少重复请求
configCache: null,
configCacheExpires: 0
},
onLaunch(options) {
@@ -109,15 +112,15 @@ App({
this.handleReferralCode(options)
},
// 小程序显示时:处理分享参数、检测更新、刷新 mpConfig(从后台切回时)
// 小程序显示时:处理分享参数、检测更新、刷新审核模式(从后台切回时)
onShow(options) {
this.handleReferralCode(options)
this.checkUpdate()
// 从后台切回时刷新审核模式等配置(节流 30 秒,避免频繁请求)
// 从后台切回时刷新审核模式(轻量接口 /config/audit-mode节流 30 秒
const now = Date.now()
if (!this.globalData.lastMpConfigCheck || now - this.globalData.lastMpConfigCheck > 30 * 1000) {
this.globalData.lastMpConfigCheck = now
this.loadMpConfig()
this.getAuditMode()
}
},
@@ -225,12 +228,6 @@ App({
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
},
// 根据业务 id 从 bookData 查 mid用于跳转
getSectionMid(sectionId) {
const list = this.globalData.bookData || []
const ch = list.find(c => c.id === sectionId)
return ch?.mid || 0
},
// 获取当前用户的邀请码(用于分享带 ref未登录返回空字符串
getMyReferralCode() {
@@ -321,32 +318,112 @@ App({
}
},
// 加载书籍数据
// 加载书籍数据totalSections不再预加载 all-chapters
async loadBookData() {
try {
// 先从缓存加载
const cachedData = wx.getStorageSync('bookData')
if (cachedData) {
this.globalData.bookData = cachedData
}
// 从服务器获取最新数据
const res = await this.request('/api/miniprogram/book/all-chapters')
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
this.globalData.bookData = chapters
this.globalData.totalSections = (chapters && chapters.length) ? chapters.length : 62
wx.setStorageSync('bookData', chapters)
const res = await this.request({ url: '/api/miniprogram/book/parts', silent: true })
if (res?.success && res.totalSections != null) {
this.globalData.totalSections = res.totalSections
}
} catch (e) {
console.error('加载书籍数据失败:', e)
try {
const statsRes = await this.request({ url: '/api/miniprogram/book/stats', silent: true })
if (statsRes?.success && statsRes?.data?.totalChapters != null) {
this.globalData.totalSections = statsRes.data.totalChapters
}
} catch (_) {}
}
},
/**
* 获取 config统一缓存 5min各页优先读缓存
* 使用拆分接口 core + audit-mode体积更小、审核模式独立刷新
* @param {boolean} forceRefresh - 强制刷新,跳过缓存
* @returns {Promise<object|null>} 完整 config 或 null
*/
async getConfig(forceRefresh = false) {
const now = Date.now()
const CACHE_TTL = 5 * 60 * 1000
if (!forceRefresh && this.globalData.configCache && now < this.globalData.configCacheExpires) {
return this.globalData.configCache
}
try {
const [coreRes, auditRes] = await Promise.all([
this.request({ url: '/api/miniprogram/config/core', silent: true, timeout: 5000 }),
this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 })
])
if (coreRes) {
const auditMode = auditRes && typeof auditRes.auditMode === 'boolean' ? auditRes.auditMode : false
const mp = (coreRes.mpConfig && typeof coreRes.mpConfig === 'object') ? { ...coreRes.mpConfig } : {}
mp.auditMode = auditMode
const res = {
success: coreRes.success,
prices: coreRes.prices,
features: coreRes.features,
userDiscount: coreRes.userDiscount,
mpConfig: mp
}
this.globalData.configCache = res
this.globalData.configCacheExpires = now + CACHE_TTL
return res
}
} catch (e) {
if (this.globalData.configCache) return this.globalData.configCache
}
return null
},
/**
* 获取阅读页扩展配置linkTags、linkedMiniprograms懒加载
*/
async getReadExtras() {
if (Array.isArray(this.globalData.linkTagsConfig) && this.globalData.linkTagsConfig.length > 0) {
return {
linkTags: this.globalData.linkTagsConfig,
linkedMiniprograms: this.globalData.linkedMiniprograms || []
}
}
try {
const res = await this.request({ url: '/api/miniprogram/config/read-extras', silent: true, timeout: 5000 })
if (res) {
if (Array.isArray(res.linkTags)) this.globalData.linkTagsConfig = res.linkTags
if (Array.isArray(res.linkedMiniprograms)) this.globalData.linkedMiniprograms = res.linkedMiniprograms
return res
}
} catch (e) {}
return { linkTags: [], linkedMiniprograms: [] }
},
/**
* 仅刷新审核模式(从后台切回时用,轻量)
*/
async getAuditMode() {
try {
const res = await this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 })
if (res && typeof res.auditMode === 'boolean') {
this.globalData.auditMode = res.auditMode
if (this.globalData.configCache && this.globalData.configCache.mpConfig) {
this.globalData.configCache.mpConfig.auditMode = res.auditMode
}
try {
const pages = getCurrentPages()
pages.forEach(p => {
if (p && p.data && 'auditMode' in p.data) {
p.setData({ auditMode: res.auditMode })
}
})
} catch (_) {}
return res.auditMode
}
} catch (e) {}
return this.globalData.auditMode
},
// 加载 mpConfigappId、mchId、withdrawSubscribeTmplId、auditMode、supportWechat 等),失败时保留 globalData 默认值
async loadMpConfig() {
try {
const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
const res = await this.getConfig()
if (!res) return
const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
if (mp && typeof mp === 'object') {
if (mp.appId) this.globalData.appId = mp.appId
@@ -423,6 +500,7 @@ App({
/**
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
* GET 请求 200ms 内相同 url 去重,避免并发重复请求。
* @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
* @param {object} options - { method, data, header, silent }
* @param {boolean} options.silent - 为 true 时不弹窗,仅 reject用于静默请求如访问统计
@@ -437,6 +515,7 @@ App({
} else {
url = ''
}
const method = (options.method || 'GET').toUpperCase()
const silent = !!options.silent
const showError = (msg) => {
if (!silent && msg) {
@@ -444,7 +523,16 @@ App({
}
}
return new Promise((resolve, reject) => {
// GET 短时去重:相同 url 的并发请求共享同一 promise
if (method === 'GET') {
const dedupKey = url + (options.data ? JSON.stringify(options.data) : '')
const pending = this._requestPending || (this._requestPending = {})
if (pending[dedupKey]) {
return pending[dedupKey].promise
}
}
const promise = new Promise((resolve, reject) => {
const token = wx.getStorageSync('token')
wx.request({
url: this.globalData.baseUrl + url,
@@ -492,6 +580,14 @@ App({
}
})
})
if (method === 'GET') {
const dedupKey = url + (options.data ? JSON.stringify(options.data) : '')
const pending = this._requestPending || (this._requestPending = {})
pending[dedupKey] = { promise, ts: Date.now() }
promise.finally(() => { delete pending[dedupKey] })
}
return promise
},
// 登录方法 - 获取openId用于支付加固错误处理避免审核报“登录报错”

View File

@@ -1,4 +1,7 @@
{
"usingComponents": {
"icon": "/components/icon/icon"
},
"pages": [
"pages/chapters/chapters",
"pages/index/index",

View File

@@ -0,0 +1,434 @@
@font-face {
font-family: "iconfont"; /* Project id 5142223 */
src: url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.woff2?t=1773819902347') format('woff2'),
url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.woff?t=1773819902347') format('woff'),
url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.ttf?t=1773819902347') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-wallet:before {
content: "\e6c8";
}
.icon-gift:before {
content: "\e6c9";
}
.icon-zap1:before {
content: "\e75c";
}
.icon-user:before {
content: "\e6b9";
}
.icon-upload:before {
content: "\e6ba";
}
.icon-work:before {
content: "\e6bb";
}
.icon-training:before {
content: "\e6bc";
}
.icon-warning:before {
content: "\e6bd";
}
.icon-zoom-in:before {
content: "\e6be";
}
.icon-zoom-out:before {
content: "\e6bf";
}
.icon-arrow-left-bold:before {
content: "\e6c1";
}
.icon-arrow-up-bold:before {
content: "\e6c2";
}
.icon-close-bold:before {
content: "\e6c3";
}
.icon-arrow-down-bold:before {
content: "\e6c4";
}
.icon-minus-bold:before {
content: "\e6c5";
}
.icon-arrow-right-bold:before {
content: "\e6c6";
}
.icon-select-bold:before {
content: "\e6c7";
}
.icon-money-wallet:before {
content: "\e833";
}
.icon-book-open:before {
content: "\e993";
}
.icon-biaoshilei_yonghuzu:before {
content: "\e61b";
}
.icon-add:before {
content: "\e664";
}
.icon-add-circle:before {
content: "\e665";
}
.icon-adjust:before {
content: "\e666";
}
.icon-arrow-up-circle:before {
content: "\e667";
}
.icon-arrow-right-circle:before {
content: "\e668";
}
.icon-arrow-down:before {
content: "\e669";
}
.icon-ashbin:before {
content: "\e66a";
}
.icon-arrow-right:before {
content: "\e66b";
}
.icon-browse:before {
content: "\e66c";
}
.icon-bottom:before {
content: "\e66d";
}
.icon-back:before {
content: "\e66e";
}
.icon-bad:before {
content: "\e66f";
}
.icon-arrow-left-circle:before {
content: "\e670";
}
.icon-camera:before {
content: "\e671";
}
.icon-chart-bar:before {
content: "\e672";
}
.icon-attachment:before {
content: "\e673";
}
.icon-code:before {
content: "\e674";
}
.icon-close:before {
content: "\e675";
}
.icon-check-item:before {
content: "\e676";
}
.icon-calendar:before {
content: "\e677";
}
.icon-comment:before {
content: "\e678";
}
.icon-complete:before {
content: "\e679";
}
.icon-direction-down:before {
content: "\e67a";
}
.icon-direction-down-circle:before {
content: "\e67b";
}
.icon-direction-right:before {
content: "\e67c";
}
.icon-direction-up:before {
content: "\e67d";
}
.icon-discount:before {
content: "\e67e";
}
.icon-direction-left:before {
content: "\e67f";
}
.icon-download:before {
content: "\e680";
}
.icon-electronics:before {
content: "\e681";
}
.icon-elipsis:before {
content: "\e682";
}
.icon-export:before {
content: "\e683";
}
.icon-explain:before {
content: "\e684";
}
.icon-edit:before {
content: "\e685";
}
.icon-eye-close:before {
content: "\e686";
}
.icon-email:before {
content: "\e687";
}
.icon-error:before {
content: "\e688";
}
.icon-favorite:before {
content: "\e689";
}
.icon-file-common:before {
content: "\e68a";
}
.icon-file-delete:before {
content: "\e68b";
}
.icon-file-add:before {
content: "\e68c";
}
.icon-film:before {
content: "\e68d";
}
.icon-fabulous:before {
content: "\e68e";
}
.icon-file:before {
content: "\e68f";
}
.icon-folder-close:before {
content: "\e690";
}
.icon-filter:before {
content: "\e691";
}
.icon-good:before {
content: "\e692";
}
.icon-hide:before {
content: "\e693";
}
.icon-home:before {
content: "\e694";
}
.icon-file-open:before {
content: "\e695";
}
.icon-forward:before {
content: "\e696";
}
.icon-import:before {
content: "\e697";
}
.icon-layers:before {
content: "\e698";
}
.icon-lock:before {
content: "\e699";
}
.icon-map:before {
content: "\e69a";
}
.icon-menu:before {
content: "\e69b";
}
.icon-help:before {
content: "\e69c";
}
.icon-minus-circle:before {
content: "\e69d";
}
.icon-notification:before {
content: "\e69e";
}
.icon-more:before {
content: "\e69f";
}
.icon-mobile-phone:before {
content: "\e6a0";
}
.icon-minus:before {
content: "\e6a1";
}
.icon-navigation:before {
content: "\e6a2";
}
.icon-prompt:before {
content: "\e6a3";
}
.icon-refresh:before {
content: "\e6a4";
}
.icon-run-up:before {
content: "\e6a5";
}
.icon-picture:before {
content: "\e6a6";
}
.icon-run-in:before {
content: "\e6a7";
}
.icon-pin:before {
content: "\e6a8";
}
.icon-save:before {
content: "\e6a9";
}
.icon-search:before {
content: "\e6aa";
}
.icon-share:before {
content: "\e6ab";
}
.icon-scanning:before {
content: "\e6ac";
}
.icon-security:before {
content: "\e6ad";
}
.icon-sign-out:before {
content: "\e6ae";
}
.icon-select:before {
content: "\e6af";
}
.icon-stop:before {
content: "\e6b0";
}
.icon-success:before {
content: "\e6b1";
}
.icon-switch:before {
content: "\e6b2";
}
.icon-setting:before {
content: "\e6b3";
}
.icon-survey:before {
content: "\e6b4";
}
.icon-time:before {
content: "\e6b5";
}
.icon-telephone:before {
content: "\e6b6";
}
.icon-top:before {
content: "\e6b7";
}
.icon-unlock:before {
content: "\e6b8";
}

View File

@@ -41,20 +41,55 @@ Component({
},
methods: {
// SVG 图标数据映射
// SVG 图标数据映射Lucide 风格,替换原 emoji
getSvgPath(name) {
const s = '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
const svgMap = {
'share': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
'arrow-up-right': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>',
'chevron-left': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
'search': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
'heart': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
'share': s + '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
'arrow-up-right': s + '<line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>',
'chevron-left': s + '<polyline points="15 18 9 12 15 6"/></svg>',
'search': s + '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
'heart': s + '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'user': s + '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
'smartphone': s + '<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>',
'map-pin': s + '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>',
'home': s + '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
'star': s + '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'message-circle': s + '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>',
'package': s + '<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
'book-open': s + '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',
'lightbulb': s + '<path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>',
'handshake': s + '<path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a1 1 0 0 0-1.4 1.4l.88.88"/><path d="M15 9 9.03 9"/><path d="m14 14-2.5-2.5"/><path d="m18 15-3-3"/><path d="m15 12-3-3"/><path d="m9 12 2 2"/></svg>',
'rocket': s + '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>',
'trophy': s + '<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>',
'refresh-cw': s + '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
'shield': s + '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
'wallet': s + '<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/></svg>',
'wrench': s + '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
'camera': s + '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>',
'phone': s + '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>',
'clipboard': s + '<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="6" width="8" height="12" rx="1"/></svg>',
'megaphone': s + '<path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>',
'image': s + '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
'gift': s + '<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>',
'lock': s + '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
'sparkles': s + '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>',
'save': s + '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
'globe': s + '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
'users': s + '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
'gamepad': s + '<line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg>',
'check': s + '<polyline points="20 6 9 17 4 12"/></svg>',
'trash-2': s + '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>',
'clock': s + '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
'plus': s + '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
'briefcase': s + '<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>',
'target': s + '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
'rotate-ccw': s + '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
'corner-down-left': s + '<polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg>',
'folder': s + '<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"/><line x1="12" y1="11" x2="12" y2="17"/></svg>',
'bar-chart': s + '<line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/></svg>',
'link': s + '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>'
}
return svgMap[name] || ''
},

View File

@@ -67,10 +67,7 @@ Component({
console.log('[TabBar] 开始加载功能配置...')
console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
// app.request 的第一个参数是 url 字符串,第二个参数是 options 对象
const res = await app.request('/api/miniprogram/config', {
method: 'GET'
})
const res = await app.getConfig()
// 兼容两种返回格式

View File

@@ -38,7 +38,7 @@
<!-- 书籍信息 -->
<view class="book-info-card" wx:if="{{bookInfo && !authorLoading}}">
<text class="card-title">📚 {{bookInfo.title}}</text>
<view class="card-title"><icon name="book-open" size="36" color="#00CED1" customClass="card-title-icon"></icon><text>{{bookInfo.title}}</text></view>
<view class="book-stats">
<view class="book-stat">
<text class="book-stat-value">{{bookInfo.totalChapters}}</text>
@@ -65,7 +65,7 @@
<view class="contact-card" wx:if="{{!authorLoading}}">
<text class="card-title">联系作者</text>
<view class="contact-item">
<text class="contact-icon">🎉</text>
<icon name="sparkles" size="40" color="#00CED1" customClass="contact-icon"></icon>
<view class="contact-info">
<text class="contact-label">Soul派对房</text>
<text class="contact-value">每天早上6-9点开播</text>

View File

@@ -17,7 +17,8 @@
.stat-value { font-size: 36rpx; font-weight: 700; color: #00CED1; display: block; }
.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.contact-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; }
.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 24rpx; }
.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 12rpx; margin-bottom: 24rpx; }
.card-title .card-title-icon { flex-shrink: 0; }
.contact-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 16rpx; }
.contact-item:last-child { margin-bottom: 0; }
.contact-icon { font-size: 40rpx; }

View File

@@ -18,7 +18,7 @@
<!-- 空状态 -->
<view class="empty-state" wx:elif="{{addressList.length === 0}}">
<text class="empty-icon">📍</text>
<icon name="map-pin" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
<text class="empty-text">暂无收货地址</text>
<text class="empty-tip">点击下方按钮添加</text>
</view>
@@ -50,7 +50,7 @@
bindtap="deleteAddress"
data-id="{{item.id}}"
>
<text class="action-icon">🗑️</text>
<icon name="trash-2" size="36" color="#ff3b30" customClass="action-icon"></icon>
<text class="action-text">删除</text>
</view>
</view>
@@ -59,7 +59,7 @@
<!-- 新增按钮 -->
<view class="add-btn" bindtap="addAddress">
<text class="add-icon"></text>
<icon name="plus" size="36" color="#00CED1" customClass="add-icon"></icon>
<text class="add-text">新增收货地址</text>
</view>
</view>

View File

@@ -15,7 +15,7 @@
<!-- 收货人 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">👤</text>
<icon name="user" size="36" color="#8e8e93" customClass="label-icon"></icon>
<text class="label-text">收货人</text>
</view>
<input
@@ -30,7 +30,7 @@
<!-- 手机号 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">📱</text>
<icon name="smartphone" size="36" color="#8e8e93" customClass="label-icon"></icon>
<text class="label-text">手机号</text>
</view>
<input
@@ -47,7 +47,7 @@
<!-- 地区选择 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">📍</text>
<icon name="map-pin" size="36" color="#8e8e93" customClass="label-icon"></icon>
<text class="label-text">所在地区</text>
</view>
<picker
@@ -65,7 +65,7 @@
<!-- 详细地址 -->
<view class="form-item">
<view class="form-label">
<text class="label-icon">🏠</text>
<icon name="home" size="36" color="#8e8e93" customClass="label-icon"></icon>
<text class="label-text">详细地址</text>
</view>
<textarea
@@ -82,7 +82,7 @@
<!-- 设为默认 -->
<view class="form-item form-switch">
<view class="form-label">
<text class="label-icon"></text>
<icon name="star" size="36" color="#8e8e93" customClass="label-icon"></icon>
<text class="label-text">设为默认地址</text>
</view>
<switch

View File

@@ -10,7 +10,7 @@
<view class="content">
<!-- 引导文案 -->
<view class="guide-card">
<text class="guide-icon">👋</text>
<icon name="handshake" size="64" color="#00CED1" customClass="guide-icon"></icon>
<text class="guide-title">完善头像和昵称</text>
<text class="guide-desc">让他人更好地认识你,展示更专业的形象</text>
</view>
@@ -22,7 +22,7 @@
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
</view>
<view class="avatar-camera">📷</view>
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
</view>
<text class="avatar-change">点击更换头像</text>
</view>

View File

@@ -65,7 +65,7 @@ Page({
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
return
}
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
const res = await app.getConfig()
const features = (res && res.features) || {}
const searchEnabled = features.searchEnabled !== false
if (!app.globalData.features) app.globalData.features = {}
@@ -76,18 +76,11 @@ Page({
}
},
// 懒加载:仅拉取篇章列表 + totalSections + fixedSections
// 优先 book/parts404 或失败时降级为 all-chapters 推导
// 懒加载:仅拉取篇章列表 + totalSections + fixedSectionsbook/parts不再用 all-chapters
async loadParts() {
this.setData({ partsLoading: true })
try {
let res
try {
res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
} catch (e) {
console.log('[Chapters] book/parts 失败,降级 all-chapters:', e?.message || e)
res = null
}
const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
let parts = []
let totalSections = 0
let fixedSections = []
@@ -95,21 +88,6 @@ Page({
parts = res.parts
totalSections = res.totalSections ?? 0
fixedSections = res.fixedSections || []
} else {
// 降级:从 all-chapters 推导 parts
const allRes = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const list = (allRes?.data || allRes?.chapters || [])
totalSections = list.length
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = (c) => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
const partMap = new Map()
list.filter(exclude).forEach(c => {
const pid = c.partId || c.part_id || 'default'
const ptitle = c.partTitle || c.part_title || '未分类'
if (!partMap.has(pid)) partMap.set(pid, { id: pid, title: ptitle, subtitle: '', chapterCount: 0 })
partMap.get(pid).chapterCount++
})
parts = Array.from(partMap.values())
}
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
const fixedMap = {}
@@ -185,7 +163,7 @@ Page({
else bookDataFlat.push(r)
})
app.globalData.bookData = bookDataFlat
wx.setStorageSync('bookData', bookDataFlat)
wx.setStorage({ key: 'bookData', data: bookDataFlat }) // 异步写入,避免阻塞主线程
this.setData({ bookData, _loadedChapters: loaded })
} catch (e) {
console.log('[Chapters] 加载章节失败:', e)
@@ -252,7 +230,7 @@ Page({
// 跳转到阅读页(优先传 mid与分享逻辑一致
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const mid = e.currentTarget.dataset.mid
trackClick('chapters', 'card_click', id || '章节')
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })

View File

@@ -314,7 +314,7 @@ Page({
goToArticle() {
const { detail } = this.data
if (!detail || detail.productType !== 'section' || !detail.productId) return
const mid = detail.productMid || app.getSectionMid?.(detail.productId)
const mid = detail.productMid
const q = mid ? `mid=${mid}` : `id=${detail.productId}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},

View File

@@ -89,7 +89,7 @@
<!-- 安全徽章(发起人视角不展示) -->
<view class="security-badge" wx:if="{{!isCreateMode && !isInitiator}}">
<text class="security-icon">🛡</text>
<icon name="shield" size="40" color="#00CED1" customClass="security-icon"></icon>
<text class="security-text">安全支付保障 · 资金由平台托管</text>
</view>
</block>
@@ -145,7 +145,7 @@
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可免费领取并阅读</text>
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">

View File

@@ -128,46 +128,35 @@ Page({
initData() {
this.setData({ loading: false })
this.loadBookData()
this.loadFeaturedFromServer()
this.loadFeaturedAndLatest()
this.loadSuperMembers()
this.loadLatestChapters()
},
async loadSuperMembers() {
this.setData({ superMembersLoading: true })
try {
// 优先加载 VIP 会员(购买 1980 fullbook/vip 订单的用户
// 并行请求 VIP 会员和普通用户,合并后取前 4 个VIP 优先
const [vipRes, usersRes] = await Promise.all([
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/users?limit=20', silent: true }).catch(() => null)
])
let members = []
try {
const res = await app.request({ url: '/api/miniprogram/vip/members', silent: true })
if (res && res.success && res.data) {
// 不再过滤无头像用户,无头像时用首字母展示
members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员', // 超级个体:用户资料优先,随「我的」修改实时生效
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) {
console.log('[Index] 超级个体加载成功:', members.length, '人')
}
}
} catch (e) {
console.log('[Index] vip/members 请求失败:', e)
if (vipRes && vipRes.success && Array.isArray(vipRes.data) && vipRes.data.length > 0) {
members = vipRes.data.slice(0, 4).map(u => ({
id: u.id,
name: u.nickname || u.vipName || u.vip_name || '会员',
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '')
}
// 不足 4 个则用有头像的普通用户补充
if (members.length < 4) {
try {
const dbRes = await app.request({ url: '/api/miniprogram/users?limit=20', silent: true })
if (dbRes && dbRes.success && dbRes.data) {
const existIds = new Set(members.map(m => m.id))
const extra = (Array.isArray(dbRes.data) ? dbRes.data : [])
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
.slice(0, 4 - members.length)
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
members = members.concat(extra)
}
} catch (e) {}
if (members.length < 4 && usersRes && usersRes.success && Array.isArray(usersRes.data)) {
const existIds = new Set(members.map(m => m.id))
const extra = usersRes.data
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
.slice(0, 4 - members.length)
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
members = members.concat(extra)
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
@@ -176,109 +165,69 @@ Page({
}
},
// 从服务端获取精选推荐最新更新stitch_soulbook/recommended、book/latest-chapters
async loadFeaturedFromServer() {
// 精选推荐 + 最新更新 + 最新列表:一次请求 recommended + latest-chapters,避免重复
async loadFeaturedAndLatest() {
try {
// 1. 精选推荐:优先用 book/recommended按阅读量+算法,带 热门/推荐/精选 标签)
let featured = []
try {
const recRes = await app.request({ url: '/api/miniprogram/book/recommended', silent: true })
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: s.tag || ['热门', '推荐', '精选'][i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
const excludeFixed = (c) => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
}
const toSection = (s, i, tagMap = ['热门', '推荐', '精选']) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
part: (s.part_title || s.partTitle || '').replace(/[_|]/g, ' ').trim(),
tag: s.tag || tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
})
// 兜底:无 recommended 时从 book/hot 取前3
const [recRes, latestRes] = await Promise.all([
app.request({ url: '/api/miniprogram/book/recommended', silent: true }).catch(() => null),
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
])
// 1. 精选推荐recommended → hot 兜底)
let featured = []
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
featured = recRes.data.map((s, i) => toSection(s, i))
}
if (featured.length === 0) {
try {
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
if (hotList.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = hotList.slice(0, 3).map((s, i) => ({
id: s.id || s.section_id,
mid: s.mid ?? s.MID ?? 0,
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
part: (s.partTitle || s.part_title || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
if (hotList.length > 0) featured = hotList.slice(0, 3).map((s, i) => toSection(s, i))
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
}
if (featured.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (valid.length > 0) {
const tagMap = ['热门', '推荐', '精选']
featured = valid
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
.slice(0, 3)
.map((s, i) => ({
id: s.id,
mid: s.mid ?? s.MID ?? 0,
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
part: (s.part_title || s.partTitle || '').replace(/[_|]/g, ' ').trim(),
tag: tagMap[i] || '精选',
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
}))
this.setData({ featuredSections: featured })
}
}
if (featured.length > 0) this.setData({ featuredSections: featured })
// 2. 最新更新:用 book/latest-chapters 取第1条排除「序言」「尾声」「附录」
try {
const latestRes = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(l => {
const pt = (l.part_title || l.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
// 2. 最新更新 + 最新列表(共用 latest-chapters 数据
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
const latestList = rawList.filter(excludeFixed)
if (latestList.length > 0) {
const l = latestList[0]
this.setData({
latestSection: {
id: l.id,
mid: l.mid ?? l.MID ?? 0,
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
part: l.part_title || l.partTitle || ''
}
})
if (latestList.length > 0) {
const l = latestList[0]
this.setData({
latestSection: {
id: l.id,
mid: l.mid ?? l.MID ?? 0,
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
part: l.part_title || l.partTitle || ''
}
})
}
} catch (e) {
// 兜底:从 all-chapters 取
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const valid = chapters.filter(c => {
const pt = (c.part_title || c.partTitle || '').toLowerCase()
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
})
if (valid.length > 0) {
valid.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
const latest = valid[0]
this.setData({
latestSection: {
id: latest.id,
mid: latest.mid ?? latest.MID ?? 0,
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
part: latest.part_title || latest.partTitle || ''
}
})
}
}
const latestChapters = latestList.slice(0, 20).map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
desc: '',
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
const display = this.data.latestExpanded ? latestChapters : latestChapters.slice(0, 5)
this.setData({ latestChapters, displayLatestChapters: display })
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
}
@@ -286,18 +235,18 @@ Page({
async loadBookData() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
if (res?.success) {
const total = res.totalSections ?? 0
const parts = res.parts || []
app.globalData.totalSections = total || 62
this.setData({
bookData: chapters,
totalSections: res.total || chapters.length || 62,
partCount: partIds.size || 5
totalSections: app.globalData.totalSections,
partCount: parts.length || 5
})
}
} catch (e) {
console.error('加载书籍数据失败:', e)
this.setData({ totalSections: app.globalData.totalSections || 62, partCount: 5 })
}
},
@@ -328,7 +277,7 @@ Page({
})
return
}
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
const res = await app.getConfig()
const features = (res && res.features) || {}
const mp = (res && res.mpConfig) || {}
const searchEnabled = features.searchEnabled !== false
@@ -349,11 +298,11 @@ Page({
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页(优先传 mid与分享逻辑一致)
// 跳转到阅读页(传 mid与分享一致;无 mid 时传 id
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid
trackClick('home', 'card_click', id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},
@@ -588,33 +537,6 @@ Page({
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
},
// 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章
async loadLatestChapters() {
try {
const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
const list = (res && res.data) ? res.data : []
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
const latest = list
.filter(exclude)
.slice(0, 20)
.map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
desc: '', // latest-chapters 不返回 content避免大表全量加载
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
})
const display = this.data.latestExpanded ? latest : latest.slice(0, 5)
this.setData({ latestChapters: latest, displayLatestChapters: display })
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
},
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
trackClick('home', 'card_click', '超级个体_' + (id || ''))
@@ -630,9 +552,8 @@ Page({
async onPullDownRefresh() {
await Promise.all([
this.loadBookData(),
this.loadFeaturedFromServer(),
this.loadSuperMembers(),
this.loadLatestChapters()
this.loadFeaturedAndLatest(),
this.loadSuperMembers()
])
this.updateUserStatus()
wx.stopPullDownRefresh()

View File

@@ -27,7 +27,7 @@
<!-- 搜索栏(根据配置显示) -->
<view class="search-bar" wx:if="{{searchEnabled}}" bindtap="goToSearch">
<view class="search-icon-wrap">
<text class="search-icon-text">🔍</text>
<icon name="search" size="40" color="#8e8e93" customClass="search-icon-text"></icon>
</view>
<text class="search-placeholder">搜索章节标题或内容...</text>
</view>

View File

@@ -14,10 +14,10 @@ const { trackClick } = require('../../utils/trackClick')
// 导师顾问:跳转到存客宝添加微信
// 团队招募:跳转到存客宝添加微信
let MATCH_TYPES = [
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: '', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: '🎮', matchFromDB: true, showJoinAfterMatch: true }
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: 'star', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: 'users', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: 'heart', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: 'gamepad', matchFromDB: true, showJoinAfterMatch: true }
]
let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
@@ -490,9 +490,9 @@ Page({
concept: concepts[index % concepts.length],
wechat: wechats[index % wechats.length],
commonInterests: [
{ icon: '📚', text: '都在读《创业派对》' },
{ icon: '💼', text: '对私域运营感兴趣' },
{ icon: '🎯', text: '相似的创业方向' }
{ icon: 'book-open', text: '都在读《创业派对》' },
{ icon: 'briefcase', text: '对私域运营感兴趣' },
{ icon: 'target', text: '相似的创业方向' }
]
}
},

View File

@@ -41,7 +41,7 @@
<text class="sphere-desc">¥1 = 1次匹配</text>
</block>
<block wx:else>
<text class="sphere-icon">👥</text>
<icon name="users" size="64" color="#00CED1" customClass="sphere-icon"></icon>
<text class="sphere-title">开始匹配</text>
<text class="sphere-desc">匹配{{currentTypeLabel}}</text>
</block>
@@ -68,7 +68,7 @@
bindtap="selectType"
data-type="{{item.id}}"
>
<text class="type-icon">{{item.icon}}</text>
<icon name="{{item.icon}}" size="48" color="{{selectedType === item.id ? '#00CED1' : '#8e8e93'}}" customClass="type-icon"></icon>
<text class="type-label {{selectedType === item.id ? 'text-brand' : ''}}">{{item.label}}</text>
</view>
</view>
@@ -86,14 +86,14 @@
<!-- 内层球体 -->
<view class="matching-core">
<view class="matching-core-inner">
<text class="matching-icon-v2">🔍</text>
<icon name="search" size="48" color="#00CED1" customClass="matching-icon-v2"></icon>
</view>
</view>
<!-- 粒子效果 -->
<view class="particle particle-1"></view>
<view class="particle particle-2">💫</view>
<view class="particle particle-3"></view>
<view class="particle particle-4">🌟</view>
<view class="particle particle-1"><icon name="sparkles" size="24" color="#FFD700"></icon></view>
<view class="particle particle-2"><icon name="sparkles" size="20" color="#00CED1"></icon></view>
<view class="particle particle-3"><icon name="star" size="22" color="#FFD700"></icon></view>
<view class="particle particle-4"><icon name="star" size="18" color="#00CED1"></icon></view>
<!-- 扩散波纹 -->
<view class="ripple-v2 ripple-v2-1"></view>
<view class="ripple-v2 ripple-v2-2"></view>
@@ -115,7 +115,7 @@
<view class="matched-state">
<!-- 成功动画 -->
<view class="success-icon-wrapper">
<text class="success-icon"></text>
<icon name="sparkles" size="64" color="#FFD700" customClass="success-icon"></icon>
</view>
<!-- 用户卡片 -->
@@ -139,7 +139,7 @@
<text class="section-title">共同兴趣</text>
<view class="interest-list">
<view class="interest-item" wx:for="{{currentMatch.commonInterests}}" wx:key="text">
<text class="interest-icon">{{item.icon}}</text>
<icon name="{{item.icon}}" size="28" color="#00CED1" customClass="interest-icon"></icon>
<text class="interest-text">{{item.text}}</text>
</view>
</view>
@@ -167,7 +167,7 @@
<!-- 成功状态 -->
<block wx:if="{{joinSuccess}}">
<view class="join-success-new">
<view class="success-icon-big"></view>
<view class="success-icon-big"><icon name="check" size="80" color="#34C759"></icon></view>
<text class="success-title-new">提交成功</text>
<text class="success-desc-new">工作人员将在24小时内与您联系</text>
</view>
@@ -178,7 +178,7 @@
<!-- 头部 -->
<view class="join-header">
<view class="join-icon-wrap">
<text class="join-icon">{{joinType === 'investor' ? '👥' : joinType === 'mentor' ? '❤️' : '🎮'}}</text>
<icon name="{{joinType === 'investor' ? 'users' : joinType === 'mentor' ? 'heart' : 'gamepad'}}" size="64" color="#00CED1" customClass="join-icon"></icon>
</view>
<text class="join-title">{{joinTypeLabel}}</text>
<text class="join-subtitle" wx:if="{{needBindFirst}}">请先绑定联系方式</text>
@@ -193,7 +193,7 @@
bindtap="switchContactType"
data-type="phone"
>
<text class="switch-icon">📱</text>
<icon name="smartphone" size="36" color="#00CED1" customClass="switch-icon"></icon>
<text>手机号</text>
</view>
<view
@@ -201,7 +201,7 @@
bindtap="switchContactType"
data-type="wechat"
>
<text class="switch-icon">💬</text>
<icon name="message-circle" size="36" color="#00CED1" customClass="switch-icon"></icon>
<text>微信号</text>
</view>
</view>
@@ -278,14 +278,14 @@
<view class="form-input-wrap">
<text class="form-label">手机号</text>
<view class="form-input-inner">
<text class="form-icon">📱</text>
<icon name="smartphone" size="36" color="#8e8e93" customClass="form-icon"></icon>
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
</view>
</view>
<view class="form-input-wrap">
<text class="form-label">微信号</text>
<view class="form-input-inner">
<text class="form-icon">💬</text>
<icon name="message-circle" size="36" color="#8e8e93" customClass="form-icon"></icon>
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
</view>
</view>

View File

@@ -25,7 +25,7 @@
<text class="profile-name">{{member.name}}</text>
<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 class="tag tag-region" wx:if="{{member.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="pin-icon"></icon><text>{{member.region}}</text></view>
</view>
</view>
</view>
@@ -33,7 +33,7 @@
<!-- 基本信息(未填写行已隐藏) -->
<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>
<icon name="user" size="48" color="#00CED1" customClass="card-icon"></icon>
<text class="card-label">基本信息</text>
</view>
<view class="card-body">
@@ -61,7 +61,7 @@
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockField" data-field="contact">
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
</view>
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact">📋</view>
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
</view>
</view>
<view class="field" wx:if="{{member.wechatRaw || member.wechatDisplay}}">
@@ -71,7 +71,7 @@
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockField" data-field="wechat">
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
</view>
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat">📋</view>
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
</view>
</view>
</view>
@@ -80,22 +80,22 @@
<!-- 个人故事(未填写行已隐藏) -->
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
<view class="card-head">
<text class="card-icon bulb">💡</text>
<icon name="lightbulb" size="48" color="#FFD700" customClass="card-icon bulb"></icon>
<text class="card-label">个人故事</text>
</view>
<view class="card-body">
<view class="story" wx:if="{{member.bestMonth}}">
<view class="story-head"><text class="story-icon">🏆</text><text class="story-q">最赚钱的一个月做的是什么</text></view>
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月做的是什么</text></view>
<text class="story-a">{{member.bestMonth}}</text>
</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>
<view class="story-head"><icon name="star" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最有成就感的一件事</text></view>
<text class="story-a">{{member.achievement}}</text>
</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>
<view class="story-head"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-icon turn"></icon><text class="story-q">人生的转折点</text></view>
<text class="story-a">{{member.turningPoint}}</text>
</view>
</view>
@@ -104,7 +104,7 @@
<!-- 互助需求(未填写行已隐藏) -->
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
<view class="card-head">
<text class="card-icon">🤝</text>
<icon name="handshake" size="48" color="#00CED1" customClass="card-icon"></icon>
<text class="card-label">互助需求</text>
</view>
<view class="card-body">
@@ -122,7 +122,7 @@
<!-- 项目介绍 -->
<view class="card" wx:if="{{member.project}}">
<view class="card-head">
<text class="card-icon rocket">🚀</text>
<icon name="rocket" size="48" color="#00CED1" customClass="card-icon rocket"></icon>
<text class="card-label">项目介绍</text>
</view>
<text class="proj-txt">{{member.project}}</text>
@@ -144,7 +144,7 @@
<text class="state-txt">加载中...</text>
</view>
<view class="state-wrap" wx:if="{{!loading && !member}}">
<text class="state-emoji">👤</text>
<icon name="user" size="80" color="#3a3a3c" customClass="state-emoji"></icon>
<text class="state-txt">暂无该超级个体信息</text>
</view>
</view>

View File

@@ -70,7 +70,7 @@
<view class="bottom-btn-area">
<view class="contact-btn" bindtap="onContactTap">
<text class="contact-icon">💬</text>
<icon name="message-circle" size="40" color="#00CED1" customClass="contact-icon"></icon>
<text>联系导师</text>
</view>
</view>

View File

@@ -9,7 +9,7 @@
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon">🔍</text>
<icon name="search" size="36" color="#8e8e93" customClass="search-icon"></icon>
<input
class="search-input"
placeholder="搜索导师、技能或行业..."

View File

@@ -116,7 +116,7 @@ Page({
async loadFeatureConfig() {
try {
const res = await app.request('/api/miniprogram/config')
const res = await app.getConfig()
const features = (res && res.features) || (res && res.data && res.data.features) || {}
const matchEnabled = features.matchEnabled === true
const referralEnabled = features.referralEnabled !== false
@@ -204,7 +204,7 @@ Page({
const recentChapters = Array.isArray(res.data.recentChapters)
? res.data.recentChapters.map((item) => ({
id: item.id,
mid: item.mid || app.getSectionMid(item.id),
mid: item.mid,
title: item.title || `章节 ${item.id}`
}))
: []
@@ -797,7 +797,7 @@ Page({
goToRead(e) {
const id = e.currentTarget.dataset.id
trackClick('my', 'card_click', id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const mid = e.currentTarget.dataset.mid
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},

View File

@@ -178,7 +178,7 @@
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal-content" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
@@ -204,14 +204,14 @@
<view class="form-input-wrap">
<text class="form-label">手机号</text>
<view class="form-input-inner">
<text class="form-icon">📱</text>
<icon name="smartphone" size="36" color="#8e8e93" customClass="form-icon"></icon>
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
</view>
</view>
<view class="form-input-wrap">
<text class="form-label">微信号</text>
<view class="form-input-inner">
<text class="form-icon">💬</text>
<icon name="message-circle" size="36" color="#8e8e93" customClass="form-icon"></icon>
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
</view>
</view>

View File

@@ -22,7 +22,7 @@
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
</view>
<view class="avatar-camera">📷</view>
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
</view>
<text class="avatar-change">更换头像</text>
</view>
@@ -55,7 +55,7 @@
<text class="form-label">地区</text>
<view class="form-input-wrap form-input-suffix">
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
<text class="form-suffix">📍</text>
<icon name="map-pin" size="32" color="#8e8e93" customClass="form-suffix"></icon>
</view>
</view>
</view>
@@ -80,7 +80,7 @@
<!-- 核心联系方式 -->
<view class="section">
<view class="section-title">
<text class="section-icon">📞</text>
<icon name="phone" size="40" color="#00CED1" customClass="section-icon"></icon>
<text>核心联系方式</text>
</view>
<view class="form-row">
@@ -96,7 +96,7 @@
<!-- 个人故事(仅 VIP 展示) -->
<view class="section" wx:if="{{isVip}}">
<view class="section-title">
<text class="section-icon">💡</text>
<icon name="lightbulb" size="40" color="#00CED1" customClass="section-icon"></icon>
<text>个人故事</text>
</view>
<view class="form-row">
@@ -116,7 +116,7 @@
<!-- 互助需求VIP 或 资源对接已填写时展示) -->
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
<view class="section-title">
<text class="section-icon">🤝</text>
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
<text>互助需求</text>
</view>
<view class="form-row">
@@ -132,7 +132,7 @@
<!-- 项目介绍(仅 VIP 展示) -->
<view class="section" wx:if="{{isVip}}">
<view class="section-title">
<text class="section-icon">🚀</text>
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
<text>项目介绍</text>
</view>
<view class="form-row">

View File

@@ -20,7 +20,7 @@
<text class="hero-name">{{profile.nickname || '未设置昵称'}}</text>
<view class="hero-tags">
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
<text class="tag tag-region" wx:if="{{profile.region}}">📍 {{profile.region}}</text>
<view class="tag tag-region" wx:if="{{profile.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="tag-icon"></icon><text>{{profile.region}}</text></view>
</view>
</view>
</view>
@@ -28,7 +28,7 @@
<!-- 基本信息 -->
<view class="section">
<view class="section-head">
<text class="section-icon">👤</text>
<icon name="user" size="40" color="#00CED1" customClass="section-icon"></icon>
<text class="section-title">基本信息</text>
</view>
<view class="section-body">
@@ -72,22 +72,22 @@
<!-- 个人故事 -->
<view class="section" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
<view class="section-head">
<text class="section-icon section-icon-yellow">💡</text>
<icon name="lightbulb" size="40" color="#FFD700" customClass="section-icon section-icon-yellow"></icon>
<text class="section-title">个人故事</text>
</view>
<view class="section-body">
<view class="story-block" wx:if="{{profile.storyBestMonth}}">
<view class="story-head"><text class="story-emoji">🏆</text><text class="story-label">最赚钱的一个月做的是什么</text></view>
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最赚钱的一个月做的是什么</text></view>
<text class="story-text">{{profile.storyBestMonth}}</text>
</view>
<view class="field-divider" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
<view class="story-block" wx:if="{{profile.storyAchievement}}">
<view class="story-head"><text class="story-emoji"></text><text class="story-label">最有成就感的一件事</text></view>
<view class="story-head"><icon name="star" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最有成就感的一件事</text></view>
<text class="story-text">{{profile.storyAchievement}}</text>
</view>
<view class="field-divider" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
<view class="story-block" wx:if="{{profile.storyTurning}}">
<view class="story-head"><text class="story-emoji">🔄</text><text class="story-label">人生的转折点</text></view>
<view class="story-head"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">人生的转折点</text></view>
<text class="story-text">{{profile.storyTurning}}</text>
</view>
</view>
@@ -96,7 +96,7 @@
<!-- 互助需求 -->
<view class="section" wx:if="{{profile.helpOffer || profile.helpNeed}}">
<view class="section-head">
<text class="section-icon">🤝</text>
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
<text class="section-title">互助需求</text>
</view>
<view class="section-body">
@@ -114,7 +114,7 @@
<!-- 项目介绍 -->
<view class="section" wx:if="{{profile.projectIntro}}">
<view class="section-head">
<text class="section-icon">🚀</text>
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
<text class="section-title">项目介绍</text>
</view>
<view class="section-body">

View File

@@ -47,7 +47,8 @@
.hero-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; }
.tag { padding: 8rpx 24rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 500; }
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94,234,212,0.2); }
.tag-region { background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
.tag-region { display: flex; align-items: center; gap: 8rpx; background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
.tag-region .tag-icon { flex-shrink: 0; }
/* 通用区块 */
.section {

View File

@@ -40,7 +40,7 @@ Page({
const orders = purchasedSections.map((id, index) => ({
id: `order_${index}`,
sectionId: id,
sectionMid: app.getSectionMid(id),
sectionMid: 0,
title: `章节 ${id}`,
amount: 1,
status: 'completed',
@@ -52,7 +52,7 @@ Page({
const purchasedSections = app.globalData.purchasedSections || []
this.setData({
orders: purchasedSections.map((id, i) => ({
id: `order_${i}`, sectionId: id, sectionMid: app.getSectionMid(id), title: `章节 ${id}`, amount: 1, status: 'completed',
id: `order_${i}`, sectionId: id, sectionMid: 0, title: `章节 ${id}`, amount: 1, status: 'completed',
createTime: new Date(Date.now() - i * 86400000).toLocaleDateString()
}))
})

View File

@@ -28,7 +28,7 @@
</view>
<view class="empty" wx:else>
<text class="empty-icon">📦</text>
<icon name="package" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
<text class="empty-text">暂无订单</text>
</view>
</view>

View File

@@ -91,16 +91,21 @@ Page({
async onLoad(options) {
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
// 预加载 configlinkTags、auditMode 等(阅读页直接进入时需主动拉取最新审核状态
app.request({ url: '/api/miniprogram/config', silent: true }).then(cfg => {
// 预加载core+auditModegetConfig+ read-extras 懒加载linkTags、linkedMiniprograms
Promise.all([
app.getConfig(),
app.getReadExtras()
]).then(([cfg, extras]) => {
if (cfg) {
if (Array.isArray(cfg.linkTags)) app.globalData.linkTagsConfig = cfg.linkTags
if (Array.isArray(cfg.linkedMiniprograms)) app.globalData.linkedMiniprograms = cfg.linkedMiniprograms
const mp = (cfg && cfg.mpConfig) || {}
const auditMode = !!mp.auditMode
app.globalData.auditMode = auditMode
if (typeof this.setData === 'function') this.setData({ auditMode })
}
if (extras && Array.isArray(extras.linkTags)) {
app.globalData.linkTagsConfig = extras.linkTags
app.globalData.linkedMiniprograms = extras.linkedMiniprograms || []
}
}).catch(() => {})
// 支持 scene扫码、mid、id、ref、gift代付
@@ -120,21 +125,15 @@ Page({
console.log("页面:",mid);
// mid 有值但无 id 时, bookData 或 API 解析 id
// 兼容:mid 有值但无 id 时, by-mid 解析 id有 id 无 mid 时,后续用 by-id 请求
if (mid && !id) {
const bookData = app.globalData.bookData || []
const ch = bookData.find(c => c.mid == mid || (c.mid && Number(c.mid) === Number(mid)))
if (ch?.id) {
id = ch.id
} else {
try {
const resolveUrl = `/api/miniprogram/book/chapter/by-mid/${mid}`
const uid = app.globalData.userInfo?.id
const chRes = await app.request({ url: uid ? resolveUrl + '?userId=' + encodeURIComponent(uid) : resolveUrl, silent: true })
if (chRes && chRes.id) id = chRes.id
} catch (e) {
console.warn('[Read] by-mid 解析失败:', e)
}
try {
const resolveUrl = `/api/miniprogram/book/chapter/by-mid/${mid}`
const uid = app.globalData.userInfo?.id
const chRes = await app.request({ url: uid ? resolveUrl + '?userId=' + encodeURIComponent(uid) : resolveUrl, silent: true })
if (chRes && chRes.id) id = chRes.id
} catch (e) {
console.warn('[Read] by-mid 解析失败:', e)
}
}
@@ -186,8 +185,8 @@ Page({
readingTracker.init(id)
}
// 5. 加载导航
this.loadNavigation(id)
// 5. 导航:文章详情已带 prev/next
this._applyPrevNext(chapterRes)
} catch (e) {
console.error('[Read] 初始化失败:', e)
@@ -342,7 +341,7 @@ Page({
return titles[id] || `章节 ${id}`
},
// 根据 id/mid 构造章节接口路径优先使用 mid)。必须带 userId 才能让后端正确判断付费用户并返回完整内容
// 根据 id/mid 构造章节接口路径优先 midby-mid否则用 idby-id兼容旧链接
_getChapterUrl(params = {}) {
const { id, mid } = params
const finalMid = (mid !== undefined && mid !== null) ? mid : this.data.sectionMid
@@ -351,7 +350,7 @@ Page({
url = `/api/miniprogram/book/chapter/by-mid/${finalMid}`
} else {
const finalId = id || this.data.sectionId
url = `/api/miniprogram/book/chapter/${finalId}`
url = `/api/miniprogram/book/chapter/by-id/${encodeURIComponent(finalId)}`
}
const userId = app.globalData.userInfo?.id
if (userId) url += (url.includes('?') ? '&' : '?') + 'userId=' + encodeURIComponent(userId)
@@ -446,47 +445,21 @@ Page({
},
// 加载导航:基于后端章节顺序计算上一篇/下一篇
async loadNavigation(id) {
try {
// 优先使用全局缓存的 bookData
let chapters = app.globalData.bookData || []
if (!chapters || !Array.isArray(chapters) || chapters.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
chapters = (res && (res.data || res.chapters)) || []
}
if (!chapters || chapters.length === 0) {
this.setData({ prevSection: null, nextSection: null })
return
}
// 过滤掉没有 id 的记录,并按 sort_order + id 排序
const ordered = chapters
.filter(c => c.id)
.sort((a, b) => {
const soA = typeof a.sort_order === 'number' ? a.sort_order : (typeof a.sortOrder === 'number' ? a.sortOrder : 0)
const soB = typeof b.sort_order === 'number' ? b.sort_order : (typeof b.sortOrder === 'number' ? b.sortOrder : 0)
if (soA !== soB) return soA - soB
return String(a.id).localeCompare(String(b.id), 'zh-Hans-CN')
})
const index = ordered.findIndex(c => String(c.id) === String(id))
const prev = index > 0 ? ordered[index - 1] : null
const next = index >= 0 && index < ordered.length - 1 ? ordered[index + 1] : null
this.setData({
prevSection: prev ? {
id: prev.id,
mid: prev.mid ?? prev.MID ?? null,
title: prev.section_title || prev.sectionTitle || prev.title || this.getSectionTitle(prev.id),
} : null,
nextSection: next ? {
id: next.id,
mid: next.mid ?? next.MID ?? null,
title: next.section_title || next.sectionTitle || next.title || this.getSectionTitle(next.id),
} : null,
})
} catch (e) {
console.warn('[Read] loadNavigation failed:', e)
this.setData({ prevSection: null, nextSection: null })
}
_applyPrevNext(res) {
const prev = res?.prev
const next = res?.next
this.setData({
prevSection: prev ? {
id: prev.id,
mid: prev.mid ?? null,
title: prev.title || this.getSectionTitle(prev.id),
} : null,
nextSection: next ? {
id: next.id,
mid: next.mid ?? null,
title: next.title || this.getSectionTitle(next.id),
} : null,
})
},
// 返回(从分享进入无栈时回首页)
@@ -1304,14 +1277,15 @@ Page({
try {
const ctx = wx.createCanvasContext('posterCanvas', this)
const { section, contentParagraphs, sectionId } = this.data
const { section, contentParagraphs, sectionId, sectionMid } = this.data
const userInfo = app.globalData.userInfo
const userId = userInfo?.id || ''
// 获取小程序码(带推荐人参数)
// 获取小程序码(带推荐人参数,优先 mid 与新链接一致
let qrcodeImage = null
try {
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const scene = userId ? `${q}&ref=${userId.slice(0,10)}` : q
const qrRes = await app.request('/api/miniprogram/qrcode', {
method: 'POST',
data: { scene, page: 'pages/read/read', width: 280 }
@@ -1545,8 +1519,7 @@ Page({
readingTracker.init(this.data.sectionId)
}
// 加载导航
this.loadNavigation(this.data.sectionId)
this._applyPrevNext(chapterRes)
wx.hideLoading()
wx.showToast({ title: '加载成功', icon: 'success' })

View File

@@ -87,7 +87,7 @@
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
<text class="btn-end-text">已是最后一篇</text>
</view>
</view>
@@ -95,15 +95,15 @@
<view class="action-section">
<view class="action-row-inline">
<view class="action-btn-inline btn-share-inline" bindtap="onShareTimelineTap">
<text class="action-icon-small">📣</text>
<icon name="megaphone" size="32" color="#00CED1" customClass="action-icon-small"></icon>
<text class="action-text-small">分享到朋友圈</text>
</view>
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
<text class="action-icon-small">🖼️</text>
<icon name="image" size="32" color="#00CED1" customClass="action-icon-small"></icon>
<text class="action-text-small">生成海报</text>
</view>
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
<text class="action-icon-small">🎁</text>
<icon name="gift" size="32" color="#00CED1" customClass="action-icon-small"></icon>
<text class="action-text-small">代付分享</text>
</view>
</view>
@@ -126,7 +126,7 @@
<!-- 付费墙 - 未登录 -->
<view class="paywall">
<view class="paywall-icon">🔒</view>
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="paywall-title">登录后继续阅读</text>
<text class="paywall-desc">已阅读50%,登录后查看完整内容</text>
@@ -160,7 +160,7 @@
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
<text class="btn-end-text">已是最后一篇</text>
</view>
</view>
</view>
@@ -177,7 +177,7 @@
<!-- 付费墙 - 已登录未购买 -->
<view class="paywall">
<view class="paywall-icon">🔒</view>
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="paywall-title">解锁完整内容</text>
<text class="paywall-desc">已阅读50%,购买后继续阅读</text>
@@ -192,7 +192,7 @@
<!-- 解锁全书 - 只有购买超过3章才显示 -->
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
<view class="btn-left">
<text class="btn-sparkle"></text>
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
</view>
<view class="btn-right">
@@ -206,7 +206,7 @@
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
<!-- 代付分享:帮好友购买(审核模式隐藏) -->
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
<text class="gift-share-icon">🎁</text>
<icon name="gift" size="40" color="#00CED1" customClass="gift-share-icon"></icon>
<text class="gift-share-text">代付分享</text>
</view>
</view>
@@ -236,7 +236,7 @@
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
<text class="btn-end-text">已是最后一篇</text>
</view>
</view>
</view>
@@ -279,7 +279,7 @@
<view class="poster-actions">
<view class="poster-btn btn-save" bindtap="savePoster">
<text class="btn-icon">💾</text>
<icon name="save" size="36" color="#8e8e93" customClass="btn-icon"></icon>
<text>保存到相册</text>
</view>
</view>
@@ -292,7 +292,7 @@
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
@@ -321,6 +321,6 @@
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
<view class="fab-share" bindtap="shareToMoments">
<text class="fab-moments-icon">🌐</text>
<icon name="globe" size="40" color="#ffffff" customClass="fab-moments-icon"></icon>
</view>
</view>

View File

@@ -121,7 +121,7 @@
<view class="binding-list">
<block wx:if="{{currentBindings.length === 0}}">
<view class="empty-state">
<text class="empty-icon">👤</text>
<icon name="user" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
<text class="empty-text">暂无用户</text>
</view>
</block>
@@ -133,7 +133,7 @@
>
<view class="user-avatar {{item.status === 'converted' ? 'avatar-converted' : item.status === 'expired' ? 'avatar-expired' : ''}}">
<text wx:if="{{item.status === 'converted'}}">✓</text>
<text wx:elif="{{item.status === 'expired'}}">⏰</text>
<icon wx:elif="{{item.status === 'expired'}}" name="clock" size="28" color="#ff9500"></icon>
<text wx:else>{{item.nickname[0] || '用'}}</text>
</view>
<view class="user-info">

View File

@@ -112,7 +112,7 @@ Page({
goToRead(e) {
const id = e.currentTarget.dataset.id
trackClick('search', 'card_click', id || '章节')
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const mid = e.currentTarget.dataset.mid
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},

View File

@@ -8,7 +8,7 @@
<text class="back-icon">←</text>
</view>
<view class="search-input-wrap">
<view class="search-icon-small">🔍</view>
<view class="search-icon-small"><icon name="search" size="36" color="#8e8e93"></icon></view>
<input
class="search-input"
placeholder="搜索章节标题或内容..."
@@ -111,7 +111,7 @@
<!-- 无结果 -->
<view class="empty-wrap" wx:elif="{{!loading}}">
<text class="empty-icon">🔍</text>
<icon name="search" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
<text class="empty-text">未找到相关章节</text>
<text class="empty-hint">换个关键词试试</text>
</view>

View File

@@ -13,7 +13,7 @@
<!-- 账号绑定 -->
<view class="bind-card" wx:if="{{isLoggedIn}}">
<view class="card-header">
<text class="card-icon">🛡️</text>
<icon name="shield" size="48" color="#00CED1" customClass="card-icon"></icon>
<view class="card-title-wrap">
<text class="card-title">账号绑定</text>
<text class="card-desc">绑定后可用于提现和找伙伴功能</text>
@@ -24,7 +24,7 @@
<!-- 手机号 - 使用微信一键获取 -->
<view class="bind-item">
<view class="bind-left">
<view class="bind-icon phone-icon">📱</view>
<view class="bind-icon phone-icon"><icon name="smartphone" size="40" color="#00CED1"></icon></view>
<view class="bind-info">
<text class="bind-label">手机号</text>
<text class="bind-value">{{phoneNumber || '未绑定'}}</text>
@@ -41,7 +41,7 @@
<!-- 微信号 - 简化输入 -->
<view class="bind-item">
<view class="bind-left">
<view class="bind-icon wechat-icon">💬</view>
<view class="bind-icon wechat-icon"><icon name="message-circle" size="40" color="#00CED1"></icon></view>
<view class="bind-info">
<text class="bind-label">微信号</text>
<input
@@ -61,7 +61,7 @@
<!-- 收货地址 - 跳转到地址管理页 -->
<view class="bind-item" bindtap="goToAddresses">
<view class="bind-left">
<view class="bind-icon address-icon">📍</view>
<view class="bind-icon address-icon"><icon name="map-pin" size="40" color="#00CED1"></icon></view>
<view class="bind-info">
<text class="bind-label">收货地址</text>
<text class="bind-value address-text">管理收货地址,用于发货与邮寄</text>
@@ -77,7 +77,7 @@
<!-- 自动提现设置 -->
<view class="bind-card auto-withdraw-card" wx:if="{{isLoggedIn && wechatId}}">
<view class="card-header">
<text class="card-icon">💰</text>
<icon name="wallet" size="48" color="#00CED1" customClass="card-icon"></icon>
<view class="card-title-wrap">
<text class="card-title">自动提现</text>
<text class="card-desc">收益自动打款到微信零钱</text>
@@ -112,7 +112,7 @@
<!-- 开发专用:切换账号(仅开发版显示) -->
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="openSwitchAccountModal">
<view class="dev-switch-inner">
<text class="dev-switch-icon">🔧</text>
<icon name="wrench" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
<text class="dev-switch-text">切换账号(开发)</text>
<text class="dev-switch-desc">输入 userId 切换为其他账号调试</text>
</view>

View File

@@ -12,15 +12,15 @@ Page({
originalPrice: 6980,
/* 按 premium_membership_landing_v1 设计稿 */
contentRights: [
{ title: '解锁全部章节', desc: '365天全案精读', icon: '📖' },
{ title: '案例库', desc: '100+创业实战案例', icon: '📚' },
{ title: '智能纪要', desc: 'AI每日精华推送', icon: '💡' },
{ title: '会议纪要库', desc: '往期完整沉淀', icon: '📁' }
{ title: '解锁全部章节', desc: '365天全案精读', icon: 'book-open' },
{ title: '案例库', desc: '100+创业实战案例', icon: 'book-open' },
{ title: '智能纪要', desc: 'AI每日精华推送', icon: 'lightbulb' },
{ title: '会议纪要库', desc: '往期完整沉淀', icon: 'folder' }
],
socialRights: [
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: '👥' },
{ title: '创业老板排行', desc: '项目曝光展示', icon: '📊' },
{ title: '链接资源', desc: '深度私域资源池', icon: '🔗' },
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: 'users' },
{ title: '创业老板排行', desc: '项目曝光展示', icon: 'bar-chart' },
{ title: '链接资源', desc: '深度私域资源池', icon: 'link' },
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: '✓' }
],
purchasing: false

View File

@@ -23,7 +23,7 @@
<text class="rights-col-title">内容权益</text>
</view>
<view class="benefit-card" wx:for="{{contentRights}}" wx:key="title">
<text class="benefit-icon">{{item.icon || '✓'}}</text>
<icon name="{{item.icon || 'check'}}" size="40" color="#00CED1" customClass="benefit-icon"></icon>
<view class="benefit-info">
<text class="benefit-title">{{item.title}}</text>
<text class="benefit-desc">{{item.desc}}</text>
@@ -36,7 +36,7 @@
<text class="rights-col-title rights-col-title-gold">社交权益</text>
</view>
<view class="benefit-card" wx:for="{{socialRights}}" wx:key="title">
<text class="benefit-icon benefit-icon-gold">{{item.icon || '✓'}}</text>
<icon name="{{item.icon || 'check'}}" size="40" color="#FFD700" customClass="benefit-icon benefit-icon-gold"></icon>
<view class="benefit-info">
<text class="benefit-title">{{item.title}}</text>
<text class="benefit-desc">{{item.desc}}</text>

View File

@@ -59,10 +59,10 @@
<view class="transactions" wx:if="{{transactions.length > 0}}">
<view class="tx-item" wx:for="{{transactions}}" wx:key="id">
<view class="tx-icon {{item.type}}">
<text wx:if="{{item.type === 'recharge'}}">💰</text>
<text wx:elif="{{item.type === 'gift'}}">🎁</text>
<text wx:elif="{{item.type === 'refund'}}">↩️</text>
<text wx:elif="{{item.type === 'consume'}}">📖</text>
<icon wx:if="{{item.type === 'recharge'}}" name="wallet" size="36" color="#34C759"></icon>
<icon wx:elif="{{item.type === 'gift'}}" name="gift" size="36" color="#00CED1"></icon>
<icon wx:elif="{{item.type === 'refund'}}" name="corner-down-left" size="36" color="#ff9500"></icon>
<icon wx:elif="{{item.type === 'consume'}}" name="book-open" size="36" color="#8e8e93"></icon>
<text wx:else>•</text>
</view>
<view class="tx-info">

View File

@@ -22,8 +22,8 @@ class ChapterAccessManager {
*/
async fetchLatestConfig() {
try {
const res = await app.request({ url: '/api/miniprogram/config', silent: true, timeout: 3000 })
if (res.success && res.prices) {
const res = await app.getConfig()
if (res && res.success && res.prices) {
return {
prices: res.prices || { section: 1, fullbook: 9.9 }
}