diff --git a/miniprogram/.gitignore b/miniprogram/.gitignore
index 50bae59b..14ea590c 100644
--- a/miniprogram/.gitignore
+++ b/miniprogram/.gitignore
@@ -1,10 +1,14 @@
-# 小程序上传密钥(敏感信息,请勿上传)
-private.key
-private.*.key
+# Windows
+[Dd]esktop.ini
+Thumbs.db
+$RECYCLE.BIN/
-# 预览二维码
-preview.jpg
-
-# 微信开发者工具生成的文件
+# macOS
.DS_Store
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+
+# Node.js
node_modules/
diff --git a/miniprogram/app.js b/miniprogram/app.js
index e0c3901b..8b507f28 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -10,6 +10,9 @@ App({
// 小程序配置 - 真实AppID
appId: 'wxb8bbb2b10dec74aa',
+
+ // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
+ withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
// 微信支付配置
mchId: '1318592501', // 商户号
@@ -27,6 +30,9 @@ App({
purchasedSections: [],
hasFullBook: false,
+ // 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
+ readSectionIds: [],
+
// 推荐绑定
pendingReferralCode: null, // 待绑定的推荐码
@@ -49,6 +55,7 @@ App({
},
onLaunch(options) {
+ this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
// 获取系统信息
this.getSystemInfo()
@@ -80,21 +87,16 @@ App({
// 立即记录访问(不需要登录,用于统计"通过链接进的人数")
this.recordReferralVisit(refCode)
+
+ // 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺)
+ this.globalData.pendingReferralCode = refCode
+ wx.setStorageSync('pendingReferralCode', refCode)
+ // 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
+ wx.setStorageSync('referral_code', refCode)
- // 检查是否已经绑定过
- const boundRef = wx.getStorageSync('boundReferralCode')
- if (boundRef && boundRef !== refCode) {
- console.log('[App] 已绑定过其他推荐码,不更换绑定关系')
- // 但仍然记录访问,不return
- } else {
- // 保存待绑定的推荐码
- this.globalData.pendingReferralCode = refCode
- wx.setStorageSync('pendingReferralCode', refCode)
-
- // 如果已登录,立即绑定
- if (this.globalData.isLoggedIn && this.globalData.userInfo) {
- this.bindReferralCode(refCode)
- }
+ // 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
+ if (this.globalData.isLoggedIn && this.globalData.userInfo) {
+ this.bindReferralCode(refCode)
}
}
},
@@ -106,7 +108,7 @@ App({
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
const userId = this.globalData.userInfo?.id || ''
- await this.request('/api/referral/visit', {
+ await this.request('/api/miniprogram/referral/visit', {
method: 'POST',
data: {
referralCode: refCode,
@@ -114,7 +116,8 @@ App({
visitorId: userId,
source: 'miniprogram',
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
- }
+ },
+ silent: true
})
console.log('[App] 记录推荐访问成功')
} catch (e) {
@@ -129,26 +132,21 @@ App({
const userId = this.globalData.userInfo?.id
if (!userId || !refCode) return
- // 检查是否已绑定
- const boundRef = wx.getStorageSync('boundReferralCode')
- if (boundRef) {
- console.log('[App] 已绑定推荐码,跳过')
- return
- }
-
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
// 调用API绑定推荐关系
- const res = await this.request('/api/referral/bind', {
+ const res = await this.request('/api/miniprogram/referral/bind', {
method: 'POST',
data: {
userId,
referralCode: refCode
- }
+ },
+ silent: true
})
if (res.success) {
console.log('[App] 推荐码绑定成功')
+ // 仅记录当前已绑定的推荐码,用于展示/调试;是否允许更换由后端根据30天规则判断
wx.setStorageSync('boundReferralCode', refCode)
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
@@ -203,9 +201,10 @@ App({
// 从服务器获取最新数据
const res = await this.request('/api/book/all-chapters')
- if (res && res.data) {
- this.globalData.bookData = res.data
- wx.setStorageSync('bookData', res.data)
+ if (res && (res.data || res.chapters)) {
+ const chapters = res.data || res.chapters || []
+ this.globalData.bookData = chapters
+ wx.setStorageSync('bookData', chapters)
}
} catch (e) {
console.error('加载书籍数据失败:', e)
@@ -244,11 +243,41 @@ App({
}
},
- // 统一请求方法
- request(url, options = {}) {
+ /**
+ * 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段)
+ */
+ _getApiErrorMsg(data, defaultMsg = '请求失败') {
+ if (!data || typeof data !== 'object') return defaultMsg
+ const msg = data.message || data.error
+ return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
+ },
+
+ /**
+ * 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
+ * @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
+ * @param {object} options - { method, data, header, silent }
+ * @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计)
+ */
+ request(urlOrOptions, options = {}) {
+ let url
+ if (typeof urlOrOptions === 'string') {
+ url = urlOrOptions
+ } else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) {
+ url = urlOrOptions.url
+ options = { ...urlOrOptions, url: undefined }
+ } else {
+ url = ''
+ }
+ const silent = !!options.silent
+ const showError = (msg) => {
+ if (!silent && msg) {
+ wx.showToast({ title: msg, icon: 'none', duration: 2500 })
+ }
+ }
+
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token')
-
+
wx.request({
url: this.globalData.baseUrl + url,
method: options.method || 'GET',
@@ -259,38 +288,50 @@ App({
...options.header
},
success: (res) => {
+ const data = res.data
if (res.statusCode === 200) {
- resolve(res.data)
- } else if (res.statusCode === 401) {
- // 未授权,清除登录状态
- this.logout()
- reject(new Error('未授权'))
- } else {
- reject(new Error(res.data?.message || '请求失败'))
+ // 业务失败:success === false,soul-api 用 message 或 error 返回原因
+ if (data && data.success === false) {
+ const msg = this._getApiErrorMsg(data, '操作失败')
+ showError(msg)
+ reject(new Error(msg))
+ return
+ }
+ resolve(data)
+ return
}
+ if (res.statusCode === 401) {
+ this.logout()
+ showError('未授权,请重新登录')
+ reject(new Error('未授权'))
+ return
+ }
+ // 4xx/5xx:优先用返回体的 message/error
+ const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
+ showError(msg)
+ reject(new Error(msg))
},
fail: (err) => {
- reject(err)
+ const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
+ showError(msg)
+ reject(new Error(msg))
}
})
})
},
- // 登录方法 - 获取openId用于支付
+ // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
async login() {
try {
- // 获取微信登录code
const loginRes = await new Promise((resolve, reject) => {
- wx.login({
- success: resolve,
- fail: reject
- })
+ wx.login({ success: resolve, fail: reject })
})
-
- console.log('[App] 获取登录code成功')
-
+ if (!loginRes || !loginRes.code) {
+ console.warn('[App] wx.login 未返回 code')
+ wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
+ return null
+ }
try {
- // 发送code到服务器获取openId
const res = await this.request('/api/miniprogram/login', {
method: 'POST',
data: { code: loginRes.code }
@@ -362,6 +403,20 @@ App({
if (res.success && res.data?.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
+ // 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
+ if (res.data.user) {
+ this.globalData.userInfo = res.data.user
+ this.globalData.isLoggedIn = true
+ this.globalData.purchasedSections = res.data.user.purchasedSections || []
+ this.globalData.hasFullBook = res.data.user.hasFullBook || false
+ wx.setStorageSync('userInfo', res.data.user)
+ wx.setStorageSync('token', res.data.token || '')
+ const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
+ if (pendingRef) {
+ console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
+ this.bindReferralCode(pendingRef)
+ }
+ }
return res.data.openId
}
} catch (e) {
@@ -378,13 +433,19 @@ App({
return null
},
- // 手机号登录
+ // 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
async loginWithPhone(phoneCode) {
try {
- // 尝试API登录
- const res = await this.request('/api/wechat/phone-login', {
+ const loginRes = await new Promise((resolve, reject) => {
+ wx.login({ success: resolve, fail: reject })
+ })
+ if (!loginRes.code) {
+ wx.showToast({ title: '获取登录态失败', icon: 'none' })
+ return null
+ }
+ const res = await this.request('/api/miniprogram/phone-login', {
method: 'POST',
- data: { code: phoneCode }
+ data: { code: loginRes.code, phoneCode }
})
if (res.success && res.data) {
@@ -430,6 +491,21 @@ App({
return this.globalData.purchasedSections.includes(sectionId)
},
+ // 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计)
+ markSectionAsRead(sectionId) {
+ if (!sectionId) return
+ const list = this.globalData.readSectionIds || []
+ if (list.includes(sectionId)) return
+ list.push(sectionId)
+ this.globalData.readSectionIds = list
+ wx.setStorageSync('readSectionIds', list)
+ },
+
+ // 已读章节数(用于首页展示)
+ getReadCount() {
+ return (this.globalData.readSectionIds || []).length
+ },
+
// 获取章节总数
getTotalSections() {
return this.globalData.totalSections
diff --git a/miniprogram/app.json b/miniprogram/app.json
index bd4f1a8c..4bbe3721 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -6,10 +6,15 @@
"pages/my/my",
"pages/read/read",
"pages/about/about",
+ "pages/agreement/agreement",
+ "pages/privacy/privacy",
"pages/referral/referral",
"pages/purchases/purchases",
"pages/settings/settings",
"pages/search/search",
+ "pages/addresses/addresses",
+ "pages/addresses/edit",
+ "pages/withdraw-records/withdraw-records",
"pages/vip/vip",
"pages/member-detail/member-detail"
],
diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss
index 4a79d19f..9ce22a06 100644
--- a/miniprogram/app.wxss
+++ b/miniprogram/app.wxss
@@ -4,10 +4,48 @@
* 开发: 卡若
*/
+/* ===== CSS 变量系统 ===== */
+page {
+ /* 品牌色 */
+ --app-brand: #00CED1;
+ --app-brand-light: rgba(0, 206, 209, 0.1);
+ --app-brand-dark: #20B2AA;
+
+ /* 背景色 */
+ --app-bg-primary: #000000;
+ --app-bg-secondary: #1c1c1e;
+ --app-bg-tertiary: #2c2c2e;
+
+ /* 文字色 */
+ --app-text-primary: #ffffff;
+ --app-text-secondary: rgba(255, 255, 255, 0.7);
+ --app-text-tertiary: rgba(255, 255, 255, 0.4);
+
+ /* 分隔线 */
+ --app-separator: rgba(255, 255, 255, 0.05);
+
+ /* iOS 系统色 */
+ --ios-indigo: #5856D6;
+ --ios-green: #30d158;
+ --ios-red: #FF3B30;
+ --ios-orange: #FF9500;
+ --ios-yellow: #FFD700;
+
+ /* 金色 */
+ --gold: #FFD700;
+ --gold-light: #FFA500;
+
+ /* 粉色 */
+ --pink: #E91E63;
+
+ /* 紫色 */
+ --purple: #7B61FF;
+}
+
/* ===== 页面基础样式 ===== */
page {
- background-color: #000000;
- color: #ffffff;
+ background-color: var(--app-bg-primary);
+ color: var(--app-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 28rpx;
line-height: 1.5;
diff --git a/miniprogram/assets/icons/alert-circle.svg b/miniprogram/assets/icons/alert-circle.svg
new file mode 100644
index 00000000..f5a441f3
--- /dev/null
+++ b/miniprogram/assets/icons/alert-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/miniprogram/assets/icons/arrow-right.svg b/miniprogram/assets/icons/arrow-right.svg
new file mode 100644
index 00000000..1dc64d3f
--- /dev/null
+++ b/miniprogram/assets/icons/arrow-right.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/bell.svg b/miniprogram/assets/icons/bell.svg
new file mode 100644
index 00000000..0e7e405b
--- /dev/null
+++ b/miniprogram/assets/icons/bell.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/book-open.svg b/miniprogram/assets/icons/book-open.svg
new file mode 100644
index 00000000..d833e86b
--- /dev/null
+++ b/miniprogram/assets/icons/book-open.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/book.svg b/miniprogram/assets/icons/book.svg
new file mode 100644
index 00000000..93579576
--- /dev/null
+++ b/miniprogram/assets/icons/book.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/chevron-left.svg b/miniprogram/assets/icons/chevron-left.svg
new file mode 100644
index 00000000..e406b2b9
--- /dev/null
+++ b/miniprogram/assets/icons/chevron-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/miniprogram/assets/icons/gift.svg b/miniprogram/assets/icons/gift.svg
new file mode 100644
index 00000000..66ac806c
--- /dev/null
+++ b/miniprogram/assets/icons/gift.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram/assets/icons/home.svg b/miniprogram/assets/icons/home.svg
new file mode 100644
index 00000000..76244091
--- /dev/null
+++ b/miniprogram/assets/icons/home.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/image.svg b/miniprogram/assets/icons/image.svg
new file mode 100644
index 00000000..50ed9e6d
--- /dev/null
+++ b/miniprogram/assets/icons/image.svg
@@ -0,0 +1,5 @@
+
diff --git a/miniprogram/assets/icons/list.svg b/miniprogram/assets/icons/list.svg
new file mode 100644
index 00000000..688326aa
--- /dev/null
+++ b/miniprogram/assets/icons/list.svg
@@ -0,0 +1,8 @@
+
diff --git a/miniprogram/assets/icons/message-circle.svg b/miniprogram/assets/icons/message-circle.svg
new file mode 100644
index 00000000..037560e9
--- /dev/null
+++ b/miniprogram/assets/icons/message-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/miniprogram/assets/icons/partners.svg b/miniprogram/assets/icons/partners.svg
new file mode 100644
index 00000000..80668312
--- /dev/null
+++ b/miniprogram/assets/icons/partners.svg
@@ -0,0 +1,18 @@
+
diff --git a/miniprogram/assets/icons/settings.svg b/miniprogram/assets/icons/settings.svg
new file mode 100644
index 00000000..c7006ea8
--- /dev/null
+++ b/miniprogram/assets/icons/settings.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/share.svg b/miniprogram/assets/icons/share.svg
new file mode 100644
index 00000000..93179fc2
--- /dev/null
+++ b/miniprogram/assets/icons/share.svg
@@ -0,0 +1,7 @@
+
diff --git a/miniprogram/assets/icons/sparkles.svg b/miniprogram/assets/icons/sparkles.svg
new file mode 100644
index 00000000..e2a4461f
--- /dev/null
+++ b/miniprogram/assets/icons/sparkles.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram/assets/icons/user.svg b/miniprogram/assets/icons/user.svg
new file mode 100644
index 00000000..8b190427
--- /dev/null
+++ b/miniprogram/assets/icons/user.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/assets/icons/users.svg b/miniprogram/assets/icons/users.svg
new file mode 100644
index 00000000..4816094b
--- /dev/null
+++ b/miniprogram/assets/icons/users.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram/assets/icons/wallet.svg b/miniprogram/assets/icons/wallet.svg
new file mode 100644
index 00000000..6d431e54
--- /dev/null
+++ b/miniprogram/assets/icons/wallet.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram/components/icon/README.md b/miniprogram/components/icon/README.md
new file mode 100644
index 00000000..34e394c8
--- /dev/null
+++ b/miniprogram/components/icon/README.md
@@ -0,0 +1,175 @@
+# Icon 图标组件
+
+SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。
+
+**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签)
+
+---
+
+## 使用方法
+
+### 1. 在页面 JSON 中引入组件
+
+```json
+{
+ "usingComponents": {
+ "icon": "/components/icon/icon"
+ }
+}
+```
+
+### 2. 在 WXML 中使用
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 属性说明
+
+| 属性 | 类型 | 默认值 | 说明 |
+|-----|------|--------|-----|
+| name | String | 'share' | 图标名称 |
+| size | Number | 48 | 图标大小(rpx) |
+| color | String | 'currentColor' | 图标颜色 |
+| customClass | String | '' | 自定义类名 |
+| customStyle | String | '' | 自定义样式 |
+
+---
+
+## 可用图标
+
+| 图标名称 | 说明 | 对应 lucide-react |
+|---------|------|-------------------|
+| `share` | 分享 | `` |
+| `arrow-up-right` | 右上箭头 | `` |
+| `chevron-left` | 左箭头 | `` |
+| `search` | 搜索 | `` |
+| `heart` | 心形 | `` |
+
+---
+
+## 添加新图标
+
+在 `icon.js` 的 `getSvgPath` 方法中添加新图标:
+
+```javascript
+getSvgPath(name) {
+ const svgMap = {
+ 'new-icon': '',
+ // ... 其他图标
+ }
+ return svgMap[name] || ''
+}
+```
+
+**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。
+**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。
+
+---
+
+## 样式定制
+
+### 1. 使用 customClass
+
+```xml
+
+```
+
+```css
+.my-icon-class {
+ opacity: 0.8;
+}
+```
+
+### 2. 使用 customStyle
+
+```xml
+
+```
+
+---
+
+## 技术说明
+
+### 为什么使用 Base64 + image?
+
+1. **矢量图标**:任意缩放不失真
+2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色
+3. **轻量级**:无需加载字体文件或外部图片
+4. **兼容性**:小程序不支持直接使用 SVG 标签,image 组件支持 Base64 SVG
+
+### 为什么不用字体图标?
+
+小程序对字体文件有限制,Base64 编码字体文件会增加包体积,SVG 图标更轻量。
+
+### 与 lucide-react 的对应关系
+
+- **lucide-react**: React 组件库,使用 SVG
+- **本组件**: 小程序自定义组件,也使用 SVG
+- **SVG path 数据**: 完全相同,从 lucide 官网复制
+
+---
+
+## 示例
+
+### 悬浮分享按钮
+
+```xml
+
+```
+
+```css
+.fab-share {
+ position: fixed;
+ right: 32rpx;
+ bottom: calc(120rpx + env(safe-area-inset-bottom));
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+```
+
+---
+
+## 扩展图标库
+
+可以继续添加更多 lucide-react 图标:
+
+- `star` - 星星
+- `wallet` - 钱包
+- `gift` - 礼物
+- `info` - 信息
+- `settings` - 设置
+- `user` - 用户
+- `book-open` - 打开的书
+- `eye` - 眼睛
+- `clock` - 时钟
+- `users` - 用户组
+
+---
+
+**图标组件创建完成!** 🎉
diff --git a/miniprogram/components/icon/icon.js b/miniprogram/components/icon/icon.js
new file mode 100644
index 00000000..b2dec23f
--- /dev/null
+++ b/miniprogram/components/icon/icon.js
@@ -0,0 +1,83 @@
+// components/icon/icon.js
+Component({
+ properties: {
+ // 图标名称
+ name: {
+ type: String,
+ value: 'share',
+ observer: 'updateIcon'
+ },
+ // 图标大小(rpx)
+ size: {
+ type: Number,
+ value: 48
+ },
+ // 图标颜色
+ color: {
+ type: String,
+ value: '#ffffff',
+ observer: 'updateIcon'
+ },
+ // 自定义类名
+ customClass: {
+ type: String,
+ value: ''
+ },
+ // 自定义样式
+ customStyle: {
+ type: String,
+ value: ''
+ }
+ },
+
+ data: {
+ svgData: ''
+ },
+
+ lifetimes: {
+ attached() {
+ this.updateIcon()
+ }
+ },
+
+ methods: {
+ // SVG 图标数据映射
+ getSvgPath(name) {
+ const svgMap = {
+ 'share': '',
+
+ 'arrow-up-right': '',
+
+ 'chevron-left': '',
+
+ 'search': '',
+
+ 'heart': ''
+ }
+
+ return svgMap[name] || ''
+ },
+
+ // 更新图标
+ updateIcon() {
+ const { name, color } = this.data
+ let svgString = this.getSvgPath(name)
+
+ if (svgString) {
+ // 替换颜色占位符
+ svgString = svgString.replace(/COLOR/g, color)
+
+ // 转换为 Base64 Data URL
+ const svgData = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`
+
+ this.setData({
+ svgData: svgData
+ })
+ } else {
+ this.setData({
+ svgData: ''
+ })
+ }
+ }
+ }
+})
diff --git a/miniprogram/components/icon/icon.json b/miniprogram/components/icon/icon.json
new file mode 100644
index 00000000..a89ef4db
--- /dev/null
+++ b/miniprogram/components/icon/icon.json
@@ -0,0 +1,4 @@
+{
+ "component": true,
+ "usingComponents": {}
+}
diff --git a/miniprogram/components/icon/icon.wxml b/miniprogram/components/icon/icon.wxml
new file mode 100644
index 00000000..b1c29a25
--- /dev/null
+++ b/miniprogram/components/icon/icon.wxml
@@ -0,0 +1,5 @@
+
+
+
+ {{name}}
+
diff --git a/miniprogram/components/icon/icon.wxss b/miniprogram/components/icon/icon.wxss
new file mode 100644
index 00000000..d12d2a0a
--- /dev/null
+++ b/miniprogram/components/icon/icon.wxss
@@ -0,0 +1,18 @@
+/* components/icon/icon.wxss */
+.icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.icon-image {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.icon-text {
+ font-size: 24rpx;
+ color: currentColor;
+}
diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js
index 7cd2fbe2..4acd9546 100644
--- a/miniprogram/custom-tab-bar/index.js
+++ b/miniprogram/custom-tab-bar/index.js
@@ -1,13 +1,19 @@
/**
* Soul创业实验 - 自定义TabBar组件
- * 实现中间突出的"找伙伴"按钮
+ * 根据后台配置动态显示/隐藏"找伙伴"按钮
*/
+console.log('[TabBar] ===== 组件文件开始加载 =====')
+
+const app = getApp()
+console.log('[TabBar] App 对象:', app)
+
Component({
data: {
selected: 0,
color: '#8e8e93',
selectedColor: '#00CED1',
+ matchEnabled: false, // 找伙伴功能开关,默认关闭
list: [
{
pagePath: '/pages/index/index',
@@ -23,20 +29,34 @@ Component({
pagePath: '/pages/match/match',
text: '找伙伴',
iconType: 'match',
- isSpecial: true,
- hidden: true // 默认隐藏,等配置加载后根据后台设置显示
+ isSpecial: true
},
{
pagePath: '/pages/my/my',
text: '我的',
iconType: 'user'
}
- ],
- matchEnabled: false // 找伙伴功能开关(默认隐藏,等待后台配置加载)
+ ]
},
+ lifetimes: {
+ attached() {
+ console.log('[TabBar] Component attached 生命周期触发')
+ this.loadFeatureConfig()
+ },
+ ready() {
+ console.log('[TabBar] Component ready 生命周期触发')
+ // 如果 attached 中没有成功加载,在 ready 中再次尝试
+ if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) {
+ console.log('[TabBar] 在 ready 中重新加载配置')
+ this.loadFeatureConfig()
+ }
+ }
+ },
+
+ // 页面加载时也调用(兼容性更好)
attached() {
- // 初始化时获取当前页面
+ console.log('[TabBar] attached() 方法触发')
this.loadFeatureConfig()
},
@@ -44,31 +64,82 @@ Component({
// 加载功能配置
async loadFeatureConfig() {
try {
- const app = getApp()
- const res = await app.request('/api/db/config')
+ console.log('[TabBar] 开始加载功能配置...')
+ console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
- if (res.success && res.features) {
- const matchEnabled = res.features.matchEnabled === true
- this.setData({ matchEnabled })
-
- // 更新list,隐藏或显示找伙伴
- const list = this.data.list.map(item => {
- if (item.iconType === 'match') {
- return { ...item, hidden: !matchEnabled }
- }
- return item
- })
- this.setData({ list })
-
- console.log('[TabBar] 功能配置加载成功,找伙伴功能:', matchEnabled ? '开启' : '关闭')
+ // app.request 的第一个参数是 url 字符串,第二个参数是 options 对象
+ const res = await app.request('/api/miniprogram/config', {
+ method: 'GET'
+ })
+
+
+ // 兼容两种返回格式
+ let matchEnabled = false
+
+ if (res && res.success && res.features) {
+ console.log('[TabBar] features配置:', JSON.stringify(res.features))
+ matchEnabled = res.features.matchEnabled === true
+ console.log('[TabBar] matchEnabled值:', matchEnabled)
+ } else if (res && res.configs && res.configs.feature_config) {
+ // 备用格式:从 configs.feature_config 读取
+ console.log('[TabBar] 使用备用格式,从configs读取')
+ matchEnabled = res.configs.feature_config.matchEnabled === true
+ console.log('[TabBar] matchEnabled值:', matchEnabled)
+ } else {
+ console.log('[TabBar] ⚠️ 未找到features配置,使用默认值false')
+ console.log('[TabBar] res对象keys:', Object.keys(res || {}))
}
- } catch (e) {
- console.log('[TabBar] 加载功能配置失败:', e)
- // 失败时默认隐藏找伙伴(与Web版保持一致)
- this.setData({ matchEnabled: false })
+
+ this.setData({ matchEnabled }, () => {
+ console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled)
+ // 配置加载完成后,根据当前路由设置选中状态
+ this.updateSelected()
+ })
+
+ // 如果当前在找伙伴页面,但功能已关闭,跳转到首页
+ if (!matchEnabled) {
+ const pages = getCurrentPages()
+ const currentPage = pages[pages.length - 1]
+ if (currentPage && currentPage.route === 'pages/match/match') {
+ console.log('[TabBar] 找伙伴功能已关闭,从match页面跳转到首页')
+ wx.switchTab({ url: '/pages/index/index' })
+ }
+ }
+ } catch (error) {
+ console.log('[TabBar] ❌ 加载功能配置失败:', error)
+ console.log('[TabBar] 错误详情:', error.message || error)
+ // 默认关闭找伙伴功能
+ this.setData({ matchEnabled: false }, () => {
+ this.updateSelected()
+ })
}
},
-
+
+ // 根据当前路由更新选中状态
+ updateSelected() {
+ const pages = getCurrentPages()
+ if (pages.length === 0) return
+
+ const currentPage = pages[pages.length - 1]
+ const route = currentPage.route
+
+ let selected = 0
+ const { matchEnabled } = this.data
+
+ // 根据路由匹配对应的索引
+ if (route === 'pages/index/index') {
+ selected = 0
+ } else if (route === 'pages/chapters/chapters') {
+ selected = 1
+ } else if (route === 'pages/match/match') {
+ selected = 2
+ } else if (route === 'pages/my/my') {
+ selected = matchEnabled ? 3 : 2
+ }
+
+ this.setData({ selected })
+ },
+
switchTab(e) {
const data = e.currentTarget.dataset
const url = data.path
diff --git a/miniprogram/custom-tab-bar/index.wxml b/miniprogram/custom-tab-bar/index.wxml
index a412ddaf..73369b2a 100644
--- a/miniprogram/custom-tab-bar/index.wxml
+++ b/miniprogram/custom-tab-bar/index.wxml
@@ -1,17 +1,14 @@
-
+
-
-
-
-
-
-
-
+
{{list[0].text}}
@@ -19,38 +16,32 @@
-
-
-
-
-
-
-
+
{{list[1].text}}
-
-
+
+
-
-
-
-
+
{{list[2].text}}
-
+
-
-
-
-
-
-
+
- {{list[3].text}}
+ {{list[3].text}}
diff --git a/miniprogram/custom-tab-bar/index.wxss b/miniprogram/custom-tab-bar/index.wxss
index 84ad115f..98036655 100644
--- a/miniprogram/custom-tab-bar/index.wxss
+++ b/miniprogram/custom-tab-bar/index.wxss
@@ -18,6 +18,16 @@
z-index: 999;
}
+/* 三个tab布局(找伙伴功能关闭时) */
+.tab-bar-three .tab-bar-item {
+ flex: 1;
+}
+
+/* 四个tab布局(找伙伴功能开启时) */
+.tab-bar-four .tab-bar-item {
+ flex: 1;
+}
+
.tab-bar-border {
position: absolute;
top: 0;
@@ -58,105 +68,18 @@
line-height: 1;
}
-/* ===== 首页图标 ===== */
-.icon-home {
- position: relative;
- width: 40rpx;
- height: 40rpx;
+/* ===== SVG 图标样式 ===== */
+.tab-icon {
+ width: 48rpx;
+ height: 48rpx;
+ display: block;
+ filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%);
}
-.home-roof {
- position: absolute;
- top: 4rpx;
- left: 50%;
- transform: translateX(-50%);
- width: 0;
- height: 0;
- border-left: 18rpx solid transparent;
- border-right: 18rpx solid transparent;
- border-bottom: 14rpx solid #8e8e93;
+.tab-icon.icon-active {
+ filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
}
-.home-body {
- position: absolute;
- bottom: 4rpx;
- left: 50%;
- transform: translateX(-50%);
- width: 28rpx;
- height: 18rpx;
- background: #8e8e93;
- border-radius: 0 0 4rpx 4rpx;
-}
-
-.icon-active .home-roof {
- border-bottom-color: #00CED1;
-}
-
-.icon-active .home-body {
- background: #00CED1;
-}
-
-/* ===== 目录图标 ===== */
-.icon-list {
- width: 36rpx;
- height: 32rpx;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-}
-
-.list-line {
- width: 100%;
- height: 6rpx;
- background: #8e8e93;
- border-radius: 3rpx;
-}
-
-.list-line:nth-child(2) {
- width: 75%;
-}
-
-.list-line:nth-child(3) {
- width: 50%;
-}
-
-.icon-active .list-line {
- background: #00CED1;
-}
-
-/* ===== 我的图标 ===== */
-.icon-user {
- position: relative;
- width: 36rpx;
- height: 40rpx;
-}
-
-.user-head {
- position: absolute;
- top: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 16rpx;
- height: 16rpx;
- background: #8e8e93;
- border-radius: 50%;
-}
-
-.user-body {
- position: absolute;
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 28rpx;
- height: 18rpx;
- background: #8e8e93;
- border-radius: 14rpx 14rpx 0 0;
-}
-
-.icon-active .user-head,
-.icon-active .user-body {
- background: #00CED1;
-}
/* ===== 找伙伴 - 中间特殊按钮 ===== */
.special-item {
@@ -189,39 +112,10 @@
margin-top: 4rpx;
}
-/* ===== 找伙伴图标 (双人) ===== */
-.icon-users {
- position: relative;
- width: 56rpx;
- height: 44rpx;
-}
-
-.user-circle {
- position: absolute;
- width: 28rpx;
- height: 28rpx;
- border-radius: 50%;
- background: #ffffff;
-}
-
-.user-circle::after {
- content: '';
- position: absolute;
- bottom: -12rpx;
- left: 50%;
- transform: translateX(-50%);
- width: 22rpx;
- height: 14rpx;
- background: #ffffff;
- border-radius: 11rpx 11rpx 0 0;
-}
-
-.user-1 {
- top: 0;
- left: 0;
-}
-
-.user-2 {
- top: 0;
- right: 0;
+/* ===== 找伙伴特殊按钮图标 ===== */
+.special-icon {
+ width: 80rpx;
+ height: 80rpx;
+ display: block;
+ filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%);
}
diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js
index dbbcabb0..8f19cc60 100644
--- a/miniprogram/pages/about/about.js
+++ b/miniprogram/pages/about/about.js
@@ -49,7 +49,7 @@ Page({
// 加载书籍统计
async loadBookStats() {
try {
- const res = await app.request('/api/book/stats')
+ const res = await app.request('/api/miniprogram/book/stats')
if (res && res.success) {
this.setData({
'bookInfo.totalChapters': res.data?.totalChapters || 62,
diff --git a/miniprogram/pages/addresses/addresses.js b/miniprogram/pages/addresses/addresses.js
new file mode 100644
index 00000000..685528cf
--- /dev/null
+++ b/miniprogram/pages/addresses/addresses.js
@@ -0,0 +1,123 @@
+/**
+ * 收货地址列表页
+ * 参考 Next.js: app/view/my/addresses/page.tsx
+ */
+
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ isLoggedIn: false,
+ addressList: [],
+ loading: true
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ this.checkLogin()
+ },
+
+ onShow() {
+ if (this.data.isLoggedIn) {
+ this.loadAddresses()
+ }
+ },
+
+ // 检查登录状态
+ checkLogin() {
+ const isLoggedIn = app.globalData.isLoggedIn
+ const userId = app.globalData.userInfo?.id
+
+ if (!isLoggedIn || !userId) {
+ wx.showModal({
+ title: '需要登录',
+ content: '请先登录后再管理收货地址',
+ confirmText: '去登录',
+ success: (res) => {
+ if (res.confirm) {
+ wx.switchTab({ url: '/pages/my/my' })
+ } else {
+ wx.navigateBack()
+ }
+ }
+ })
+ return
+ }
+
+ this.setData({ isLoggedIn: true })
+ this.loadAddresses()
+ },
+
+ // 加载地址列表
+ async loadAddresses() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+
+ this.setData({ loading: true })
+
+ try {
+ const res = await app.request(`/api/miniprogram/user/addresses?userId=${userId}`)
+ if (res.success && res.list) {
+ this.setData({
+ addressList: res.list,
+ loading: false
+ })
+ } else {
+ this.setData({ addressList: [], loading: false })
+ }
+ } catch (e) {
+ console.error('加载地址列表失败:', e)
+ this.setData({ loading: false })
+ wx.showToast({ title: '加载失败', icon: 'none' })
+ }
+ },
+
+ // 编辑地址
+ editAddress(e) {
+ const id = e.currentTarget.dataset.id
+ wx.navigateTo({ url: `/pages/addresses/edit?id=${id}` })
+ },
+
+ // 删除地址
+ deleteAddress(e) {
+ const id = e.currentTarget.dataset.id
+
+ wx.showModal({
+ title: '确认删除',
+ content: '确定要删除该收货地址吗?',
+ confirmColor: '#FF3B30',
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ const result = await app.request(`/api/miniprogram/user/addresses/${id}`, {
+ method: 'DELETE'
+ })
+
+ if (result.success) {
+ wx.showToast({ title: '删除成功', icon: 'success' })
+ this.loadAddresses()
+ } else {
+ wx.showToast({ title: result.message || '删除失败', icon: 'none' })
+ }
+ } catch (e) {
+ console.error('删除地址失败:', e)
+ wx.showToast({ title: '删除失败', icon: 'none' })
+ }
+ }
+ }
+ })
+ },
+
+ // 新增地址
+ addAddress() {
+ wx.navigateTo({ url: '/pages/addresses/edit' })
+ },
+
+ // 返回
+ goBack() {
+ wx.navigateBack()
+ }
+})
diff --git a/miniprogram/pages/addresses/addresses.json b/miniprogram/pages/addresses/addresses.json
new file mode 100644
index 00000000..2e45b65e
--- /dev/null
+++ b/miniprogram/pages/addresses/addresses.json
@@ -0,0 +1,5 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "enablePullDownRefresh": false
+}
diff --git a/miniprogram/pages/addresses/addresses.wxml b/miniprogram/pages/addresses/addresses.wxml
new file mode 100644
index 00000000..cec2ef6e
--- /dev/null
+++ b/miniprogram/pages/addresses/addresses.wxml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ ‹
+
+ 收货地址
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+ 📍
+ 暂无收货地址
+ 点击下方按钮添加
+
+
+
+
+
+
+ {{item.fullAddress}}
+
+
+ ✏️
+ 编辑
+
+
+ 🗑️
+ 删除
+
+
+
+
+
+
+
+ ➕
+ 新增收货地址
+
+
+
diff --git a/miniprogram/pages/addresses/addresses.wxss b/miniprogram/pages/addresses/addresses.wxss
new file mode 100644
index 00000000..9ff21637
--- /dev/null
+++ b/miniprogram/pages/addresses/addresses.wxss
@@ -0,0 +1,217 @@
+/**
+ * 收货地址列表页样式
+ */
+
+.page {
+ min-height: 100vh;
+ background: #000000;
+ padding-bottom: 200rpx;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(40rpx);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.nav-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+ height: 88rpx;
+}
+
+.nav-back {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav-back:active {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.back-icon {
+ font-size: 48rpx;
+ color: #ffffff;
+ line-height: 1;
+}
+
+.nav-title {
+ flex: 1;
+ text-align: center;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.nav-placeholder {
+ width: 64rpx;
+}
+
+/* ===== 内容区 ===== */
+.content {
+ padding: 32rpx;
+}
+
+/* ===== 加载状态 ===== */
+.loading-state {
+ padding: 240rpx 0;
+ text-align: center;
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 空状态 ===== */
+.empty-state {
+ padding: 240rpx 0;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.empty-icon {
+ font-size: 96rpx;
+ margin-bottom: 24rpx;
+ opacity: 0.3;
+}
+
+.empty-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 16rpx;
+}
+
+.empty-tip {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 地址列表 ===== */
+.address-list {
+ margin-bottom: 24rpx;
+}
+
+.address-card {
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ padding: 32rpx;
+ margin-bottom: 24rpx;
+}
+
+/* 地址头部 */
+.address-header {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 16rpx;
+}
+
+.receiver-name {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.receiver-phone {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.default-tag {
+ font-size: 22rpx;
+ color: #00CED1;
+ background: rgba(0, 206, 209, 0.2);
+ padding: 6rpx 16rpx;
+ border-radius: 8rpx;
+ margin-left: auto;
+}
+
+/* 地址文本 */
+.address-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ line-height: 1.6;
+ display: block;
+ margin-bottom: 24rpx;
+ padding-bottom: 24rpx;
+ border-bottom: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+/* 操作按钮 */
+.address-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 32rpx;
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 8rpx 0;
+}
+
+.action-btn:active {
+ opacity: 0.6;
+}
+
+.edit-btn {
+ color: #00CED1;
+}
+
+.delete-btn {
+ color: #FF3B30;
+}
+
+.action-icon {
+ font-size: 28rpx;
+}
+
+.action-text {
+ font-size: 28rpx;
+}
+
+/* ===== 新增按钮 ===== */
+.add-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 32rpx;
+ background: #00CED1;
+ border-radius: 24rpx;
+ font-weight: 600;
+ margin-top: 48rpx;
+}
+
+.add-btn:active {
+ opacity: 0.8;
+ transform: scale(0.98);
+}
+
+.add-icon {
+ font-size: 36rpx;
+ color: #000000;
+}
+
+.add-text {
+ font-size: 32rpx;
+ color: #000000;
+}
diff --git a/miniprogram/pages/addresses/edit.js b/miniprogram/pages/addresses/edit.js
new file mode 100644
index 00000000..9542c1dc
--- /dev/null
+++ b/miniprogram/pages/addresses/edit.js
@@ -0,0 +1,201 @@
+/**
+ * 地址编辑页(新增/编辑)
+ * 参考 Next.js: app/view/my/addresses/[id]/page.tsx
+ */
+
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ isEdit: false, // 是否为编辑模式
+ addressId: null,
+
+ // 表单数据
+ name: '',
+ phone: '',
+ province: '',
+ city: '',
+ district: '',
+ detail: '',
+ isDefault: false,
+
+ // 地区选择器
+ region: [],
+
+ saving: false
+ },
+
+ onLoad(options) {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+
+ // 如果有 id 参数,则为编辑模式
+ if (options.id) {
+ this.setData({
+ isEdit: true,
+ addressId: options.id
+ })
+ this.loadAddress(options.id)
+ }
+ },
+
+ // 加载地址详情(编辑模式)
+ async loadAddress(id) {
+ wx.showLoading({ title: '加载中...', mask: true })
+
+ try {
+ const res = await app.request(`/api/miniprogram/user/addresses/${id}`)
+ if (res.success && res.data) {
+ const addr = res.data
+ this.setData({
+ name: addr.name || '',
+ phone: addr.phone || '',
+ province: addr.province || '',
+ city: addr.city || '',
+ district: addr.district || '',
+ detail: addr.detail || '',
+ isDefault: addr.isDefault || false,
+ region: [addr.province, addr.city, addr.district]
+ })
+ } else {
+ wx.showToast({ title: '加载失败', icon: 'none' })
+ }
+ } catch (e) {
+ console.error('加载地址详情失败:', e)
+ wx.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ wx.hideLoading()
+ }
+ },
+
+ // 表单输入
+ onNameInput(e) {
+ this.setData({ name: e.detail.value })
+ },
+
+ onPhoneInput(e) {
+ this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) })
+ },
+
+ onDetailInput(e) {
+ this.setData({ detail: e.detail.value })
+ },
+
+ // 地区选择
+ onRegionChange(e) {
+ const region = e.detail.value
+ this.setData({
+ region,
+ province: region[0],
+ city: region[1],
+ district: region[2]
+ })
+ },
+
+ // 切换默认地址
+ onDefaultChange(e) {
+ this.setData({ isDefault: e.detail.value })
+ },
+
+ // 表单验证
+ validateForm() {
+ const { name, phone, province, city, district, detail } = this.data
+
+ if (!name || name.trim().length === 0) {
+ wx.showToast({ title: '请输入收货人姓名', icon: 'none' })
+ return false
+ }
+
+ if (!phone || phone.length !== 11) {
+ wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
+ return false
+ }
+
+ if (!province || !city || !district) {
+ wx.showToast({ title: '请选择省市区', icon: 'none' })
+ return false
+ }
+
+ if (!detail || detail.trim().length === 0) {
+ wx.showToast({ title: '请输入详细地址', icon: 'none' })
+ return false
+ }
+
+ return true
+ },
+
+ // 保存地址
+ async saveAddress() {
+ if (!this.validateForm()) return
+ if (this.data.saving) return
+
+ this.setData({ saving: true })
+ wx.showLoading({ title: '保存中...', mask: true })
+
+ const { isEdit, addressId, name, phone, province, city, district, detail, isDefault } = this.data
+ const userId = app.globalData.userInfo?.id
+
+ if (!userId) {
+ wx.hideLoading()
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ this.setData({ saving: false })
+ return
+ }
+
+ const addressData = {
+ userId,
+ name,
+ phone,
+ province,
+ city,
+ district,
+ detail,
+ fullAddress: `${province}${city}${district}${detail}`,
+ isDefault
+ }
+
+ try {
+ let res
+ if (isEdit) {
+ // 编辑模式 - PUT 请求
+ res = await app.request(`/api/miniprogram/user/addresses/${addressId}`, {
+ method: 'PUT',
+ data: addressData
+ })
+ } else {
+ // 新增模式 - POST 请求
+ res = await app.request('/api/miniprogram/user/addresses', {
+ method: 'POST',
+ data: addressData
+ })
+ }
+
+ if (res.success) {
+ wx.hideLoading()
+ wx.showToast({
+ title: isEdit ? '保存成功' : '添加成功',
+ icon: 'success'
+ })
+ setTimeout(() => {
+ wx.navigateBack()
+ }, 1500)
+ } else {
+ wx.hideLoading()
+ wx.showToast({ title: res.message || '保存失败', icon: 'none' })
+ this.setData({ saving: false })
+ }
+ } catch (e) {
+ console.error('保存地址失败:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '保存失败', icon: 'none' })
+ this.setData({ saving: false })
+ }
+ },
+
+ // 返回
+ goBack() {
+ wx.navigateBack()
+ }
+})
diff --git a/miniprogram/pages/addresses/edit.json b/miniprogram/pages/addresses/edit.json
new file mode 100644
index 00000000..2e45b65e
--- /dev/null
+++ b/miniprogram/pages/addresses/edit.json
@@ -0,0 +1,5 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "enablePullDownRefresh": false
+}
diff --git a/miniprogram/pages/addresses/edit.wxml b/miniprogram/pages/addresses/edit.wxml
new file mode 100644
index 00000000..c5429207
--- /dev/null
+++ b/miniprogram/pages/addresses/edit.wxml
@@ -0,0 +1,101 @@
+
+
+
+
+
+ ‹
+
+ {{isEdit ? '编辑地址' : '新增地址'}}
+
+
+
+
+
+
+
+
+
+ 👤
+ 收货人
+
+
+
+
+
+
+
+ 📱
+ 手机号
+
+
+
+
+
+
+
+ 📍
+ 所在地区
+
+
+
+ {{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}
+
+
+
+
+
+
+
+ 🏠
+ 详细地址
+
+
+
+
+
+
+
+ ⭐
+ 设为默认地址
+
+
+
+
+
+
+
+ {{saving ? '保存中...' : '保存'}}
+
+
+
diff --git a/miniprogram/pages/addresses/edit.wxss b/miniprogram/pages/addresses/edit.wxss
new file mode 100644
index 00000000..1045a287
--- /dev/null
+++ b/miniprogram/pages/addresses/edit.wxss
@@ -0,0 +1,186 @@
+/**
+ * 地址编辑页样式
+ */
+
+.page {
+ min-height: 100vh;
+ background: #000000;
+ padding-bottom: 200rpx;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(40rpx);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+ height: 88rpx;
+}
+
+.nav-back {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav-back:active {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.back-icon {
+ font-size: 48rpx;
+ color: #ffffff;
+ line-height: 1;
+}
+
+.nav-title {
+ flex: 1;
+ text-align: center;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.nav-placeholder {
+ width: 64rpx;
+}
+
+/* ===== 内容区 ===== */
+.content {
+ padding: 32rpx;
+}
+
+/* ===== 表单卡片 ===== */
+.form-card {
+ background: #1c1c1e;
+ border-radius: 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ padding: 32rpx;
+ margin-bottom: 32rpx;
+}
+
+/* 表单项 */
+.form-item {
+ margin-bottom: 32rpx;
+}
+
+.form-item:last-child {
+ margin-bottom: 0;
+}
+
+.form-label {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 16rpx;
+}
+
+.label-icon {
+ font-size: 28rpx;
+}
+
+.label-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+/* 输入框 */
+.form-input {
+ width: 100%;
+ padding: 24rpx 32rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 24rpx;
+ color: #ffffff;
+ font-size: 28rpx;
+}
+
+.form-input:focus {
+ border-color: rgba(0, 206, 209, 0.5);
+}
+
+.input-placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* 地区选择器 */
+.region-picker {
+ width: 100%;
+ padding: 24rpx 32rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 24rpx;
+}
+
+.picker-value {
+ color: #ffffff;
+ font-size: 28rpx;
+}
+
+.picker-value:empty::before {
+ content: '请选择省市区';
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* 多行文本框 */
+.form-textarea {
+ width: 100%;
+ padding: 24rpx 32rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 24rpx;
+ color: #ffffff;
+ font-size: 28rpx;
+ min-height: 160rpx;
+ line-height: 1.6;
+}
+
+.form-textarea:focus {
+ border-color: rgba(0, 206, 209, 0.5);
+}
+
+/* 开关项 */
+.form-switch {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16rpx 0;
+}
+
+.form-switch .form-label {
+ margin-bottom: 0;
+}
+
+/* ===== 保存按钮 ===== */
+.save-btn {
+ padding: 32rpx;
+ background: #00CED1;
+ border-radius: 24rpx;
+ text-align: center;
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #000000;
+ margin-top: 48rpx;
+}
+
+.save-btn:active {
+ opacity: 0.8;
+ transform: scale(0.98);
+}
+
+.btn-disabled {
+ opacity: 0.5;
+ pointer-events: none;
+}
diff --git a/miniprogram/pages/agreement/agreement.js b/miniprogram/pages/agreement/agreement.js
new file mode 100644
index 00000000..aedd4c68
--- /dev/null
+++ b/miniprogram/pages/agreement/agreement.js
@@ -0,0 +1,21 @@
+/**
+ * Soul创业派对 - 用户协议
+ * 审核要求:登录前可点击《用户协议》查看完整内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ }
+})
diff --git a/miniprogram/pages/agreement/agreement.json b/miniprogram/pages/agreement/agreement.json
new file mode 100644
index 00000000..9fff5b87
--- /dev/null
+++ b/miniprogram/pages/agreement/agreement.json
@@ -0,0 +1 @@
+{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}
diff --git a/miniprogram/pages/agreement/agreement.wxml b/miniprogram/pages/agreement/agreement.wxml
new file mode 100644
index 00000000..4060b50c
--- /dev/null
+++ b/miniprogram/pages/agreement/agreement.wxml
@@ -0,0 +1,37 @@
+
+
+
+ ←
+ 用户协议
+
+
+
+
+
+
+ Soul创业实验 用户服务协议
+ 更新日期:以小程序内展示为准
+
+ 一、接受条款
+ 欢迎使用 Soul创业实验 小程序。使用本服务即表示您已阅读、理解并同意受本协议约束。若不同意,请勿使用本服务。
+
+ 二、服务说明
+ 本小程序提供《一场Soul的创业实验》等数字内容阅读、推广与相关服务。我们保留变更、中断或终止部分或全部服务的权利。
+
+ 三、用户行为规范
+ 您应合法、合规使用本服务,不得利用本服务从事违法违规活动,不得侵犯他人权益。违规行为可能导致账号限制或追究责任。
+
+ 四、知识产权
+ 本小程序内全部内容(包括但不限于文字、图片、音频、视频)的知识产权归本小程序或权利人所有,未经授权不得复制、传播或用于商业用途。
+
+ 五、免责与限制
+ 在法律允许范围内,因网络、设备或不可抗力导致的服务中断或数据丢失,我们尽力减少损失但不承担超出法律规定的责任。
+
+ 六、协议变更
+ 我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。
+
+ 七、联系我们
+ 如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。
+
+
+
diff --git a/miniprogram/pages/agreement/agreement.wxss b/miniprogram/pages/agreement/agreement.wxss
new file mode 100644
index 00000000..08fadc43
--- /dev/null
+++ b/miniprogram/pages/agreement/agreement.wxss
@@ -0,0 +1,11 @@
+.page { min-height: 100vh; background: #000; }
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.95); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
+.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
+.nav-placeholder { width: 72rpx; }
+.content { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
+.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
+.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
+.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
+.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
+.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }
diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js
index 6ac20c0b..d002ed2a 100644
--- a/miniprogram/pages/chapters/chapters.js
+++ b/miniprogram/pages/chapters/chapters.js
@@ -199,7 +199,10 @@ Page({
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
- ]
+ ],
+
+ // 每日新增章节
+ dailyChapters: []
},
onLoad() {
@@ -208,12 +211,28 @@ Page({
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
+ this.loadDailyChapters()
+ this.loadTotalFromServer()
+ },
+
+ async loadTotalFromServer() {
+ try {
+ const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ if (res && res.total) {
+ this.setData({ totalSections: res.total })
+ }
+ } catch (e) {}
},
onShow() {
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
- this.getTabBar().setData({ selected: 1 })
+ const tabBar = this.getTabBar()
+ if (tabBar.updateSelected) {
+ tabBar.updateSelected()
+ } else {
+ tabBar.setData({ selected: 1 })
+ }
}
this.updateUserStatus()
},
@@ -247,5 +266,33 @@ Page({
// 返回首页
goBack() {
wx.switchTab({ url: '/pages/index/index' })
+ },
+
+ async loadDailyChapters() {
+ try {
+ const res = await app.request({ url: '/api/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,
+ 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/chapters/chapters.wxml b/miniprogram/pages/chapters/chapters.wxml
index 222d8fa9..f769c6ee 100644
--- a/miniprogram/pages/chapters/chapters.wxml
+++ b/miniprogram/pages/chapters/chapters.wxml
@@ -4,7 +4,13 @@
+
+
+ 🔍
+
+
目录
+
diff --git a/miniprogram/pages/chapters/chapters.wxss b/miniprogram/pages/chapters/chapters.wxss
index 1b8de46c..7ff3d64c 100644
--- a/miniprogram/pages/chapters/chapters.wxss
+++ b/miniprogram/pages/chapters/chapters.wxss
@@ -26,12 +26,45 @@
height: 88rpx;
display: flex;
align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+}
+
+.nav-left,
+.nav-right {
+ width: 64rpx;
+ display: flex;
+ align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
+ flex: 1;
+ text-align: center;
+}
+
+/* 搜索按钮 */
+.search-btn {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: #2c2c2e;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+}
+
+.search-btn:active {
+ background: #3c3c3e;
+ transform: scale(0.95);
+}
+
+.search-icon {
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.6);
}
.brand-color {
@@ -47,7 +80,7 @@
display: flex;
align-items: center;
gap: 24rpx;
- margin: 32rpx;
+ margin: 32rpx 32rpx 24rpx 32rpx;
padding: 32rpx;
}
@@ -55,6 +88,7 @@
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
border-radius: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
+ box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
}
.book-icon {
@@ -442,6 +476,21 @@
color: rgba(255, 255, 255, 0.3);
}
+/* ===== 每日新增章节 ===== */
+.daily-section { margin: 20rpx 0; padding: 24rpx; background: rgba(255,215,0,0.04); border: 1rpx solid rgba(255,215,0,0.12); border-radius: 16rpx; }
+.daily-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; }
+.daily-title { font-size: 30rpx; font-weight: 600; color: #FFD700; }
+.daily-badge { font-size: 22rpx; background: #FFD700; color: #000; padding: 2rpx 12rpx; border-radius: 20rpx; font-weight: bold; }
+.daily-list { display: flex; flex-direction: column; gap: 12rpx; }
+.daily-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: rgba(255,255,255,0.03); border-radius: 12rpx; }
+.daily-left { display: flex; align-items: center; gap: 10rpx; flex: 1; min-width: 0; }
+.daily-new-tag { font-size: 18rpx; background: #FF4444; color: #fff; padding: 2rpx 8rpx; border-radius: 6rpx; font-weight: bold; flex-shrink: 0; }
+.daily-item-title { font-size: 26rpx; color: rgba(255,255,255,0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.daily-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
+.daily-price { font-size: 26rpx; color: #FFD700; font-weight: 600; }
+.daily-date { font-size: 20rpx; color: rgba(255,255,255,0.35); }
+.daily-note { display: block; font-size: 22rpx; color: rgba(255,215,0,0.5); margin-top: 12rpx; text-align: center; }
+
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;
diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js
index a65b586c..1cf16fc9 100644
--- a/miniprogram/pages/index/index.js
+++ b/miniprogram/pages/index/index.js
@@ -1,166 +1,289 @@
/**
* Soul创业派对 - 首页
* 开发: 卡若
+ * 技术支持: 存客宝
*/
+console.log('[Index] ===== 首页文件开始加载 =====')
+
const app = getApp()
Page({
data: {
+ // 系统信息
statusBarHeight: 44,
navBarHeight: 88,
+
+ // 用户信息
isLoggedIn: false,
hasFullBook: false,
- purchasedCount: 0,
+ readCount: 0,
+
+ // 书籍数据
totalSections: 62,
bookData: [],
-
- featuredSections: [],
+
+ // 推荐章节
+ featuredSections: [
+ { id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
+ { id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
+ { id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
+ ],
+
+ // 最新章节(动态计算)
latestSection: null,
latestLabel: '最新更新',
+
+ // 内容概览
+ partsList: [
+ { id: 'part-1', number: '一', title: '真实的人', subtitle: '人与人之间的底层逻辑' },
+ { id: 'part-2', number: '二', title: '真实的行业', subtitle: '电商、内容、传统行业解析' },
+ { id: 'part-3', number: '三', title: '真实的错误', subtitle: '我和别人犯过的错' },
+ { id: 'part-4', number: '四', title: '真实的赚钱', subtitle: '底层结构与真实案例' },
+ { id: 'part-5', number: '五', title: '真实的社会', subtitle: '未来职业与商业生态' }
+ ],
+
+ // 超级个体(VIP会员)
+ superMembers: [],
- // 超级个体(VIP会员展示)
- vipMembers: [],
-
+ // 最新新增章节
+ latestChapters: [],
+
+ // 加载状态
loading: true
},
onLoad(options) {
+ console.log('[Index] ===== onLoad 触发 =====')
+
+ // 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
+
+ // 处理分享参数(推荐码绑定)
if (options && options.ref) {
+ console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
+
+ // 初始化数据
this.initData()
},
onShow() {
+ console.log('[Index] onShow 触发')
+
+ // 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
- this.getTabBar().setData({ selected: 0 })
+ const tabBar = this.getTabBar()
+ console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
+
+ // 主动触发配置加载
+ if (tabBar && tabBar.loadFeatureConfig) {
+ console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
+ tabBar.loadFeatureConfig()
+ }
+
+ // 更新选中状态
+ if (tabBar && tabBar.updateSelected) {
+ tabBar.updateSelected()
+ } else if (tabBar) {
+ tabBar.setData({ selected: 0 })
+ }
+ } else {
+ console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
+
+ // 更新用户状态
this.updateUserStatus()
},
+ // 初始化数据
async initData() {
this.setData({ loading: true })
try {
- await Promise.all([
- this.loadBookData(),
- this.loadLatestSection(),
- this.loadHotSections(),
- this.loadVipMembers()
- ])
+ await this.loadBookData()
+ await this.loadFeaturedFromServer()
+ this.loadSuperMembers()
+ this.loadLatestChapters()
} catch (e) {
console.error('初始化失败:', e)
- this.computeLatestSectionFallback()
} finally {
this.setData({ loading: false })
}
},
- // 从hot接口获取精选推荐(按阅读量排序)
- async loadHotSections() {
+ async loadSuperMembers() {
try {
- const res = await app.request('/api/book/hot')
- if (res?.success && res.chapters?.length) {
- this.setData({ featuredSections: res.chapters.slice(0, 5) })
+ // 优先加载VIP会员
+ let members = []
+ try {
+ const res = await app.request({ url: '/api/vip/members', silent: true })
+ if (res && res.success && res.data) {
+ members = res.data.filter(u => u.avatar || u.vip_avatar).slice(0, 4).map(u => ({
+ id: u.id, name: u.vip_name || u.nickname || '会员',
+ avatar: u.vip_avatar || u.avatar, isVip: true
+ }))
+ }
+ } catch (e) {}
+ // 不足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 = 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) {}
}
- } catch (e) {
- console.log('[Index] 热门章节加载失败', e)
- }
+ this.setData({ superMembers: members })
+ } catch (e) { console.log('[Index] 加载超级个体失败:', e) }
},
- // 加载VIP会员列表
- async loadVipMembers() {
+ // 从服务端获取精选推荐(加权算法:阅读量50% + 时效30% + 付款率20%)和最新更新
+ async loadFeaturedFromServer() {
try {
- const res = await app.request('/api/vip/members?limit=8')
- if (res?.success && res.data?.length) {
- this.setData({ vipMembers: res.data })
+ const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const chapters = (res && res.data) ? res.data : (res && res.chapters) ? res.chapters : []
+ let featured = (res && res.featuredSections) ? res.featuredSections : []
+ // 服务端未返回精选时,从前端按更新时间取前3条有效章节作为回退
+ if (featured.length === 0 && chapters.length > 0) {
+ const valid = chapters.filter(c => {
+ const id = (c.id || '').toLowerCase()
+ const pt = (c.part_title || c.partTitle || '').toLowerCase()
+ return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
+ && !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
+ })
+ featured = valid
+ .sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
+ .slice(0, 5)
+ }
+ if (featured.length > 0) {
+ this.setData({
+ featuredSections: featured.slice(0, 3).map(s => ({
+ id: s.id || s.section_id,
+ title: s.section_title || s.title,
+ part: (s.cleanPartTitle || s.part_title || '').replace(/[_||]/g, ' ').trim()
+ }))
+ })
+ }
+
+ // 最新更新 = 按 updated_at 排序第1篇(排除序言/尾声/附录)
+ const validChapters = chapters.filter(c => {
+ const id = (c.id || '').toLowerCase()
+ const pt = (c.part_title || c.partTitle || '').toLowerCase()
+ return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
+ && !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
+ })
+ if (validChapters.length > 0) {
+ validChapters.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
+ const latest = validChapters[0]
+ this.setData({
+ latestSection: {
+ id: latest.id || latest.section_id,
+ title: latest.section_title || latest.title,
+ part: latest.cleanPartTitle || latest.part_title || ''
+ }
+ })
}
} catch (e) {
- console.log('[Index] VIP会员加载失败', e)
+ console.log('[Index] 从服务端加载推荐失败,使用默认:', e)
}
},
- async loadLatestSection() {
- try {
- const res = await app.request('/api/book/latest-chapters')
- if (res?.success && res.banner) {
- this.setData({ latestSection: res.banner, latestLabel: res.label || '最新更新' })
- return
- }
- } catch (e) {
- console.warn('latest-chapters API 失败:', e.message)
- }
- this.computeLatestSectionFallback()
- },
-
- computeLatestSectionFallback() {
- const bookData = app.globalData.bookData || this.data.bookData || []
- let sections = []
- if (Array.isArray(bookData)) {
- sections = bookData.map(s => ({
- id: s.id, title: s.title || s.sectionTitle,
- part: s.part || s.sectionTitle || '真实的行业',
- isFree: s.isFree, price: s.price
- }))
- }
- const free = sections.filter(s => s.isFree !== false && (s.price === 0 || !s.price))
- const candidates = free.length > 0 ? free : sections
- if (!candidates.length) {
- this.setData({ latestSection: { id: '1.1', title: '开始阅读', part: '真实的人' }, latestLabel: '为你推荐' })
- return
- }
- const idx = Math.floor(Math.random() * candidates.length)
- this.setData({ latestSection: candidates[idx], latestLabel: '为你推荐' })
- },
-
async loadBookData() {
try {
- const res = await app.request('/api/book/all-chapters')
- if (res?.data) {
+ const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ if (res && (res.data || res.chapters)) {
+ const chapters = res.data || res.chapters || []
this.setData({
- bookData: res.data,
- totalSections: res.totalSections || res.data?.length || 62
+ bookData: chapters,
+ totalSections: res.total || chapters.length || 62
})
}
- } catch (e) { console.error('加载书籍数据失败:', e) }
+ } catch (e) {
+ console.error('加载书籍数据失败:', e)
+ }
},
+ // 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
+ const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
this.setData({
- isLoggedIn, hasFullBook,
- purchasedCount: hasFullBook ? this.data.totalSections : (purchasedSections?.length || 0)
+ isLoggedIn,
+ hasFullBook,
+ readCount
})
},
- // 阅读时记录行为轨迹
+ // 跳转到目录
+ goToChapters() {
+ wx.switchTab({ url: '/pages/chapters/chapters' })
+ },
+
+ // 跳转到搜索页
+ goToSearch() {
+ wx.navigateTo({ url: '/pages/search/search' })
+ },
+
+ // 跳转到阅读页
goToRead(e) {
const id = e.currentTarget.dataset.id
- // 记录阅读行为(异步,不阻塞跳转)
- const userId = app.globalData.userInfo?.id
- if (userId) {
- app.request('/api/user/track', {
- method: 'POST',
- data: { userId, action: 'view_chapter', target: id }
- }).catch(() => {})
- }
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
- goToChapters() { wx.switchTab({ url: '/pages/chapters/chapters' }) },
- goToSearch() { wx.navigateTo({ url: '/pages/search/search' }) },
- goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
- goToMy() { wx.switchTab({ url: '/pages/my/my' }) },
+ // 跳转到匹配页
+ goToMatch() {
+ wx.switchTab({ url: '/pages/match/match' })
+ },
+
+ goToVip() {
+ wx.navigateTo({ url: '/pages/vip/vip' })
+ },
+
+ goToSuperList() {
+ wx.switchTab({ url: '/pages/match/match' })
+ },
+
+ async loadLatestChapters() {
+ try {
+ const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const chapters = (res && res.data) || (res && res.chapters) || []
+ const latest = 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, 10)
+ .map(c => {
+ const d = new Date(c.updatedAt || c.updated_at || Date.now())
+ return {
+ id: c.id,
+ title: c.section_title || c.title || c.sectionTitle,
+ price: c.price || 1,
+ dateStr: `${d.getMonth() + 1}/${d.getDate()}`
+ }
+ })
+ this.setData({ latestChapters: latest })
+ } catch (e) { console.log('[Index] 加载最新新增失败:', e) }
+ },
goToMemberDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
},
+ // 跳转到我的页面
+ goToMy() {
+ wx.switchTab({ url: '/pages/my/my' })
+ },
+
+ // 下拉刷新
async onPullDownRefresh() {
await this.initData()
this.updateUserStatus()
diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml
index c478c020..470c18b5 100644
--- a/miniprogram/pages/index/index.wxml
+++ b/miniprogram/pages/index/index.wxml
@@ -1,11 +1,16 @@
-
+
+
+
-
-