From 52c5a8abab0bdeba99a46b35b07a603727dcd8f0 Mon Sep 17 00:00:00 2001
From: Alex-larget <33240357+Alex-larget@users.noreply.github.com>
Date: Wed, 25 Feb 2026 11:47:36 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=EF=BC=8C=E5=BC=BA=E8=B0=83=E6=8E=A5=E5=8F=A3=E8=B7=AF?=
=?UTF-8?q?=E5=BE=84=E5=BF=85=E9=A1=BB=E6=8C=89=E4=BD=BF=E7=94=A8=E6=96=B9?=
=?UTF-8?q?=E5=8C=BA=E5=88=86=EF=BC=8C=E7=A6=81=E6=AD=A2=E9=80=9A=E7=94=A8?=
=?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=B7=B7=E7=94=A8=E3=80=82=E6=96=B0=E5=A2=9E?=
=?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=88=86=E4=BA=AB=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8=E6=8E=A8=E8=8D=90?=
=?UTF-8?q?=E7=A0=81=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E4=BD=93?=
=?UTF-8?q?=E9=AA=8C=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.cursor/rules/soul-api-boundary.mdc | 8 +
.cursor/rules/soul-api-coding.mdc | 6 +
.cursor/rules/soul-change-checklist.mdc | 4 +-
.cursor/skills/SKILL-API开发.md | 2 +-
.cursor/skills/SKILL-变更关联检查.md | 3 +-
miniprogram/RESTORE-ANALYSIS.md | 54 +
miniprogram/app.js | 32 +-
miniprogram/pages/about/about.js | 8 +
miniprogram/pages/addresses/addresses.js | 8 +
miniprogram/pages/addresses/edit.js | 8 +
miniprogram/pages/agreement/agreement.js | 8 +
miniprogram/pages/chapters/chapters.js | 12 +-
miniprogram/pages/index/index.js | 14 +-
miniprogram/pages/match/match.js | 10 +-
.../pages/member-detail/member-detail.js | 11 +-
miniprogram/pages/my/my.js | 10 +-
miniprogram/pages/privacy/privacy.js | 8 +
miniprogram/pages/purchases/purchases.js | 10 +-
miniprogram/pages/read/read.js | 12 +-
miniprogram/pages/referral/referral.js | 12 +-
miniprogram/pages/search/search.js | 8 +
miniprogram/pages/settings/settings.js | 8 +
miniprogram/pages/vip/vip.js | 10 +-
.../withdraw-records/withdraw-records.js | 8 +
miniprogram/utils/scene.js | 45 +
miniprogram2/.gitignore | 14 +
miniprogram2/README.md | 138 ++
miniprogram2/app.js | 575 +++++++
miniprogram2/app.json | 1 +
miniprogram2/app.wxss | 606 ++++++++
miniprogram2/assets/icons/alert-circle.svg | 5 +
miniprogram2/assets/icons/arrow-right.svg | 4 +
miniprogram2/assets/icons/bell.svg | 4 +
miniprogram2/assets/icons/book-open.svg | 4 +
miniprogram2/assets/icons/book.svg | 4 +
miniprogram2/assets/icons/chevron-left.svg | 3 +
miniprogram2/assets/icons/gift.svg | 6 +
miniprogram2/assets/icons/home-active.png | Bin 0 -> 699 bytes
miniprogram2/assets/icons/home.png | Bin 0 -> 611 bytes
miniprogram2/assets/icons/home.svg | 4 +
miniprogram2/assets/icons/image.svg | 5 +
miniprogram2/assets/icons/list.svg | 8 +
miniprogram2/assets/icons/match-active.png | Bin 0 -> 907 bytes
miniprogram2/assets/icons/match.png | Bin 0 -> 725 bytes
miniprogram2/assets/icons/message-circle.svg | 3 +
miniprogram2/assets/icons/my-active.png | Bin 0 -> 907 bytes
miniprogram2/assets/icons/my.png | Bin 0 -> 725 bytes
miniprogram2/assets/icons/partners.svg | 18 +
miniprogram2/assets/icons/settings.svg | 4 +
miniprogram2/assets/icons/share.svg | 7 +
miniprogram2/assets/icons/sparkles.svg | 6 +
miniprogram2/assets/icons/user.svg | 4 +
miniprogram2/assets/icons/users.svg | 6 +
miniprogram2/assets/icons/wallet.svg | 4 +
miniprogram2/components/icon/README.md | 175 +++
miniprogram2/components/icon/icon.js | 83 +
miniprogram2/components/icon/icon.json | 4 +
miniprogram2/components/icon/icon.wxml | 5 +
miniprogram2/components/icon/icon.wxss | 18 +
miniprogram2/custom-tab-bar/index.js | 153 ++
miniprogram2/custom-tab-bar/index.json | 3 +
miniprogram2/custom-tab-bar/index.wxml | 47 +
miniprogram2/custom-tab-bar/index.wxss | 121 ++
miniprogram2/pages/about/about.js | 89 ++
miniprogram2/pages/about/about.json | 4 +
miniprogram2/pages/about/about.wxml | 75 +
miniprogram2/pages/about/about.wxss | 40 +
miniprogram2/pages/addresses/addresses.js | 131 ++
miniprogram2/pages/addresses/addresses.json | 5 +
miniprogram2/pages/addresses/addresses.wxml | 66 +
miniprogram2/pages/addresses/addresses.wxss | 217 +++
miniprogram2/pages/addresses/edit.js | 209 +++
miniprogram2/pages/addresses/edit.json | 5 +
miniprogram2/pages/addresses/edit.wxml | 101 ++
miniprogram2/pages/addresses/edit.wxss | 186 +++
miniprogram2/pages/agreement/agreement.js | 29 +
miniprogram2/pages/agreement/agreement.json | 1 +
miniprogram2/pages/agreement/agreement.wxml | 37 +
miniprogram2/pages/agreement/agreement.wxss | 11 +
miniprogram2/pages/chapters/chapters.js | 169 ++
miniprogram2/pages/chapters/chapters.json | 6 +
miniprogram2/pages/chapters/chapters.wxml | 127 ++
miniprogram2/pages/chapters/chapters.wxss | 482 ++++++
miniprogram2/pages/index/index.js | 223 +++
miniprogram2/pages/index/index.json | 6 +
miniprogram2/pages/index/index.wxml | 147 ++
miniprogram2/pages/index/index.wxss | 504 ++++++
miniprogram2/pages/match/match.js | 936 +++++++++++
miniprogram2/pages/match/match.json | 6 +
miniprogram2/pages/match/match.wxml | 373 +++++
miniprogram2/pages/match/match.wxss | 1380 +++++++++++++++++
miniprogram2/pages/my/my.js | 745 +++++++++
miniprogram2/pages/my/my.json | 6 +
miniprogram2/pages/my/my.wxml | 307 ++++
miniprogram2/pages/my/my.wxss | 1296 ++++++++++++++++
miniprogram2/pages/privacy/privacy.js | 29 +
miniprogram2/pages/privacy/privacy.json | 1 +
miniprogram2/pages/privacy/privacy.wxml | 40 +
miniprogram2/pages/privacy/privacy.wxss | 11 +
.../pages/profile-edit/profile-edit.js | 188 +++
.../pages/profile-edit/profile-edit.json | 4 +
.../pages/profile-edit/profile-edit.wxml | 125 ++
.../pages/profile-edit/profile-edit.wxss | 263 ++++
miniprogram2/pages/purchases/purchases.js | 72 +
miniprogram2/pages/purchases/purchases.json | 4 +
miniprogram2/pages/purchases/purchases.wxml | 36 +
miniprogram2/pages/purchases/purchases.wxss | 21 +
miniprogram2/pages/read/FLOW.txt | 199 +++
miniprogram2/pages/read/read.js | 1355 ++++++++++++++++
miniprogram2/pages/read/read.js.backup | 1055 +++++++++++++
miniprogram2/pages/read/read.json | 9 +
miniprogram2/pages/read/read.wxml | 313 ++++
miniprogram2/pages/read/read.wxss | 1009 ++++++++++++
.../referral/earnings-detail-styles.wxss | 182 +++
.../pages/referral/referral-temp.wxss | 379 +++++
miniprogram2/pages/referral/referral.js | 856 ++++++++++
miniprogram2/pages/referral/referral.json | 6 +
miniprogram2/pages/referral/referral.wxml | 307 ++++
miniprogram2/pages/referral/referral.wxss | 376 +++++
.../pages/referral/referral.wxss.backup | 379 +++++
.../pages/referral/referral.wxss.broken | 379 +++++
miniprogram2/pages/scan/scan.js | 157 ++
miniprogram2/pages/scan/scan.json | 1 +
miniprogram2/pages/scan/scan.wxml | 81 +
miniprogram2/pages/scan/scan.wxss | 248 +++
miniprogram2/pages/search/search.js | 118 ++
miniprogram2/pages/search/search.json | 5 +
miniprogram2/pages/search/search.wxml | 115 ++
miniprogram2/pages/search/search.wxss | 335 ++++
miniprogram2/pages/settings/settings.js | 446 ++++++
miniprogram2/pages/settings/settings.json | 4 +
miniprogram2/pages/settings/settings.wxml | 146 ++
miniprogram2/pages/settings/settings.wxss | 114 ++
.../withdraw-records/withdraw-records.js | 131 ++
.../withdraw-records/withdraw-records.json | 4 +
.../withdraw-records/withdraw-records.wxml | 25 +
.../withdraw-records/withdraw-records.wxss | 71 +
miniprogram2/project.config.json | 62 +
miniprogram2/project.private.config.json | 36 +
miniprogram2/sitemap.json | 7 +
miniprogram2/utils/chapterAccessManager.js | 206 +++
miniprogram2/utils/payment.js | 211 +++
miniprogram2/utils/readingTracker.js | 246 +++
miniprogram2/utils/scene.js | 45 +
miniprogram2/utils/util.js | 182 +++
145 files changed, 20844 insertions(+), 30 deletions(-)
create mode 100644 miniprogram/RESTORE-ANALYSIS.md
create mode 100644 miniprogram/utils/scene.js
create mode 100644 miniprogram2/.gitignore
create mode 100644 miniprogram2/README.md
create mode 100644 miniprogram2/app.js
create mode 100644 miniprogram2/app.json
create mode 100644 miniprogram2/app.wxss
create mode 100644 miniprogram2/assets/icons/alert-circle.svg
create mode 100644 miniprogram2/assets/icons/arrow-right.svg
create mode 100644 miniprogram2/assets/icons/bell.svg
create mode 100644 miniprogram2/assets/icons/book-open.svg
create mode 100644 miniprogram2/assets/icons/book.svg
create mode 100644 miniprogram2/assets/icons/chevron-left.svg
create mode 100644 miniprogram2/assets/icons/gift.svg
create mode 100644 miniprogram2/assets/icons/home-active.png
create mode 100644 miniprogram2/assets/icons/home.png
create mode 100644 miniprogram2/assets/icons/home.svg
create mode 100644 miniprogram2/assets/icons/image.svg
create mode 100644 miniprogram2/assets/icons/list.svg
create mode 100644 miniprogram2/assets/icons/match-active.png
create mode 100644 miniprogram2/assets/icons/match.png
create mode 100644 miniprogram2/assets/icons/message-circle.svg
create mode 100644 miniprogram2/assets/icons/my-active.png
create mode 100644 miniprogram2/assets/icons/my.png
create mode 100644 miniprogram2/assets/icons/partners.svg
create mode 100644 miniprogram2/assets/icons/settings.svg
create mode 100644 miniprogram2/assets/icons/share.svg
create mode 100644 miniprogram2/assets/icons/sparkles.svg
create mode 100644 miniprogram2/assets/icons/user.svg
create mode 100644 miniprogram2/assets/icons/users.svg
create mode 100644 miniprogram2/assets/icons/wallet.svg
create mode 100644 miniprogram2/components/icon/README.md
create mode 100644 miniprogram2/components/icon/icon.js
create mode 100644 miniprogram2/components/icon/icon.json
create mode 100644 miniprogram2/components/icon/icon.wxml
create mode 100644 miniprogram2/components/icon/icon.wxss
create mode 100644 miniprogram2/custom-tab-bar/index.js
create mode 100644 miniprogram2/custom-tab-bar/index.json
create mode 100644 miniprogram2/custom-tab-bar/index.wxml
create mode 100644 miniprogram2/custom-tab-bar/index.wxss
create mode 100644 miniprogram2/pages/about/about.js
create mode 100644 miniprogram2/pages/about/about.json
create mode 100644 miniprogram2/pages/about/about.wxml
create mode 100644 miniprogram2/pages/about/about.wxss
create mode 100644 miniprogram2/pages/addresses/addresses.js
create mode 100644 miniprogram2/pages/addresses/addresses.json
create mode 100644 miniprogram2/pages/addresses/addresses.wxml
create mode 100644 miniprogram2/pages/addresses/addresses.wxss
create mode 100644 miniprogram2/pages/addresses/edit.js
create mode 100644 miniprogram2/pages/addresses/edit.json
create mode 100644 miniprogram2/pages/addresses/edit.wxml
create mode 100644 miniprogram2/pages/addresses/edit.wxss
create mode 100644 miniprogram2/pages/agreement/agreement.js
create mode 100644 miniprogram2/pages/agreement/agreement.json
create mode 100644 miniprogram2/pages/agreement/agreement.wxml
create mode 100644 miniprogram2/pages/agreement/agreement.wxss
create mode 100644 miniprogram2/pages/chapters/chapters.js
create mode 100644 miniprogram2/pages/chapters/chapters.json
create mode 100644 miniprogram2/pages/chapters/chapters.wxml
create mode 100644 miniprogram2/pages/chapters/chapters.wxss
create mode 100644 miniprogram2/pages/index/index.js
create mode 100644 miniprogram2/pages/index/index.json
create mode 100644 miniprogram2/pages/index/index.wxml
create mode 100644 miniprogram2/pages/index/index.wxss
create mode 100644 miniprogram2/pages/match/match.js
create mode 100644 miniprogram2/pages/match/match.json
create mode 100644 miniprogram2/pages/match/match.wxml
create mode 100644 miniprogram2/pages/match/match.wxss
create mode 100644 miniprogram2/pages/my/my.js
create mode 100644 miniprogram2/pages/my/my.json
create mode 100644 miniprogram2/pages/my/my.wxml
create mode 100644 miniprogram2/pages/my/my.wxss
create mode 100644 miniprogram2/pages/privacy/privacy.js
create mode 100644 miniprogram2/pages/privacy/privacy.json
create mode 100644 miniprogram2/pages/privacy/privacy.wxml
create mode 100644 miniprogram2/pages/privacy/privacy.wxss
create mode 100644 miniprogram2/pages/profile-edit/profile-edit.js
create mode 100644 miniprogram2/pages/profile-edit/profile-edit.json
create mode 100644 miniprogram2/pages/profile-edit/profile-edit.wxml
create mode 100644 miniprogram2/pages/profile-edit/profile-edit.wxss
create mode 100644 miniprogram2/pages/purchases/purchases.js
create mode 100644 miniprogram2/pages/purchases/purchases.json
create mode 100644 miniprogram2/pages/purchases/purchases.wxml
create mode 100644 miniprogram2/pages/purchases/purchases.wxss
create mode 100644 miniprogram2/pages/read/FLOW.txt
create mode 100644 miniprogram2/pages/read/read.js
create mode 100644 miniprogram2/pages/read/read.js.backup
create mode 100644 miniprogram2/pages/read/read.json
create mode 100644 miniprogram2/pages/read/read.wxml
create mode 100644 miniprogram2/pages/read/read.wxss
create mode 100644 miniprogram2/pages/referral/earnings-detail-styles.wxss
create mode 100644 miniprogram2/pages/referral/referral-temp.wxss
create mode 100644 miniprogram2/pages/referral/referral.js
create mode 100644 miniprogram2/pages/referral/referral.json
create mode 100644 miniprogram2/pages/referral/referral.wxml
create mode 100644 miniprogram2/pages/referral/referral.wxss
create mode 100644 miniprogram2/pages/referral/referral.wxss.backup
create mode 100644 miniprogram2/pages/referral/referral.wxss.broken
create mode 100644 miniprogram2/pages/scan/scan.js
create mode 100644 miniprogram2/pages/scan/scan.json
create mode 100644 miniprogram2/pages/scan/scan.wxml
create mode 100644 miniprogram2/pages/scan/scan.wxss
create mode 100644 miniprogram2/pages/search/search.js
create mode 100644 miniprogram2/pages/search/search.json
create mode 100644 miniprogram2/pages/search/search.wxml
create mode 100644 miniprogram2/pages/search/search.wxss
create mode 100644 miniprogram2/pages/settings/settings.js
create mode 100644 miniprogram2/pages/settings/settings.json
create mode 100644 miniprogram2/pages/settings/settings.wxml
create mode 100644 miniprogram2/pages/settings/settings.wxss
create mode 100644 miniprogram2/pages/withdraw-records/withdraw-records.js
create mode 100644 miniprogram2/pages/withdraw-records/withdraw-records.json
create mode 100644 miniprogram2/pages/withdraw-records/withdraw-records.wxml
create mode 100644 miniprogram2/pages/withdraw-records/withdraw-records.wxss
create mode 100644 miniprogram2/project.config.json
create mode 100644 miniprogram2/project.private.config.json
create mode 100644 miniprogram2/sitemap.json
create mode 100644 miniprogram2/utils/chapterAccessManager.js
create mode 100644 miniprogram2/utils/payment.js
create mode 100644 miniprogram2/utils/readingTracker.js
create mode 100644 miniprogram2/utils/scene.js
create mode 100644 miniprogram2/utils/util.js
diff --git a/.cursor/rules/soul-api-boundary.mdc b/.cursor/rules/soul-api-boundary.mdc
index 025e8bea..7339b3a2 100644
--- a/.cursor/rules/soul-api-boundary.mdc
+++ b/.cursor/rules/soul-api-boundary.mdc
@@ -10,10 +10,18 @@ alwaysApply: false
## 路由按使用方归类(强制)
+**新增接口时,必须先判断使用方(小程序 / 管理端 / 两端共用),再决定挂到哪个 Group。禁止写「通用路径」让两端混用。**
+
- **仅管理端用的接口**:只挂在 `admin` 或 `db` 组(`/api/admin/*`、`/api/db/*`),**不得**在 `miniprogram` 组注册。
- **仅小程序用的接口**:只挂在 `miniprogram` 组(`/api/miniprogram/*`),**不得**仅在 admin/db 下注册而让小程序去调 `/api/xxx`。
- **两端共用的接口**:在 `api` 下挂一份,并在 `miniprogram` 组内用同一 handler 再挂一遍,保证小程序统一走 `/api/miniprogram/xxx`;handler 注释中标明使用方(如「小程序-提现记录」「管理端-提现列表」)。
+**重要**:即使业务逻辑完全相同,**也必须按使用方做路径区分**。例如 VIP 相关接口,若小程序和管理端都要用,应提供:
+- 管理端:`/api/admin/vip/*` 或 `/api/db/vip/*`
+- 小程序:`/api/miniprogram/vip/*`(可复用同一 handler,但路径必须显式挂到 miniprogram 组)
+
+不得仅提供 `/api/vip/*` 让两端共用,违反项目边界约定。
+
## 禁止行为
- 禁止在 `miniprogram` 组挂仅管理端调用的接口(如后台审核、DB 初始化)。
diff --git a/.cursor/rules/soul-api-coding.mdc b/.cursor/rules/soul-api-coding.mdc
index ee75f35c..c08aca39 100644
--- a/.cursor/rules/soul-api-coding.mdc
+++ b/.cursor/rules/soul-api-coding.mdc
@@ -61,6 +61,12 @@ alwaysApply: false
- **两端共用的接口**:在 `router.go` 里两处都注册同一 handler:先写在 `api` 的对应区块(如「推荐」「用户」),再在 `// ----- 小程序组 -----` 里用 `miniprogram.GET/POST(... path, handler.XXX)` 挂一遍,保证小程序统一走 `/api/miniprogram/xxx`。
- handler 注释和路由注释中标明使用方,例如:`// GET /api/miniprogram/withdraw/records 小程序-提现记录`、`// GET /api/admin/withdrawals 管理端-提现列表`。
+**工程师必守:即使业务逻辑完全相同,也必须按使用方做路径区分。** 例如 VIP 相关接口(status、profile、members 等),若小程序和管理端都要用:
+- 管理端:`/api/admin/vip/*` 或 `/api/db/vip/*`
+- 小程序:`/api/miniprogram/vip/*`(可复用同一 handler,但路径必须显式挂到 miniprogram 组)
+
+禁止仅提供 `/api/vip/*` 等「通用路径」让两端混用,违反项目边界。
+
**管理端列表接口返回约定**:列表类接口(如 withdrawals、orders、users)的响应应包含 soul-admin 通用展示所需字段:`user_name` 或 `userNickname`、`userAvatar`、`status`、`amount`(金额用数字)。提现状态:数据库存值 `pending`/`processing`/`success`/`failed`,前端展示可映射 `success`→`completed`、`failed`→`rejected`。
## 5. 目录与包约定
diff --git a/.cursor/rules/soul-change-checklist.mdc b/.cursor/rules/soul-change-checklist.mdc
index e075d8f8..a8e1a4e2 100644
--- a/.cursor/rules/soul-change-checklist.mdc
+++ b/.cursor/rules/soul-change-checklist.mdc
@@ -15,7 +15,7 @@ alwaysApply: false
| **前端(小程序或管理端)** 新增/改了**字段**或**接口入参/出参** | soul-api 对应接口的 request/response、model 是否已改?数据库表是否有对应列(无则加迁移/字段)? |
| **小程序** 新增或改了一个**功能**(页面、能力、配置项) | soul-api 是否已有或需新增接口(挂到 `/api/miniprogram/...`)?**管理端**是否需要对应的**配置、审核、统计、列表**? |
| **管理端** 新增或改了**列表/表单/配置项** | soul-api 的 admin/db 接口是否已提供对应数据或写接口?字段名与类型是否与前端一致? |
-| **soul-api** 新增/改了**接口**(路径、请求体、响应体、model) | 小程序或管理端是否有**调用处**?类型/字段是否已同步更新?若改了表结构,迁移是否已加? |
+| **soul-api** 新增/改了**接口**(路径、请求体、响应体、model) | 小程序或管理端是否有**调用处**?类型/字段是否已同步更新?若改了表结构,迁移是否已加?**路径是否按使用方区分**(小程序用 `/api/miniprogram/*`,管理端用 `/api/admin/*` 或 `/api/db/*`,禁止通用路径混用)? |
| **soul-api** 新增/改了**表或字段** | 相关 handler、model 是否已改?是否有接口暴露给小程序/管理端?若有,前端是否已对接? |
## 二、按「业务功能」想三端
@@ -23,7 +23,7 @@ alwaysApply: false
以**功能/领域**为单位(如:提现、推荐、章节权限、找伙伴、配置项),问一句:
- **小程序**:用户侧是否已实现/已更新?
-- **soul-api**:接口是否在正确路由组(miniprogram / admin / db)、请求响应是否一致?
+- **soul-api**:接口是否在正确路由组(miniprogram / admin / db)、请求响应是否一致?若两端共用,是否显式挂到 miniprogram 组(`/api/miniprogram/xxx`),禁止仅提供 `/api/xxx` 混用?
- **管理端**:该功能是否需要**配置、审核、统计、列表**?有则需在 soul-admin 与 soul-api 的 admin/db 下补齐。
## 三、执行约定
diff --git a/.cursor/skills/SKILL-API开发.md b/.cursor/skills/SKILL-API开发.md
index e12072e1..b3e855e8 100644
--- a/.cursor/skills/SKILL-API开发.md
+++ b/.cursor/skills/SKILL-API开发.md
@@ -30,7 +30,7 @@ description: Soul 创业派对后端 API 开发规范。在 soul-api/ 下编辑
- **仅小程序用的接口**:只挂在 `miniprogram`(如小程序登录、支付、提现、小程序码、推荐绑定等)。
- **两端共用**:在 `api` 下挂一份,再在 `miniprogram` 组里用同 handler 挂一遍,保证小程序统一走 `/api/miniprogram/xxx`;handler 注释标明使用方。
-新增或修改接口时:**先确定使用方(小程序 / 管理端 / 共用) → 再决定挂到哪个 Group → 再实现 handler**。
+**工程师必守**:新增或修改接口时,**先判断使用方(小程序 / 管理端 / 两端共用) → 再决定挂到哪个 Group → 再实现 handler**。即使业务逻辑完全相同,**也必须按使用方做路径区分**,禁止仅提供 `/api/xxx` 等通用路径让两端混用。例如 VIP 相关接口,若两端都要用,应提供 `/api/admin/vip/*`(或 `/api/db/vip/*`)和 `/api/miniprogram/vip/*`,可复用同一 handler,但路径必须显式挂到对应组。
---
diff --git a/.cursor/skills/SKILL-变更关联检查.md b/.cursor/skills/SKILL-变更关联检查.md
index a450d695..7a167728 100644
--- a/.cursor/skills/SKILL-变更关联检查.md
+++ b/.cursor/skills/SKILL-变更关联检查.md
@@ -37,6 +37,7 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a
- **新增/改了接口**
- 谁在调?**小程序**还是**管理端**?确认调用方已更新请求/响应类型或字段;若尚未有调用方,在清单中注明「待小程序/管理端对接」。
+ - **路径是否按使用方区分**?小程序必须走 `/api/miniprogram/*`,管理端走 `/api/admin/*` 或 `/api/db/*`。若两端共用同一逻辑,需在 miniprogram 组显式挂一份(如 `/api/miniprogram/vip/status`),禁止仅提供 `/api/vip/*` 让两端混用。
- **新增/改了 model 或表结构**
- 是否有接口暴露该表/字段?有则请求/响应要带上;前端若展示或提交该字段,需同步改。
- **在 miniprogram 组挂了新接口**
@@ -81,7 +82,7 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a
## 5. 与其它约定配合
-- **路径与使用方**:仍遵守 soul-miniprogram-boundary、soul-admin-boundary、soul-api-boundary(谁调哪组接口、谁挂哪条路由)。
+- **路径与使用方**:仍遵守 soul-miniprogram-boundary、soul-admin-boundary、soul-api-boundary(谁调哪组接口、谁挂哪条路由)。后端工程师新增接口时,必须先判断使用方,再挂到对应 Group;即使逻辑完全相同,也必须按使用方做路径区分,禁止通用路径混用。
- **业务逻辑图/文档**:若项目内有「业务代码逻辑图」或架构说明,本次变更若影响模块/接口/数据流,建议同步更新该图或文档,便于新 Agent 或新人快速了解当前状态。
本 Skill 与 **soul-change-checklist.mdc** 一起用,可系统化减少「只改一端、其它端漏改」的问题。
diff --git a/miniprogram/RESTORE-ANALYSIS.md b/miniprogram/RESTORE-ANALYSIS.md
new file mode 100644
index 00000000..bcca5be5
--- /dev/null
+++ b/miniprogram/RESTORE-ANALYSIS.md
@@ -0,0 +1,54 @@
+# miniprogram 功能还原分析报告
+
+## 一、对比结论
+
+| 项目 | miniprogram(甲方) | miniprogram2(你写的) |
+|------|-------------------|------------------------|
+| 页面分享 | 仅 read、referral 有 | 几乎所有页面都有 |
+| scene 解析 | 无 | 有 utils/scene.js |
+| 推荐码获取 | 分散(userInfo?.referralCode 等) | 统一 app.getMyReferralCode() |
+| 书籍 API | /api/book/all-chapters | /api/miniprogram/book/all-chapters |
+| 特有页面 | vip、member-detail | scan、profile-edit |
+
+## 二、已完成的还原项
+
+### 1. 基础能力(app.js + utils/scene.js)
+
+- **新增** `utils/scene.js`:扫码 scene 参数编解码,支持 `mid`、`id`、`ref`
+- **app.js**:
+ - 引入 `parseScene`,`handleReferralCode` 支持 `options.scene` 解析
+ - 新增 `getMyReferralCode()`:统一获取邀请码
+ - 新增 `getSectionMid(sectionId)`:根据 id 查 mid
+ - `loadBookData` 改为 `/api/miniprogram/book/all-chapters`
+
+### 2. 页面分享(onShareAppMessage)
+
+已为以下页面补充分享,路径统一带 `ref` 参数:
+
+- index、chapters、match、my
+- read、referral(原有,已统一用 getMyReferralCode)
+- search、settings、purchases、privacy
+- withdraw-records、addresses、addresses/edit
+- agreement、about、vip、member-detail
+
+### 3. read.js 分享逻辑
+
+- 使用 `app.getMyReferralCode()` 替代 `userInfo?.referralCode || wx.getStorageSync('referralCode')`
+- 保持 `onShareAppMessage`、`onShareTimeline` 行为不变
+
+### 4. API 路径修正
+
+- `app.loadBookData`:`/api/book/all-chapters` → `/api/miniprogram/book/all-chapters`
+- `index.loadBookData`、`loadFeaturedFromServer`、`loadLatestChapters`:同上
+- `chapters.loadDailyChapters`:同上
+
+## 三、未改动项(保留甲方逻辑)
+
+- **vip 相关接口**:`/api/vip/members`、`/api/vip/status`、`/api/vip/profile` 仍为原路径,未改为 `/api/miniprogram/*`(若 soul-api 无对应 miniprogram 接口,需后端补充)
+- **页面结构**:保留 vip、member-detail,未引入 scan、profile-edit
+
+## 四、后续建议
+
+1. **soul-api 路由**:确认 `/api/miniprogram/book/all-chapters` 已注册;若 vip 需在小程序使用,建议在 miniprogram 组下增加等价接口。
+2. **referral.js**:检查是否已使用 `app.getMyReferralCode()`,若仍用旧方式可统一替换。
+3. **read.js 的 mid 支持**:miniprogram2 的 read 支持 `mid` 参数(便于扫码直达),若需要可在 miniprogram 的 read 中补充 `sectionMid` 与 `getShareConfig` 的 mid 逻辑。
diff --git a/miniprogram/app.js b/miniprogram/app.js
index 8b507f28..c88ab908 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -3,6 +3,8 @@
* 开发: 卡若
*/
+const { parseScene } = require('./utils/scene.js')
+
App({
globalData: {
// API基础地址 - 连接真实后端
@@ -77,11 +79,17 @@ App({
this.handleReferralCode(options)
},
- // 处理推荐码绑定
+ // 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
handleReferralCode(options) {
const query = options?.query || {}
- const refCode = query.ref || query.referralCode
-
+ let refCode = query.ref || query.referralCode
+ const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || ''
+ if (sceneStr) {
+ const parsed = parseScene(sceneStr)
+ if (parsed.mid) this.globalData.initialSectionMid = parsed.mid
+ if (parsed.id) this.globalData.initialSectionId = parsed.id
+ if (parsed.ref) refCode = parsed.ref
+ }
if (refCode) {
console.log('[App] 检测到推荐码:', refCode)
@@ -156,6 +164,22 @@ App({
}
},
+ // 根据业务 id 从 bookData 查 mid(用于跳转)
+ getSectionMid(sectionId) {
+ const list = this.globalData.bookData || []
+ const ch = list.find(c => c.id === sectionId)
+ return ch?.mid || 0
+ },
+
+ // 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串)
+ getMyReferralCode() {
+ const user = this.globalData.userInfo
+ if (!user) return ''
+ if (user.referralCode) return user.referralCode
+ if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
+ return ''
+ },
+
// 获取系统信息
getSystemInfo() {
try {
@@ -200,7 +224,7 @@ App({
}
// 从服务器获取最新数据
- const res = await this.request('/api/book/all-chapters')
+ 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
diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js
index 8f19cc60..b337a620 100644
--- a/miniprogram/pages/about/about.js
+++ b/miniprogram/pages/about/about.js
@@ -77,5 +77,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 关于',
+ path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
+ }
}
})
diff --git a/miniprogram/pages/addresses/addresses.js b/miniprogram/pages/addresses/addresses.js
index 685528cf..c3dfda61 100644
--- a/miniprogram/pages/addresses/addresses.js
+++ b/miniprogram/pages/addresses/addresses.js
@@ -119,5 +119,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 地址管理',
+ path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
+ }
}
})
diff --git a/miniprogram/pages/addresses/edit.js b/miniprogram/pages/addresses/edit.js
index 9542c1dc..4f45893c 100644
--- a/miniprogram/pages/addresses/edit.js
+++ b/miniprogram/pages/addresses/edit.js
@@ -197,5 +197,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 编辑地址',
+ path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
+ }
}
})
diff --git a/miniprogram/pages/agreement/agreement.js b/miniprogram/pages/agreement/agreement.js
index aedd4c68..cff31e3b 100644
--- a/miniprogram/pages/agreement/agreement.js
+++ b/miniprogram/pages/agreement/agreement.js
@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 用户协议',
+ path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
+ }
}
})
diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js
index d002ed2a..ff1c4787 100644
--- a/miniprogram/pages/chapters/chapters.js
+++ b/miniprogram/pages/chapters/chapters.js
@@ -217,7 +217,7 @@ Page({
async loadTotalFromServer() {
try {
- const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
if (res && res.total) {
this.setData({ totalSections: res.total })
}
@@ -270,7 +270,7 @@ Page({
async loadDailyChapters() {
try {
- const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
const daily = chapters
.filter(c => (c.sectionOrder || c.sort_order || 0) > 62)
@@ -294,5 +294,13 @@ Page({
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 目录',
+ path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
+ }
}
})
diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js
index 1cf16fc9..75154d66 100644
--- a/miniprogram/pages/index/index.js
+++ b/miniprogram/pages/index/index.js
@@ -149,7 +149,7 @@ Page({
// 从服务端获取精选推荐(加权算法:阅读量50% + 时效30% + 付款率20%)和最新更新
async loadFeaturedFromServer() {
try {
- const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const res = await app.request({ url: '/api/miniprogram/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条有效章节作为回退
@@ -199,7 +199,7 @@ Page({
async loadBookData() {
try {
- const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ 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 || []
this.setData({
@@ -254,7 +254,7 @@ Page({
async loadLatestChapters() {
try {
- const res = await app.request({ url: '/api/book/all-chapters', silent: true })
+ const res = await app.request({ url: '/api/miniprogram/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)
@@ -288,5 +288,13 @@ Page({
await this.initData()
this.updateUserStatus()
wx.stopPullDownRefresh()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 真实商业故事',
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
+ }
}
})
diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js
index 6d3b9d87..31a5055f 100644
--- a/miniprogram/pages/match/match.js
+++ b/miniprogram/pages/match/match.js
@@ -659,5 +659,13 @@ Page({
},
// 阻止事件冒泡
- preventBubble() {}
+ preventBubble() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 找伙伴',
+ path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
+ }
+ }
})
diff --git a/miniprogram/pages/member-detail/member-detail.js b/miniprogram/pages/member-detail/member-detail.js
index 04533440..a70cc4fc 100644
--- a/miniprogram/pages/member-detail/member-detail.js
+++ b/miniprogram/pages/member-detail/member-detail.js
@@ -87,5 +87,14 @@ Page({
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
- goBack() { wx.navigateBack() }
+ goBack() { wx.navigateBack() },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ const id = this.data.member?.id
+ return {
+ title: 'Soul创业派对 - 创业者详情',
+ path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail'
+ }
+ }
})
diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js
index becc5a0f..dd659d65 100644
--- a/miniprogram/pages/my/my.js
+++ b/miniprogram/pages/my/my.js
@@ -711,5 +711,13 @@ Page({
},
// 阻止冒泡
- stopPropagation() {}
+ stopPropagation() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 我的',
+ path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
+ }
+ }
})
diff --git a/miniprogram/pages/privacy/privacy.js b/miniprogram/pages/privacy/privacy.js
index 0cd665db..0c95c06e 100644
--- a/miniprogram/pages/privacy/privacy.js
+++ b/miniprogram/pages/privacy/privacy.js
@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 隐私政策',
+ path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
+ }
}
})
diff --git a/miniprogram/pages/purchases/purchases.js b/miniprogram/pages/purchases/purchases.js
index c458ad74..ab6b73bb 100644
--- a/miniprogram/pages/purchases/purchases.js
+++ b/miniprogram/pages/purchases/purchases.js
@@ -63,5 +63,13 @@ Page({
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
- goBack() { wx.navigateBack() }
+ goBack() { wx.navigateBack() },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 购买记录',
+ path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
+ }
+ }
})
diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js
index 6e9fface..d9e7cee9 100644
--- a/miniprogram/pages/read/read.js
+++ b/miniprogram/pages/read/read.js
@@ -454,11 +454,10 @@ Page({
})
},
- // 分享到微信 - 自动带分享人ID
+ // 分享到微信 - 自动带分享人ID(统一使用 app.getMyReferralCode)
onShareAppMessage() {
const { section, sectionId } = this.data
- const userInfo = app.globalData.userInfo
- const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
+ const ref = app.getMyReferralCode()
// 分享标题优化
const shareTitle = section?.title
@@ -467,7 +466,7 @@ Page({
return {
title: shareTitle,
- path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
+ path: `/pages/read/read?id=${sectionId}${ref ? '&ref=' + ref : ''}`,
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
}
},
@@ -475,12 +474,11 @@ Page({
// 分享到朋友圈
onShareTimeline() {
const { section, sectionId } = this.data
- const userInfo = app.globalData.userInfo
- const referralCode = userInfo?.referralCode || ''
+ const ref = app.getMyReferralCode()
return {
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
- query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
+ query: `id=${sectionId}${ref ? '&ref=' + ref : ''}`
}
},
diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js
index b56fdc74..9985baff 100644
--- a/miniprogram/pages/referral/referral.js
+++ b/miniprogram/pages/referral/referral.js
@@ -877,12 +877,13 @@ Page({
})
},
- // 分享 - 带推荐码
+ // 分享 - 带推荐码(优先用页面数据,空时用 app.getMyReferralCode)
onShareAppMessage() {
- console.log('[Referral] 分享给好友,推荐码:', this.data.referralCode)
+ const ref = this.data.referralCode || app.getMyReferralCode()
+ console.log('[Referral] 分享给好友,推荐码:', ref)
return {
title: 'Soul创业派对 - 来自派对房的真实商业故事',
- path: `/pages/index/index?ref=${this.data.referralCode}`
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
// 不设置 imageUrl,使用小程序默认截图
// 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
}
@@ -890,10 +891,11 @@ Page({
// 分享到朋友圈
onShareTimeline() {
- console.log('[Referral] 分享到朋友圈,推荐码:', this.data.referralCode)
+ const ref = this.data.referralCode || app.getMyReferralCode()
+ console.log('[Referral] 分享到朋友圈,推荐码:', ref)
return {
title: `Soul创业派对 - 62个真实商业案例`,
- query: `ref=${this.data.referralCode}`
+ query: ref ? `ref=${ref}` : ''
// 不设置 imageUrl,使用小程序默认截图
}
},
diff --git a/miniprogram/pages/search/search.js b/miniprogram/pages/search/search.js
index 1ac887d9..0eeb383c 100644
--- a/miniprogram/pages/search/search.js
+++ b/miniprogram/pages/search/search.js
@@ -105,5 +105,13 @@ Page({
// 返回上一页
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 搜索',
+ path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search'
+ }
}
})
diff --git a/miniprogram/pages/settings/settings.js b/miniprogram/pages/settings/settings.js
index 157af2f0..c46ab852 100644
--- a/miniprogram/pages/settings/settings.js
+++ b/miniprogram/pages/settings/settings.js
@@ -493,5 +493,13 @@ Page({
// 跳转到地址管理页
goToAddresses() {
wx.navigateTo({ url: '/pages/addresses/addresses' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 设置',
+ path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
+ }
}
})
diff --git a/miniprogram/pages/vip/vip.js b/miniprogram/pages/vip/vip.js
index 1f9795b0..958da1a5 100644
--- a/miniprogram/pages/vip/vip.js
+++ b/miniprogram/pages/vip/vip.js
@@ -128,5 +128,13 @@ Page({
} catch (e) { wx.showToast({ title: '保存失败', icon: 'none' }) }
},
- goBack() { wx.navigateBack() }
+ goBack() { wx.navigateBack() },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - VIP会员',
+ path: ref ? `/pages/vip/vip?ref=${ref}` : '/pages/vip/vip'
+ }
+ }
})
diff --git a/miniprogram/pages/withdraw-records/withdraw-records.js b/miniprogram/pages/withdraw-records/withdraw-records.js
index 5ef1c2fd..9757efac 100644
--- a/miniprogram/pages/withdraw-records/withdraw-records.js
+++ b/miniprogram/pages/withdraw-records/withdraw-records.js
@@ -119,5 +119,13 @@ Page({
wx.hideLoading()
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
}
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 提现记录',
+ path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
+ }
}
})
diff --git a/miniprogram/utils/scene.js b/miniprogram/utils/scene.js
new file mode 100644
index 00000000..b6b58704
--- /dev/null
+++ b/miniprogram/utils/scene.js
@@ -0,0 +1,45 @@
+/**
+ * Soul创业派对 - 小程序码 scene 参数统一编解码(海报生成 ↔ 扫码解析闭环)
+ * 官方以 options.scene 接收扫码参数;后端生成码时会把 & 转为 _,故解析时同时支持 & 和 _
+ * scene 同时可带两个参数:章节标识(mid/id) + 推荐人(ref)
+ */
+
+const SEP = '_' // 生成时统一用 _,与微信实际存储一致,且不占 32 字符限制
+
+/**
+ * 编码:生成海报/分享时组 scene 字符串(同时带 mid或id + ref)
+ * @param {{ mid?: number, id?: string, ref?: string }} opts
+ * @returns {string} 如 "mid=1_ref=ogpTW5fmXR" 或 "id=1.1_ref=xxx"
+ */
+function buildScene(opts) {
+ const parts = []
+ if (opts.mid != null && opts.mid !== '') parts.push(`mid=${opts.mid}`)
+ if (opts.id) parts.push(`id=${opts.id}`)
+ if (opts.ref) parts.push(`ref=${opts.ref}`)
+ return parts.join(SEP)
+}
+
+/**
+ * 解码:从 options.scene 解析出 mid、id、ref(支持 & 或 _ 分隔)
+ * @param {string} sceneStr 原始 scene(可能未 decodeURIComponent)
+ * @returns {{ mid: number, id: string, ref: string }}
+ */
+function parseScene(sceneStr) {
+ const res = { mid: 0, id: '', ref: '' }
+ if (!sceneStr || typeof sceneStr !== 'string') return res
+ const decoded = decodeURIComponent(String(sceneStr)).trim()
+ const parts = decoded.split(/[&_]/)
+ for (const part of parts) {
+ const eq = part.indexOf('=')
+ if (eq > 0) {
+ const k = part.slice(0, eq)
+ const v = part.slice(eq + 1)
+ if (k === 'mid') res.mid = parseInt(v, 10) || 0
+ if (k === 'id' && v) res.id = v
+ if (k === 'ref' && v) res.ref = v
+ }
+ }
+ return res
+}
+
+module.exports = { buildScene, parseScene }
diff --git a/miniprogram2/.gitignore b/miniprogram2/.gitignore
new file mode 100644
index 00000000..14ea590c
--- /dev/null
+++ b/miniprogram2/.gitignore
@@ -0,0 +1,14 @@
+# Windows
+[Dd]esktop.ini
+Thumbs.db
+$RECYCLE.BIN/
+
+# macOS
+.DS_Store
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+
+# Node.js
+node_modules/
diff --git a/miniprogram2/README.md b/miniprogram2/README.md
new file mode 100644
index 00000000..2cc11dbf
--- /dev/null
+++ b/miniprogram2/README.md
@@ -0,0 +1,138 @@
+# Soul创业实验 - 微信小程序
+
+> 一场SOUL的创业实验场 - 来自Soul派对房的真实商业故事
+
+## 📱 项目简介
+
+本项目是《一场SOUL的创业实验场》的微信小程序版本,完整还原了Web端的所有UI界面和功能。
+
+## 🎨 设计特点
+
+- **主题色**: Soul青色 (#00CED1)
+- **设计风格**: 深色主题 + 毛玻璃效果
+- **1:1还原**: 完全复刻Web端的UI设计
+
+## 📂 项目结构
+
+```
+miniprogram/
+├── app.js # 应用入口
+├── app.json # 应用配置
+├── app.wxss # 全局样式
+├── custom-tab-bar/ # 自定义TabBar组件
+│ ├── index.js
+│ ├── index.json
+│ ├── index.wxml
+│ └── index.wxss
+├── pages/
+│ ├── index/ # 首页
+│ ├── chapters/ # 目录页
+│ ├── match/ # 找伙伴页
+│ ├── my/ # 我的页面
+│ ├── read/ # 阅读页
+│ ├── about/ # 关于作者
+│ ├── referral/ # 推广中心
+│ ├── purchases/ # 订单页
+│ └── settings/ # 设置页
+├── utils/
+│ ├── util.js # 工具函数
+│ └── payment.js # 支付工具
+├── assets/
+│ └── icons/ # 图标资源
+├── project.config.json # 项目配置
+└── sitemap.json # 站点地图
+```
+
+## 🚀 功能列表
+
+### 核心功能
+- ✅ 首页 - 书籍展示、推荐章节、阅读进度
+- ✅ 目录 - 完整章节列表、篇章折叠展开
+- ✅ 找伙伴 - 匹配动画、匹配类型选择
+- ✅ 我的 - 个人信息、订单、推广中心
+- ✅ 阅读 - 付费墙、章节导航、分享功能
+
+### 特色功能
+- ✅ 自定义TabBar(中间突出的找伙伴按钮)
+- ✅ 阅读进度条
+- ✅ 匹配动画效果
+- ✅ 付费墙与购买流程
+- ✅ 分享海报功能
+- ✅ 推广佣金系统
+
+## 🛠 开发指南
+
+### 环境要求
+- 微信开发者工具 >= 1.06.2308310
+- 基础库版本 >= 3.3.4
+
+### 快速开始
+
+1. **下载微信开发者工具**
+ - 前往 [微信开发者工具下载页面](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
+
+2. **导入项目**
+ - 打开微信开发者工具
+ - 选择"导入项目"
+ - 项目目录选择 `miniprogram` 文件夹
+ - AppID 使用: `wx432c93e275548671`
+
+3. **编译运行**
+ - 点击"编译"按钮
+ - 在模拟器中预览效果
+
+### 真机调试
+
+1. 点击工具栏的"预览"按钮
+2. 使用微信扫描二维码
+3. 在真机上测试所有功能
+
+## 📝 配置说明
+
+### API配置
+在 `app.js` 中修改 `globalData.baseUrl`:
+
+```javascript
+globalData: {
+ baseUrl: 'https://soul.ckb.fit', // 你的API地址
+ // ...
+}
+```
+
+### AppID配置
+在 `project.config.json` 中修改:
+
+```json
+{
+ "appid": "你的小程序AppID"
+}
+```
+
+## 🎯 上线发布
+
+1. **准备工作**
+ - 确保所有功能测试通过
+ - 检查API接口是否正常
+ - 确认支付功能已配置
+
+2. **上传代码**
+ - 在开发者工具中点击"上传"
+ - 填写版本号和项目备注
+
+3. **提交审核**
+ - 登录[微信公众平台](https://mp.weixin.qq.com)
+ - 进入"版本管理"
+ - 提交审核
+
+4. **发布上线**
+ - 审核通过后点击"发布"
+
+## 🔗 相关链接
+
+- **Web版本**: https://soul.ckb.fit
+- **作者微信**: 28533368
+- **技术支持**: 存客宝
+
+## 📄 版权信息
+
+© 2024 卡若. All rights reserved.
diff --git a/miniprogram2/app.js b/miniprogram2/app.js
new file mode 100644
index 00000000..7d3ab6c9
--- /dev/null
+++ b/miniprogram2/app.js
@@ -0,0 +1,575 @@
+/**
+ * Soul创业派对 - 小程序入口
+ * 开发: 卡若
+ */
+
+const { parseScene } = require('./utils/scene.js')
+
+App({
+ globalData: {
+ // API基础地址 - 连接真实后端
+ baseUrl: 'https://soulapi.quwanzhi.com',
+ // baseUrl: 'https://souldev.quwanzhi.com',
+ // baseUrl: 'http://localhost:3006',
+ // baseUrl: 'http://localhost:8080',
+
+ // 小程序配置 - 真实AppID
+ appId: 'wxb8bbb2b10dec74aa',
+
+ // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
+ withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
+
+ // 微信支付配置
+ mchId: '1318592501', // 商户号
+
+ // 用户信息
+ userInfo: null,
+ openId: null, // 微信openId,支付必需
+ isLoggedIn: false,
+
+ // 书籍数据
+ bookData: null,
+ totalSections: 62,
+
+ // 购买记录
+ purchasedSections: [],
+ sectionMidMap: {}, // id -> mid,来自 purchase-status
+ hasFullBook: false,
+ matchCount: 0,
+ matchQuota: null,
+
+ // 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
+ readSectionIds: [],
+
+ // 推荐绑定
+ pendingReferralCode: null, // 待绑定的推荐码
+
+ // 主题配置
+ theme: {
+ brandColor: '#00CED1',
+ brandSecondary: '#20B2AA',
+ goldColor: '#FFD700',
+ bgColor: '#000000',
+ cardBg: '#1c1c1e'
+ },
+
+ // 系统信息
+ systemInfo: null,
+ statusBarHeight: 44,
+ navBarHeight: 88,
+
+ // TabBar相关
+ currentTab: 0
+ },
+
+ onLaunch(options) {
+ this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
+ // 获取系统信息
+ this.getSystemInfo()
+
+ // 检查登录状态
+ this.checkLoginStatus()
+
+ // 加载书籍数据
+ this.loadBookData()
+
+ // 检查更新
+ this.checkUpdate()
+
+ // 处理分享参数(推荐码绑定)
+ this.handleReferralCode(options)
+ },
+
+ // 小程序显示时也检查分享参数
+ onShow(options) {
+ this.handleReferralCode(options)
+ },
+
+ // 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
+ handleReferralCode(options) {
+ const query = options?.query || {}
+ let refCode = query.ref || query.referralCode
+ const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || ''
+ if (sceneStr) {
+ const parsed = parseScene(sceneStr)
+ if (parsed.mid) this.globalData.initialSectionMid = parsed.mid
+ if (parsed.id) this.globalData.initialSectionId = parsed.id
+ if (parsed.ref) refCode = parsed.ref
+ }
+ if (refCode) {
+ console.log('[App] 检测到推荐码:', refCode)
+
+ // 立即记录访问(不需要登录,用于统计"通过链接进的人数")
+ this.recordReferralVisit(refCode)
+
+ // 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺)
+ this.globalData.pendingReferralCode = refCode
+ wx.setStorageSync('pendingReferralCode', refCode)
+ // 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
+ wx.setStorageSync('referral_code', refCode)
+
+ // 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
+ if (this.globalData.isLoggedIn && this.globalData.userInfo) {
+ this.bindReferralCode(refCode)
+ }
+ }
+ },
+
+ // 记录推荐访问(不需要登录,用于统计)
+ async recordReferralVisit(refCode) {
+ try {
+ // 获取openId(如果有)
+ const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
+ const userId = this.globalData.userInfo?.id || ''
+
+ await this.request('/api/miniprogram/referral/visit', {
+ method: 'POST',
+ data: {
+ referralCode: refCode,
+ visitorOpenId: openId,
+ visitorId: userId,
+ source: 'miniprogram',
+ page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
+ },
+ silent: true
+ })
+ console.log('[App] 记录推荐访问成功')
+ } catch (e) {
+ console.log('[App] 记录推荐访问失败:', e.message)
+ // 忽略错误,不影响用户体验
+ }
+ },
+
+ // 绑定推荐码到用户
+ async bindReferralCode(refCode) {
+ try {
+ const userId = this.globalData.userInfo?.id
+ if (!userId || !refCode) return
+
+ console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
+
+ // 调用API绑定推荐关系
+ 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')
+ }
+ } catch (e) {
+ console.error('[App] 绑定推荐码失败:', e)
+ }
+ },
+
+ // 根据业务 id 从 bookData 查 mid(用于跳转)
+ getSectionMid(sectionId) {
+ const list = this.globalData.bookData || []
+ const ch = list.find(c => c.id === sectionId)
+ return ch?.mid || 0
+ },
+
+ // 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串)
+ getMyReferralCode() {
+ const user = this.globalData.userInfo
+ if (!user) return ''
+ if (user.referralCode) return user.referralCode
+ if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
+ return ''
+ },
+
+ // 获取系统信息
+ getSystemInfo() {
+ try {
+ const systemInfo = wx.getSystemInfoSync()
+ this.globalData.systemInfo = systemInfo
+ this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
+
+ // 计算导航栏高度
+ const menuButton = wx.getMenuButtonBoundingClientRect()
+ if (menuButton) {
+ this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight
+ }
+ } catch (e) {
+ console.error('获取系统信息失败:', e)
+ }
+ },
+
+ // 检查登录状态
+ checkLoginStatus() {
+ try {
+ const userInfo = wx.getStorageSync('userInfo')
+ const token = wx.getStorageSync('token')
+
+ if (userInfo && token) {
+ this.globalData.userInfo = userInfo
+ this.globalData.isLoggedIn = true
+ this.globalData.purchasedSections = userInfo.purchasedSections || []
+ this.globalData.hasFullBook = userInfo.hasFullBook || false
+ }
+ } catch (e) {
+ console.error('检查登录状态失败:', e)
+ }
+ },
+
+ // 加载书籍数据
+ 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) {
+ this.globalData.bookData = res.data
+ wx.setStorageSync('bookData', res.data)
+ }
+ } catch (e) {
+ console.error('加载书籍数据失败:', e)
+ }
+ },
+
+ // 版本更新检测:发现新版本时提示用户立即更新
+ checkUpdate() {
+ if (!wx.canIUse('getUpdateManager')) return
+ const updateManager = wx.getUpdateManager()
+
+ updateManager.onCheckForUpdate((res) => {
+ if (res.hasUpdate) {
+ console.log('[App] 发现新版本,正在后台下载')
+ wx.showToast({ title: '发现新版本,正在准备…', icon: 'none', duration: 2000 })
+ }
+ })
+
+ updateManager.onUpdateReady(() => {
+ wx.showModal({
+ title: '发现新版本',
+ content: '小程序已更新,请立即重启以使用最新版本。',
+ confirmText: '立即更新',
+ cancelText: '稍后',
+ showCancel: true,
+ success: (res) => {
+ if (res.confirm) {
+ updateManager.applyUpdate()
+ }
+ }
+ })
+ })
+
+ updateManager.onUpdateFailed(() => {
+ wx.showModal({
+ title: '更新失败',
+ content: '新版本下载失败,请稍后重新打开小程序或删除后重新搜索打开。',
+ showCancel: false
+ })
+ })
+ },
+
+ /**
+ * 从 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',
+ data: options.data || {},
+ header: {
+ 'Content-Type': 'application/json',
+ 'Authorization': token ? `Bearer ${token}` : '',
+ ...options.header
+ },
+ success: (res) => {
+ const data = res.data
+ if (res.statusCode === 200) {
+ // 业务失败: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) => {
+ const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
+ showError(msg)
+ reject(new Error(msg))
+ }
+ })
+ })
+ },
+
+ // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
+ async login() {
+ try {
+ const loginRes = await new Promise((resolve, reject) => {
+ wx.login({ success: resolve, fail: reject })
+ })
+ if (!loginRes || !loginRes.code) {
+ console.warn('[App] wx.login 未返回 code')
+ wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
+ return null
+ }
+ try {
+ const res = await this.request('/api/miniprogram/login', {
+ method: 'POST',
+ data: { code: loginRes.code }
+ })
+
+ if (res.success && res.data) {
+ // 保存openId
+ if (res.data.openId) {
+ this.globalData.openId = res.data.openId
+ wx.setStorageSync('openId', res.data.openId)
+ console.log('[App] 获取openId成功')
+ }
+
+ // 保存用户信息
+ 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] 登录后自动绑定推荐码:', pendingRef)
+ this.bindReferralCode(pendingRef)
+ }
+ }
+
+ return res.data
+ }
+ } catch (apiError) {
+ console.log('[App] API登录失败:', apiError.message)
+ // 不使用模拟登录,提示用户网络问题
+ wx.showToast({ title: '网络异常,请重试', icon: 'none' })
+ return null
+ }
+
+ return null
+ } catch (e) {
+ console.error('[App] 登录失败:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ return null
+ }
+ },
+
+ // 获取openId (支付必需)
+ async getOpenId() {
+ // 先检查缓存
+ const cachedOpenId = wx.getStorageSync('openId')
+ if (cachedOpenId) {
+ this.globalData.openId = cachedOpenId
+ return cachedOpenId
+ }
+
+ // 没有缓存则登录获取
+ try {
+ const loginRes = await new Promise((resolve, reject) => {
+ wx.login({ success: resolve, fail: reject })
+ })
+
+ const res = await this.request('/api/miniprogram/login', {
+ method: 'POST',
+ data: { code: loginRes.code }
+ })
+
+ 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) {
+ console.error('[App] 获取openId失败:', e)
+ }
+
+ return null
+ },
+
+ // 模拟登录已废弃 - 不再使用
+ // 现在必须使用真实的微信登录获取openId作为唯一标识
+ mockLogin() {
+ console.warn('[App] mockLogin已废弃,请使用真实登录')
+ return null
+ },
+
+ // 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
+ async loginWithPhone(phoneCode) {
+ try {
+ 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: loginRes.code, phoneCode }
+ })
+
+ if (res.success && res.data) {
+ 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] 手机号登录后自动绑定推荐码:', pendingRef)
+ this.bindReferralCode(pendingRef)
+ }
+
+ return res.data
+ }
+ } catch (e) {
+ console.log('[App] 手机号登录失败:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ }
+
+ return null
+ },
+
+ // 退出登录
+ logout() {
+ this.globalData.userInfo = null
+ this.globalData.isLoggedIn = false
+ this.globalData.purchasedSections = []
+ this.globalData.hasFullBook = false
+ this.globalData.matchCount = 0
+ this.globalData.matchQuota = null
+
+ wx.removeStorageSync('userInfo')
+ wx.removeStorageSync('token')
+ },
+
+ // 检查是否已购买章节
+ hasPurchased(sectionId) {
+ if (this.globalData.hasFullBook) return true
+ 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
+ },
+
+ // 切换TabBar
+ switchTab(index) {
+ this.globalData.currentTab = index
+ },
+
+ // 显示Toast
+ showToast(title, icon = 'none') {
+ wx.showToast({
+ title,
+ icon,
+ duration: 2000
+ })
+ },
+
+ // 显示Loading
+ showLoading(title = '加载中...') {
+ wx.showLoading({
+ title,
+ mask: true
+ })
+ },
+
+ // 隐藏Loading
+ hideLoading() {
+ wx.hideLoading()
+ }
+})
diff --git a/miniprogram2/app.json b/miniprogram2/app.json
new file mode 100644
index 00000000..e70546ce
--- /dev/null
+++ b/miniprogram2/app.json
@@ -0,0 +1 @@
+{"pages":["pages/index/index","pages/chapters/chapters","pages/match/match","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/scan/scan","pages/profile-edit/profile-edit"],"window":{"backgroundTextStyle":"light","navigationBarBackgroundColor":"#000000","navigationBarTitleText":"Soul创业派对","navigationBarTextStyle":"white","backgroundColor":"#000000","navigationStyle":"custom"},"tabBar":{"custom":true,"color":"#8e8e93","selectedColor":"#00CED1","backgroundColor":"#1c1c1e","borderStyle":"black","list":[{"pagePath":"pages/index/index","text":"首页"},{"pagePath":"pages/chapters/chapters","text":"目录"},{"pagePath":"pages/match/match","text":"找伙伴"},{"pagePath":"pages/my/my","text":"我的"}]},"usingComponents":{},"__usePrivacyCheck__":true,"permission":{"scope.userLocation":{"desc":"用于匹配附近的书友"}},"requiredPrivateInfos":["getLocation"],"lazyCodeLoading":"requiredComponents","style":"v2","sitemapLocation":"sitemap.json"}
diff --git a/miniprogram2/app.wxss b/miniprogram2/app.wxss
new file mode 100644
index 00000000..9ce22a06
--- /dev/null
+++ b/miniprogram2/app.wxss
@@ -0,0 +1,606 @@
+/**
+ * Soul创业实验 - 全局样式
+ * 主题色: #00CED1 (Soul青色)
+ * 开发: 卡若
+ */
+
+/* ===== 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: 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;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* ===== 全局容器 ===== */
+.container {
+ min-height: 100vh;
+ padding: 0;
+ background: #000000;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+/* ===== 品牌色系 ===== */
+.brand-color {
+ color: #00CED1;
+}
+
+.brand-bg {
+ background-color: #00CED1;
+}
+
+.brand-gradient {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+}
+
+.gold-color {
+ color: #FFD700;
+}
+
+.gold-bg {
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+}
+
+/* ===== 文字渐变 ===== */
+.gradient-text {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.gold-gradient-text {
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+/* ===== 按钮样式 ===== */
+.btn-primary {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #ffffff;
+ border: none;
+ border-radius: 48rpx;
+ padding: 28rpx 48rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+ box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn-primary::after {
+ border: none;
+}
+
+.btn-primary:active {
+ opacity: 0.85;
+ transform: scale(0.98);
+}
+
+.btn-secondary {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+ border: 2rpx solid rgba(0, 206, 209, 0.3);
+ border-radius: 48rpx;
+ padding: 28rpx 48rpx;
+ font-size: 32rpx;
+ font-weight: 500;
+}
+
+.btn-secondary::after {
+ border: none;
+}
+
+.btn-secondary:active {
+ background: rgba(0, 206, 209, 0.2);
+}
+
+.btn-ghost {
+ background: rgba(255, 255, 255, 0.05);
+ color: #ffffff;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 48rpx;
+ padding: 28rpx 48rpx;
+ font-size: 32rpx;
+}
+
+.btn-ghost::after {
+ border: none;
+}
+
+.btn-ghost:active {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.btn-gold {
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+ color: #000000;
+ border: none;
+ border-radius: 48rpx;
+ padding: 28rpx 48rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+ box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
+}
+
+.btn-gold::after {
+ border: none;
+}
+
+/* ===== 卡片样式 ===== */
+.card {
+ background: rgba(28, 28, 30, 0.9);
+ border-radius: 32rpx;
+ padding: 32rpx;
+ margin: 24rpx 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.card-light {
+ background: rgba(44, 44, 46, 0.8);
+ border-radius: 24rpx;
+ padding: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.08);
+}
+
+.card-gradient {
+ background: linear-gradient(135deg, rgba(28, 28, 30, 1) 0%, rgba(44, 44, 46, 1) 100%);
+ border-radius: 32rpx;
+ padding: 32rpx;
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+}
+
+.card-brand {
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.05) 100%);
+ border-radius: 32rpx;
+ padding: 32rpx;
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+}
+
+/* ===== 输入框样式 ===== */
+.input-ios {
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 24rpx;
+ padding: 28rpx 32rpx;
+ font-size: 32rpx;
+ color: #ffffff;
+}
+
+.input-ios:focus {
+ border-color: rgba(0, 206, 209, 0.5);
+}
+
+.input-ios-placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* ===== 列表项样式 ===== */
+.list-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 28rpx 32rpx;
+ background: rgba(28, 28, 30, 0.9);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.list-item:first-child {
+ border-radius: 24rpx 24rpx 0 0;
+}
+
+.list-item:last-child {
+ border-radius: 0 0 24rpx 24rpx;
+ border-bottom: none;
+}
+
+.list-item:only-child {
+ border-radius: 24rpx;
+}
+
+.list-item:active {
+ background: rgba(44, 44, 46, 1);
+}
+
+/* ===== 标签样式 ===== */
+.tag {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8rpx 20rpx;
+ min-width: 80rpx;
+ border-radius: 8rpx;
+ font-size: 22rpx;
+ font-weight: 500;
+ box-sizing: border-box;
+ text-align: center;
+}
+
+.tag-brand {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+}
+
+.tag-gold {
+ background: rgba(255, 215, 0, 0.1);
+ color: #FFD700;
+}
+
+.tag-pink {
+ background: rgba(233, 30, 99, 0.1);
+ color: #E91E63;
+}
+
+.tag-purple {
+ background: rgba(123, 97, 255, 0.1);
+ color: #7B61FF;
+}
+
+.tag-free {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+}
+
+/* ===== 分隔线 ===== */
+.divider {
+ height: 1rpx;
+ background: rgba(255, 255, 255, 0.05);
+ margin: 24rpx 0;
+}
+
+.divider-vertical {
+ width: 2rpx;
+ height: 48rpx;
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* ===== 骨架屏动画 ===== */
+.skeleton {
+ background: linear-gradient(90deg,
+ rgba(28, 28, 30, 1) 25%,
+ rgba(44, 44, 46, 1) 50%,
+ rgba(28, 28, 30, 1) 75%
+ );
+ background-size: 200% 100%;
+ animation: skeleton-loading 1.5s ease-in-out infinite;
+ border-radius: 8rpx;
+}
+
+@keyframes skeleton-loading {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+/* ===== 页面过渡动画 ===== */
+.page-transition {
+ animation: fadeIn 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20rpx);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* ===== 弹窗动画 ===== */
+.modal-overlay {
+ animation: modalOverlayIn 0.25s ease-out;
+}
+
+.modal-content {
+ animation: modalContentIn 0.3s cubic-bezier(0.32, 0.72, 0, 1);
+}
+
+@keyframes modalOverlayIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes modalContentIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(20rpx);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+/* ===== 脉动动画 ===== */
+.pulse {
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.05);
+ opacity: 0.8;
+ }
+}
+
+/* ===== 发光效果 ===== */
+.glow {
+ box-shadow: 0 0 40rpx rgba(0, 206, 209, 0.3);
+}
+
+.glow-gold {
+ box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.3);
+}
+
+/* ===== 文字样式 ===== */
+.text-xs {
+ font-size: 22rpx;
+}
+
+.text-sm {
+ font-size: 26rpx;
+}
+
+.text-base {
+ font-size: 28rpx;
+}
+
+.text-lg {
+ font-size: 32rpx;
+}
+
+.text-xl {
+ font-size: 36rpx;
+}
+
+.text-2xl {
+ font-size: 44rpx;
+}
+
+.text-3xl {
+ font-size: 56rpx;
+}
+
+.text-white {
+ color: #ffffff;
+}
+
+.text-gray {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.text-muted {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.text-center {
+ text-align: center;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.font-bold {
+ font-weight: 700;
+}
+
+/* ===== Flex布局 ===== */
+.flex {
+ display: flex;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.justify-around {
+ justify-content: space-around;
+}
+
+.flex-1 {
+ flex: 1;
+}
+
+.gap-1 {
+ gap: 8rpx;
+}
+
+.gap-2 {
+ gap: 16rpx;
+}
+
+.gap-3 {
+ gap: 24rpx;
+}
+
+.gap-4 {
+ gap: 32rpx;
+}
+
+/* ===== 间距 ===== */
+.p-2 { padding: 16rpx; }
+.p-3 { padding: 24rpx; }
+.p-4 { padding: 32rpx; }
+.p-5 { padding: 40rpx; }
+
+.px-4 { padding-left: 32rpx; padding-right: 32rpx; }
+.py-2 { padding-top: 16rpx; padding-bottom: 16rpx; }
+.py-3 { padding-top: 24rpx; padding-bottom: 24rpx; }
+
+.m-4 { margin: 32rpx; }
+.mx-4 { margin-left: 32rpx; margin-right: 32rpx; }
+.my-3 { margin-top: 24rpx; margin-bottom: 24rpx; }
+.mb-2 { margin-bottom: 16rpx; }
+.mb-3 { margin-bottom: 24rpx; }
+.mb-4 { margin-bottom: 32rpx; }
+.mt-4 { margin-top: 32rpx; }
+
+/* ===== 圆角 ===== */
+.rounded { border-radius: 8rpx; }
+.rounded-lg { border-radius: 16rpx; }
+.rounded-xl { border-radius: 24rpx; }
+.rounded-2xl { border-radius: 32rpx; }
+.rounded-full { border-radius: 50%; }
+
+/* ===== 安全区域 ===== */
+.safe-bottom {
+ padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+}
+
+.pb-tabbar {
+ padding-bottom: 200rpx;
+}
+
+/* ===== 头部导航占位 ===== */
+.nav-placeholder {
+ height: calc(88rpx + env(safe-area-inset-top, 44rpx));
+}
+
+/* ===== 隐藏滚动条 ===== */
+::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+/* ===== 触摸反馈 ===== */
+.touch-feedback {
+ transition: all 0.15s ease;
+}
+
+.touch-feedback:active {
+ opacity: 0.7;
+ transform: scale(0.98);
+}
+
+/* ===== 进度条 ===== */
+.progress-bar {
+ height: 8rpx;
+ background: rgba(44, 44, 46, 1);
+ border-radius: 4rpx;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
+ border-radius: 4rpx;
+ transition: width 0.3s ease;
+}
+
+/* ===== 头像样式 ===== */
+.avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #00CED1;
+ font-weight: 700;
+ font-size: 32rpx;
+ border: 4rpx solid rgba(0, 206, 209, 0.3);
+}
+
+.avatar-lg {
+ width: 120rpx;
+ height: 120rpx;
+ font-size: 48rpx;
+}
+
+/* ===== 图标容器 ===== */
+.icon-box {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.icon-box-brand {
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
+}
+
+.icon-box-gold {
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 165, 0, 0.1) 100%);
+}
+
+/* ===== 渐变背景 ===== */
+.bg-gradient-dark {
+ background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%);
+}
+
+.bg-gradient-brand {
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
+}
diff --git a/miniprogram2/assets/icons/alert-circle.svg b/miniprogram2/assets/icons/alert-circle.svg
new file mode 100644
index 00000000..f5a441f3
--- /dev/null
+++ b/miniprogram2/assets/icons/alert-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/miniprogram2/assets/icons/arrow-right.svg b/miniprogram2/assets/icons/arrow-right.svg
new file mode 100644
index 00000000..1dc64d3f
--- /dev/null
+++ b/miniprogram2/assets/icons/arrow-right.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/bell.svg b/miniprogram2/assets/icons/bell.svg
new file mode 100644
index 00000000..0e7e405b
--- /dev/null
+++ b/miniprogram2/assets/icons/bell.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/book-open.svg b/miniprogram2/assets/icons/book-open.svg
new file mode 100644
index 00000000..d833e86b
--- /dev/null
+++ b/miniprogram2/assets/icons/book-open.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/book.svg b/miniprogram2/assets/icons/book.svg
new file mode 100644
index 00000000..93579576
--- /dev/null
+++ b/miniprogram2/assets/icons/book.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/chevron-left.svg b/miniprogram2/assets/icons/chevron-left.svg
new file mode 100644
index 00000000..e406b2b9
--- /dev/null
+++ b/miniprogram2/assets/icons/chevron-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/miniprogram2/assets/icons/gift.svg b/miniprogram2/assets/icons/gift.svg
new file mode 100644
index 00000000..66ac806c
--- /dev/null
+++ b/miniprogram2/assets/icons/gift.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram2/assets/icons/home-active.png b/miniprogram2/assets/icons/home-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6090d87610396c4e046dd531906e3f4aeaeee09
GIT binary patch
literal 699
zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm3?%0U?qUH_3dtTpz6=aiY77hwEes65fIUwRG5VK4FYb!C6W$j8E@-(9CP(<^;b
zR+8R{-Kzt1UUod~dg>GEyyj}|lD4KBJx$xLPV3HJcKPkh5AU=)(hO?WUYv2w#c|H1
zIcM)iojCeb>rS@0`M!udpYC0}*L{@vz4F`15jLOqUD(%kRQi3xw^*IJy7SCRE*%1b
zP8^C~CfhQGev1{DBX_YA=wj6p*NBpo#FA92CCE6dF8*(}7if-ZKuxwC}J8ub)+W
zZO+pTZ&M~mym`$VxJYQ~iX(fN>%zHe7ib!}w`$JY@%YR=_h}iYayA_La(qFuRL-qy
z|NVG>IM0sMTWQ%Ckkq(r<`VRhu<)JU8pk8;M?1KeKUI$3q=~tmdjjRGW&*f
z`py3jCiHtx*Zr0wec()i&()jJDc8T>nDD=Q^|pU<9N)TBCbu8SQu%-6_LV1=8>LR!e>61p+?Q9m|3s^I
zji+yw-13xJGbB~D48^=cl60n6D0C`8z^C&3Ki#JC_pKuq0Yh1}#5JNMC9x#cD!C{X
zNHG{07@6rB80s3Dgcz7u85mg^8)+LDSQ!|oyG-v#(U6;;l9^VCTSJ(nzA;dP2Hb{{
e%-q!ClEmBs+
+
+
+
diff --git a/miniprogram2/assets/icons/image.svg b/miniprogram2/assets/icons/image.svg
new file mode 100644
index 00000000..50ed9e6d
--- /dev/null
+++ b/miniprogram2/assets/icons/image.svg
@@ -0,0 +1,5 @@
+
diff --git a/miniprogram2/assets/icons/list.svg b/miniprogram2/assets/icons/list.svg
new file mode 100644
index 00000000..688326aa
--- /dev/null
+++ b/miniprogram2/assets/icons/list.svg
@@ -0,0 +1,8 @@
+
diff --git a/miniprogram2/assets/icons/match-active.png b/miniprogram2/assets/icons/match-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..da62b4367dbc809c2e64a00821cce980ce3d1c30
GIT binary patch
literal 907
zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm3?%0U?qUH_3dtTpz6=aiY77hwEes65fIl8_s;5>q48lEBhUuMByV?@@LH~$
z@<0xIiKnkC`zuC17A7UP*Y34IpH79fui}4y
z^g1w!^=MUw^E|V|(UX6BltdS)i>2$mdl%_{%1->}Ed9Y};Is
zc6OR=`NyWs8Pj*4iGRAxnANyGHbCF?TGGm#)kTj#Y|FYFt^8un(cQbA?0w|yvdJ;4
zNG$ruwnc6}lcai%_9ab>cr7H~8F(zof1|&N==_fJJx9flG8V45q1q?Wy}ob%--gXU
zp2gMt|NE%Dy0FJpC#kScToCB=FVlaqurMBfv2Et&1He#FEpd$~Nl7e8wMs5Z1yT$~
z21aJO28OzZCLsnURt82^#wOYZ237_JE>jk5MbVI(pOTqYiCcr|NhwaC1`W6kC7HRY
b#U+Wk1-SJj-Lbv~)WhKE>gTe~DWM4f?pT9{
literal 0
HcmV?d00001
diff --git a/miniprogram2/assets/icons/match.png b/miniprogram2/assets/icons/match.png
new file mode 100644
index 0000000000000000000000000000000000000000..b15582e3720ae81bfd63a759fb0aaf1ac555fa51
GIT binary patch
literal 725
zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm0wfvQn)U)og=CK)Uj~LMH3o);76yi2K%s^g
z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWSuh@
z85LUuoH!tai*Hc)U$J}IR+YzCt5x2dxNdsEo88}IZ6z2iRX#*6FrShYz!
zO#K(xcE0EKv;AMs_I&J{R#Sb}Bzc?kLM7dr1?w{+ySAJ0$LU|*BDytw&&H=KMB+b8
zeXc6L?8UOVk#bXV&cEFBR-tQSLVk$Do*o68C(83raX1N2KFHQ;e^e&&+Kq)%=S|YD
zcAg}^<@2qXlb^1X4YS*BcjV{a^v=ZJd=E-DI&5C=`{~|3#)`jZF3zZzTRdBTDnIA{
zg^C9g`~-9&zx*_lJi*zdYxJ~4*6~=#VvDSoO(yTN)!!^pI6C)Tp-I>NoTDeJ{dT1s
zJ83^RtjgNNe&$rGzY~*KG7PLTGAnxOV>Feiid7pjRkL$~A-t{l$NqsxsxqRQ%
zn-_{#1)RBaYnk?)r*WeBdAX}Cw(bt@;nDQk7&c*%NH(h`7c4B5qW&=*o0Bx}L0Kax
zmQ_n!BT7;dOH!?pi&B9UgOP!enXZAMuAxbYfr*uYk(IHDwt<0_fq~1EMO#rc
+
+
diff --git a/miniprogram2/assets/icons/my-active.png b/miniprogram2/assets/icons/my-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..da62b4367dbc809c2e64a00821cce980ce3d1c30
GIT binary patch
literal 907
zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm3?%0U?qUH_3dtTpz6=aiY77hwEes65fIl8_s;5>q48lEBhUuMByV?@@LH~$
z@<0xIiKnkC`zuC17A7UP*Y34IpH79fui}4y
z^g1w!^=MUw^E|V|(UX6BltdS)i>2$mdl%_{%1->}Ed9Y};Is
zc6OR=`NyWs8Pj*4iGRAxnANyGHbCF?TGGm#)kTj#Y|FYFt^8un(cQbA?0w|yvdJ;4
zNG$ruwnc6}lcai%_9ab>cr7H~8F(zof1|&N==_fJJx9flG8V45q1q?Wy}ob%--gXU
zp2gMt|NE%Dy0FJpC#kScToCB=FVlaqurMBfv2Et&1He#FEpd$~Nl7e8wMs5Z1yT$~
z21aJO28OzZCLsnURt82^#wOYZ237_JE>jk5MbVI(pOTqYiCcr|NhwaC1`W6kC7HRY
b#U+Wk1-SJj-Lbv~)WhKE>gTe~DWM4f?pT9{
literal 0
HcmV?d00001
diff --git a/miniprogram2/assets/icons/my.png b/miniprogram2/assets/icons/my.png
new file mode 100644
index 0000000000000000000000000000000000000000..b15582e3720ae81bfd63a759fb0aaf1ac555fa51
GIT binary patch
literal 725
zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm0wfvQn)U)og=CK)Uj~LMH3o);76yi2K%s^g
z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWSuh@
z85LUuoH!tai*Hc)U$J}IR+YzCt5x2dxNdsEo88}IZ6z2iRX#*6FrShYz!
zO#K(xcE0EKv;AMs_I&J{R#Sb}Bzc?kLM7dr1?w{+ySAJ0$LU|*BDytw&&H=KMB+b8
zeXc6L?8UOVk#bXV&cEFBR-tQSLVk$Do*o68C(83raX1N2KFHQ;e^e&&+Kq)%=S|YD
zcAg}^<@2qXlb^1X4YS*BcjV{a^v=ZJd=E-DI&5C=`{~|3#)`jZF3zZzTRdBTDnIA{
zg^C9g`~-9&zx*_lJi*zdYxJ~4*6~=#VvDSoO(yTN)!!^pI6C)Tp-I>NoTDeJ{dT1s
zJ83^RtjgNNe&$rGzY~*KG7PLTGAnxOV>Feiid7pjRkL$~A-t{l$NqsxsxqRQ%
zn-_{#1)RBaYnk?)r*WeBdAX}Cw(bt@;nDQk7&c*%NH(h`7c4B5qW&=*o0Bx}L0Kax
zmQ_n!BT7;dOH!?pi&B9UgOP!enXZAMuAxbYfr*uYk(IHDwt<0_fq~1EMO#rc
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram2/assets/icons/settings.svg b/miniprogram2/assets/icons/settings.svg
new file mode 100644
index 00000000..c7006ea8
--- /dev/null
+++ b/miniprogram2/assets/icons/settings.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/share.svg b/miniprogram2/assets/icons/share.svg
new file mode 100644
index 00000000..93179fc2
--- /dev/null
+++ b/miniprogram2/assets/icons/share.svg
@@ -0,0 +1,7 @@
+
diff --git a/miniprogram2/assets/icons/sparkles.svg b/miniprogram2/assets/icons/sparkles.svg
new file mode 100644
index 00000000..e2a4461f
--- /dev/null
+++ b/miniprogram2/assets/icons/sparkles.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram2/assets/icons/user.svg b/miniprogram2/assets/icons/user.svg
new file mode 100644
index 00000000..8b190427
--- /dev/null
+++ b/miniprogram2/assets/icons/user.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/assets/icons/users.svg b/miniprogram2/assets/icons/users.svg
new file mode 100644
index 00000000..4816094b
--- /dev/null
+++ b/miniprogram2/assets/icons/users.svg
@@ -0,0 +1,6 @@
+
diff --git a/miniprogram2/assets/icons/wallet.svg b/miniprogram2/assets/icons/wallet.svg
new file mode 100644
index 00000000..6d431e54
--- /dev/null
+++ b/miniprogram2/assets/icons/wallet.svg
@@ -0,0 +1,4 @@
+
diff --git a/miniprogram2/components/icon/README.md b/miniprogram2/components/icon/README.md
new file mode 100644
index 00000000..34e394c8
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/components/icon/icon.js b/miniprogram2/components/icon/icon.js
new file mode 100644
index 00000000..b2dec23f
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/components/icon/icon.json b/miniprogram2/components/icon/icon.json
new file mode 100644
index 00000000..a89ef4db
--- /dev/null
+++ b/miniprogram2/components/icon/icon.json
@@ -0,0 +1,4 @@
+{
+ "component": true,
+ "usingComponents": {}
+}
diff --git a/miniprogram2/components/icon/icon.wxml b/miniprogram2/components/icon/icon.wxml
new file mode 100644
index 00000000..b1c29a25
--- /dev/null
+++ b/miniprogram2/components/icon/icon.wxml
@@ -0,0 +1,5 @@
+
+
+
+ {{name}}
+
diff --git a/miniprogram2/components/icon/icon.wxss b/miniprogram2/components/icon/icon.wxss
new file mode 100644
index 00000000..d12d2a0a
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/custom-tab-bar/index.js b/miniprogram2/custom-tab-bar/index.js
new file mode 100644
index 00000000..4acd9546
--- /dev/null
+++ b/miniprogram2/custom-tab-bar/index.js
@@ -0,0 +1,153 @@
+/**
+ * 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',
+ text: '首页',
+ iconType: 'home'
+ },
+ {
+ pagePath: '/pages/chapters/chapters',
+ text: '目录',
+ iconType: 'list'
+ },
+ {
+ pagePath: '/pages/match/match',
+ text: '找伙伴',
+ iconType: 'match',
+ isSpecial: true
+ },
+ {
+ pagePath: '/pages/my/my',
+ text: '我的',
+ iconType: 'user'
+ }
+ ]
+ },
+
+ 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()
+ },
+
+ methods: {
+ // 加载功能配置
+ async loadFeatureConfig() {
+ try {
+ 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'
+ })
+
+
+ // 兼容两种返回格式
+ 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 || {}))
+ }
+
+ 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
+ const index = data.index
+
+ if (this.data.selected === index) return
+
+ wx.switchTab({ url })
+ }
+ }
+})
diff --git a/miniprogram2/custom-tab-bar/index.json b/miniprogram2/custom-tab-bar/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/miniprogram2/custom-tab-bar/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/miniprogram2/custom-tab-bar/index.wxml b/miniprogram2/custom-tab-bar/index.wxml
new file mode 100644
index 00000000..73369b2a
--- /dev/null
+++ b/miniprogram2/custom-tab-bar/index.wxml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+ {{list[0].text}}
+
+
+
+
+
+
+
+ {{list[1].text}}
+
+
+
+
+
+
+
+ {{list[2].text}}
+
+
+
+
+
+
+
+ {{list[3].text}}
+
+
diff --git a/miniprogram2/custom-tab-bar/index.wxss b/miniprogram2/custom-tab-bar/index.wxss
new file mode 100644
index 00000000..98036655
--- /dev/null
+++ b/miniprogram2/custom-tab-bar/index.wxss
@@ -0,0 +1,121 @@
+/**
+ * Soul创业实验 - 自定义TabBar样式
+ * 实现中间突出的"找伙伴"按钮
+ */
+
+.tab-bar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 100rpx;
+ background: rgba(28, 28, 30, 0.95);
+ backdrop-filter: blur(40rpx);
+ -webkit-backdrop-filter: blur(40rpx);
+ display: flex;
+ align-items: flex-end;
+ padding-bottom: env(safe-area-inset-bottom);
+ 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;
+ left: 0;
+ right: 0;
+ height: 1rpx;
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.tab-bar-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10rpx 0 16rpx;
+}
+
+.icon-wrapper {
+ width: 48rpx;
+ height: 48rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 4rpx;
+}
+
+.icon {
+ width: 44rpx;
+ height: 44rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tab-bar-text {
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+/* ===== 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%);
+}
+
+.tab-icon.icon-active {
+ filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
+}
+
+
+/* ===== 找伙伴 - 中间特殊按钮 ===== */
+.special-item {
+ position: relative;
+ margin-top: -32rpx;
+}
+
+.special-button {
+ width: 112rpx;
+ height: 112rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
+ margin-bottom: 4rpx;
+ transition: all 0.2s ease;
+}
+
+.special-button:active {
+ transform: scale(0.95);
+}
+
+.special-active {
+ box-shadow: 0 8rpx 40rpx rgba(0, 206, 209, 0.6);
+}
+
+.special-text {
+ margin-top: 4rpx;
+}
+
+/* ===== 找伙伴特殊按钮图标 ===== */
+.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/miniprogram2/pages/about/about.js b/miniprogram2/pages/about/about.js
new file mode 100644
index 00000000..c4f6b68e
--- /dev/null
+++ b/miniprogram2/pages/about/about.js
@@ -0,0 +1,89 @@
+/**
+ * Soul创业派对 - 关于作者页
+ * 开发: 卡若
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ author: {
+ name: '卡若',
+ avatar: 'K',
+ title: 'Soul派对房主理人 · 私域运营专家',
+ bio: '每天早上6点到9点,在Soul派对房分享真实的创业故事。专注私域运营与项目变现,用"云阿米巴"模式帮助创业者构建可持续的商业体系。本书记录了62个真实商业案例,涵盖电商、内容、传统行业等多个领域。',
+ stats: [
+ { label: '商业案例', value: '62' },
+ { label: '连续直播', value: '365天' },
+ { label: '派对分享', value: '1000+' }
+ ],
+ // 联系方式已移至后台配置
+ contact: null,
+ highlights: [
+ '5年私域运营经验',
+ '帮助100+品牌从0到1增长',
+ '连续创业者,擅长商业模式设计'
+ ]
+ },
+ bookInfo: {
+ title: '一场Soul的创业实验',
+ totalChapters: 62,
+ parts: [
+ { name: '真实的人', chapters: 10 },
+ { name: '真实的行业', chapters: 15 },
+ { name: '真实的错误', chapters: 9 },
+ { name: '真实的赚钱', chapters: 20 },
+ { name: '真实的社会', chapters: 9 }
+ ],
+ price: 9.9
+ }
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight
+ })
+ this.loadBookStats()
+ },
+
+ // 加载书籍统计
+ async loadBookStats() {
+ try {
+ const res = await app.request('/api/miniprogram/book/stats')
+ if (res && res.success) {
+ this.setData({
+ 'bookInfo.totalChapters': res.data?.totalChapters || 62,
+ 'author.stats': [
+ { label: '商业案例', value: String(res.data?.totalChapters || 62) },
+ { label: '连续直播', value: '365天' },
+ { label: '派对分享', value: '1000+' }
+ ]
+ })
+ }
+ } catch (e) {
+ console.log('[About] 加载书籍统计失败,使用默认值')
+ }
+ },
+
+ // 联系方式功能已禁用
+ copyWechat() {
+ wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
+ },
+
+ callPhone() {
+ wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
+ },
+
+ // 返回
+ goBack() {
+ wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 关于作者',
+ path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
+ }
+ }
+})
diff --git a/miniprogram2/pages/about/about.json b/miniprogram2/pages/about/about.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram2/pages/about/about.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/about/about.wxml b/miniprogram2/pages/about/about.wxml
new file mode 100644
index 00000000..598e9464
--- /dev/null
+++ b/miniprogram2/pages/about/about.wxml
@@ -0,0 +1,75 @@
+
+
+
+ ←
+ 关于作者
+
+
+
+
+
+
+
+ {{author.avatar}}
+ {{author.name}}
+ {{author.title}}
+ {{author.bio}}
+
+
+
+
+ {{item.value}}
+ {{item.label}}
+
+
+
+
+
+
+ ✓
+ {{item}}
+
+
+
+
+
+
+ 📚 {{bookInfo.title}}
+
+
+ {{bookInfo.totalChapters}}
+ 篇章节
+
+
+ 5
+ 大篇章
+
+
+ ¥{{bookInfo.price}}
+ 全书价格
+
+
+
+
+ {{item.name}}
+ {{item.chapters}}节
+
+
+
+
+
+
+ 联系作者
+
+ 🎉
+
+ Soul派对房
+ 每天早上6-9点开播
+
+
+
+ 在Soul App搜索"创业实验"或"卡若",加入派对房直接交流
+
+
+
+
diff --git a/miniprogram2/pages/about/about.wxss b/miniprogram2/pages/about/about.wxss
new file mode 100644
index 00000000..337aa041
--- /dev/null
+++ b/miniprogram2/pages/about/about.wxss
@@ -0,0 +1,40 @@
+.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.9); backdrop-filter: blur(40rpx); 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 { padding: 32rpx; }
+.author-card { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; padding: 48rpx; text-align: center; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
+.author-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; margin: 0 auto 24rpx; font-size: 64rpx; color: #fff; font-weight: 700; border: 4rpx solid rgba(0,206,209,0.3); }
+.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 8rpx; }
+.author-title { font-size: 26rpx; color: #00CED1; display: block; margin-bottom: 24rpx; }
+.author-bio { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.8; display: block; margin-bottom: 32rpx; }
+.stats-row { display: flex; justify-content: space-around; padding-top: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.1); }
+.stat-item { text-align: center; }
+.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; }
+.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; }
+.contact-info { flex: 1; }
+.contact-label { font-size: 22rpx; color: rgba(255,255,255,0.5); display: block; }
+.contact-value { font-size: 28rpx; color: #fff; }
+.contact-btn { padding: 12rpx 24rpx; background: rgba(0,206,209,0.2); color: #00CED1; font-size: 24rpx; border-radius: 16rpx; }
+
+/* 亮点标签 */
+.highlights { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.1); justify-content: center; }
+.highlight-tag { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0,206,209,0.15); border-radius: 24rpx; font-size: 24rpx; color: rgba(255,255,255,0.8); }
+.tag-icon { color: #00CED1; font-size: 22rpx; }
+
+/* 书籍信息卡片 */
+.book-info-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; }
+.book-stats { display: flex; justify-content: space-around; padding: 24rpx 0; margin: 16rpx 0; background: rgba(0,0,0,0.3); border-radius: 16rpx; }
+.book-stat { text-align: center; }
+.book-stat-value { font-size: 36rpx; font-weight: 700; color: #FFD700; display: block; }
+.book-stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
+.parts-list { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx; }
+.part-item { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: rgba(255,255,255,0.05); border-radius: 12rpx; }
+.part-name { font-size: 24rpx; color: rgba(255,255,255,0.8); }
+.part-chapters { font-size: 22rpx; color: #00CED1; }
diff --git a/miniprogram2/pages/addresses/addresses.js b/miniprogram2/pages/addresses/addresses.js
new file mode 100644
index 00000000..cd13fe90
--- /dev/null
+++ b/miniprogram2/pages/addresses/addresses.js
@@ -0,0 +1,131 @@
+/**
+ * 收货地址列表页
+ * 参考 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()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 收货地址',
+ path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
+ }
+ }
+})
diff --git a/miniprogram2/pages/addresses/addresses.json b/miniprogram2/pages/addresses/addresses.json
new file mode 100644
index 00000000..2e45b65e
--- /dev/null
+++ b/miniprogram2/pages/addresses/addresses.json
@@ -0,0 +1,5 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "enablePullDownRefresh": false
+}
diff --git a/miniprogram2/pages/addresses/addresses.wxml b/miniprogram2/pages/addresses/addresses.wxml
new file mode 100644
index 00000000..cec2ef6e
--- /dev/null
+++ b/miniprogram2/pages/addresses/addresses.wxml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ ‹
+
+ 收货地址
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+ 📍
+ 暂无收货地址
+ 点击下方按钮添加
+
+
+
+
+
+
+ {{item.fullAddress}}
+
+
+ ✏️
+ 编辑
+
+
+ 🗑️
+ 删除
+
+
+
+
+
+
+
+ ➕
+ 新增收货地址
+
+
+
diff --git a/miniprogram2/pages/addresses/addresses.wxss b/miniprogram2/pages/addresses/addresses.wxss
new file mode 100644
index 00000000..9ff21637
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/pages/addresses/edit.js b/miniprogram2/pages/addresses/edit.js
new file mode 100644
index 00000000..4f45893c
--- /dev/null
+++ b/miniprogram2/pages/addresses/edit.js
@@ -0,0 +1,209 @@
+/**
+ * 地址编辑页(新增/编辑)
+ * 参考 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()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 编辑地址',
+ path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
+ }
+ }
+})
diff --git a/miniprogram2/pages/addresses/edit.json b/miniprogram2/pages/addresses/edit.json
new file mode 100644
index 00000000..2e45b65e
--- /dev/null
+++ b/miniprogram2/pages/addresses/edit.json
@@ -0,0 +1,5 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "enablePullDownRefresh": false
+}
diff --git a/miniprogram2/pages/addresses/edit.wxml b/miniprogram2/pages/addresses/edit.wxml
new file mode 100644
index 00000000..c5429207
--- /dev/null
+++ b/miniprogram2/pages/addresses/edit.wxml
@@ -0,0 +1,101 @@
+
+
+
+
+
+ ‹
+
+ {{isEdit ? '编辑地址' : '新增地址'}}
+
+
+
+
+
+
+
+
+
+ 👤
+ 收货人
+
+
+
+
+
+
+
+ 📱
+ 手机号
+
+
+
+
+
+
+
+ 📍
+ 所在地区
+
+
+
+ {{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}
+
+
+
+
+
+
+
+ 🏠
+ 详细地址
+
+
+
+
+
+
+
+ ⭐
+ 设为默认地址
+
+
+
+
+
+
+
+ {{saving ? '保存中...' : '保存'}}
+
+
+
diff --git a/miniprogram2/pages/addresses/edit.wxss b/miniprogram2/pages/addresses/edit.wxss
new file mode 100644
index 00000000..1045a287
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/pages/agreement/agreement.js b/miniprogram2/pages/agreement/agreement.js
new file mode 100644
index 00000000..cff31e3b
--- /dev/null
+++ b/miniprogram2/pages/agreement/agreement.js
@@ -0,0 +1,29 @@
+/**
+ * Soul创业派对 - 用户协议
+ * 审核要求:登录前可点击《用户协议》查看完整内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 用户协议',
+ path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
+ }
+ }
+})
diff --git a/miniprogram2/pages/agreement/agreement.json b/miniprogram2/pages/agreement/agreement.json
new file mode 100644
index 00000000..9fff5b87
--- /dev/null
+++ b/miniprogram2/pages/agreement/agreement.json
@@ -0,0 +1 @@
+{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}
diff --git a/miniprogram2/pages/agreement/agreement.wxml b/miniprogram2/pages/agreement/agreement.wxml
new file mode 100644
index 00000000..4060b50c
--- /dev/null
+++ b/miniprogram2/pages/agreement/agreement.wxml
@@ -0,0 +1,37 @@
+
+
+
+ ←
+ 用户协议
+
+
+
+
+
+
+ Soul创业实验 用户服务协议
+ 更新日期:以小程序内展示为准
+
+ 一、接受条款
+ 欢迎使用 Soul创业实验 小程序。使用本服务即表示您已阅读、理解并同意受本协议约束。若不同意,请勿使用本服务。
+
+ 二、服务说明
+ 本小程序提供《一场Soul的创业实验》等数字内容阅读、推广与相关服务。我们保留变更、中断或终止部分或全部服务的权利。
+
+ 三、用户行为规范
+ 您应合法、合规使用本服务,不得利用本服务从事违法违规活动,不得侵犯他人权益。违规行为可能导致账号限制或追究责任。
+
+ 四、知识产权
+ 本小程序内全部内容(包括但不限于文字、图片、音频、视频)的知识产权归本小程序或权利人所有,未经授权不得复制、传播或用于商业用途。
+
+ 五、免责与限制
+ 在法律允许范围内,因网络、设备或不可抗力导致的服务中断或数据丢失,我们尽力减少损失但不承担超出法律规定的责任。
+
+ 六、协议变更
+ 我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。
+
+ 七、联系我们
+ 如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。
+
+
+
diff --git a/miniprogram2/pages/agreement/agreement.wxss b/miniprogram2/pages/agreement/agreement.wxss
new file mode 100644
index 00000000..08fadc43
--- /dev/null
+++ b/miniprogram2/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/miniprogram2/pages/chapters/chapters.js b/miniprogram2/pages/chapters/chapters.js
new file mode 100644
index 00000000..5f4dd460
--- /dev/null
+++ b/miniprogram2/pages/chapters/chapters.js
@@ -0,0 +1,169 @@
+/**
+ * Soul创业派对 - 目录页
+ * 开发: 卡若
+ * 技术支持: 存客宝
+ * 数据: 完整真实文章标题
+ */
+
+const app = getApp()
+const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
+
+function buildNestedBookData(list) {
+ const parts = {}
+ const appendices = []
+ let epilogueMid = 0
+ let prefaceMid = 0
+ list.forEach(ch => {
+ if (ch.id === 'preface') {
+ prefaceMid = ch.mid || 0
+ return
+ }
+ const section = {
+ id: ch.id,
+ mid: ch.mid || 0,
+ title: ch.sectionTitle || ch.chapterTitle || ch.id,
+ isFree: !!ch.isFree,
+ price: ch.price != null ? Number(ch.price) : 1
+ }
+ if (ch.id === 'epilogue') {
+ epilogueMid = ch.mid || 0
+ return
+ }
+ if ((ch.id || '').startsWith('appendix')) {
+ appendices.push({ id: ch.id, mid: ch.mid || 0, title: ch.sectionTitle || ch.chapterTitle || ch.id })
+ return
+ }
+ if (!ch.partId || ch.id === 'preface') return
+ const pid = ch.partId
+ const cid = ch.chapterId || 'chapter-' + (ch.id || '').split('.')[0]
+ if (!parts[pid]) {
+ parts[pid] = { id: pid, number: PART_NUMBERS[pid] || pid, title: ch.partTitle || pid, subtitle: ch.chapterTitle || '', chapters: {} }
+ }
+ if (!parts[pid].chapters[cid]) {
+ parts[pid].chapters[cid] = { id: cid, title: ch.chapterTitle || cid, sections: [] }
+ }
+ parts[pid].chapters[cid].sections.push(section)
+ })
+ const bookData = Object.values(parts)
+ .sort((a, b) => (a.id || '').localeCompare(b.id || ''))
+ .map(p => ({
+ ...p,
+ chapters: Object.values(p.chapters).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
+ }))
+ return { bookData, appendixList: appendices, epilogueMid, prefaceMid }
+}
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ navBarHeight: 88,
+ isLoggedIn: false,
+ hasFullBook: false,
+ purchasedSections: [],
+ totalSections: 62,
+ bookData: [],
+ expandedPart: null,
+ appendixList: [],
+ epilogueMid: 0,
+ prefaceMid: 0
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ navBarHeight: app.globalData.navBarHeight
+ })
+ this.updateUserStatus()
+ this.loadAndEnrichBookData()
+ },
+
+ async loadAndEnrichBookData() {
+ try {
+ let list = app.globalData.bookData || []
+ if (!list.length) {
+ const res = await app.request('/api/miniprogram/book/all-chapters')
+ if (res?.data) {
+ list = res.data
+ app.globalData.bookData = list
+ }
+ }
+ if (!list.length) {
+ this.setData({ bookData: [], appendixList: [] })
+ return
+ }
+ const { bookData, appendixList, epilogueMid, prefaceMid } = buildNestedBookData(list)
+ const firstPartId = bookData[0]?.id || null
+ this.setData({
+ bookData,
+ appendixList,
+ epilogueMid,
+ prefaceMid,
+ totalSections: list.length,
+ expandedPart: firstPartId || this.data.expandedPart
+ })
+ } catch (e) {
+ console.error('[Chapters] 加载目录失败:', e)
+ this.setData({ bookData: [], appendixList: [] })
+ }
+ },
+
+ onShow() {
+ this.updateUserStatus()
+ if (!app.globalData.bookData?.length) {
+ this.loadAndEnrichBookData()
+ }
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ const tabBar = this.getTabBar()
+ if (tabBar.updateSelected) {
+ tabBar.updateSelected()
+ } else {
+ tabBar.setData({ selected: 1 })
+ }
+ }
+ },
+
+ // 更新用户状态
+ updateUserStatus() {
+ const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
+ this.setData({ isLoggedIn, hasFullBook, purchasedSections })
+ },
+
+ // 切换展开状态
+ togglePart(e) {
+ const partId = e.currentTarget.dataset.id
+ this.setData({
+ expandedPart: this.data.expandedPart === partId ? null : partId
+ })
+ },
+
+ goToRead(e) {
+ const id = e.currentTarget.dataset.id
+ const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ // 检查是否已购买
+ hasPurchased(sectionId) {
+ if (this.data.hasFullBook) return true
+ return this.data.purchasedSections.includes(sectionId)
+ },
+
+ // 返回首页
+ goBack() {
+ wx.switchTab({ url: '/pages/index/index' })
+ },
+
+ // 跳转到搜索页
+ goToSearch() {
+ wx.navigateTo({ url: '/pages/search/search' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 目录',
+ path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
+ }
+ }
+})
diff --git a/miniprogram2/pages/chapters/chapters.json b/miniprogram2/pages/chapters/chapters.json
new file mode 100644
index 00000000..e7696321
--- /dev/null
+++ b/miniprogram2/pages/chapters/chapters.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {},
+ "enablePullDownRefresh": false,
+ "backgroundTextStyle": "light",
+ "backgroundColor": "#000000"
+}
diff --git a/miniprogram2/pages/chapters/chapters.wxml b/miniprogram2/pages/chapters/chapters.wxml
new file mode 100644
index 00000000..41a3166d
--- /dev/null
+++ b/miniprogram2/pages/chapters/chapters.wxml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+ 🔍
+
+
+ 目录
+
+
+
+
+
+
+
+
+
+
+ 📚
+
+
+ 一场SOUL的创业实验场
+ 来自Soul派对房的真实商业故事
+
+
+ {{totalSections}}
+ 章节
+
+
+
+
+
+
+
+
+ 📖
+ 序言|为什么我每天早上6点在Soul开播?
+
+
+ 免费
+ →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '○' : '●'}}
+ {{section.id}} {{section.title}}
+
+
+ 免费
+ 已购
+ ¥{{section.price}}
+ ›
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📖
+ 尾声|这本书的真实目的
+
+
+ 免费
+ →
+
+
+
+
+
+ 附录
+
+
+ {{item.title}}
+ →
+
+
+
+
+
+
+
+
diff --git a/miniprogram2/pages/chapters/chapters.wxss b/miniprogram2/pages/chapters/chapters.wxss
new file mode 100644
index 00000000..6cbbb747
--- /dev/null
+++ b/miniprogram2/pages/chapters/chapters.wxss
@@ -0,0 +1,482 @@
+/**
+ * Soul创业实验 - 目录页样式
+ * 1:1还原Web版本UI
+ */
+
+.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);
+ -webkit-backdrop-filter: blur(40rpx);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.nav-content {
+ 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 {
+ color: #00CED1;
+}
+
+.nav-placeholder {
+ width: 100%;
+}
+
+/* ===== 书籍信息卡 ===== */
+.book-info-card {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ margin: 32rpx 32rpx 24rpx 32rpx;
+ padding: 32rpx;
+}
+
+.card-gradient {
+ 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 {
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.book-icon-inner {
+ font-size: 48rpx;
+}
+
+.book-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.book-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #ffffff;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.book-subtitle {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.book-count {
+ text-align: right;
+}
+
+.count-value {
+ font-size: 40rpx;
+ font-weight: 700;
+ display: block;
+}
+
+.count-label {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 目录内容 ===== */
+.chapters-content {
+ padding: 0 32rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* ===== 章节项 ===== */
+.chapter-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ margin-bottom: 24rpx;
+}
+
+.chapter-item:active {
+ background: #2c2c2e;
+}
+
+.item-left {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ flex: 1;
+ min-width: 0;
+}
+
+.item-icon {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ flex-shrink: 0;
+}
+
+.icon-brand {
+ background: rgba(0, 206, 209, 0.2);
+}
+
+.item-title {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #ffffff;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.item-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ flex-shrink: 0;
+}
+
+.item-arrow {
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 标签 ===== */
+.tag {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ padding: 6rpx 16rpx;
+ min-width: 80rpx;
+ border-radius: 8rpx;
+ box-sizing: border-box;
+ text-align: center;
+}
+
+.tag-free {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+}
+
+.text-brand {
+ color: #00CED1;
+}
+
+.text-muted {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.text-xs {
+ font-size: 22rpx;
+}
+
+/* ===== 篇章列表 ===== */
+.part-list {
+ margin-bottom: 24rpx;
+}
+
+.part-item {
+ margin-bottom: 24rpx;
+}
+
+.part-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.part-header:active {
+ background: #2c2c2e;
+}
+
+.part-left {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.part-icon {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ font-weight: 700;
+ color: #ffffff;
+ flex-shrink: 0;
+}
+
+.part-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.part-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.part-subtitle {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 4rpx;
+}
+
+.part-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.part-count {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.part-arrow {
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.4);
+ transition: transform 0.3s ease;
+}
+
+.arrow-down {
+ transform: rotate(90deg);
+}
+
+/* ===== 章节组 ===== */
+.chapters-list {
+ margin-top: 16rpx;
+ margin-left: 16rpx;
+}
+
+.chapter-group {
+ background: rgba(28, 28, 30, 0.5);
+ border-radius: 16rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ overflow: hidden;
+ margin-bottom: 8rpx;
+}
+
+.chapter-header {
+ padding: 16rpx 24rpx;
+ font-size: 24rpx;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.6);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.section-list {
+ /* 小节列表 */
+}
+
+.section-item {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 20rpx 24rpx;
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.section-item:last-child {
+ border-bottom: none;
+}
+
+.section-item:active {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.section-left {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 16rpx;
+ flex: 1;
+ min-width: 0;
+}
+
+/* 小节锁图标 */
+.section-lock {
+ width: 32rpx;
+ min-width: 32rpx;
+ font-size: 24rpx;
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.lock-open {
+ color: #00CED1;
+}
+
+.lock-closed {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* 小节标题 */
+.section-title {
+ font-size: 26rpx;
+ color: #ffffff;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+/* 小节价格 */
+.section-price {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* 已购标签 */
+.tag-purchased {
+ background: rgba(0, 206, 209, 0.15);
+ color: #00CED1;
+}
+
+.section-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ flex-shrink: 0;
+ margin-left: 16rpx;
+}
+
+.section-arrow {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* ===== 附录 ===== */
+.card {
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ margin: 0 0 24rpx 0;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.appendix-card {
+ padding: 24rpx;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0 0 24rpx 0;
+}
+
+.appendix-title {
+ font-size: 24rpx;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.6);
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.appendix-list {
+ /* 附录列表 */
+}
+
+.appendix-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16rpx 0;
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.appendix-item:last-child {
+ border-bottom: none;
+}
+
+.appendix-item:active {
+ opacity: 0.7;
+}
+
+.appendix-text {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.appendix-arrow {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* ===== 底部留白 ===== */
+.bottom-space {
+ height: 40rpx;
+}
diff --git a/miniprogram2/pages/index/index.js b/miniprogram2/pages/index/index.js
new file mode 100644
index 00000000..b97297d0
--- /dev/null
+++ b/miniprogram2/pages/index/index.js
@@ -0,0 +1,223 @@
+/**
+ * Soul创业派对 - 首页
+ * 开发: 卡若
+ * 技术支持: 存客宝
+ */
+
+console.log('[Index] ===== 首页文件开始加载 =====')
+
+const app = getApp()
+const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
+
+Page({
+ data: {
+ // 系统信息
+ statusBarHeight: 44,
+ navBarHeight: 88,
+
+ // 用户信息
+ isLoggedIn: false,
+ hasFullBook: false,
+ readCount: 0,
+
+ totalSections: 62,
+ bookData: [],
+ featuredSections: [],
+ latestSection: null,
+ latestLabel: '最新更新',
+ partsList: [],
+ prefaceMid: 0,
+ loading: true
+ },
+
+ onLoad(options) {
+ console.log('[Index] ===== onLoad 触发 =====')
+
+ // 获取系统信息
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ navBarHeight: app.globalData.navBarHeight
+ })
+
+ // 处理分享参数与扫码 scene(推荐码绑定)
+ if (options && (options.ref || options.scene)) {
+ app.handleReferralCode(options)
+ }
+
+ // 初始化数据
+ this.initData()
+ },
+
+ onShow() {
+ console.log('[Index] onShow 触发')
+
+ // 设置TabBar选中状态
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ 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 this.loadBookData()
+ } catch (e) {
+ console.error('初始化失败:', e)
+ } finally {
+ this.setData({ loading: false })
+ }
+ },
+
+ async loadBookData() {
+ try {
+ const [chaptersRes, hotRes] = await Promise.all([
+ app.request('/api/miniprogram/book/all-chapters'),
+ app.request('/api/miniprogram/book/hot')
+ ])
+ const list = chaptersRes?.data || []
+ const hotList = hotRes?.data || []
+ app.globalData.bookData = list
+
+ const toSection = (ch) => ({
+ id: ch.id,
+ mid: ch.mid || 0,
+ title: ch.sectionTitle || ch.chapterTitle || ch.id,
+ part: ch.partTitle || ''
+ })
+
+ let featuredSections = []
+ if (hotList.length >= 3) {
+ const freeCh = list.find(c => c.isFree || c.id === '1.1' || c.id === 'preface')
+ const picks = []
+ if (freeCh) picks.push({ ...toSection(freeCh), tag: '免费', tagClass: 'tag-free' })
+ hotList.slice(0, 3 - picks.length).forEach((ch, i) => {
+ if (!picks.find(p => p.id === ch.id)) {
+ picks.push({ ...toSection(ch), tag: i === 0 ? '热门' : '推荐', tagClass: i === 0 ? 'tag-pink' : 'tag-purple' })
+ }
+ })
+ featuredSections = picks.slice(0, 3)
+ }
+ if (featuredSections.length < 3 && list.length > 0) {
+ const fallback = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id)).slice(0, 3)
+ featuredSections = fallback.map((ch, i) => ({
+ ...toSection(ch),
+ tag: ch.isFree ? '免费' : (i === 0 ? '热门' : '推荐'),
+ tagClass: ch.isFree ? 'tag-free' : (i === 0 ? 'tag-pink' : 'tag-purple')
+ }))
+ }
+
+ const partMap = {}
+ list.forEach(ch => {
+ if (!ch.partId || ch.id === 'preface' || ch.id === 'epilogue' || (ch.id || '').startsWith('appendix')) return
+ if (!partMap[ch.partId]) {
+ partMap[ch.partId] = { id: ch.partId, number: PART_NUMBERS[ch.partId] || ch.partId, title: ch.partTitle || ch.partId, subtitle: ch.chapterTitle || '' }
+ }
+ })
+ const partsList = Object.values(partMap).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
+
+ const paidCandidates = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id) && !(c.id || '').startsWith('appendix') && c.partId)
+ const { hasFullBook, purchasedSections } = app.globalData
+ let candidates = paidCandidates
+ if (!hasFullBook && purchasedSections?.length) {
+ const unpurchased = paidCandidates.filter(c => !purchasedSections.includes(c.id))
+ if (unpurchased.length > 0) candidates = unpurchased
+ }
+ const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest'
+ const today = new Date().toISOString().split('T')[0]
+ const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0)
+ const selectedCh = candidates[seed % Math.max(candidates.length, 1)]
+ const latestSection = selectedCh ? { ...toSection(selectedCh), mid: selectedCh.mid || 0 } : null
+ const latestLabel = candidates.length === paidCandidates.length ? '推荐阅读' : '为你推荐'
+
+ const prefaceCh = list.find(c => c.id === 'preface')
+ const prefaceMid = prefaceCh?.mid || 0
+
+ this.setData({
+ bookData: list,
+ totalSections: list.length || 62,
+ featuredSections,
+ partsList,
+ latestSection,
+ latestLabel,
+ prefaceMid
+ })
+ } catch (e) {
+ console.error('加载书籍数据失败:', e)
+ this.setData({ featuredSections: [], partsList: [], latestSection: null })
+ }
+ },
+
+ // 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
+ updateUserStatus() {
+ const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
+ const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
+ this.setData({
+ 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 mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ // 跳转到匹配页
+ goToMatch() {
+ wx.switchTab({ url: '/pages/match/match' })
+ },
+
+ // 跳转到我的页面
+ goToMy() {
+ wx.switchTab({ url: '/pages/my/my' })
+ },
+
+ // 下拉刷新
+ async onPullDownRefresh() {
+ await this.initData()
+ this.updateUserStatus()
+ wx.stopPullDownRefresh()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 真实商业故事',
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
+ }
+ }
+})
diff --git a/miniprogram2/pages/index/index.json b/miniprogram2/pages/index/index.json
new file mode 100644
index 00000000..1246275b
--- /dev/null
+++ b/miniprogram2/pages/index/index.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {},
+ "enablePullDownRefresh": true,
+ "backgroundTextStyle": "light",
+ "backgroundColor": "#000000"
+}
diff --git a/miniprogram2/pages/index/index.wxml b/miniprogram2/pages/index/index.wxml
new file mode 100644
index 00000000..76914636
--- /dev/null
+++ b/miniprogram2/pages/index/index.wxml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 最新更新
+ {{latestSection.title}}
+ {{latestSection.part}}
+
+ 开始阅读
+ →
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{readCount}}
+ 已读
+
+
+ {{totalSections - readCount}}
+ 待读
+
+
+ 5
+ 篇章
+
+
+ 11
+ 章节
+
+
+
+
+
+
+
+
+
+
+
+ {{item.id}}
+ {{item.tag}}
+
+ {{item.title}}
+ {{item.part}}
+
+ →
+
+
+
+
+
+
+ 内容概览
+
+
+
+ {{item.number}}
+
+
+ {{item.title}}
+ {{item.subtitle}}
+
+ →
+
+
+
+
+
+
+
+ 序言
+ 为什么我每天早上6点在Soul开播?
+
+ 免费
+
+
+
+
+
+
diff --git a/miniprogram2/pages/index/index.wxss b/miniprogram2/pages/index/index.wxss
new file mode 100644
index 00000000..ec316d47
--- /dev/null
+++ b/miniprogram2/pages/index/index.wxss
@@ -0,0 +1,504 @@
+/**
+ * Soul创业实验 - 首页样式
+ * 1:1还原Web版本UI
+ */
+
+.page {
+ min-height: 100vh;
+ background: #000000;
+ padding-bottom: 200rpx;
+}
+
+/* ===== 导航栏占位 ===== */
+.nav-placeholder {
+ width: 100%;
+}
+
+/* ===== 顶部区域 ===== */
+.header {
+ padding: 0 32rpx 32rpx;
+}
+
+.header-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 32rpx;
+ padding-top: 24rpx;
+}
+
+.logo-section {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.logo-icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 20rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
+}
+
+.logo-text {
+ color: #ffffff;
+ font-size: 36rpx;
+ font-weight: 700;
+}
+
+.logo-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.logo-title {
+ font-size: 36rpx;
+ font-weight: 700;
+}
+
+.text-white {
+ color: #ffffff;
+}
+
+.brand-color {
+ color: #00CED1;
+}
+
+.logo-subtitle {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 4rpx;
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.chapter-badge {
+ font-size: 22rpx;
+ color: #00CED1;
+ background: rgba(0, 206, 209, 0.1);
+ padding: 8rpx 16rpx;
+ border-radius: 32rpx;
+}
+
+/* ===== 搜索栏 ===== */
+.search-bar {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ padding: 24rpx 32rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.search-icon {
+ position: relative;
+ width: 32rpx;
+ height: 32rpx;
+}
+
+.search-circle {
+ width: 20rpx;
+ height: 20rpx;
+ border: 4rpx solid rgba(255, 255, 255, 0.4);
+ border-radius: 50%;
+}
+
+.search-handle {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 12rpx;
+ height: 4rpx;
+ background: rgba(255, 255, 255, 0.4);
+ transform: rotate(45deg);
+ border-radius: 2rpx;
+}
+
+.search-placeholder {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 主内容区 ===== */
+.main-content {
+ padding: 0 32rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* ===== Banner卡片 ===== */
+.banner-card {
+ position: relative;
+ padding: 40rpx;
+ border-radius: 32rpx;
+ overflow: hidden;
+ background: linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%);
+ margin-bottom: 24rpx;
+}
+
+.banner-glow {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 256rpx;
+ height: 256rpx;
+ background: #00CED1;
+ border-radius: 50%;
+ filter: blur(120rpx);
+ opacity: 0.2;
+}
+
+.banner-tag {
+ display: inline-block;
+ padding: 8rpx 16rpx;
+ background: #00CED1;
+ color: #000000;
+ font-size: 22rpx;
+ font-weight: 500;
+ border-radius: 8rpx;
+ margin-bottom: 24rpx;
+}
+
+.banner-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 16rpx;
+ padding-right: 64rpx;
+}
+
+.banner-part {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 24rpx;
+}
+
+.banner-action {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.banner-action-text {
+ font-size: 28rpx;
+ color: #00CED1;
+ font-weight: 500;
+}
+
+.banner-arrow {
+ color: #00CED1;
+ font-size: 28rpx;
+}
+
+/* ===== 通用卡片 ===== */
+.card {
+ background: #1c1c1e;
+ border-radius: 32rpx;
+ padding: 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ margin: 0 0 24rpx 0;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* ===== 阅读进度卡 ===== */
+.progress-card {
+ width: 100%;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ padding: 28rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ margin: 0 0 24rpx 0;
+ box-sizing: border-box;
+}
+
+.progress-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.progress-title {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+}
+
+.progress-count {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.progress-bar-wrapper {
+ margin-bottom: 24rpx;
+}
+
+.progress-bar-bg {
+ width: 100%;
+ height: 16rpx;
+ background: #2c2c2e;
+ border-radius: 8rpx;
+ overflow: hidden;
+}
+
+.progress-bar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
+ border-radius: 8rpx;
+ transition: width 0.3s ease;
+}
+
+.progress-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 24rpx;
+}
+
+.stat-item {
+ text-align: center;
+}
+
+.stat-value {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ display: block;
+}
+
+.stat-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 区块标题 ===== */
+.section {
+ margin-bottom: 24rpx;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.section-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.section-more {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.more-text {
+ font-size: 24rpx;
+ color: #00CED1;
+}
+
+.more-arrow {
+ font-size: 24rpx;
+ color: #00CED1;
+}
+
+/* ===== 精选推荐列表 ===== */
+.featured-list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.featured-item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 32rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.featured-item:active {
+ transform: scale(0.98);
+ background: #2c2c2e;
+}
+
+.featured-content {
+ flex: 1;
+}
+
+.featured-meta {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 16rpx;
+}
+
+.featured-id {
+ font-size: 24rpx;
+ font-weight: 500;
+}
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ padding: 6rpx 16rpx;
+ min-width: 80rpx;
+ border-radius: 8rpx;
+ box-sizing: border-box;
+ text-align: center;
+}
+
+.tag-free {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+}
+
+.tag-pink {
+ background: rgba(233, 30, 99, 0.1);
+ color: #E91E63;
+}
+
+.tag-purple {
+ background: rgba(123, 97, 255, 0.1);
+ color: #7B61FF;
+}
+
+.featured-title {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+ display: block;
+ margin-bottom: 8rpx;
+}
+
+.featured-part {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.featured-arrow {
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.3);
+ margin-top: 8rpx;
+}
+
+/* ===== 内容概览列表 ===== */
+.parts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.part-item {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ padding: 32rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.part-item:active {
+ transform: scale(0.98);
+ background: #2c2c2e;
+}
+
+.part-icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 16rpx;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.part-number {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: #00CED1;
+}
+
+.part-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.part-title {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.part-subtitle {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.part-arrow {
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.3);
+ flex-shrink: 0;
+}
+
+/* ===== 序言入口 ===== */
+.preface-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 32rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+ margin-bottom: 24rpx;
+}
+
+.preface-card:active {
+ opacity: 0.8;
+}
+
+.preface-content {
+ flex: 1;
+}
+
+.preface-title {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+ display: block;
+ margin-bottom: 8rpx;
+}
+
+.preface-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 底部留白 ===== */
+.bottom-space {
+ height: 40rpx;
+}
diff --git a/miniprogram2/pages/match/match.js b/miniprogram2/pages/match/match.js
new file mode 100644
index 00000000..194cc3d8
--- /dev/null
+++ b/miniprogram2/pages/match/match.js
@@ -0,0 +1,936 @@
+/**
+ * Soul创业派对 - 找伙伴页
+ * 按H5网页端完全重构
+ * 开发: 卡若
+ */
+
+const app = getApp()
+
+// 默认匹配类型配置
+// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
+// 资源对接:需要登录+购买章节才能使用,填写2项信息(我能帮到你什么、我需要什么帮助)
+// 导师顾问:跳转到存客宝添加微信
+// 团队招募:跳转到存客宝添加微信
+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 }
+]
+
+let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
+
+Page({
+ data: {
+ statusBarHeight: 44,
+
+ // 匹配类型
+ matchTypes: MATCH_TYPES,
+ selectedType: 'partner',
+ currentTypeLabel: '找伙伴',
+
+ // 用户状态
+ isLoggedIn: false,
+ hasPurchased: false,
+ hasFullBook: false,
+
+ // 匹配次数
+ todayMatchCount: 0,
+ totalMatchesAllowed: FREE_MATCH_LIMIT,
+ matchesRemaining: FREE_MATCH_LIMIT,
+ showQuotaExhausted: false,
+ needPayToMatch: false,
+
+ // 匹配状态
+ isMatching: false,
+ matchAttempts: 0,
+ currentMatch: null,
+
+ // 加入弹窗
+ showJoinModal: false,
+ joinType: null,
+ joinTypeLabel: '',
+ contactType: 'phone',
+ phoneNumber: '',
+ wechatId: '',
+ userPhone: '',
+ isJoining: false,
+ joinSuccess: false,
+ joinError: '',
+ needBindFirst: false,
+
+ // 资源对接表单
+ canHelp: '',
+ needHelp: '',
+ goodAt: '',
+
+ // 解锁弹窗
+ showUnlockModal: false,
+
+ // 手机号绑定弹窗(一键加好友前校验)
+ showBindPhoneModal: false,
+ pendingAddWechatAfterBind: false,
+ bindPhoneInput: '',
+ showMatchPhoneManualInput: false,
+
+ // 登录弹窗(未登录时点击匹配弹出)
+ showLoginModal: false,
+ isLoggingIn: false,
+ agreeProtocol: false,
+
+ // 匹配价格(可配置)
+ matchPrice: 1,
+ extraMatches: 0,
+ // 好友优惠展示(与 read 页一致)
+ userDiscount: 5,
+ hasReferralDiscount: false,
+ showDiscountHint: false,
+ displayMatchPrice: 1
+ },
+
+ onLoad(options = {}) {
+ // ref:支持 query.ref 或 scene 中的 ref=xxx(分享进入时)
+ let ref = options.ref
+ if (!ref && options.scene) {
+ const sceneStr = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim()
+ const parts = sceneStr.split(/[&_]/)
+ for (const part of parts) {
+ const eq = part.indexOf('=')
+ if (eq > 0) {
+ const k = part.slice(0, eq)
+ const v = part.slice(eq + 1)
+ if (k === 'ref' && v) { ref = v; break }
+ }
+ }
+ }
+ if (ref) {
+ wx.setStorageSync('referral_code', ref)
+ app.handleReferralCode({ query: { ref } })
+ }
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44,
+ _ref: ref
+ })
+ this.loadMatchConfig()
+ this.loadStoredContact()
+ this.refreshMatchCountAndStatus()
+ },
+
+ onShow() {
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ const tabBar = this.getTabBar()
+ if (tabBar.updateSelected) {
+ tabBar.updateSelected()
+ } else {
+ tabBar.setData({ selected: 2 })
+ }
+ }
+ this.loadStoredContact()
+ this.refreshMatchCountAndStatus()
+ },
+
+ // 加载匹配配置(含 userDiscount 用于好友优惠展示)
+ async loadMatchConfig() {
+ try {
+ const userId = app.globalData.userInfo?.id
+ const [matchRes, configRes] = await Promise.all([
+ app.request('/api/miniprogram/match/config', { method: 'GET' }),
+ app.request('/api/miniprogram/config', { method: 'GET' })
+ ])
+
+ const matchPrice = matchRes?.success && matchRes?.data ? (matchRes.data.matchPrice || 1) : 1
+ const userDiscount = configRes?.userDiscount ?? 5
+ const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref)
+ const hasReferralDiscount = hasReferral && userDiscount > 0
+ const displayMatchPrice = hasReferralDiscount
+ ? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
+ : matchPrice
+
+ if (matchRes?.success && matchRes?.data) {
+ MATCH_TYPES = matchRes.data.matchTypes || MATCH_TYPES
+ FREE_MATCH_LIMIT = matchRes.data.freeMatchLimit || FREE_MATCH_LIMIT
+ }
+
+ this.setData({
+ matchTypes: MATCH_TYPES,
+ totalMatchesAllowed: FREE_MATCH_LIMIT,
+ matchPrice,
+ userDiscount,
+ hasReferralDiscount,
+ showDiscountHint: userDiscount > 0,
+ displayMatchPrice
+ })
+
+ console.log('[Match] 加载匹配配置成功:', { matchPrice, userDiscount, hasReferralDiscount, displayMatchPrice })
+ } catch (e) {
+ console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
+ }
+ },
+
+ // 加载本地存储的联系方式(含用户资料的手机号、微信号)
+ loadStoredContact() {
+ const ui = app.globalData.userInfo || {}
+ const phone = wx.getStorageSync('user_phone') || ui.phone || ''
+ const wechat = wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || ''
+ this.setData({
+ phoneNumber: phone,
+ wechatId: wechat,
+ userPhone: phone
+ })
+ },
+
+ // 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威)
+ async refreshMatchCountAndStatus() {
+ if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
+ try {
+ const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`)
+ if (res.success && res.data) {
+ app.globalData.matchCount = res.data.matchCount ?? 0
+ app.globalData.matchQuota = res.data.matchQuota || null
+ // 根据 hasReferrer 更新优惠展示(与 read 页一致)
+ const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref || res.data.hasReferrer)
+ const matchPrice = this.data.matchPrice ?? 1
+ const userDiscount = this.data.userDiscount ?? 5
+ const hasReferralDiscount = hasReferral && userDiscount > 0
+ const displayMatchPrice = hasReferralDiscount
+ ? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
+ : matchPrice
+ this.setData({ hasReferralDiscount, displayMatchPrice })
+ }
+ } catch (e) {
+ console.log('[Match] 拉取 matchQuota 失败:', e)
+ }
+ }
+ this.initUserStatus()
+ },
+
+ // 初始化用户状态(matchQuota 服务端纯计算:订单+match_records)
+ initUserStatus() {
+ const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
+ const quota = app.globalData.matchQuota
+
+ // 今日剩余次数、今日已用:来自服务端 matchQuota(未登录无法计算,不能显示已用完)
+ const remainToday = quota?.remainToday ?? 0
+ const matchesUsedToday = quota?.matchesUsedToday ?? 0
+ const purchasedRemain = quota?.purchasedRemain ?? 0
+ const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT)
+ // 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」
+ const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
+ const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
+
+ this.setData({
+ isLoggedIn,
+ hasFullBook,
+ hasPurchased: true,
+ todayMatchCount: matchesUsedToday,
+ totalMatchesAllowed,
+ matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)),
+ needPayToMatch,
+ showQuotaExhausted,
+ extraMatches: purchasedRemain
+ })
+ },
+
+ // 选择匹配类型
+ selectType(e) {
+ const typeId = e.currentTarget.dataset.type
+ const type = MATCH_TYPES.find(t => t.id === typeId)
+ this.setData({
+ selectedType: typeId,
+ currentTypeLabel: type?.matchLabel || type?.label || '创业伙伴'
+ })
+ },
+
+ // 点击匹配按钮
+ handleMatchClick() {
+ // 检测是否登录,未登录则弹出登录弹窗
+ if (!this.data.isLoggedIn) {
+ this.setData({ showLoginModal: true, agreeProtocol: false })
+ return
+ }
+
+ const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
+
+ // 资源对接类型需要购买章节才能使用
+ if (currentType && currentType.id === 'investor') {
+ // 检查是否购买过章节
+ const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook
+ if (!hasPurchased) {
+ wx.showModal({
+ title: '需要购买章节',
+ content: '购买任意章节后即可使用资源对接功能',
+ confirmText: '去购买',
+ success: (res) => {
+ if (res.confirm) {
+ wx.switchTab({ url: '/pages/chapters/chapters' })
+ }
+ }
+ })
+ return
+ }
+ }
+
+ // 如果是需要填写联系方式的类型(资源对接、导师顾问、团队招募)
+ if (currentType && currentType.showJoinAfterMatch) {
+ // 先检查是否已绑定联系方式
+ const hasPhone = !!this.data.phoneNumber
+ const hasWechat = !!this.data.wechatId
+
+ if (!hasPhone && !hasWechat) {
+ // 没有绑定联系方式,先显示绑定提示(仍尝试加载已有资料填充)
+ this.loadStoredContact()
+ this.setData({
+ showJoinModal: true,
+ joinType: currentType.id,
+ joinTypeLabel: currentType.matchLabel || currentType.label,
+ joinSuccess: false,
+ joinError: '',
+ needBindFirst: true
+ })
+ return
+ }
+
+ // 已绑定联系方式,先显示匹配动画1-3秒,再弹出确认
+ this.startMatchingAnimation(currentType)
+ return
+ }
+
+ // 创业合伙类型 - 超过匹配次数时直接弹出付费弹窗
+ if (this.data.showQuotaExhausted || this.data.needPayToMatch) {
+ this.setData({ showUnlockModal: true })
+ return
+ }
+
+ this.startMatch()
+ },
+
+ // 匹配动画后弹出加入确认
+ startMatchingAnimation(currentType) {
+ // 显示匹配中状态
+ this.setData({
+ isMatching: true,
+ matchAttempts: 0,
+ currentMatch: null
+ })
+
+ // 动画计时
+ const timer = setInterval(() => {
+ this.setData({ matchAttempts: this.data.matchAttempts + 1 })
+ }, 500)
+
+ // 1-3秒随机延迟后显示弹窗
+ const delay = Math.random() * 2000 + 1000
+ setTimeout(() => {
+ clearInterval(timer)
+ // 打开弹窗前调取用户资料填充手机号、微信号
+ this.loadStoredContact()
+ this.setData({
+ isMatching: false,
+ showJoinModal: true,
+ joinType: currentType.id,
+ joinTypeLabel: currentType.matchLabel || currentType.label,
+ joinSuccess: false,
+ joinError: '',
+ needBindFirst: false
+ })
+ }, delay)
+ },
+
+ // 显示购买提示
+ showPurchaseTip() {
+ wx.showModal({
+ title: '需要购买书籍',
+ content: '购买《Soul创业派对》后即可使用匹配功能,仅需9.9元',
+ confirmText: '去购买',
+ success: (res) => {
+ if (res.confirm) {
+ this.goToChapters()
+ }
+ }
+ })
+ },
+
+ // 开始匹配 - 只匹配数据库中的真实用户
+ async startMatch() {
+ this.loadStoredContact()
+ this.setData({
+ isMatching: true,
+ matchAttempts: 0,
+ currentMatch: null
+ })
+
+ // 匹配动画计时器
+ const timer = setInterval(() => {
+ this.setData({ matchAttempts: this.data.matchAttempts + 1 })
+ }, 1000)
+
+ // 从数据库获取真实用户匹配(后端会校验剩余次数)
+ let matchedUser = null
+ let quotaExceeded = false
+ try {
+ const ui = app.globalData.userInfo || {}
+ const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim()
+ const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim()
+ const res = await app.request('/api/miniprogram/match/users', {
+ method: 'POST',
+ data: {
+ matchType: this.data.selectedType,
+ userId: app.globalData.userInfo?.id || '',
+ phone,
+ wechatId
+ }
+ })
+
+ if (res.success && res.data) {
+ matchedUser = res.data
+ console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
+ } else if (res.code === 'QUOTA_EXCEEDED') {
+ quotaExceeded = true
+ }
+ } catch (e) {
+ console.log('[Match] 数据库匹配失败:', e)
+ }
+
+ // 延迟显示结果(模拟匹配过程)
+ const delay = Math.random() * 2000 + 2000
+ setTimeout(() => {
+ clearInterval(timer)
+
+ // 次数用尽(后端校验)- 直接弹出付费弹窗
+ if (quotaExceeded) {
+ this.setData({ isMatching: false, showUnlockModal: true })
+ this.refreshMatchCountAndStatus()
+ return
+ }
+
+ // 如果没有匹配到用户,提示用户
+ if (!matchedUser) {
+ this.setData({ isMatching: false })
+ wx.showModal({
+ title: '暂无匹配',
+ content: '当前暂无合适的匹配用户,请稍后再试',
+ showCancel: false,
+ confirmText: '知道了'
+ })
+ return
+ }
+
+ // 匹配成功:从服务端刷新配额(后端已写入 match_records)
+ this.setData({
+ isMatching: false,
+ currentMatch: matchedUser,
+ needPayToMatch: false
+ })
+ this.refreshMatchCountAndStatus()
+
+ // 上报匹配行为到存客宝
+ this.reportMatch(matchedUser)
+ }, delay)
+ },
+
+ // 生成模拟匹配数据
+ generateMockMatch() {
+ const nicknames = ['创业先锋', '资源整合者', '私域专家', '商业导师', '连续创业者']
+ const concepts = [
+ '专注私域流量运营5年,帮助100+品牌实现从0到1的增长。',
+ '连续创业者,擅长商业模式设计和资源整合。',
+ '在Soul分享真实创业故事,希望找到志同道合的合作伙伴。'
+ ]
+ const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
+
+ const index = Math.floor(Math.random() * nicknames.length)
+ const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
+
+ return {
+ id: `user_${Date.now()}`,
+ nickname: nicknames[index],
+ avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
+ tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
+ matchScore: Math.floor(Math.random() * 20) + 80,
+ concept: concepts[index % concepts.length],
+ wechat: wechats[index % wechats.length],
+ commonInterests: [
+ { icon: '📚', text: '都在读《创业派对》' },
+ { icon: '💼', text: '对私域运营感兴趣' },
+ { icon: '🎯', text: '相似的创业方向' }
+ ]
+ }
+ },
+
+ // 上报匹配行为
+ async reportMatch(matchedUser) {
+ try {
+ await app.request('/api/miniprogram/ckb/match', {
+ method: 'POST',
+ data: {
+ matchType: this.data.selectedType,
+ phone: this.data.phoneNumber,
+ wechat: this.data.wechatId,
+ userId: app.globalData.userInfo?.id || '',
+ nickname: app.globalData.userInfo?.nickname || '',
+ matchedUser: {
+ id: matchedUser.id,
+ nickname: matchedUser.nickname,
+ matchScore: matchedUser.matchScore
+ }
+ }
+ })
+ } catch (e) {
+ console.log('上报匹配失败:', e)
+ }
+ },
+
+ // 取消匹配
+ cancelMatch() {
+ this.setData({ isMatching: false, matchAttempts: 0 })
+ },
+
+ // 重置匹配(返回)
+ resetMatch() {
+ this.setData({ currentMatch: null })
+ },
+
+ // 添加微信好友(先校验手机号绑定)
+ handleAddWechat() {
+ if (!this.data.currentMatch) return
+
+ // 未登录需先登录
+ if (!app.globalData.isLoggedIn) {
+ wx.showModal({
+ title: '需要登录',
+ content: '请先登录后再添加好友',
+ confirmText: '去登录',
+ success: (res) => {
+ if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
+ }
+ })
+ return
+ }
+
+ // 判断是否已绑定手机号(本地缓存或用户资料)
+ const hasPhone = !!(
+ wx.getStorageSync('user_phone') ||
+ app.globalData.userInfo?.phone
+ )
+
+ if (!hasPhone) {
+ this.setData({
+ showBindPhoneModal: true,
+ pendingAddWechatAfterBind: true
+ })
+ return
+ }
+
+ this.doCopyWechat()
+ },
+
+ // 执行复制联系方式(优先微信号,无则复制手机号)
+ doCopyWechat() {
+ if (!this.data.currentMatch) return
+ const wechat = (this.data.currentMatch.wechat || this.data.currentMatch.wechatId || '').trim()
+ const phone = (this.data.currentMatch.phone || '').trim()
+ const toCopy = wechat || phone
+ if (!toCopy) {
+ wx.showModal({
+ title: '暂无可复制',
+ content: '该用户未提供微信号或手机号,请通过其他方式联系',
+ showCancel: false,
+ confirmText: '知道了'
+ })
+ return
+ }
+ const label = wechat ? '微信号' : '手机号'
+ wx.setClipboardData({
+ data: toCopy,
+ success: () => {
+ wx.showModal({
+ title: wechat ? '微信号已复制' : '手机号已复制',
+ content: wechat
+ ? `${label}:${toCopy}\n\n请打开微信添加好友,备注"创业合作"即可`
+ : `${label}:${toCopy}\n\n可通过微信搜索该手机号添加好友`,
+ showCancel: false,
+ confirmText: '知道了'
+ })
+ },
+ fail: () => {
+ wx.showToast({ title: '复制失败,请重试', icon: 'none' })
+ }
+ })
+ },
+
+ // 切换联系方式类型(同步刷新用户资料填充)
+ switchContactType(e) {
+ const type = e.currentTarget.dataset.type
+ this.loadStoredContact()
+ this.setData({ contactType: type, joinError: '' })
+ },
+
+ // 手机号输入
+ onPhoneInput(e) {
+ this.setData({
+ phoneNumber: e.detail.value.replace(/\D/g, '').slice(0, 11),
+ joinError: ''
+ })
+ },
+
+ // 资源对接表单输入
+ onCanHelpInput(e) {
+ this.setData({ canHelp: e.detail.value })
+ },
+ onNeedHelpInput(e) {
+ this.setData({ needHelp: e.detail.value })
+ },
+ onGoodAtInput(e) {
+ this.setData({ goodAt: e.detail.value })
+ },
+
+ // 微信号输入
+ onWechatInput(e) {
+ this.setData({
+ wechatId: e.detail.value,
+ joinError: ''
+ })
+ },
+
+ // 提交加入
+ async handleJoinSubmit() {
+ const { contactType, phoneNumber, wechatId, joinType, isJoining, canHelp, needHelp } = this.data
+
+ if (isJoining) return
+
+ // 验证联系方式
+ if (contactType === 'phone') {
+ if (!phoneNumber || phoneNumber.length !== 11) {
+ this.setData({ joinError: '请输入正确的11位手机号' })
+ return
+ }
+ } else {
+ if (!wechatId || wechatId.length < 6) {
+ this.setData({ joinError: '请输入正确的微信号(至少6位)' })
+ return
+ }
+ }
+
+ // 资源对接需要填写两项信息
+ if (joinType === 'investor') {
+ if (!canHelp || canHelp.trim().length < 2) {
+ this.setData({ joinError: '请填写"我能帮到你什么"' })
+ return
+ }
+ if (!needHelp || needHelp.trim().length < 2) {
+ this.setData({ joinError: '请填写"我需要什么帮助"' })
+ return
+ }
+ }
+
+ this.setData({ isJoining: true, joinError: '' })
+
+ try {
+ const res = await app.request('/api/miniprogram/ckb/join', {
+ method: 'POST',
+ data: {
+ type: joinType,
+ phone: contactType === 'phone' ? phoneNumber : '',
+ wechat: contactType === 'wechat' ? wechatId : '',
+ userId: app.globalData.userInfo?.id || '',
+ // 资源对接专属字段
+ canHelp: joinType === 'investor' ? canHelp : '',
+ needHelp: joinType === 'investor' ? needHelp : ''
+ }
+ })
+
+ // 保存联系方式到本地
+ if (phoneNumber) wx.setStorageSync('user_phone', phoneNumber)
+ if (wechatId) wx.setStorageSync('user_wechat', wechatId)
+
+ if (res.success) {
+ this.setData({ joinSuccess: true })
+ setTimeout(() => {
+ this.setData({ showJoinModal: false, joinSuccess: false })
+ }, 2000)
+ } else {
+ // 即使API返回失败,也模拟成功(因为已保存本地)
+ this.setData({ joinSuccess: true })
+ setTimeout(() => {
+ this.setData({ showJoinModal: false, joinSuccess: false })
+ }, 2000)
+ }
+ } catch (e) {
+ // 网络错误时也模拟成功
+ this.setData({ joinSuccess: true })
+ setTimeout(() => {
+ this.setData({ showJoinModal: false, joinSuccess: false })
+ }, 2000)
+ } finally {
+ this.setData({ isJoining: false })
+ }
+ },
+
+ // 关闭加入弹窗
+ closeJoinModal() {
+ if (this.data.isJoining) return
+ this.setData({ showJoinModal: false, joinError: '' })
+ },
+
+ // 关闭手机绑定弹窗
+ closeBindPhoneModal() {
+ this.setData({
+ showBindPhoneModal: false,
+ pendingAddWechatAfterBind: false,
+ bindPhoneInput: '',
+ showMatchPhoneManualInput: false
+ })
+ },
+
+ // 关闭登录弹窗
+ closeLoginModal() {
+ if (this.data.isLoggingIn) return
+ this.setData({ showLoginModal: false })
+ },
+
+ // 切换协议勾选
+ toggleAgree() {
+ this.setData({ agreeProtocol: !this.data.agreeProtocol })
+ },
+
+ // 打开用户协议
+ openUserProtocol() {
+ wx.navigateTo({ url: '/pages/agreement/agreement' })
+ },
+
+ // 打开隐私政策
+ openPrivacy() {
+ wx.navigateTo({ url: '/pages/privacy/privacy' })
+ },
+
+ // 微信登录(匹配页)
+ async handleMatchWechatLogin() {
+ if (!this.data.agreeProtocol) {
+ wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+ return
+ }
+ this.setData({ isLoggingIn: true })
+ try {
+ const result = await app.login()
+ if (result) {
+ // 登录成功后必须拉取 matchQuota,否则无法正确显示剩余次数
+ await this.refreshMatchCountAndStatus()
+ this.setData({ showLoginModal: false, agreeProtocol: false })
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ } else {
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ }
+ } catch (e) {
+ console.error('[Match] 微信登录错误:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ } finally {
+ this.setData({ isLoggingIn: false })
+ }
+ },
+
+ // 一键获取手机号(匹配页加好友前绑定)
+ async onMatchGetPhoneNumber(e) {
+ if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+ wx.showToast({ title: '授权失败', icon: 'none' })
+ return
+ }
+ const code = e.detail.code
+ if (!code) {
+ this.setData({ showMatchPhoneManualInput: true })
+ return
+ }
+ try {
+ wx.showLoading({ title: '获取中...', mask: true })
+ const userId = app.globalData.userInfo?.id
+ const res = await app.request('/api/miniprogram/phone', {
+ method: 'POST',
+ data: { code, userId }
+ })
+ wx.hideLoading()
+ if (res.success && res.phoneNumber) {
+ await this.saveMatchPhoneAndContinue(res.phoneNumber)
+ } else {
+ this.setData({ showMatchPhoneManualInput: true })
+ }
+ } catch (err) {
+ wx.hideLoading()
+ this.setData({ showMatchPhoneManualInput: true })
+ }
+ },
+
+ // 切换为手动输入
+ onMatchShowManualInput() {
+ this.setData({ showMatchPhoneManualInput: true })
+ },
+
+ // 手动输入手机号
+ onMatchPhoneInput(e) {
+ this.setData({
+ bindPhoneInput: e.detail.value.replace(/\D/g, '').slice(0, 11)
+ })
+ },
+
+ // 确认手动绑定手机号
+ async confirmMatchPhoneBind() {
+ const { bindPhoneInput } = this.data
+ if (!bindPhoneInput || bindPhoneInput.length !== 11) {
+ wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
+ return
+ }
+ if (!/^1[3-9]\d{9}$/.test(bindPhoneInput)) {
+ wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
+ return
+ }
+ await this.saveMatchPhoneAndContinue(bindPhoneInput)
+ },
+
+ // 保存手机号到本地+服务器,并继续加好友
+ async saveMatchPhoneAndContinue(phone) {
+ wx.setStorageSync('user_phone', phone)
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.phone = phone
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+ this.setData({
+ phoneNumber: phone,
+ userPhone: phone,
+ bindPhoneInput: ''
+ })
+ this.loadStoredContact()
+ try {
+ const userId = app.globalData.userInfo?.id
+ if (userId) {
+ await app.request('/api/miniprogram/user/profile', {
+ method: 'POST',
+ data: { userId, phone }
+ })
+ }
+ } catch (e) {
+ console.log('[Match] 同步手机号到服务器失败:', e)
+ }
+ const pending = this.data.pendingAddWechatAfterBind
+ this.closeBindPhoneModal()
+ if (pending) {
+ wx.showToast({ title: '绑定成功', icon: 'success' })
+ setTimeout(() => this.doCopyWechat(), 500)
+ }
+ },
+
+ // 显示解锁弹窗
+ showUnlockModal() {
+ this.setData({ showUnlockModal: true })
+ },
+
+ // 关闭解锁弹窗
+ closeUnlockModal() {
+ this.setData({ showUnlockModal: false })
+ },
+
+ // 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试)
+ async pollOrderAndRefresh(orderSn) {
+ const maxAttempts = 12
+ const interval = 800
+ for (let i = 0; i < maxAttempts; i++) {
+ try {
+ const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
+ if (r?.data?.status === 'paid') {
+ await this.refreshMatchCountAndStatus()
+ return
+ }
+ } catch (_) {}
+ if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval))
+ }
+ await this.refreshMatchCountAndStatus()
+ },
+
+ // 购买匹配次数(与购买章节逻辑一致,写入订单)
+ async buyMatchCount() {
+ this.setData({ showUnlockModal: false })
+
+ try {
+ // 获取openId
+ let openId = app.globalData.openId || wx.getStorageSync('openId')
+ if (!openId) {
+ openId = await app.getOpenId()
+ }
+
+ if (!openId) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+
+ const matchPrice = this.data.matchPrice || 1
+ // 邀请码:与章节支付一致,写入订单便于分销归属与对账
+ const referralCode = wx.getStorageSync('referral_code') || ''
+ // 调用支付接口购买匹配次数(productType: match,订单类型:购买匹配次数)
+ const res = await app.request('/api/miniprogram/pay', {
+ method: 'POST',
+ data: {
+ openId,
+ productType: 'match',
+ productId: 'match_1',
+ amount: matchPrice,
+ description: '匹配次数x1',
+ userId: app.globalData.userInfo?.id || '',
+ referralCode: referralCode || undefined
+ }
+ })
+
+ if (res.success && res.data?.payParams) {
+ const orderSn = res.data.orderSn
+ // 调用微信支付
+ await new Promise((resolve, reject) => {
+ wx.requestPayment({
+ ...res.data.payParams,
+ success: resolve,
+ fail: reject
+ })
+ })
+ wx.showToast({ title: '购买成功', icon: 'success' })
+ // 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机)
+ this.pollOrderAndRefresh(orderSn)
+ } else {
+ throw new Error(res.error || '创建订单失败')
+ }
+ } catch (e) {
+ if (e.errMsg && e.errMsg.includes('cancel')) {
+ wx.showToast({ title: '已取消', icon: 'none' })
+ } else {
+ // 测试模式(无支付环境时本地模拟)
+ wx.showModal({
+ title: '支付服务暂不可用',
+ content: '是否使用测试模式购买?',
+ success: (res) => {
+ if (res.confirm) {
+ app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1
+ wx.showToast({ title: '测试购买成功', icon: 'success' })
+ this.initUserStatus()
+ }
+ }
+ })
+ }
+ }
+ },
+
+ // 跳转到目录页购买
+ goToChapters() {
+ this.setData({ showUnlockModal: false })
+ wx.switchTab({ url: '/pages/chapters/chapters' })
+ },
+
+ // 打开设置
+ openSettings() {
+ wx.navigateTo({ url: '/pages/settings/settings' })
+ },
+
+ // 阻止事件冒泡
+ preventBubble() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 找伙伴',
+ path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
+ }
+ }
+})
diff --git a/miniprogram2/pages/match/match.json b/miniprogram2/pages/match/match.json
new file mode 100644
index 00000000..e7696321
--- /dev/null
+++ b/miniprogram2/pages/match/match.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {},
+ "enablePullDownRefresh": false,
+ "backgroundTextStyle": "light",
+ "backgroundColor": "#000000"
+}
diff --git a/miniprogram2/pages/match/match.wxml b/miniprogram2/pages/match/match.wxml
new file mode 100644
index 00000000..fd174f77
--- /dev/null
+++ b/miniprogram2/pages/match/match.wxml
@@ -0,0 +1,373 @@
+
+
+
+
+
+
+ 找伙伴
+
+ ⚙️
+
+
+
+
+
+
+
+
+
+
+ ⚡
+ 今日免费次数已用完
+ 购买次数
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚡
+ 购买次数
+ ¥{{displayMatchPrice || matchPrice || 1}} = 1次匹配
+
+
+ 👥
+ 开始匹配
+ 匹配{{currentTypeLabel}}
+
+
+
+
+
+
+
+ 当前模式: {{currentTypeLabel}}
+
+
+
+
+
+
+
+ 选择匹配类型
+
+
+ {{item.icon}}
+ {{item.label}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔍
+
+
+
+ ✨
+ 💫
+ ⭐
+ 🌟
+
+
+
+
+
+ 正在匹配{{currentTypeLabel}}...
+ 正在从 {{matchAttempts * 127 + 89}} 位创业者中为你寻找
+
+ ✓ 分析兴趣标签
+ ✓ 匹配创业方向
+ ✓ 筛选优质伙伴
+
+ 取消
+
+
+
+
+
+
+
+
+ ✨
+
+
+
+
+
+
+
+
+ 共同兴趣
+
+
+ {{item.icon}}
+ {{item.text}}
+
+
+
+
+
+
+ 核心理念
+ {{currentMatch.concept}}
+
+
+
+
+
+ 一键加好友
+ 返回
+
+
+
+
+
+
+
+
+
+
+
+ ✅
+ 提交成功
+ 工作人员将在24小时内与您联系
+
+
+
+
+
+
+
+
+
+
+
+ 📱
+ 手机号
+
+
+ 💬
+ 微信号
+
+
+
+
+
+
+
+ 我能帮到你什么 *
+
+
+
+ 我需要什么帮助 *
+
+
+
+
+
+
+
+
+ {{contactType === 'phone' ? '+86' : '@'}}
+
+
+
+ {{joinError}}
+
+
+
+
+ {{isJoining ? '提交中...' : '确认提交'}}
+
+
+ 提交后我们会尽快与您联系
+
+
+
+
+
+
+
+ ✕
+ 🔐
+ 登录 Soul创业实验
+ 登录后可使用找伙伴功能
+
+
+ 取消
+
+
+ {{agreeProtocol ? '✓' : ''}}
+ 我已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+
+
+
+
+
+
+
+
+
+ 手动输入手机号
+
+
+
+
+
+
+ +86
+
+
+
+ 确认绑定
+
+
+
+
+
+
+
+ ⚡
+ 购买匹配次数
+ 今日3次免费匹配已用完,可付费购买额外次数
+
+
+
+ 单价
+
+ ¥{{matchPrice || 1}}
+ ¥{{displayMatchPrice || matchPrice || 1}} / 次
+ 省{{userDiscount}}%
+
+
+ ¥{{matchPrice || 1}} / 次
+ 好友链接立省{{userDiscount}}%
+
+ ¥{{matchPrice || 1}} / 次
+
+
+ 已购买
+ {{extraMatches || 0}} 次
+
+
+
+
+ 立即购买 ¥{{hasReferralDiscount ? (displayMatchPrice || matchPrice || 1) : (matchPrice || 1)}}
+ 明天再来
+
+
+
+
+
+
+
diff --git a/miniprogram2/pages/match/match.wxss b/miniprogram2/pages/match/match.wxss
new file mode 100644
index 00000000..b662a249
--- /dev/null
+++ b/miniprogram2/pages/match/match.wxss
@@ -0,0 +1,1380 @@
+/**
+ * Soul创业实验 - 找伙伴页样式
+ * 按H5网页端完全重构
+ */
+
+.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);
+}
+
+.nav-content {
+ height: 88rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+}
+
+.nav-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.nav-settings {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: #1c1c1e;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.settings-icon {
+ font-size: 36rpx;
+}
+
+.nav-placeholder {
+ width: 100%;
+}
+
+/* ===== 匹配提示条 - 简化版 ===== */
+.match-tip-bar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ margin: 24rpx 32rpx;
+ padding: 20rpx 32rpx;
+ background: rgba(255, 215, 0, 0.1);
+ border-radius: 16rpx;
+ border: 1rpx solid rgba(255, 215, 0, 0.2);
+}
+
+.tip-icon {
+ font-size: 28rpx;
+}
+
+.tip-text {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.tip-btn {
+ padding: 10rpx 24rpx;
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+ color: #000;
+ font-size: 24rpx;
+ font-weight: 500;
+ border-radius: 20rpx;
+}
+
+.text-brand {
+ color: #00CED1;
+}
+
+.text-red {
+ color: #ff4444;
+}
+
+.text-gray {
+ color: #666666;
+}
+
+.text-muted {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.gold-text {
+ color: #FFD700;
+}
+
+/* ===== 主内容区 ===== */
+.main-content {
+ padding: 0 32rpx;
+}
+
+/* ===== 匹配圆环 ===== */
+.match-circle-wrapper {
+ position: relative;
+ width: 480rpx;
+ height: 480rpx;
+ margin: 48rpx auto;
+}
+
+.outer-glow {
+ position: absolute;
+ inset: -60rpx;
+ border-radius: 50%;
+ animation: pulseGlow 2s ease-in-out infinite;
+}
+
+.outer-glow.glow-active {
+ background: radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%);
+}
+
+.outer-glow.glow-inactive {
+ background: radial-gradient(circle, transparent 50%, rgba(100, 100, 100, 0.1) 70%, transparent 100%);
+}
+
+@keyframes pulseGlow {
+ 0%, 100% { transform: scale(1); opacity: 0.5; }
+ 50% { transform: scale(1.1); opacity: 0.8; }
+}
+
+.middle-ring {
+ position: absolute;
+ inset: -30rpx;
+ border-radius: 50%;
+ border: 4rpx solid;
+ animation: pulseRing 1.5s ease-in-out infinite;
+}
+
+.middle-ring.ring-active {
+ border-color: rgba(0, 229, 255, 0.3);
+}
+
+.middle-ring.ring-inactive {
+ border-color: rgba(100, 100, 100, 0.3);
+}
+
+@keyframes pulseRing {
+ 0%, 100% { transform: scale(1); opacity: 0.3; }
+ 50% { transform: scale(1.05); opacity: 0.6; }
+}
+
+.inner-sphere {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ animation: floatSphere 3s ease-in-out infinite;
+ overflow: hidden;
+}
+
+.inner-sphere.sphere-active {
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+ box-shadow: 0 0 120rpx rgba(0, 229, 255, 0.3), inset 0 0 120rpx rgba(123, 97, 255, 0.2);
+}
+
+.inner-sphere.sphere-inactive {
+ background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%);
+ box-shadow: 0 0 60rpx rgba(100, 100, 100, 0.2);
+}
+
+@keyframes floatSphere {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10rpx); }
+}
+
+.sphere-gradient {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: radial-gradient(circle at 30% 30%, rgba(123, 97, 255, 0.4) 0%, transparent 50%),
+ radial-gradient(circle at 70% 70%, rgba(233, 30, 99, 0.3) 0%, transparent 50%);
+}
+
+.sphere-content {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.sphere-icon {
+ font-size: 96rpx;
+ margin-bottom: 16rpx;
+}
+
+.sphere-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 8rpx;
+}
+
+.sphere-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 当前模式 ===== */
+.current-mode {
+ text-align: center;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.5);
+ margin-bottom: 16rpx;
+}
+
+/* ===== 免费次数提示 ===== */
+.free-tip {
+ text-align: center;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-bottom: 32rpx;
+}
+
+/* ===== 购买提示卡片 ===== */
+.purchase-tip-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 32rpx;
+ background: linear-gradient(90deg, rgba(0, 229, 255, 0.1) 0%, transparent 100%);
+ border: 2rpx solid rgba(0, 229, 255, 0.2);
+ border-radius: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.tip-left {
+ flex: 1;
+}
+
+.tip-title {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #ffffff;
+ margin-bottom: 8rpx;
+}
+
+.tip-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.tip-btn {
+ padding: 16rpx 32rpx;
+ background: #00CED1;
+ color: #000000;
+ font-size: 26rpx;
+ font-weight: 500;
+ border-radius: 16rpx;
+}
+
+/* ===== 分隔线 ===== */
+.divider {
+ height: 2rpx;
+ background: rgba(255, 255, 255, 0.1);
+ margin: 32rpx 0;
+}
+
+/* ===== 匹配类型选择 ===== */
+.type-section {
+ margin-bottom: 32rpx;
+}
+
+.type-section-title {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.4);
+ text-align: center;
+ margin-bottom: 24rpx;
+}
+
+.type-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20rpx;
+}
+
+.type-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16rpx;
+ padding: 32rpx 16rpx;
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid transparent;
+ transition: all 0.2s;
+}
+
+.type-item.type-active {
+ background: rgba(0, 229, 255, 0.1);
+ border-color: rgba(0, 229, 255, 0.5);
+}
+
+.type-icon {
+ font-size: 48rpx;
+}
+
+.type-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.6);
+ text-align: center;
+}
+
+/* ===== 匹配中状态 ===== */
+.matching-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 48rpx 0;
+}
+
+.matching-animation {
+ position: relative;
+ width: 400rpx;
+ height: 400rpx;
+ margin-bottom: 48rpx;
+}
+
+.matching-ring {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00CED1, #7B61FF, #E91E63);
+ animation: rotateRing 3s linear infinite;
+}
+
+@keyframes rotateRing {
+ to { transform: rotate(360deg); }
+}
+
+.matching-center {
+ position: absolute;
+ inset: 16rpx;
+ border-radius: 50%;
+ background: #000000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.matching-icon {
+ font-size: 96rpx;
+ animation: pulseIcon 1s ease-in-out infinite;
+}
+
+@keyframes pulseIcon {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.2); }
+}
+
+.ripple {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ border: 4rpx solid rgba(0, 229, 255, 0.3);
+ animation: rippleExpand 2s ease-out infinite;
+}
+
+.ripple-1 { animation-delay: 0s; }
+.ripple-2 { animation-delay: 0.5s; }
+.ripple-3 { animation-delay: 1s; }
+
+@keyframes rippleExpand {
+ 0% { transform: scale(1); opacity: 0.6; }
+ 100% { transform: scale(2); opacity: 0; }
+}
+
+.matching-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 16rpx;
+}
+
+.matching-count {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.5);
+ margin-bottom: 48rpx;
+}
+
+.cancel-btn {
+ padding: 24rpx 64rpx;
+ background: #1c1c1e;
+ color: #ffffff;
+ font-size: 28rpx;
+ border-radius: 48rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+/* ===== 匹配成功状态 ===== */
+.matched-state {
+ padding: 32rpx 0;
+}
+
+.success-icon-wrapper {
+ text-align: center;
+ margin-bottom: 32rpx;
+}
+
+.success-icon {
+ font-size: 120rpx;
+}
+
+.match-card {
+ background: #1c1c1e;
+ border-radius: 32rpx;
+ padding: 40rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ margin-bottom: 32rpx;
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.match-avatar {
+ width: 128rpx;
+ height: 128rpx;
+ border-radius: 50%;
+ border: 4rpx solid #00CED1;
+ flex-shrink: 0;
+}
+
+.match-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.match-name {
+ display: block;
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 12rpx;
+}
+
+.match-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8rpx;
+}
+
+.match-tag {
+ padding: 8rpx 16rpx;
+ background: rgba(0, 229, 255, 0.2);
+ color: #00CED1;
+ font-size: 20rpx;
+ border-radius: 8rpx;
+}
+
+.match-score-box {
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.score-value {
+ display: block;
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #00CED1;
+}
+
+.score-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.card-section {
+ padding-top: 24rpx;
+ border-top: 2rpx solid rgba(255, 255, 255, 0.1);
+ margin-top: 24rpx;
+}
+
+.section-title {
+ display: block;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 16rpx;
+}
+
+.interest-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12rpx;
+}
+
+.interest-item {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.interest-icon {
+ font-size: 28rpx;
+}
+
+.interest-text {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.concept-text {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.7);
+ line-height: 1.6;
+}
+
+/* ===== 操作按钮 ===== */
+.action-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.btn-primary {
+ padding: 32rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #ffffff;
+ font-size: 32rpx;
+ font-weight: 600;
+ text-align: center;
+ border-radius: 24rpx;
+}
+
+.btn-secondary {
+ padding: 32rpx;
+ background: #1c1c1e;
+ color: #ffffff;
+ font-size: 32rpx;
+ text-align: center;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.btn-disabled {
+ opacity: 0.5;
+}
+
+/* ===== 弹窗 ===== */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(20rpx);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 48rpx;
+}
+
+.modal-content {
+ width: 100%;
+ max-width: 640rpx;
+ background: #1c1c1e;
+ border-radius: 32rpx;
+ overflow: hidden;
+}
+
+/* ===== 登录弹窗 ===== */
+.login-modal-content {
+ padding: 48rpx;
+ position: relative;
+}
+
+.login-modal-content .modal-close {
+ position: absolute;
+ top: 24rpx;
+ right: 24rpx;
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.login-icon {
+ font-size: 96rpx;
+ text-align: center;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.login-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ text-align: center;
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.login-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+ text-align: center;
+ display: block;
+ margin-bottom: 48rpx;
+}
+
+.btn-wechat {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 28rpx;
+ background: #07C160;
+ color: #ffffff;
+ font-size: 28rpx;
+ font-weight: 500;
+ border-radius: 24rpx;
+ margin-bottom: 24rpx;
+ border: none;
+}
+
+.btn-wechat::after {
+ border: none;
+}
+
+.btn-wechat-icon {
+ width: 40rpx;
+ height: 40rpx;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24rpx;
+}
+
+.login-modal-cancel {
+ margin-top: 24rpx;
+ padding: 24rpx;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.login-agree-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ margin-top: 32rpx;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.agree-checkbox {
+ width: 32rpx;
+ height: 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.5);
+ border-radius: 6rpx;
+ margin-right: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.agree-checked {
+ background: #00CED1;
+ border-color: #00CED1;
+}
+
+.agree-text {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.agree-link {
+ color: #00CED1;
+ text-decoration: underline;
+ padding: 0 4rpx;
+}
+
+.btn-wechat-disabled {
+ opacity: 0.6;
+}
+
+/* ===== 加入弹窗 ===== */
+.join-modal {
+ padding: 40rpx;
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.modal-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.close-btn {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.form-tip {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 24rpx;
+}
+
+.contact-tabs {
+ display: flex;
+ gap: 16rpx;
+ margin-bottom: 24rpx;
+}
+
+.contact-tab {
+ flex: 1;
+ padding: 20rpx;
+ text-align: center;
+ font-size: 28rpx;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.6);
+ background: rgba(255, 255, 255, 0.05);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 16rpx;
+}
+
+.contact-tab.tab-active-phone {
+ background: rgba(0, 229, 255, 0.2);
+ color: #00CED1;
+ border-color: rgba(0, 229, 255, 0.3);
+}
+
+.contact-tab.tab-active-wechat {
+ background: rgba(7, 193, 96, 0.2);
+ color: #07C160;
+ border-color: rgba(7, 193, 96, 0.3);
+}
+
+.input-group {
+ margin-bottom: 24rpx;
+}
+
+.input-label {
+ display: block;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-bottom: 12rpx;
+}
+
+.form-input {
+ width: 100%;
+ padding: 28rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 20rpx;
+ font-size: 32rpx;
+ color: #ffffff;
+ box-sizing: border-box;
+}
+
+.input-placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+.error-text {
+ display: block;
+ font-size: 24rpx;
+ color: #ff4444;
+ margin-bottom: 16rpx;
+}
+
+.submit-btn {
+ margin-top: 16rpx;
+}
+
+.form-notice {
+ display: block;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.3);
+ text-align: center;
+ margin-top: 24rpx;
+}
+
+/* ===== 新版加入弹窗 ===== */
+.join-modal-new {
+ padding: 0;
+ border-radius: 32rpx;
+ overflow: hidden;
+}
+
+.join-header {
+ position: relative;
+ padding: 48rpx 48rpx 32rpx;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.15) 0%, rgba(123, 97, 255, 0.1) 100%);
+ text-align: center;
+}
+
+.join-icon-wrap {
+ width: 100rpx;
+ height: 100rpx;
+ margin: 0 auto 20rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.join-icon {
+ font-size: 48rpx;
+}
+
+.join-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 8rpx;
+}
+
+.join-subtitle {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.close-btn-new {
+ position: absolute;
+ top: 24rpx;
+ right: 24rpx;
+ width: 56rpx;
+ height: 56rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* 手机号绑定弹窗 */
+.bind-phone-actions {
+ padding: 32rpx 40rpx 48rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.get-phone-btn-modal {
+ width: 100%;
+ padding: 28rpx 48rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #fff;
+ font-size: 32rpx;
+ font-weight: 600;
+ border-radius: 20rpx;
+ border: none;
+}
+
+.get-phone-btn-modal::after {
+ border: none;
+}
+
+.manual-bind-link {
+ font-size: 28rpx;
+ color: rgba(0, 206, 209, 0.9);
+}
+
+.contact-switch {
+ display: flex;
+ gap: 16rpx;
+ padding: 24rpx 48rpx;
+}
+
+.switch-item {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 16rpx;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ border: 2rpx solid transparent;
+}
+
+.switch-item.switch-active {
+ background: rgba(0, 206, 209, 0.15);
+ color: #00CED1;
+ border-color: rgba(0, 206, 209, 0.3);
+}
+
+.switch-icon {
+ font-size: 32rpx;
+}
+
+.input-area {
+ padding: 0 48rpx 24rpx;
+}
+
+.input-wrapper {
+ display: flex;
+ align-items: center;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 20rpx;
+ overflow: hidden;
+}
+
+.input-prefix {
+ padding: 0 24rpx;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.5);
+ border-right: 1rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.input-field {
+ flex: 1;
+ padding: 28rpx 24rpx;
+ font-size: 32rpx;
+ color: #ffffff;
+}
+
+.input-placeholder-new {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+.error-msg {
+ display: block;
+ font-size: 24rpx;
+ color: #ff4444;
+ margin-top: 12rpx;
+ padding-left: 8rpx;
+}
+
+.submit-btn-new {
+ margin: 8rpx 48rpx 24rpx;
+ padding: 28rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #ffffff;
+ font-size: 32rpx;
+ font-weight: 600;
+ text-align: center;
+ border-radius: 20rpx;
+}
+
+.btn-disabled-new {
+ opacity: 0.5;
+}
+
+.form-notice-new {
+ display: block;
+ text-align: center;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.3);
+ padding: 0 48rpx 32rpx;
+}
+
+/* ===== 新版加入成功 ===== */
+.join-success-new {
+ padding: 64rpx 40rpx;
+ text-align: center;
+}
+
+.success-icon-big {
+ font-size: 96rpx;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.success-title-new {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 12rpx;
+}
+
+.success-desc-new {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 旧版加入成功 (保留兼容) ===== */
+.join-success {
+ padding: 48rpx;
+ text-align: center;
+}
+
+.success-check {
+ font-size: 128rpx;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.success-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 12rpx;
+}
+
+.success-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 解锁弹窗 ===== */
+.unlock-modal {
+ padding: 48rpx;
+ text-align: center;
+}
+
+.unlock-icon {
+ width: 128rpx;
+ height: 128rpx;
+ margin: 0 auto 24rpx;
+ background: rgba(255, 215, 0, 0.2);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 64rpx;
+}
+
+.unlock-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 12rpx;
+}
+
+.unlock-desc {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 32rpx;
+}
+
+.unlock-info {
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 20rpx;
+ padding: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.info-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12rpx 0;
+}
+
+.info-label {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.info-value {
+ font-size: 26rpx;
+ font-weight: 500;
+ color: #ffffff;
+}
+
+.info-value-row {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.info-original {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+ text-decoration: line-through;
+}
+
+.info-discount {
+ font-size: 22rpx;
+ color: #00CED1;
+}
+
+.info-discount-hint {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.unlock-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.btn-gold {
+ padding: 28rpx;
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+ color: #000000;
+ font-size: 30rpx;
+ font-weight: 600;
+ text-align: center;
+ border-radius: 24rpx;
+}
+
+.btn-ghost {
+ padding: 28rpx;
+ background: rgba(255, 255, 255, 0.05);
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 28rpx;
+ text-align: center;
+ border-radius: 24rpx;
+}
+
+/* ===== 底部留白 ===== */
+.bottom-space {
+ height: 40rpx;
+}
+
+/* ===== 新版匹配动画 V2 ===== */
+.matching-animation-v2 {
+ position: relative;
+ width: 440rpx;
+ height: 440rpx;
+ margin: 0 auto 48rpx;
+}
+
+/* 外层旋转光环 */
+.matching-outer-ring {
+ position: absolute;
+ inset: -20rpx;
+ border-radius: 50%;
+ background: conic-gradient(
+ from 0deg,
+ transparent 0deg,
+ #00CED1 60deg,
+ #7B61FF 120deg,
+ #E91E63 180deg,
+ #FFD700 240deg,
+ #00CED1 300deg,
+ transparent 360deg
+ );
+ animation: rotateRingV2 2s linear infinite;
+ opacity: 0.8;
+}
+
+.matching-outer-ring::before {
+ content: '';
+ position: absolute;
+ inset: 8rpx;
+ border-radius: 50%;
+ background: #000;
+}
+
+@keyframes rotateRingV2 {
+ to { transform: rotate(360deg); }
+}
+
+/* 中层脉冲环 */
+.matching-pulse-ring {
+ position: absolute;
+ inset: 20rpx;
+ border-radius: 50%;
+ border: 4rpx solid rgba(0, 206, 209, 0.5);
+ animation: pulseRingV2 1.5s ease-in-out infinite;
+}
+
+@keyframes pulseRingV2 {
+ 0%, 100% { transform: scale(1); opacity: 0.5; }
+ 50% { transform: scale(1.1); opacity: 1; }
+}
+
+/* 内层核心球体 */
+.matching-core {
+ position: absolute;
+ inset: 60rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #1a2a4a 0%, #0a1628 50%, #16213e 100%);
+ box-shadow:
+ 0 0 60rpx rgba(0, 206, 209, 0.4),
+ 0 0 120rpx rgba(123, 97, 255, 0.2),
+ inset 0 0 80rpx rgba(0, 206, 209, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: floatCoreV2 2s ease-in-out infinite;
+}
+
+.matching-core-inner {
+ width: 160rpx;
+ height: 160rpx;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(0, 206, 209, 0.3) 0%, transparent 70%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@keyframes floatCoreV2 {
+ 0%, 100% { transform: translateY(0) scale(1); }
+ 50% { transform: translateY(-10rpx) scale(1.02); }
+}
+
+.matching-icon-v2 {
+ font-size: 80rpx;
+ animation: searchIconV2 1s ease-in-out infinite;
+}
+
+@keyframes searchIconV2 {
+ 0%, 100% { transform: rotate(-15deg); }
+ 50% { transform: rotate(15deg); }
+}
+
+/* 粒子效果 */
+.particle {
+ position: absolute;
+ font-size: 32rpx;
+ animation: floatParticle 3s ease-in-out infinite;
+ opacity: 0.8;
+}
+
+.particle-1 { top: 10%; left: 15%; animation-delay: 0s; }
+.particle-2 { top: 20%; right: 10%; animation-delay: 0.5s; }
+.particle-3 { bottom: 20%; left: 10%; animation-delay: 1s; }
+.particle-4 { bottom: 15%; right: 15%; animation-delay: 1.5s; }
+
+@keyframes floatParticle {
+ 0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.4; }
+ 50% { transform: translateY(-20rpx) rotate(180deg); opacity: 1; }
+}
+
+/* 扩散波纹 V2 */
+.ripple-v2 {
+ position: absolute;
+ inset: 40rpx;
+ border-radius: 50%;
+ border: 3rpx solid;
+ border-color: rgba(0, 206, 209, 0.6);
+ animation: rippleExpandV2 2.5s ease-out infinite;
+}
+
+.ripple-v2-1 { animation-delay: 0s; }
+.ripple-v2-2 { animation-delay: 0.8s; }
+.ripple-v2-3 { animation-delay: 1.6s; }
+
+@keyframes rippleExpandV2 {
+ 0% { transform: scale(1); opacity: 0.8; }
+ 100% { transform: scale(1.8); opacity: 0; }
+}
+
+/* 新版匹配文字 */
+.matching-title-v2 {
+ display: block;
+ font-size: 38rpx;
+ font-weight: 700;
+ color: #ffffff;
+ text-align: center;
+ margin-bottom: 12rpx;
+ background: linear-gradient(90deg, #00CED1, #7B61FF, #00CED1);
+ background-size: 200% auto;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shineText 2s linear infinite;
+}
+
+@keyframes shineText {
+ to { background-position: 200% center; }
+}
+
+.matching-subtitle-v2 {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.5);
+ text-align: center;
+ margin-bottom: 32rpx;
+}
+
+.matching-tips {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 40rpx;
+}
+
+.tip-item {
+ font-size: 26rpx;
+ color: #00CED1;
+ animation: fadeInUp 0.5s ease-out forwards;
+ opacity: 0;
+}
+
+.tip-item:nth-child(1) { animation-delay: 0.5s; }
+.tip-item:nth-child(2) { animation-delay: 1.5s; }
+.tip-item:nth-child(3) { animation-delay: 2.5s; }
+
+@keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(20rpx); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.cancel-btn-v2 {
+ display: inline-block;
+ padding: 20rpx 60rpx;
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 28rpx;
+ border-radius: 40rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.2);
+}
+
+/* 资源对接表单 */
+.resource-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+ margin: 0 48rpx 24rpx;
+}
+.resource-form .form-item {
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+}
+.resource-form .form-label {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.6);
+}
+.resource-form .form-input-new {
+ background: #1c1c1e;
+ border: 2rpx solid rgba(0,206,209,0.3);
+ border-radius: 16rpx;
+ padding: 20rpx;
+ font-size: 28rpx;
+ color: #fff;
+}
diff --git a/miniprogram2/pages/my/my.js b/miniprogram2/pages/my/my.js
new file mode 100644
index 00000000..117e954c
--- /dev/null
+++ b/miniprogram2/pages/my/my.js
@@ -0,0 +1,745 @@
+/**
+ * Soul创业派对 - 我的页面
+ * 开发: 卡若
+ * 技术支持: 存客宝
+ */
+
+const app = getApp()
+
+Page({
+ data: {
+ // 系统信息
+ statusBarHeight: 44,
+ navBarHeight: 88,
+
+ // 用户状态
+ isLoggedIn: false,
+ userInfo: null,
+
+ // 统计数据
+ totalSections: 62,
+ readCount: 0,
+ referralCount: 0,
+ earnings: '-',
+ pendingEarnings: '-',
+ earningsLoading: true,
+ earningsRefreshing: false,
+
+ // 阅读统计
+ totalReadTime: 0,
+ matchHistory: 0,
+
+ // Tab切换
+ activeTab: 'overview', // overview | footprint
+
+ // 最近阅读
+ recentChapters: [],
+
+ // 功能配置
+ matchEnabled: false, // 找伙伴功能开关
+
+ // 菜单列表
+ menuList: [
+ { id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'gray' },
+ { id: 'orders', title: '我的订单', icon: '📦', count: 0 },
+ { id: 'referral', title: '推广中心', icon: '🎁', iconBg: 'gold', badge: '90%佣金' },
+ { id: 'withdrawRecords', title: '提现记录', icon: '📋', iconBg: 'gray' },
+ { id: 'about', title: '关于作者', icon: 'ℹ️', iconBg: 'brand' },
+ { id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' }
+ ],
+
+ // 待确认收款(用户确认模式)
+ pendingConfirmList: [],
+ withdrawMchId: '',
+ withdrawAppId: '',
+
+ // 未登录假资料(展示用)
+ guestNickname: '游客',
+ guestAvatar: '',
+
+ // 登录弹窗
+ showLoginModal: false,
+ isLoggingIn: false,
+ // 用户须主动勾选同意协议(审核要求:不得默认同意)
+ agreeProtocol: false,
+
+ // 修改昵称弹窗
+ showNicknameModal: false,
+ editingNickname: '',
+
+ // 扫一扫结果弹窗
+ showScanResultModal: false,
+ scanResult: ''
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ navBarHeight: app.globalData.navBarHeight
+ })
+ this.loadFeatureConfig()
+ this.initUserStatus()
+ },
+
+ onShow() {
+ // 设置TabBar选中状态(根据 matchEnabled 动态设置)
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ const tabBar = this.getTabBar()
+ if (tabBar.updateSelected) {
+ tabBar.updateSelected()
+ } else {
+ const selected = tabBar.data.matchEnabled ? 3 : 2
+ tabBar.setData({ selected })
+ }
+ }
+ this.initUserStatus()
+ },
+
+ // 加载功能配置
+ async loadFeatureConfig() {
+ try {
+ const res = await app.request({
+ url: '/api/miniprogram/config',
+ method: 'GET'
+ })
+
+ if (res && res.features) {
+ this.setData({
+ matchEnabled: res.features.matchEnabled === true
+ })
+ }
+ } catch (error) {
+ console.log('加载功能配置失败:', error)
+ // 默认关闭找伙伴功能
+ this.setData({ matchEnabled: false })
+ }
+ },
+
+ // 登录后刷新购买状态(与 match/read 一致,避免其他页面用旧数据)
+ async refreshPurchaseStatus() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+ try {
+ const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
+ if (res.success && res.data) {
+ app.globalData.hasFullBook = res.data.hasFullBook || false
+ app.globalData.purchasedSections = res.data.purchasedSections || []
+ app.globalData.sectionMidMap = res.data.sectionMidMap || {}
+ app.globalData.matchCount = res.data.matchCount ?? 0
+ app.globalData.matchQuota = res.data.matchQuota || null
+ const userInfo = app.globalData.userInfo || {}
+ userInfo.hasFullBook = res.data.hasFullBook
+ userInfo.purchasedSections = res.data.purchasedSections
+ wx.setStorageSync('userInfo', userInfo)
+ }
+ } catch (e) {
+ console.log('[My] 刷新购买状态失败:', e)
+ }
+ },
+
+ // 初始化用户状态
+ initUserStatus() {
+ const { isLoggedIn, userInfo } = app.globalData
+
+ if (isLoggedIn && userInfo) {
+ const readIds = app.globalData.readSectionIds || []
+ const recentList = readIds.slice(-5).reverse().map(id => ({
+ id,
+ mid: app.getSectionMid(id),
+ title: `章节 ${id}`
+ }))
+
+ const userId = userInfo.id || ''
+ const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
+ const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
+
+ // 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
+ this.setData({
+ isLoggedIn: true,
+ userInfo,
+ userIdShort,
+ userWechat,
+ readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
+ referralCount: userInfo.referralCount || 0,
+ earnings: '-',
+ pendingEarnings: '-',
+ earningsLoading: true,
+ recentChapters: recentList,
+ totalReadTime: Math.floor(Math.random() * 200) + 50
+ })
+ this.loadMyEarnings()
+ this.loadPendingConfirm()
+ } else {
+ this.setData({
+ isLoggedIn: false,
+ userInfo: null,
+ userIdShort: '',
+ readCount: app.getReadCount(),
+ referralCount: 0,
+ earnings: '-',
+ pendingEarnings: '-',
+ earningsLoading: false,
+ recentChapters: []
+ })
+ }
+ },
+
+ // 拉取待确认收款列表(用于「确认收款」按钮)
+ async loadPendingConfirm() {
+ const userInfo = app.globalData.userInfo
+ if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
+ try {
+ const res = await app.request('/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id)
+ if (res && res.success && res.data) {
+ const list = (res.data.list || []).map(item => ({
+ id: item.id,
+ amount: (item.amount || 0).toFixed(2),
+ package: item.package,
+ createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
+ }))
+ this.setData({
+ pendingConfirmList: list,
+ withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
+ withdrawAppId: res.data.appId ?? res.data.app_id ?? ''
+ })
+ } else {
+ this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
+ }
+ } catch (e) {
+ this.setData({ pendingConfirmList: [] })
+ }
+ },
+
+ formatDateMy(dateStr) {
+ if (!dateStr) return '--'
+ const d = new Date(dateStr)
+ const m = (d.getMonth() + 1).toString().padStart(2, '0')
+ const day = d.getDate().toString().padStart(2, '0')
+ return `${m}-${day}`
+ },
+
+ // 确认收款:有 package 时调起微信收款页,成功后记录;无 package 时仅调用后端记录「已确认收款」
+ async confirmReceive(e) {
+ const index = e.currentTarget.dataset.index
+ const id = e.currentTarget.dataset.id
+ const list = this.data.pendingConfirmList || []
+ let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
+ if (!item && id) item = list.find(x => x.id === id) || null
+ if (!item) {
+ wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
+ return
+ }
+ const mchId = this.data.withdrawMchId
+ const appId = this.data.withdrawAppId
+ const hasPackage = item.package && mchId && appId && wx.canIUse('requestMerchantTransfer')
+
+ const recordConfirmReceived = async () => {
+ const userInfo = app.globalData.userInfo
+ if (userInfo && userInfo.id) {
+ try {
+ await app.request({
+ url: '/api/miniprogram/withdraw/confirm-received',
+ method: 'POST',
+ data: { withdrawalId: item.id, userId: userInfo.id }
+ })
+ } catch (e) { /* 仅记录,不影响前端展示 */ }
+ }
+ const newList = list.filter(x => x.id !== item.id)
+ this.setData({ pendingConfirmList: newList })
+ this.loadPendingConfirm()
+ }
+
+ if (hasPackage) {
+ wx.showLoading({ title: '调起收款...', mask: true })
+ wx.requestMerchantTransfer({
+ mchId,
+ appId,
+ package: item.package,
+ success: async () => {
+ wx.hideLoading()
+ wx.showToast({ title: '收款成功', icon: 'success' })
+ await recordConfirmReceived()
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
+ wx.showToast({ title: msg, icon: 'none' })
+ },
+ complete: () => { wx.hideLoading() }
+ })
+ return
+ }
+
+ // 无 package 时仅记录「确认已收款」(当前直接打款无 package,用户点按钮即记录)
+ wx.showLoading({ title: '提交中...', mask: true })
+ try {
+ await recordConfirmReceived()
+ wx.hideLoading()
+ wx.showToast({ title: '已记录确认收款', icon: 'success' })
+ } catch (e) {
+ wx.hideLoading()
+ wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
+ }
+ },
+
+ // 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数)
+ async loadMyEarnings() {
+ const userInfo = app.globalData.userInfo
+ if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
+ this.setData({ earningsLoading: false })
+ return
+ }
+ const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
+ try {
+ const res = await app.request('/api/miniprogram/earnings?userId=' + userInfo.id)
+ if (!res || !res.success || !res.data) {
+ this.setData({ earningsLoading: false, earnings: '0.00', pendingEarnings: '0.00' })
+ return
+ }
+ const d = res.data
+ this.setData({
+ earnings: formatMoney(d.totalCommission),
+ pendingEarnings: formatMoney(d.availableEarnings),
+ referralCount: d.referralCount ?? this.data.referralCount,
+ earningsLoading: false,
+ earningsRefreshing: false
+ })
+ } catch (e) {
+ console.log('[My] 拉取我的收益失败:', e && e.message)
+ this.setData({
+ earningsLoading: false,
+ earningsRefreshing: false,
+ earnings: '0.00',
+ pendingEarnings: '0.00'
+ })
+ }
+ },
+
+ // 点击刷新图标:刷新我的收益
+ async refreshEarnings() {
+ if (!this.data.isLoggedIn) return
+ if (this.data.earningsRefreshing) return
+ this.setData({ earningsRefreshing: true })
+ wx.showToast({ title: '刷新中...', icon: 'loading', duration: 2000 })
+ await this.loadMyEarnings()
+ wx.showToast({ title: '已刷新', icon: 'success' })
+ },
+
+ // 微信原生获取头像(button open-type="chooseAvatar" 回调)
+ async onChooseAvatar(e) {
+ const tempAvatarUrl = e.detail.avatarUrl
+ if (!tempAvatarUrl) return
+
+ wx.showLoading({ title: '上传中...', mask: true })
+
+ try {
+ // 1. 先上传图片到服务器
+ console.log('[My] 开始上传头像:', tempAvatarUrl)
+
+ const uploadRes = await new Promise((resolve, reject) => {
+ wx.uploadFile({
+ url: app.globalData.baseUrl + '/api/miniprogram/upload',
+ filePath: tempAvatarUrl,
+ name: 'file',
+ formData: {
+ folder: 'avatars'
+ },
+ success: (res) => {
+ try {
+ const data = JSON.parse(res.data)
+ if (data.success) {
+ resolve(data)
+ } else {
+ reject(new Error(data.error || '上传失败'))
+ }
+ } catch (err) {
+ reject(new Error('解析响应失败'))
+ }
+ },
+ fail: (err) => {
+ reject(err)
+ }
+ })
+ })
+
+ // 2. 获取上传后的完整URL
+ const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
+ console.log('[My] 头像上传成功:', avatarUrl)
+
+ // 3. 更新本地头像
+ const userInfo = this.data.userInfo
+ userInfo.avatar = avatarUrl
+ this.setData({ userInfo })
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+
+ // 4. 同步到服务器数据库
+ await app.request('/api/miniprogram/user/update', {
+ method: 'POST',
+ data: { userId: userInfo.id, avatar: avatarUrl }
+ })
+
+ wx.hideLoading()
+ wx.showToast({ title: '头像更新成功', icon: 'success' })
+
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[My] 上传头像失败:', e)
+ wx.showToast({
+ title: e.message || '上传失败,请重试',
+ icon: 'none'
+ })
+ }
+ },
+
+ // 微信原生获取昵称回调(针对 input type="nickname" 的 bindblur 或 bindchange)
+ async handleNicknameChange(nickname) {
+ if (!nickname || nickname === this.data.userInfo?.nickname) return
+
+ try {
+ const userInfo = this.data.userInfo
+ userInfo.nickname = nickname
+ this.setData({ userInfo })
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+
+ // 同步到服务器
+ await app.request('/api/miniprogram/user/update', {
+ method: 'POST',
+ data: { userId: userInfo.id, nickname }
+ })
+
+ wx.showToast({ title: '昵称已更新', icon: 'success' })
+ } catch (e) {
+ console.error('[My] 同步昵称失败:', e)
+ }
+ },
+
+ // 打开昵称修改弹窗
+ editNickname() {
+ this.setData({
+ showNicknameModal: true,
+ editingNickname: this.data.userInfo?.nickname || ''
+ })
+ },
+
+ // 关闭昵称弹窗
+ closeNicknameModal() {
+ this.setData({
+ showNicknameModal: false,
+ editingNickname: ''
+ })
+ },
+
+ // 阻止事件冒泡
+ stopPropagation() {},
+
+ // 昵称输入实时更新
+ onNicknameInput(e) {
+ this.setData({
+ editingNickname: e.detail.value
+ })
+ },
+
+ // 昵称变化(微信自动填充时触发)
+ onNicknameChange(e) {
+ const nickname = e.detail.value
+ console.log('[My] 昵称已自动填充:', nickname)
+ this.setData({
+ editingNickname: nickname
+ })
+ // 自动填充时也尝试直接同步
+ this.handleNicknameChange(nickname)
+ },
+
+ // 确认修改昵称
+ async confirmNickname() {
+ const newNickname = this.data.editingNickname.trim()
+
+ if (!newNickname) {
+ wx.showToast({ title: '昵称不能为空', icon: 'none' })
+ return
+ }
+
+ if (newNickname.length < 1 || newNickname.length > 20) {
+ wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
+ return
+ }
+
+ // 关闭弹窗
+ this.closeNicknameModal()
+
+ // 显示加载
+ wx.showLoading({ title: '更新中...', mask: true })
+
+ try {
+ // 1. 同步到服务器
+ const res = await app.request('/api/miniprogram/user/update', {
+ method: 'POST',
+ data: {
+ userId: this.data.userInfo.id,
+ nickname: newNickname
+ }
+ })
+
+ if (res && res.success) {
+ // 2. 更新本地状态
+ const userInfo = this.data.userInfo
+ userInfo.nickname = newNickname
+ this.setData({ userInfo })
+
+ // 3. 更新全局和缓存
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+
+ wx.hideLoading()
+ wx.showToast({ title: '昵称已修改', icon: 'success' })
+ } else {
+ throw new Error(res?.message || '更新失败')
+ }
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[My] 修改昵称失败:', e)
+ wx.showToast({ title: '修改失败,请重试', icon: 'none' })
+ }
+ },
+
+ // 复制用户ID
+ copyUserId() {
+ const userId = this.data.userInfo?.id || ''
+ if (!userId) {
+ wx.showToast({ title: '暂无ID', icon: 'none' })
+ return
+ }
+ wx.setClipboardData({
+ data: userId,
+ success: () => {
+ wx.showToast({ title: 'ID已复制', icon: 'success' })
+ }
+ })
+ },
+
+ // 切换Tab
+ switchTab(e) {
+ const tab = e.currentTarget.dataset.tab
+ this.setData({ activeTab: tab })
+ },
+
+ // 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
+ showLogin() {
+ try {
+ this.setData({ showLoginModal: true, agreeProtocol: false })
+ } catch (e) {
+ console.error('[My] showLogin error:', e)
+ this.setData({ showLoginModal: true })
+ }
+ },
+
+ // 切换协议勾选(用户主动勾选,非默认同意)
+ toggleAgree() {
+ this.setData({ agreeProtocol: !this.data.agreeProtocol })
+ },
+
+ // 打开用户协议页(审核要求:点击《用户协议》需有响应)
+ openUserProtocol() {
+ wx.navigateTo({ url: '/pages/agreement/agreement' })
+ },
+
+ // 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
+ openPrivacy() {
+ wx.navigateTo({ url: '/pages/privacy/privacy' })
+ },
+
+ // 关闭登录弹窗
+ closeLoginModal() {
+ if (this.data.isLoggingIn) return
+ this.setData({ showLoginModal: false })
+ },
+
+ // 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
+ async handleWechatLogin() {
+ if (!this.data.agreeProtocol) {
+ wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+ return
+ }
+ this.setData({ isLoggingIn: true })
+ try {
+ const result = await app.login()
+ if (result) {
+ await this.refreshPurchaseStatus()
+ this.initUserStatus()
+ this.setData({ showLoginModal: false, agreeProtocol: false })
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ } else {
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ }
+ } catch (e) {
+ console.error('[My] 微信登录错误:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ } finally {
+ this.setData({ isLoggingIn: false })
+ }
+ },
+
+ // 手机号登录(需要用户授权)
+ async handlePhoneLogin(e) {
+ // 检查是否有授权code
+ if (!e.detail.code) {
+ // 用户拒绝授权或获取失败,尝试使用微信登录
+ console.log('手机号授权失败,尝试微信登录')
+ return this.handleWechatLogin()
+ }
+
+ this.setData({ isLoggingIn: true })
+
+ try {
+ const result = await app.loginWithPhone(e.detail.code)
+ if (result) {
+ await this.refreshPurchaseStatus()
+ this.initUserStatus()
+ this.setData({ showLoginModal: false })
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ } else {
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ }
+ } catch (e) {
+ console.error('手机号登录错误:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ } finally {
+ this.setData({ isLoggingIn: false })
+ }
+ },
+
+ // 跳转编辑资料页
+ goEditProfile() {
+ if (!this.data.isLoggedIn) {
+ this.showLogin()
+ return
+ }
+ wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
+ },
+
+ // 点击菜单
+ handleMenuTap(e) {
+ const id = e.currentTarget.dataset.id
+
+ if (id === 'scan') {
+ this.doScanCode()
+ return
+ }
+
+ if (!this.data.isLoggedIn && id !== 'about') {
+ this.showLogin()
+ return
+ }
+
+ const routes = {
+ orders: '/pages/purchases/purchases',
+ referral: '/pages/referral/referral',
+ withdrawRecords: '/pages/withdraw-records/withdraw-records',
+ about: '/pages/about/about',
+ settings: '/pages/settings/settings'
+ }
+
+ if (routes[id]) {
+ wx.navigateTo({ url: routes[id] })
+ }
+ },
+
+ // 扫一扫:调起扫码,展示解析值
+ doScanCode() {
+ wx.scanCode({
+ onlyFromCamera: false,
+ scanType: ['qrCode', 'barCode'],
+ success: (res) => {
+ const result = res.result || ''
+ this.setData({
+ showScanResultModal: true,
+ scanResult: result
+ })
+ },
+ fail: (err) => {
+ if (err.errMsg && !err.errMsg.includes('cancel')) {
+ wx.showToast({ title: '扫码失败', icon: 'none' })
+ }
+ }
+ })
+ },
+
+ // 关闭扫码结果弹窗
+ closeScanResultModal() {
+ this.setData({ showScanResultModal: false, scanResult: '' })
+ },
+
+ // 复制扫码结果
+ copyScanResult() {
+ const text = this.data.scanResult || ''
+ if (!text) return
+ wx.setClipboardData({
+ data: text,
+ success: () => wx.showToast({ title: '已复制', icon: 'success' })
+ })
+ },
+
+ goToRead(e) {
+ const id = e.currentTarget.dataset.id
+ const mid = e.currentTarget.dataset.mid
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ // 跳转到目录
+ goToChapters() {
+ wx.switchTab({ url: '/pages/chapters/chapters' })
+ },
+
+ // 跳转到关于页
+ goToAbout() {
+ wx.navigateTo({ url: '/pages/about/about' })
+ },
+
+ // 跳转到匹配
+ goToMatch() {
+ wx.switchTab({ url: '/pages/match/match' })
+ },
+
+ // 跳转到推广中心
+ goToReferral() {
+ if (!this.data.isLoggedIn) {
+ this.showLogin()
+ return
+ }
+ wx.navigateTo({ url: '/pages/referral/referral' })
+ },
+
+ // 跳转到找伙伴页面
+ goToMatch() {
+ wx.switchTab({ url: '/pages/match/match' })
+ },
+
+ // 退出登录
+ handleLogout() {
+ wx.showModal({
+ title: '退出登录',
+ content: '确定要退出登录吗?',
+ success: (res) => {
+ if (res.confirm) {
+ app.logout()
+ this.initUserStatus()
+ wx.showToast({ title: '已退出登录', icon: 'success' })
+ }
+ }
+ })
+ },
+
+ // 阻止冒泡
+ stopPropagation() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 我的',
+ path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
+ }
+ }
+})
diff --git a/miniprogram2/pages/my/my.json b/miniprogram2/pages/my/my.json
new file mode 100644
index 00000000..e7696321
--- /dev/null
+++ b/miniprogram2/pages/my/my.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {},
+ "enablePullDownRefresh": false,
+ "backgroundTextStyle": "light",
+ "backgroundColor": "#000000"
+}
diff --git a/miniprogram2/pages/my/my.wxml b/miniprogram2/pages/my/my.wxml
new file mode 100644
index 00000000..5d243308
--- /dev/null
+++ b/miniprogram2/pages/my/my.wxml
@@ -0,0 +1,307 @@
+
+
+
+
+
+
+ 我的
+
+
+
+
+
+
+
+
+
+
+
+ --
+ 已读章节
+
+
+ --
+ 推荐好友
+
+
+ --
+ 待领收益
+
+
+
+
+
+
+
+
+
+
+ ✏️
+ 编辑资料
+
+
+
+
+ {{readCount}}
+ 已读章节
+
+
+ {{referralCount}}
+ 推荐好友
+
+
+ {{pendingEarnings > 0 ? '¥' + pendingEarnings : '--'}}
+ 待领收益
+
+
+
+
+
+
+
+
+
+
+ ¥{{item.amount}}
+ {{item.createdAt}}
+
+ 确认收款
+
+
+
+
+
+
+ 概览
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 👁️
+ 阅读统计
+
+
+
+
+ 📖
+ {{readCount}}
+ 已读章节
+
+
+ ⏱️
+ {{totalReadTime}}
+ 阅读分钟
+
+
+ 👥
+ {{matchHistory}}
+ 匹配伙伴
+
+
+
+
+
+
+
+ 📖
+ 最近阅读
+
+
+
+
+ {{index + 1}}
+ {{item.title}}
+
+ 继续阅读
+
+
+
+ 📖
+ 暂无阅读记录
+ 去阅读 →
+
+
+
+
+
+
+ 👥
+ 匹配记录
+
+
+ 👥
+ 暂无匹配记录
+ 去匹配 →
+
+
+
+
+
+
+
+ ✕
+ 🔐
+ 登录 Soul创业实验
+ 登录后可购买章节、解锁更多内容
+
+
+ 取消
+
+
+ {{agreeProtocol ? '✓' : ''}}
+ 我已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+
+
+
+
+ ✕
+
+
+
+
+ 微信用户可点击自动填充昵称
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+ ✕
+
+ {{scanResult}}
+
+ 复制
+ 关闭
+
+
+
+
+
+
+
diff --git a/miniprogram2/pages/my/my.wxss b/miniprogram2/pages/my/my.wxss
new file mode 100644
index 00000000..79d5b2bf
--- /dev/null
+++ b/miniprogram2/pages/my/my.wxss
@@ -0,0 +1,1296 @@
+/**
+ * Soul创业实验 - 我的页面样式
+ * 1:1还原Web版本UI
+ */
+
+.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-content {
+ height: 88rpx;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 0 32rpx;
+}
+
+.nav-title {
+ font-size: 36rpx;
+ font-weight: 600;
+}
+
+.nav-title-left {
+ font-size: 36rpx;
+ font-weight: 600;
+}
+
+.brand-color {
+ color: #00CED1;
+}
+
+.gold-color {
+ color: #FFD700;
+}
+
+.pink-color {
+ color: #E91E63;
+}
+
+.nav-placeholder {
+ width: 100%;
+}
+
+/* ===== 用户卡片 ===== */
+.user-card {
+ margin: 32rpx;
+ padding: 32rpx;
+}
+.margin-partner-badge{
+}
+/* 创业伙伴按钮 - inline 布局 */
+.partner-badge {
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ padding: 10rpx 18rpx;
+ background: rgba(0, 206, 209, 0.15);
+ border: 1rpx solid rgba(0, 206, 209, 0.3);
+ border-radius: 20rpx;
+ flex-shrink: 0;
+ transition: all 0.2s;
+ margin-top:-20px;
+}
+
+.partner-badge:active {
+ background: rgba(0, 206, 209, 0.3);
+ transform: scale(0.95);
+}
+
+.partner-icon {
+ font-size: 18rpx;
+ color: #00CED1;
+ line-height: 1;
+}
+
+.partner-text {
+ font-size: 20rpx;
+ font-weight: 500;
+ color: #00CED1;
+ line-height: 1;
+ white-space: nowrap;
+}
+
+/* 编辑资料入口 - 深色圆角按钮,与卡片统一 */
+.edit-profile-entry {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: nowrap;
+ width: 100%;
+ min-height: 88rpx;
+ padding: 24rpx 32rpx;
+ margin-bottom: 24rpx;
+ box-sizing: border-box;
+ background: rgba(255, 255, 255, 0.06);
+ border: 2rpx solid rgba(0, 206, 209, 0.3);
+ border-radius: 20rpx;
+}
+
+.edit-profile-entry:active {
+ background: rgba(0, 206, 209, 0.12);
+}
+
+.edit-profile-icon {
+ font-size: 36rpx;
+ margin-right: 12rpx;
+ flex-shrink: 0;
+}
+
+.edit-profile-text {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #00CED1;
+ white-space: nowrap;
+}
+
+.card-gradient {
+ background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
+ border-radius: 32rpx;
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+}
+
+/* ===== 新版用户头部布局 ===== */
+.user-header-row {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ margin-bottom: 32rpx;
+ width: 100%;
+}
+
+/* 头像容器 */
+.avatar-wrapper {
+ position: relative;
+ flex-shrink: 0;
+ width: 120rpx;
+ height: 120rpx;
+}
+
+/* 头像按钮样式 - 简化版 */
+.avatar-btn-simple {
+ flex-shrink: 0;
+ width: 60rpx!important;
+ height: 120rpx;
+ min-width: 120rpx;
+ min-height: 120rpx;
+ padding: 0;
+ margin: 0;
+ background: transparent !important;
+ border: none;
+ line-height: normal;
+ border-radius: 50%;
+ overflow: visible;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.avatar-btn-simple::after { border: none; }
+
+.edit-icon-small {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.5);
+ margin-left: 12rpx;
+}
+
+.avatar {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 50%;
+ border: 3rpx solid #00CED1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, transparent 100%);
+ overflow: hidden;
+ box-sizing: border-box;
+ flex-shrink: 0;
+}
+
+.avatar-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.avatar-edit-hint {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 36rpx;
+ height: 36rpx;
+ background: #00CED1;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 3rpx solid #000;
+}
+
+.edit-icon {
+ font-size: 20rpx;
+ color: #000;
+}
+
+.avatar-empty {
+ border-style: dashed;
+ border-color: rgba(0, 206, 209, 0.5);
+}
+
+.avatar-icon {
+ font-size: 64rpx;
+ opacity: 0.3;
+}
+
+.avatar-text {
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #00CED1;
+ line-height: 1;
+}
+
+.user-info-block {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 12rpx;
+ min-width: 0;
+}
+
+.user-name-row {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ line-height: 1.3;
+}
+
+.user-name {
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #ffffff;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.edit-name-icon {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.4);
+ flex-shrink: 0;
+}
+
+.user-id-row {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ line-height: 1.3;
+}
+
+.user-id {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.copy-icon {
+ font-size: 22rpx;
+ opacity: 0.5;
+ flex-shrink: 0;
+}
+
+.user-wechat {
+ font-size: 24rpx;
+ color: #00CED1;
+}
+
+.user-badge-small {
+ display: inline-flex;
+ align-items: center;
+ gap: 6rpx;
+ padding: 6rpx 14rpx;
+ background: rgba(0, 206, 209, 0.15);
+ border: 1rpx solid rgba(0, 206, 209, 0.3);
+ border-radius: 20rpx;
+}
+
+.badge-star {
+ font-size: 18rpx;
+}
+
+.badge-label {
+ font-size: 20rpx;
+ color: #00CED1;
+}
+
+/* 兼容旧样式 */
+.user-header {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.user-info {
+ flex: 1;
+}
+
+.login-btn {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #00CED1;
+}
+
+.user-subtitle {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.3);
+ display: block;
+ margin-top: 4rpx;
+}
+
+/* ===== 未登录假资料样式 ===== */
+.avatar-placeholder {
+ cursor: default;
+}
+
+.user-id-guest {
+ color: rgba(255, 255, 255, 0.35) !important;
+}
+
+.btn-login-inline {
+ flex-shrink: 0;
+ margin-left: 16rpx;
+ padding: 10rpx 24rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #000;
+ font-size: 24rpx;
+ font-weight: 600;
+ border-radius: 24rpx;
+}
+
+.user-name {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+ display: block;
+}
+
+.user-id {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.3);
+ display: block;
+ margin-top: 4rpx;
+}
+
+.user-badge {
+ padding: 8rpx 20rpx;
+ background: rgba(0, 206, 209, 0.2);
+ border: 2rpx solid rgba(0, 206, 209, 0.3);
+ border-radius: 32rpx;
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.badge-icon {
+ font-size: 22rpx;
+}
+
+.badge-text {
+ font-size: 22rpx;
+ color: #00CED1;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16rpx;
+ padding-top: 32rpx;
+ border-top: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.stat-item {
+ text-align: center;
+ padding: 16rpx 8rpx;
+}
+
+.stat-value {
+ font-size: 40rpx;
+ font-weight: 700;
+ display: block;
+}
+
+.stat-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===== 收益卡片 - 艺术化设计 ===== */
+.earnings-card {
+ margin: 32rpx;
+ padding: 32rpx;
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+ border-radius: 32rpx;
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+ position: relative;
+ overflow: hidden;
+ box-shadow: 0 16rpx 32rpx rgba(0, 0, 0, 0.3);
+}
+
+/* 背景装饰圆 */
+.bg-decoration {
+ position: absolute;
+ border-radius: 50%;
+ z-index: 0;
+}
+
+.bg-decoration-gold {
+ top: 0;
+ right: 0;
+ width: 256rpx;
+ height: 256rpx;
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, transparent 100%);
+ transform: translate(50%, -50%);
+}
+
+.bg-decoration-brand {
+ bottom: 0;
+ left: 0;
+ width: 192rpx;
+ height: 192rpx;
+ background: linear-gradient(45deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
+ transform: translate(-50%, 50%);
+}
+
+.earnings-content {
+ position: relative;
+ z-index: 1;
+}
+
+/* 标题行 */
+.earnings-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 40rpx;
+}
+
+.earnings-title-wrap {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.earnings-icon {
+ font-size: 32rpx;
+}
+
+.earnings-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.earnings-link {
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ padding: 8rpx 16rpx;
+ background: rgba(0, 206, 209, 0.1);
+ border-radius: 16rpx;
+}
+
+.earnings-link:active {
+ background: rgba(0, 206, 209, 0.2);
+}
+
+.link-text {
+ font-size: 24rpx;
+ font-weight: 500;
+}
+
+.link-arrow {
+ font-size: 20rpx;
+ font-weight: 600;
+}
+
+/* 我的收益 - 刷新图标 */
+.earnings-refresh-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(0, 206, 209, 0.15);
+}
+
+.earnings-refresh-wrap:active {
+ background: rgba(0, 206, 209, 0.3);
+}
+
+.earnings-refresh-icon {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #00CED1;
+}
+
+.earnings-refresh-spin {
+ animation: earnings-spin 0.8s linear infinite;
+}
+
+@keyframes earnings-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* 收益数据 */
+.earnings-data {
+ display: flex;
+ align-items: flex-end;
+ gap: 48rpx;
+ margin-bottom: 32rpx;
+}
+
+.earnings-main,
+.earnings-secondary {
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+}
+
+.earnings-main {
+ flex: 1;
+}
+
+.earnings-secondary {
+ flex: 1;
+}
+
+.earnings-label {
+ font-size: 24rpx;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.6);
+ letter-spacing: 0.5rpx;
+}
+
+.earnings-amount-large {
+ font-size: 64rpx;
+ font-weight: 700;
+ line-height: 1;
+ display: block;
+}
+
+.earnings-amount-medium {
+ font-size: 48rpx;
+ font-weight: 700;
+ line-height: 1;
+ color: #ffffff;
+}
+
+/* 渐变文字效果 */
+.gold-gradient {
+ background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: transparent;
+}
+
+/* 操作按钮 */
+.earnings-action {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 24rpx;
+ background: linear-gradient(90deg, rgba(255, 215, 0, 0.9) 0%, rgba(255, 165, 0, 0.9) 100%);
+ border-radius: 24rpx;
+ font-weight: 700;
+ box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3);
+ margin-top: 8rpx;
+}
+
+.earnings-action:active {
+ opacity: 0.85;
+ transform: scale(0.98);
+}
+
+.action-icon {
+ font-size: 32rpx;
+}
+
+.action-text {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #000000;
+ letter-spacing: 1rpx;
+}
+
+/* ===== 推广入口 ===== */
+.referral-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 32rpx;
+ padding: 32rpx;
+ background: linear-gradient(90deg, rgba(255, 215, 0, 0.1) 0%, #1c1c1e 100%);
+ border: 2rpx solid rgba(255, 215, 0, 0.2);
+ border-radius: 32rpx;
+}
+
+.referral-left {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.referral-icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40rpx;
+}
+
+.gold-bg {
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+}
+
+.referral-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.referral-title {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #ffffff;
+}
+
+.referral-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 4rpx;
+}
+
+.referral-btn {
+ padding: 16rpx 32rpx;
+ background: rgba(255, 215, 0, 0.2);
+ color: #FFD700;
+ font-size: 26rpx;
+ font-weight: 500;
+ border-radius: 16rpx;
+}
+
+/* ===== Tab切换 ===== */
+.tab-bar-custom {
+ display: flex;
+ gap: 16rpx;
+ margin: 32rpx;
+}
+
+.tab-item {
+ flex: 1;
+ padding: 20rpx;
+ text-align: center;
+ font-size: 28rpx;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.6);
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8rpx;
+}
+
+.tab-active {
+ background: rgba(0, 206, 209, 0.2);
+ color: #00CED1;
+ border: 2rpx solid rgba(0, 206, 209, 0.3);
+}
+
+.tab-icon {
+ font-size: 28rpx;
+}
+
+/* ===== Tab内容 ===== */
+.tab-content {
+ padding: 0 32rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* ===== 菜单卡片 ===== */
+.card {
+ background: #1c1c1e;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ overflow: hidden;
+ margin: 0 0 24rpx 0;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.menu-card {
+ padding: 0;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 28rpx 32rpx;
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.menu-item:last-child {
+ border-bottom: none;
+}
+
+.menu-item:active {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.menu-left {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.menu-icon {
+ width: 52rpx;
+ height: 52rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ flex-shrink: 0;
+}
+
+/* 有背景的图标样式 */
+.icon-brand,
+.icon-gold,
+.icon-gray {
+ width: 52rpx;
+ height: 52rpx;
+ font-size: 20rpx;
+}
+
+.icon-brand {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+}
+
+.icon-gold {
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+}
+
+.icon-gray {
+ background: rgba(128, 128, 128, 0.2);
+}
+
+.menu-title {
+ font-size: 28rpx;
+ color: #ffffff;
+}
+
+.menu-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.menu-count {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.menu-badge {
+ font-size: 26rpx;
+ font-weight: 500;
+}
+
+.menu-arrow {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* ===== 统计卡片 ===== */
+.stats-card {
+ padding: 32rpx;
+}
+
+.card-title {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #ffffff;
+ margin-bottom: 24rpx;
+}
+
+.stats-row {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 24rpx;
+}
+
+/* 两列布局(当找伙伴功能关闭时) */
+.stats-row-two-cols {
+ grid-template-columns: repeat(2, 1fr) !important;
+}
+
+.stat-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 24rpx;
+}
+
+.stat-icon {
+ font-size: 36rpx;
+ margin-bottom: 8rpx;
+}
+
+.stat-num {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.stat-text {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 4rpx;
+}
+
+/* ===== 最近阅读 ===== */
+.recent-card {
+ padding: 32rpx;
+}
+
+.recent-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.recent-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 24rpx;
+}
+
+.recent-left {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.recent-index {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.3);
+}
+
+.recent-title {
+ font-size: 26rpx;
+ color: #ffffff;
+}
+
+.recent-btn {
+ font-size: 24rpx;
+ color: #00CED1;
+}
+
+/* ===== 空状态 ===== */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 48rpx;
+}
+
+.empty-icon {
+ font-size: 64rpx;
+ opacity: 0.5;
+ margin-bottom: 16rpx;
+}
+
+.empty-text {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.empty-btn {
+ margin-top: 16rpx;
+ font-size: 26rpx;
+ color: #00CED1;
+}
+
+/* ===== 匹配记录 ===== */
+.match-card {
+ padding: 32rpx;
+}
+
+/* ===== 登录弹窗 ===== */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(20rpx);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 48rpx;
+}
+
+.modal-content {
+ width: 100%;
+ max-width: 640rpx;
+ background: #1c1c1e;
+ border-radius: 32rpx;
+ padding: 48rpx;
+ position: relative;
+}
+
+.modal-close {
+ position: absolute;
+ top: 24rpx;
+ right: 24rpx;
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.login-icon {
+ font-size: 96rpx;
+ text-align: center;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.login-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ text-align: center;
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.login-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+ text-align: center;
+ display: block;
+ margin-bottom: 48rpx;
+}
+
+.btn-wechat {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 28rpx;
+ background: #07C160;
+ color: #ffffff;
+ font-size: 28rpx;
+ font-weight: 500;
+ border-radius: 24rpx;
+ margin-bottom: 24rpx;
+ border: none;
+}
+
+.btn-wechat::after {
+ border: none;
+}
+
+.btn-wechat-icon {
+ width: 40rpx;
+ height: 40rpx;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24rpx;
+}
+
+.btn-phone {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 28rpx;
+ background: rgba(255, 255, 255, 0.1);
+ color: #ffffff;
+ font-size: 28rpx;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.btn-phone::after {
+ border: none;
+}
+
+.btn-phone-icon {
+ font-size: 32rpx;
+}
+
+/* 登录弹窗内取消按钮 */
+.login-modal-cancel {
+ margin-top: 24rpx;
+ padding: 24rpx;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+/* 协议勾选行(审核:用户须主动勾选,协议可点击查看) */
+.login-agree-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ margin-top: 32rpx;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+.agree-checkbox {
+ width: 32rpx;
+ height: 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.5);
+ border-radius: 6rpx;
+ margin-right: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ color: #fff;
+ flex-shrink: 0;
+}
+.agree-checked {
+ background: #00CED1;
+ border-color: #00CED1;
+}
+.agree-text {
+ color: rgba(255, 255, 255, 0.6);
+}
+.agree-link {
+ color: #00CED1;
+ text-decoration: underline;
+ padding: 0 4rpx;
+}
+.btn-wechat-disabled {
+ opacity: 0.6;
+}
+
+/* ===== 底部留白 ===== */
+.bottom-space {
+ height: 40rpx;
+}
+
+/* ===== 推广入口卡片 ===== */
+.promo-entry-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 24rpx 24rpx 0;
+ padding: 32rpx;
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.15) 0%, rgba(255, 165, 0, 0.1) 100%);
+ border: 2rpx solid rgba(255, 215, 0, 0.3);
+ border-radius: 24rpx;
+}
+
+.promo-entry-left {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+}
+
+.promo-entry-icon {
+ width: 80rpx;
+ height: 80rpx;
+ background: rgba(255, 215, 0, 0.2);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36rpx;
+}
+
+.promo-entry-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+}
+
+.promo-entry-title {
+ font-size: 30rpx;
+ color: #ffffff;
+ font-weight: 600;
+}
+
+.promo-entry-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.promo-entry-right {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.promo-entry-earnings {
+ font-size: 32rpx;
+ color: #FFD700;
+ font-weight: 600;
+}
+
+.promo-entry-arrow {
+ font-size: 28rpx;
+ color: #FFD700;
+}
+
+/* ===== 修改昵称弹窗 ===== */
+.nickname-modal {
+ width: 600rpx;
+ max-width: 90%;
+}
+
+.modal-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 40rpx;
+}
+
+.modal-icon {
+ font-size: 60rpx;
+ margin-bottom: 16rpx;
+}
+
+.modal-title {
+ font-size: 32rpx;
+ color: #ffffff;
+ font-weight: 600;
+}
+
+.nickname-input-wrap {
+ margin-bottom: 40rpx;
+}
+
+.nickname-input {
+ width: 100%;
+ height: 88rpx;
+ padding: 0 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border: 2rpx solid rgba(56, 189, 172, 0.3);
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ color: #ffffff;
+ box-sizing: border-box;
+}
+
+.nickname-placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+.input-tip {
+ display: block;
+ margin-top: 12rpx;
+ font-size: 22rpx;
+ color: rgba(56, 189, 172, 0.6);
+ text-align: center;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 20rpx;
+}
+
+.modal-btn {
+ flex: 1;
+ height: 80rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ font-weight: 500;
+ transition: all 0.3s;
+}
+
+.modal-btn-cancel {
+ background: rgba(255, 255, 255, 0.05);
+ color: rgba(255, 255, 255, 0.5);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.modal-btn-confirm {
+ background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
+ color: #ffffff;
+ box-shadow: 0 8rpx 24rpx rgba(56, 189, 172, 0.3);
+}
+
+/* 待确认收款 */
+.pending-confirm-card {
+ margin: 32rpx;
+ padding: 28rpx 32rpx;
+ background: rgba(76, 175, 80, 0.08);
+ border: 2rpx solid rgba(76, 175, 80, 0.25);
+ border-radius: 24rpx;
+}
+.pending-confirm-header { margin-bottom: 20rpx; }
+.pending-confirm-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; }
+.pending-confirm-desc { font-size: 24rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
+.pending-confirm-list { display: flex; flex-direction: column; gap: 16rpx; }
+.pending-confirm-item {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 20rpx 24rpx; background: rgba(28,28,30,0.6); border-radius: 16rpx;
+}
+.pending-confirm-info { display: flex; flex-direction: column; gap: 4rpx; }
+.pending-confirm-amount { font-size: 32rpx; font-weight: 600; color: #4CAF50; }
+.pending-confirm-time { font-size: 22rpx; color: rgba(255,255,255,0.5); }
+.pending-confirm-btn {
+ padding: 16rpx 32rpx;
+ background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
+ color: #fff; font-size: 26rpx; font-weight: 500; border-radius: 20rpx;
+}
+
+/* ===== 扫一扫结果弹窗 ===== */
+.scan-result-modal .modal-close { top: 24rpx; right: 24rpx; }
+.scan-result-header { margin-bottom: 24rpx; }
+.scan-result-title { font-size: 32rpx; font-weight: 600; color: #fff; }
+.scan-result-body {
+ max-height: 320rpx;
+ padding: 24rpx;
+ background: rgba(255,255,255,0.06);
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+ word-break: break-all;
+}
+.scan-result-text { font-size: 26rpx; color: rgba(255,255,255,0.9); line-height: 1.5; }
+.scan-result-actions { display: flex; gap: 24rpx; }
+.scan-result-btn {
+ flex: 1;
+ padding: 24rpx;
+ text-align: center;
+ font-size: 28rpx;
+ color: rgba(255,255,255,0.9);
+ background: rgba(255,255,255,0.1);
+ border-radius: 24rpx;
+}
+.scan-result-btn.primary { background: #00CED1; color: #000; }
diff --git a/miniprogram2/pages/privacy/privacy.js b/miniprogram2/pages/privacy/privacy.js
new file mode 100644
index 00000000..0c95c06e
--- /dev/null
+++ b/miniprogram2/pages/privacy/privacy.js
@@ -0,0 +1,29 @@
+/**
+ * Soul创业派对 - 隐私政策
+ * 审核要求:登录前可点击《隐私政策》查看完整内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 隐私政策',
+ path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
+ }
+ }
+})
diff --git a/miniprogram2/pages/privacy/privacy.json b/miniprogram2/pages/privacy/privacy.json
new file mode 100644
index 00000000..f567904d
--- /dev/null
+++ b/miniprogram2/pages/privacy/privacy.json
@@ -0,0 +1 @@
+{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"隐私政策"}
diff --git a/miniprogram2/pages/privacy/privacy.wxml b/miniprogram2/pages/privacy/privacy.wxml
new file mode 100644
index 00000000..cf414ad7
--- /dev/null
+++ b/miniprogram2/pages/privacy/privacy.wxml
@@ -0,0 +1,40 @@
+
+
+
+ ←
+ 隐私政策
+
+
+
+
+
+
+ Soul创业实验 隐私政策
+ 更新日期:以小程序内展示为准
+
+ 一、信息收集
+ 为向您提供阅读、购买、推广与提现等服务,我们可能收集:微信昵称、头像、openId、手机号(在您授权时)、订单与收益相关数据。我们仅在法律允许及您同意的范围内收集必要信息。
+
+ 二、信息使用
+ 所收集信息用于账号识别、订单与收益结算、客服与纠纷处理、产品优化及法律义务履行,不会用于与上述目的无关的营销或向第三方出售。
+
+ 三、信息存储与安全
+ 数据存储在中华人民共和国境内,我们采取合理技术和管理措施保障数据安全,防止未经授权的访问、泄露或篡改。
+
+ 四、信息共享
+ 未经您同意,我们不会将您的个人信息共享给第三方,法律法规要求或为完成支付、提现等必要合作除外(如微信支付、微信商家转账)。
+
+ 五、您的权利
+ 您有权查询、更正、删除您的个人信息,或撤回授权。部分权限撤回可能影响相关功能使用。您可通过小程序设置或联系我们就隐私问题提出请求。
+
+ 六、未成年人
+ 如您为未成年人,请在监护人同意下使用本服务。我们不会主动收集未成年人个人信息。
+
+ 七、政策更新
+ 我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。
+
+ 八、联系我们
+ 如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。
+
+
+
diff --git a/miniprogram2/pages/privacy/privacy.wxss b/miniprogram2/pages/privacy/privacy.wxss
new file mode 100644
index 00000000..08fadc43
--- /dev/null
+++ b/miniprogram2/pages/privacy/privacy.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/miniprogram2/pages/profile-edit/profile-edit.js b/miniprogram2/pages/profile-edit/profile-edit.js
new file mode 100644
index 00000000..ff426211
--- /dev/null
+++ b/miniprogram2/pages/profile-edit/profile-edit.js
@@ -0,0 +1,188 @@
+/**
+ * Soul创业派对 - 编辑资料页
+ * 图二样式,行业/业务体量拆成两个独立输入框
+ */
+
+const app = getApp()
+
+const MBTI_LIST = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ navBarTotalHeight: 88,
+ avatar: '',
+ nickname: '',
+ mbtiList: MBTI_LIST,
+ mbtiIndex: 0,
+ mbti: '',
+ region: '',
+ industry: '',
+ businessVolume: '',
+ position: '',
+ mostProfitableMonth: '',
+ howCanHelp: ''
+ },
+
+ onLoad() {
+ const statusBarHeight = app.globalData.statusBarHeight || 44
+ const navBarTotalHeight = statusBarHeight + 44
+ this.setData({
+ statusBarHeight,
+ navBarTotalHeight
+ })
+ this.loadProfile()
+ },
+
+ loadProfile() {
+ const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
+ const ext = wx.getStorageSync('userProfileExt') || {}
+ const mbti = ext.mbti || ''
+ const mbtiIndex = MBTI_LIST.indexOf(mbti)
+ this.setData({
+ avatar: userInfo.avatar || '',
+ nickname: userInfo.nickname || '',
+ mbti: mbti,
+ mbtiIndex: mbtiIndex >= 0 ? mbtiIndex : 0,
+ region: ext.region || '',
+ industry: ext.industry || '',
+ businessVolume: ext.businessVolume || '',
+ position: ext.position || '',
+ mostProfitableMonth: ext.mostProfitableMonth || '',
+ howCanHelp: ext.howCanHelp || ''
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ },
+
+ onNicknameInput(e) {
+ this.setData({ nickname: e.detail.value })
+ },
+
+ onMbtiChange(e) {
+ const i = parseInt(e.detail.value, 10)
+ this.setData({
+ mbtiIndex: i,
+ mbti: MBTI_LIST[i] || ''
+ })
+ },
+
+ onRegionInput(e) {
+ this.setData({ region: e.detail.value })
+ },
+
+ onIndustryInput(e) {
+ this.setData({ industry: e.detail.value })
+ },
+
+ onBusinessVolumeInput(e) {
+ this.setData({ businessVolume: e.detail.value })
+ },
+
+ onPositionInput(e) {
+ this.setData({ position: e.detail.value })
+ },
+
+ onMostProfitableMonthInput(e) {
+ this.setData({ mostProfitableMonth: e.detail.value })
+ },
+
+ onHowCanHelpInput(e) {
+ this.setData({ howCanHelp: e.detail.value })
+ },
+
+ async onChooseAvatar(e) {
+ const tempAvatarUrl = e.detail.avatarUrl
+ if (!tempAvatarUrl) return
+
+ wx.showLoading({ title: '上传中...', mask: true })
+
+ try {
+ const uploadRes = await new Promise((resolve, reject) => {
+ wx.uploadFile({
+ url: app.globalData.baseUrl + '/api/miniprogram/upload',
+ filePath: tempAvatarUrl,
+ name: 'file',
+ formData: { folder: 'avatars' },
+ success: (res) => {
+ try {
+ const data = JSON.parse(res.data)
+ if (data.success) resolve(data)
+ else reject(new Error(data.error || '上传失败'))
+ } catch (err) {
+ reject(new Error('解析响应失败'))
+ }
+ },
+ fail: (err) => reject(err)
+ })
+ })
+
+ const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
+ this.setData({ avatar: avatarUrl })
+
+ const userInfo = app.globalData.userInfo
+ if (userInfo && userInfo.id) {
+ await app.request({
+ url: '/api/miniprogram/user/update',
+ method: 'POST',
+ data: { userId: userInfo.id, avatar: avatarUrl }
+ })
+ userInfo.avatar = avatarUrl
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+ }
+
+ wx.hideLoading()
+ wx.showToast({ title: '头像已更新', icon: 'success' })
+ } catch (e) {
+ wx.hideLoading()
+ wx.showToast({ title: e.message || '上传失败', icon: 'none' })
+ }
+ },
+
+ async saveProfile() {
+ const { nickname, mbti, region, industry, businessVolume, position, mostProfitableMonth, howCanHelp } = this.data
+ const userInfo = app.globalData.userInfo
+
+ wx.showLoading({ title: '保存中...', mask: true })
+
+ try {
+ if (userInfo && userInfo.id) {
+ const payload = { userId: userInfo.id }
+ if (nickname !== undefined && nickname !== userInfo.nickname) payload.nickname = nickname.trim()
+ if (Object.keys(payload).length > 1) {
+ await app.request({
+ url: '/api/miniprogram/user/update',
+ method: 'POST',
+ data: payload
+ })
+ if (payload.nickname !== undefined) {
+ userInfo.nickname = payload.nickname
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+ }
+ }
+ }
+
+ const ext = {
+ mbti: (mbti || '').trim(),
+ region: (region || '').trim(),
+ industry: (industry || '').trim(),
+ businessVolume: (businessVolume || '').trim(),
+ position: (position || '').trim(),
+ mostProfitableMonth: (mostProfitableMonth || '').trim(),
+ howCanHelp: (howCanHelp || '').trim()
+ }
+ wx.setStorageSync('userProfileExt', ext)
+
+ wx.hideLoading()
+ wx.showToast({ title: '保存成功', icon: 'success' })
+ setTimeout(() => wx.navigateBack(), 500)
+ } catch (e) {
+ wx.hideLoading()
+ wx.showToast({ title: e.message || '保存失败', icon: 'none' })
+ }
+ }
+})
diff --git a/miniprogram2/pages/profile-edit/profile-edit.json b/miniprogram2/pages/profile-edit/profile-edit.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram2/pages/profile-edit/profile-edit.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/profile-edit/profile-edit.wxml b/miniprogram2/pages/profile-edit/profile-edit.wxml
new file mode 100644
index 00000000..02a2bb25
--- /dev/null
+++ b/miniprogram2/pages/profile-edit/profile-edit.wxml
@@ -0,0 +1,125 @@
+
+
+
+
+ ‹
+
+ 编辑资料
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 昵称
+
+
+
+
+
+
+ MBTI
+
+
+ {{mbti || '请选择'}}
+ ▼
+
+
+
+
+ 地区
+
+ 📍
+
+
+
+
+
+
+
+ 行业
+
+
+
+
+
+ 业务体量
+
+
+
+
+
+ 职位
+
+
+
+
+
+ 你最赚钱的一个月做的是什么
+
+
+
+
+
+ 我能帮到大家什么
+
+
+
+ 保存
+
+
+
diff --git a/miniprogram2/pages/profile-edit/profile-edit.wxss b/miniprogram2/pages/profile-edit/profile-edit.wxss
new file mode 100644
index 00000000..11fa707f
--- /dev/null
+++ b/miniprogram2/pages/profile-edit/profile-edit.wxss
@@ -0,0 +1,263 @@
+/* 编辑资料页 - 深色主题,避免与 app 全局样式冲突 */
+page {
+ background: #000;
+}
+
+.page.profile-edit-page {
+ min-height: 100vh;
+ background: #000;
+ padding-bottom: 64rpx;
+ box-sizing: border-box;
+}
+
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(40rpx);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32rpx;
+ height: 88rpx;
+ box-sizing: border-box;
+}
+
+.nav-back {
+ width: 64rpx;
+ height: 64rpx;
+ background: #1c1c1e;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.back-icon {
+ font-size: 40rpx;
+ color: rgba(255, 255, 255, 0.6);
+ font-weight: 300;
+}
+
+.nav-title {
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #fff;
+ flex: 1;
+ text-align: center;
+}
+
+.nav-right-placeholder {
+ width: 64rpx;
+ flex-shrink: 0;
+}
+
+.nav-placeholder-bar {
+ width: 100%;
+ flex-shrink: 0;
+}
+
+.content {
+ padding: 32rpx;
+ box-sizing: border-box;
+}
+
+/* 头像区域 */
+.avatar-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 48rpx;
+}
+
+.avatar-btn {
+ width: 160rpx;
+ height: 160rpx;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: none;
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.avatar-btn::after {
+ border: none;
+}
+
+.avatar-img {
+ width: 160rpx;
+ height: 160rpx;
+ border-radius: 50%;
+ border: 4rpx solid rgba(0, 206, 209, 0.3);
+ object-fit: cover;
+ display: block;
+}
+
+.avatar-text {
+ width: 160rpx;
+ height: 160rpx;
+ border-radius: 50%;
+ border: 4rpx solid rgba(0, 206, 209, 0.3);
+ background: rgba(0, 206, 209, 0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 56rpx;
+ font-weight: 600;
+ color: #00CED1;
+ box-sizing: border-box;
+}
+
+.avatar-change-btn {
+ margin-top: 16rpx;
+ padding: 0;
+ margin-left: 0;
+ margin-right: 0;
+ background: transparent;
+ border: none;
+ font-size: 26rpx;
+ color: #00CED1;
+ line-height: 1.4;
+ height: auto;
+ min-height: auto;
+}
+
+.avatar-change-btn::after {
+ border: none;
+}
+
+/* 表单项 */
+.form-group {
+ margin-bottom: 32rpx;
+}
+
+.form-label {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.7);
+ margin-bottom: 12rpx;
+}
+
+.form-input {
+ width: 100%;
+ height: 80rpx;
+ padding: 0 24rpx;
+ background: rgba(255, 255, 255, 0.06);
+ border: 2rpx solid rgba(0, 206, 209, 0.25);
+ border-radius: 16rpx;
+ font-size: 28rpx;
+ color: #fff;
+ box-sizing: border-box;
+}
+
+.form-placeholder {
+ color: rgba(255, 255, 255, 0.35);
+}
+
+.form-picker {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ min-height: 80rpx;
+ padding: 0 24rpx;
+ background: rgba(255, 255, 255, 0.06);
+ border: 2rpx solid rgba(0, 206, 209, 0.25);
+ border-radius: 16rpx;
+ font-size: 28rpx;
+ color: #fff;
+ box-sizing: border-box;
+}
+
+.form-picker .placeholder {
+ color: rgba(255, 255, 255, 0.35);
+}
+
+.picker-arrow {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-left: 8rpx;
+}
+
+.form-input-wrap {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 80rpx;
+ padding: 0 24rpx;
+ background: rgba(255, 255, 255, 0.06);
+ border: 2rpx solid rgba(0, 206, 209, 0.25);
+ border-radius: 16rpx;
+ box-sizing: border-box;
+}
+
+.form-input-wrap .form-input-inline {
+ flex: 1;
+ min-width: 0;
+ height: 76rpx;
+ padding: 0 12rpx 0 0;
+ border: none;
+ background: transparent !important;
+}
+
+.form-input-wrap .form-input-inline.form-input {
+ height: 76rpx;
+ background: transparent !important;
+}
+
+.region-icon {
+ font-size: 28rpx;
+ flex-shrink: 0;
+ margin-right: 8rpx;
+}
+
+/* MBTI 与 地区 并排 */
+.form-row {
+ display: flex;
+ gap: 24rpx;
+ align-items: flex-start;
+}
+
+.form-group.half {
+ flex: 1;
+ min-width: 0;
+}
+
+.form-textarea {
+ width: 100%;
+ min-height: 160rpx;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.06);
+ border: 2rpx solid rgba(0, 206, 209, 0.25);
+ border-radius: 16rpx;
+ font-size: 28rpx;
+ color: #fff;
+ box-sizing: border-box;
+}
+
+.save-btn {
+ margin-top: 48rpx;
+ padding: 28rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #000;
+ font-size: 30rpx;
+ font-weight: 600;
+ text-align: center;
+ border-radius: 24rpx;
+}
+
+.save-btn:active {
+ opacity: 0.9;
+}
+
+.bottom-space {
+ height: 80rpx;
+}
diff --git a/miniprogram2/pages/purchases/purchases.js b/miniprogram2/pages/purchases/purchases.js
new file mode 100644
index 00000000..eda2907b
--- /dev/null
+++ b/miniprogram2/pages/purchases/purchases.js
@@ -0,0 +1,72 @@
+/**
+ * Soul创业实验 - 订单页
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ orders: [],
+ loading: true
+ },
+
+ onLoad() {
+ this.setData({ statusBarHeight: app.globalData.statusBarHeight })
+ this.loadOrders()
+ },
+
+ onShow() {
+ this.loadOrders()
+ },
+
+ async loadOrders() {
+ this.setData({ loading: true })
+ try {
+ let purchasedSections = app.globalData.purchasedSections || []
+ let sectionMidMap = app.globalData.sectionMidMap || {}
+ const userId = app.globalData.userInfo?.id
+ if (userId) {
+ try {
+ const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
+ if (res?.success && res.data) {
+ purchasedSections = res.data.purchasedSections || []
+ sectionMidMap = res.data.sectionMidMap || {}
+ app.globalData.purchasedSections = purchasedSections
+ app.globalData.sectionMidMap = sectionMidMap
+ }
+ } catch (_) { /* 使用缓存 */ }
+ }
+ const orders = purchasedSections.map((id, index) => ({
+ id: `order_${index}`,
+ sectionId: id,
+ mid: sectionMidMap[id] || 0,
+ title: `章节 ${id}`,
+ amount: 1,
+ status: 'completed',
+ createTime: new Date(Date.now() - index * 86400000).toLocaleDateString()
+ }))
+ this.setData({ orders })
+ } catch (e) {
+ console.error('加载订单失败:', e)
+ } finally {
+ this.setData({ loading: false })
+ }
+ },
+
+ goToRead(e) {
+ const id = e.currentTarget.dataset.id
+ const mid = e.currentTarget.dataset.mid
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ goBack() { wx.navigateBack() },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 我的订单',
+ path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
+ }
+ }
+})
diff --git a/miniprogram2/pages/purchases/purchases.json b/miniprogram2/pages/purchases/purchases.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram2/pages/purchases/purchases.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/purchases/purchases.wxml b/miniprogram2/pages/purchases/purchases.wxml
new file mode 100644
index 00000000..2dc0a564
--- /dev/null
+++ b/miniprogram2/pages/purchases/purchases.wxml
@@ -0,0 +1,36 @@
+
+
+
+ ←
+ 我的订单
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.title}}
+ {{item.createTime}}
+
+
+ ¥{{item.amount}}
+ 已完成
+
+
+
+
+
+ 📦
+ 暂无订单
+
+
+
diff --git a/miniprogram2/pages/purchases/purchases.wxss b/miniprogram2/pages/purchases/purchases.wxss
new file mode 100644
index 00000000..c2f6cf3e
--- /dev/null
+++ b/miniprogram2/pages/purchases/purchases.wxss
@@ -0,0 +1,21 @@
+.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.9); backdrop-filter: blur(40rpx); 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 { padding: 32rpx; }
+.loading { display: flex; flex-direction: column; gap: 24rpx; }
+.skeleton { height: 120rpx; background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%); background-size: 200% 100%; animation: skeleton 1.5s ease-in-out infinite; border-radius: 24rpx; }
+@keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+.orders-list { display: flex; flex-direction: column; gap: 16rpx; }
+.order-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #1c1c1e; border-radius: 24rpx; }
+.order-item:active { background: #2c2c2e; }
+.order-info { flex: 1; }
+.order-title { font-size: 28rpx; color: #fff; display: block; margin-bottom: 8rpx; }
+.order-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
+.order-right { text-align: right; }
+.order-amount { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin-bottom: 4rpx; }
+.order-status { font-size: 22rpx; color: rgba(255,255,255,0.4); }
+.empty { display: flex; flex-direction: column; align-items: center; padding: 96rpx; }
+.empty-icon { font-size: 96rpx; margin-bottom: 24rpx; opacity: 0.5; }
+.empty-text { font-size: 28rpx; color: rgba(255,255,255,0.4); }
diff --git a/miniprogram2/pages/read/FLOW.txt b/miniprogram2/pages/read/FLOW.txt
new file mode 100644
index 00000000..74506f49
--- /dev/null
+++ b/miniprogram2/pages/read/FLOW.txt
@@ -0,0 +1,199 @@
+================================================================================
+ miniprogram/pages/read 阅读页 - 当前处理流程 (ASCII)
+================================================================================
+
+---------- 1. 进入页面 onLoad(options) 总览 ----------
+
+ +------------------+
+ | onLoad(options) |
+ +--------+---------+
+ |
+ v
+ +----------------------------------------+
+ | sceneStr = options.scene || '' |
+ | parsed = parseScene(sceneStr) | <-- utils/scene.js
+ | mid = options.mid || parsed.mid || |
+ | app.globalData.initialSectionMid|
+ | id = options.id || parsed.id || |
+ | app.globalData.initialSectionId |
+ | ref = options.ref || parsed.ref |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | 清除 app.globalData.initialSection* |
+ +--------+-------------------------------+
+ |
+ v
+ [ mid || id ? ]
+ |
+ No ---+---> 提示「章节参数缺失」, return
+ |
+ Yes v
+ +----------------------------------------+
+ | setData: sectionMid, loading=true |
+ | if (ref): 存 referral_code, |
+ | app.handleReferralCode(ref) |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | Promise.all: |
+ | - accessManager.fetchLatestConfig() |
+ | - /api/.../purchase-status (若登录) |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | 计算价格/推荐折扣, setData 配置 |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | resolvedId = id |
+ | if (mid && !id): |
+ | GET /book/chapter/by-mid/:mid |
+ | resolvedId = chRes.id |
+ | prefetchedChapter = chRes |
+ | setData(sectionId: resolvedId) |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | accessManager.determineAccessState() |
+ | setData: accessState, canAccess, |
+ | showPaywall, isLoggedIn |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | await loadContent(mid, resolvedId, |
+ | accessState, |
+ | prefetchedChapter) |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | if (canAccess): readingTracker.init() |
+ | loadNavigation(resolvedId) |
+ | setData(loading: false) |
+ +----------------------------------------+
+
+
+---------- 2. scene 闭环(海报生成 <-> 扫码解析)----------
+
+ [ 生成海报 ]
+ |
+ v
+ +----------------------------------------+
+ | buildScene({ mid?, id?, ref? }) | utils/scene.js
+ | 例: mid=1, ref=ogpTW5fmXR |
+ | => "mid=1_ref=ogpTW5fmXR" |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | GET /api/miniprogram/qrcode/image |
+ | ?scene=mid%3D1_ref%3Dxxx |
+ | &page=pages/read/read&width=280 |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | 后端: & -> _ (若需要), 调微信生成图 |
+ | 返回 image/png |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | wx.downloadFile(url) -> tempFilePath |
+ | canvas drawImage(tempFilePath) 画海报 |
+ +----------------------------------------+
+
+ ---------- 用户扫码 ----------
+
+ +----------------------------------------+
+ | 微信: 打开 pages/read/read |
+ | options.scene = "mid=1_ref=xxx" |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | App.onLaunch(options) |
+ | handleReferralCode(options) |
+ | parseScene(options.scene) |
+ | -> initialSectionMid, initialSectionId|
+ | -> ref 存 referral_code / 绑定 |
+ +----------------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | Read.onLoad(options) |
+ | parseScene(options.scene) -> mid,id,ref|
+ | 与 app.globalData.initial* 合并 |
+ | -> 用 mid/id 拉章节, ref 已存 |
+ +----------------------------------------+
+
+
+---------- 3. loadContent(mid, id, accessState, prefetched) ----------
+
+ +------------------+
+ | loadContent(...) |
+ +--------+---------+
+ |
+ v
+ +----------------------------------------+
+ | section = getSectionInfo(id) |
+ | setData({ section }) |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | prefetched 有内容? |
+ +--------+-------------------------------+
+ | Yes
+ +-----> res = prefetched (不请求)
+ |
+ | No
+ v
+ +----------------------------------------+
+ | mid ? GET /book/chapter/by-mid/:mid |
+ | : GET /book/chapter/:id |
+ +--------+-------------------------------+
+ |
+ v
+ +----------------------------------------+
+ | res && res.content ? |
+ +--------+-------------------------------+
+ | Yes
+ +-----> setData(content, paragraphs, partTitle, chapterTitle)
+ | if (canAccess): markSectionAsRead(id)
+ | setTimeout(drawShareCard, 600)
+ |
+ | No / 请求异常
+ v
+ +----------------------------------------+
+ | 尝试 wx.getStorageSync(cacheKey) |
+ | 有缓存 -> setData 内容; 无 -> throw |
+ +----------------------------------------+
+
+
+---------- 4. 参数来源优先级(onLoad 内 mid / id / ref)----------
+
+ mid = options.mid
+ ?? parseInt(parsed.mid)
+ ?? app.globalData.initialSectionMid
+ ?? 0
+
+ id = options.id
+ ?? parsed.id
+ ?? app.globalData.initialSectionId
+ ?? ''
+
+ ref = options.ref
+ ?? parsed.ref
+ ?? ''
+
+ (解析 parsed = parseScene(options.scene),支持 scene 内 & 或 _ 分隔)
+
+================================================================================
diff --git a/miniprogram2/pages/read/read.js b/miniprogram2/pages/read/read.js
new file mode 100644
index 00000000..2deae32b
--- /dev/null
+++ b/miniprogram2/pages/read/read.js
@@ -0,0 +1,1355 @@
+/**
+ * Soul创业派对 - 阅读页(标准流程版)
+ * 开发: 卡若
+ * 技术支持: 存客宝
+ *
+ * 更新: 2026-02-04
+ * - 引入权限管理器(chapterAccessManager)统一权限判断
+ * - 引入阅读追踪器(readingTracker)记录阅读进度、时长、是否读完
+ * - 使用状态机(accessState)规范权限流转
+ * - 异常统一保守处理,避免误解锁
+ */
+
+import accessManager from '../../utils/chapterAccessManager'
+import readingTracker from '../../utils/readingTracker'
+const { buildScene, parseScene } = require('../../utils/scene.js')
+
+const app = getApp()
+
+Page({
+ data: {
+ // 系统信息
+ statusBarHeight: 44,
+ navBarHeight: 88,
+
+ // 章节信息
+ sectionId: '',
+ section: null,
+ partTitle: '',
+ chapterTitle: '',
+
+ // 内容
+ content: '',
+ previewContent: '',
+ contentParagraphs: [],
+ previewParagraphs: [],
+ loading: true,
+
+ // 【新增】权限状态机(替代 canAccess)
+ // unknown: 加载中 | free: 免费 | locked_not_login: 未登录 | locked_not_purchased: 未购买 | unlocked_purchased: 已购买 | error: 错误
+ accessState: 'unknown',
+
+ // 用户状态
+ isLoggedIn: false,
+ hasFullBook: false,
+ canAccess: false, // 保留兼容性,从 accessState 派生
+ purchasedCount: 0,
+
+ // 阅读进度
+ readingProgress: 0,
+ showPaywall: false,
+
+ // 上一篇/下一篇
+ prevSection: null,
+ nextSection: null,
+
+ // 价格
+ sectionPrice: 1,
+ fullBookPrice: 9.9,
+ totalSections: 62,
+ // 好友优惠展示
+ userDiscount: 5,
+ hasReferralDiscount: false,
+ showDiscountHint: false,
+ displaySectionPrice: 1,
+ displayFullBookPrice: 9.9,
+
+ // 弹窗
+ showShareModal: false,
+ showLoginModal: false,
+ agreeProtocol: false,
+ showPosterModal: false,
+ isPaying: false,
+ isGeneratingPoster: false,
+
+ // 免费章节
+ freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
+
+ // 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用)
+ shareImagePath: ''
+ },
+
+ async onLoad(options) {
+ // 官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与海报生成 buildScene 闭环
+ const sceneStr = (options && options.scene) || ''
+ const parsed = parseScene(sceneStr)
+ const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0)
+ const id = options.id || parsed.id || app.globalData.initialSectionId
+ const ref = options.ref || parsed.ref
+ if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
+ if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
+
+ console.log('[Read] onLoad:', { options, sceneRaw: sceneStr || undefined, parsed, mid, id, ref })
+ console.log('[Read] onLoad options:', options)
+
+ if (!mid && !id) {
+ console.warn('[Read] 未获取到章节 mid/id,options:', options)
+ wx.showToast({ title: '章节参数缺失', icon: 'none' })
+ this.setData({ accessState: 'error', loading: false })
+ return
+ }
+
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ navBarHeight: app.globalData.navBarHeight,
+ sectionId: '', // 加载后填充
+ sectionMid: mid || null,
+ loading: true,
+ accessState: 'unknown'
+ })
+
+ if (ref) {
+ console.log('[Read] 检测到推荐码:', ref)
+ wx.setStorageSync('referral_code', ref)
+ app.handleReferralCode({ query: { ref } })
+ }
+
+ try {
+ const userId = app.globalData.userInfo?.id
+ const [config, purchaseRes] = await Promise.all([
+ accessManager.fetchLatestConfig(),
+ userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
+ ])
+ const sectionPrice = config.prices?.section ?? 1
+ const fullBookPrice = config.prices?.fullbook ?? 9.9
+ const userDiscount = config.userDiscount ?? 5
+ // 有推荐人 = ref/ referral_code 或 用户信息中有推荐人绑定
+ const hasReferral = !!(wx.getStorageSync('referral_code') || ref || purchaseRes?.data?.hasReferrer)
+ const hasReferralDiscount = hasReferral && userDiscount > 0
+ const showDiscountHint = userDiscount > 0
+ const displaySectionPrice = hasReferralDiscount
+ ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
+ : sectionPrice
+ const displayFullBookPrice = hasReferralDiscount
+ ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
+ : fullBookPrice
+ this.setData({
+ freeIds: config.freeChapters,
+ sectionPrice,
+ fullBookPrice,
+ userDiscount,
+ hasReferralDiscount,
+ showDiscountHint: userDiscount > 0,
+ displaySectionPrice,
+ displayFullBookPrice,
+ purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
+ })
+
+ // 先拉取章节获取 id(mid 时必需;id 时可直接用)
+ let resolvedId = id
+ let prefetchedChapter = null
+ if (mid && !id) {
+ const chRes = await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
+ if (chRes && chRes.id) {
+ resolvedId = chRes.id
+ prefetchedChapter = chRes
+ }
+ }
+ this.setData({ sectionId: resolvedId })
+
+ const accessState = await accessManager.determineAccessState(resolvedId, config.freeChapters)
+ const canAccess = accessManager.canAccessFullContent(accessState)
+
+ this.setData({
+ accessState,
+ canAccess,
+ isLoggedIn: !!app.globalData.userInfo?.id,
+ showPaywall: !canAccess,
+ purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? 0
+ })
+
+ await this.loadContent(mid, resolvedId, accessState, prefetchedChapter)
+
+ if (canAccess) {
+ readingTracker.init(resolvedId)
+ }
+
+ this.loadNavigation(resolvedId)
+
+ } catch (e) {
+ console.error('[Read] 初始化失败:', e)
+ wx.showToast({ title: '加载失败,请重试', icon: 'none' })
+ this.setData({ accessState: 'error', loading: false })
+ } finally {
+ this.setData({ loading: false })
+ }
+ },
+
+ // 从后端加载免费章节配置
+ onPageScroll(e) {
+ // 只在有权限时追踪阅读进度
+ if (!accessManager.canAccessFullContent(this.data.accessState)) {
+ return
+ }
+
+ // 获取滚动信息并更新追踪器
+ const query = wx.createSelectorQuery()
+ query.select('.page').boundingClientRect()
+ query.selectViewport().scrollOffset()
+ query.exec((res) => {
+ if (res[0] && res[1]) {
+ const scrollInfo = {
+ scrollTop: res[1].scrollTop,
+ scrollHeight: res[0].height,
+ clientHeight: res[1].height
+ }
+
+ // 计算进度条显示(用于 UI)
+ const totalScrollable = scrollInfo.scrollHeight - scrollInfo.clientHeight
+ const progress = totalScrollable > 0
+ ? Math.min((scrollInfo.scrollTop / totalScrollable) * 100, 100)
+ : 0
+ this.setData({ readingProgress: progress })
+
+ // 更新阅读追踪器(记录最大进度、判断是否读完)
+ readingTracker.updateProgress(scrollInfo)
+ }
+ })
+ },
+
+ // 【重构】加载章节内容。mid 优先用 by-mid 接口,id 用旧接口;prefetched 避免重复请求
+ async loadContent(mid, id, accessState, prefetched) {
+ console.log('[Read] loadContent 请求参数:', { mid, id, accessState: accessState, prefetched: !!prefetched })
+ try {
+ const section = this.getSectionInfo(id)
+ const sectionPrice = this.data.sectionPrice ?? 1
+ if (section.price === undefined || section.price === null) {
+ section.price = sectionPrice
+ }
+ this.setData({ section })
+
+ let res = prefetched
+ if (!res) {
+ res = mid
+ ? await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
+ : await app.request(`/api/miniprogram/book/chapter/${id}`)
+ }
+
+ if (res && res.content) {
+ const lines = res.content.split('\n').filter(line => line.trim())
+ const previewCount = Math.ceil(lines.length * 0.2)
+ const updates = {
+ content: res.content,
+ contentParagraphs: lines,
+ previewParagraphs: lines.slice(0, previewCount),
+ partTitle: res.partTitle || '',
+ chapterTitle: res.chapterTitle || ''
+ }
+ if (res.mid) updates.sectionMid = res.mid
+ this.setData(updates)
+ if (accessManager.canAccessFullContent(accessState)) {
+ app.markSectionAsRead(id)
+ }
+ // 始终用接口返回的 price/isFree 更新 section(不写死 1 元)
+ const section = this.data.section || {}
+ if (res.price !== undefined && res.price !== null) section.price = Number(res.price)
+ if (res.isFree !== undefined) section.isFree = !!res.isFree
+ // 0元即免费:接口返回 price 为 0 或 isFree 为 true 时,不展示付费墙
+ const isFreeByPrice = res.price === 0 || res.price === '0' || Number(res.price) === 0
+ const isFreeByFlag = res.isFree === true
+ if (isFreeByPrice || isFreeByFlag) {
+ this.setData({ section, showPaywall: false, canAccess: true, accessState: 'free' })
+ app.markSectionAsRead(id)
+ } else {
+ this.setData({ section })
+ }
+ setTimeout(() => this.drawShareCard(), 600)
+ }
+ } catch (e) {
+ console.error('[Read] 加载内容失败:', e)
+ // 尝试从本地缓存加载
+ const cacheKey = `chapter_${id}`
+ try {
+ const cached = wx.getStorageSync(cacheKey)
+ if (cached && cached.content) {
+ const lines = cached.content.split('\n').filter(line => line.trim())
+ const previewCount = Math.ceil(lines.length * 0.2)
+
+ this.setData({
+ content: cached.content,
+ contentParagraphs: lines,
+ previewParagraphs: lines.slice(0, previewCount)
+ })
+ console.log('[Read] 从本地缓存加载成功')
+ }
+ } catch (cacheErr) {
+ console.warn('[Read] 本地缓存也失败:', cacheErr)
+ }
+ throw e
+ }
+ },
+
+ // 获取章节信息
+ getSectionInfo(id) {
+ // 特殊章节
+ if (id === 'preface') {
+ return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 }
+ }
+ if (id === 'epilogue') {
+ return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 }
+ }
+ if (id.startsWith('appendix')) {
+ const appendixTitles = {
+ 'appendix-1': 'Soul派对房精选对话',
+ 'appendix-2': '创业者自检清单',
+ 'appendix-3': '本书提到的工具和资源'
+ }
+ return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
+ }
+
+ // 普通章节:price 不写死,由 loadContent 从 config/接口 填充
+ return {
+ id: id,
+ title: this.getSectionTitle(id),
+ isFree: id === '1.1',
+ price: undefined
+ }
+ },
+
+ // 获取章节标题
+ getSectionTitle(id) {
+ const titles = {
+ '1.1': '荷包:电动车出租的被动收入模式',
+ '1.2': '老墨:资源整合高手的社交方法',
+ '1.3': '笑声背后的MBTI',
+ '1.4': '人性的三角结构:利益、情感、价值观',
+ '1.5': '沟通差的问题:为什么你说的别人听不懂',
+ '2.1': '相亲故事:你以为找的是人,实际是在找模式',
+ '2.2': '找工作迷茫者:为什么简历解决不了人生',
+ '2.3': '撸运费险:小钱困住大脑的真实心理',
+ '2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力',
+ '2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒',
+ '3.1': '3000万流水如何跑出来(退税模式解析)',
+ '8.1': '流量杠杆:抖音、Soul、飞书',
+ '9.14': '大健康私域:一个月150万的70后'
+ }
+ return titles[id] || `章节 ${id}`
+ },
+
+ // 带超时的章节请求
+ fetchChapterWithTimeout(id, timeout = 5000) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(new Error('请求超时'))
+ }, timeout)
+
+ app.request(`/api/miniprogram/book/chapter/${id}`)
+ .then(res => {
+ clearTimeout(timer)
+ resolve(res)
+ })
+ .catch(err => {
+ clearTimeout(timer)
+ reject(err)
+ })
+ })
+ },
+
+ // 设置章节内容
+ setChapterContent(res) {
+ const lines = res.content.split('\n').filter(line => line.trim())
+ const previewCount = Math.ceil(lines.length * 0.2)
+
+ this.setData({
+ content: res.content,
+ previewContent: lines.slice(0, previewCount).join('\n'),
+ contentParagraphs: lines,
+ previewParagraphs: lines.slice(0, previewCount),
+ partTitle: res.partTitle || '',
+ chapterTitle: res.chapterTitle || ''
+ })
+ },
+
+ // 静默刷新(后台更新缓存)
+ async silentRefresh(id) {
+ try {
+ const res = await this.fetchChapterWithTimeout(id, 10000)
+ if (res && res.content) {
+ wx.setStorageSync(`chapter_${id}`, res)
+ console.log('[Read] 后台缓存更新成功:', id)
+ }
+ } catch (e) {
+ // 静默失败不处理
+ }
+ },
+
+ // 重试加载
+ retryLoadContent(id, maxRetries, currentRetry = 0) {
+ if (currentRetry >= maxRetries) {
+ this.setData({
+ contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
+ previewParagraphs: ['内容加载失败']
+ })
+ return
+ }
+
+ setTimeout(async () => {
+ try {
+ const res = await this.fetchChapterWithTimeout(id, 8000)
+ if (res && res.content) {
+ this.setData({ section: this.getSectionInfo(id) })
+ this.setChapterContent(res)
+ wx.setStorageSync(`chapter_${id}`, res)
+ console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
+ setTimeout(() => this.drawShareCard(), 600)
+ return
+ }
+ } catch (e) {
+ console.warn('[Read] 重试失败,继续重试:', currentRetry + 1)
+ }
+ this.retryLoadContent(id, maxRetries, currentRetry + 1)
+ }, 2000 * (currentRetry + 1))
+ },
+
+
+ // 加载导航(prevSection/nextSection 含 mid 时用于跳转,否则用 id)
+ loadNavigation(id) {
+ const sectionOrder = [
+ 'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
+ '2.1', '2.2', '2.3', '2.4', '2.5',
+ '3.1', '3.2', '3.3', '3.4',
+ '4.1', '4.2', '4.3', '4.4', '4.5',
+ '5.1', '5.2', '5.3', '5.4', '5.5',
+ '6.1', '6.2', '6.3', '6.4',
+ '7.1', '7.2', '7.3', '7.4', '7.5',
+ '8.1', '8.2', '8.3', '8.4', '8.5', '8.6',
+ '9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14',
+ '10.1', '10.2', '10.3', '10.4',
+ '11.1', '11.2', '11.3', '11.4', '11.5',
+ 'epilogue'
+ ]
+ const bookData = app.globalData.bookData || []
+ const idToMid = {}
+ bookData.forEach(ch => {
+ if (ch.id && ch.mid) idToMid[ch.id] = ch.mid
+ })
+
+ const currentIndex = sectionOrder.indexOf(id)
+ const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
+ const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
+
+ this.setData({
+ prevSection: prevId ? { id: prevId, mid: idToMid[prevId], title: this.getSectionTitle(prevId) } : null,
+ nextSection: nextId ? { id: nextId, mid: idToMid[nextId], title: this.getSectionTitle(nextId) } : null
+ })
+ },
+
+ // 返回
+ goBack() {
+ wx.navigateBack({
+ fail: () => wx.switchTab({ url: '/pages/chapters/chapters' })
+ })
+ },
+
+ // 分享弹窗
+ showShare() {
+ this.setData({ showShareModal: true })
+ },
+
+ closeShareModal() {
+ this.setData({ showShareModal: false })
+ },
+
+ // 复制分享文案(朋友圈风格)
+ copyShareText() {
+ const { section } = this.data
+
+ const shareText = `🔥 刚看完这篇《${section?.title || 'Soul创业派对'}》,太上头了!
+
+62个真实商业案例,每个都是从0到1的实战经验。私域运营、资源整合、商业变现,干货满满。
+
+推荐给正在创业或想创业的朋友,搜"Soul创业派对"小程序就能看!
+
+#创业派对 #私域运营 #商业案例`
+
+ wx.setClipboardData({
+ data: shareText,
+ success: () => {
+ wx.showToast({ title: '文案已复制', icon: 'success' })
+ }
+ })
+ },
+
+ // 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用
+ drawShareCard() {
+ const { section, sectionId, contentParagraphs } = this.data
+ const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
+ const raw = (contentParagraphs && contentParagraphs.length)
+ ? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim()
+ : ''
+ const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事')
+ const ctx = wx.createCanvasContext('shareCardCanvas', this)
+ const w = 500
+ const h = 400
+ // 白底
+ ctx.setFillStyle('#ffffff')
+ ctx.fillRect(0, 0, w, h)
+ // 顶部:平台名
+ ctx.setFillStyle('#333333')
+ ctx.setFontSize(14)
+ ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36)
+ // 深色内容区(模拟参考图效果)
+ const boxX = 24
+ const boxY = 52
+ const boxW = w - 48
+ const boxH = 300
+ ctx.setFillStyle('#2c2c2e')
+ ctx.fillRect(boxX, boxY, boxW, boxH)
+ // 文章标题(白字)
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(15)
+ const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15)
+ let y = boxY + 28
+ titleLines.slice(0, 2).forEach(line => {
+ ctx.fillText(line, boxX + 16, y)
+ y += 22
+ })
+ y += 8
+ // 正文摘要(浅灰)
+ ctx.setFillStyle('rgba(255,255,255,0.88)')
+ ctx.setFontSize(12)
+ const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12)
+ excerptLines.slice(0, 8).forEach(line => {
+ ctx.fillText(line, boxX + 16, y)
+ y += 20
+ })
+ // 底部:小程序标识
+ ctx.setFillStyle('#999999')
+ ctx.setFontSize(11)
+ ctx.fillText('小程序', 24, h - 16)
+ ctx.draw(false, () => {
+ wx.canvasToTempFilePath({
+ canvasId: 'shareCardCanvas',
+ fileType: 'png',
+ success: (res) => {
+ this.setData({ shareImagePath: res.tempFilePath })
+ }
+ }, this)
+ })
+ },
+
+ // 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
+ getShareConfig() {
+ const { section, sectionId, sectionMid, shareImagePath } = this.data
+ const ref = app.getMyReferralCode()
+ const shareTitle = section?.title
+ ? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
+ : '📚 Soul创业派对 - 真实商业故事'
+ const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
+ const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
+ return {
+ title: shareTitle,
+ path,
+ imageUrl: shareImagePath || undefined
+ }
+ },
+
+ onShareAppMessage() {
+ return this.getShareConfig()
+ },
+
+ onShareTimeline() {
+ const { section, sectionId, sectionMid } = this.data
+ const ref = app.getMyReferralCode()
+ const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
+ return {
+ title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
+ query: ref ? `${q}&ref=${ref}` : q
+ }
+ },
+
+ // 显示登录弹窗(每次打开协议未勾选,符合审核要求)
+ showLoginModal() {
+ try {
+ this.setData({ showLoginModal: true, agreeProtocol: false })
+ } catch (e) {
+ console.error('[Read] showLoginModal error:', e)
+ this.setData({ showLoginModal: true })
+ }
+ },
+
+ closeLoginModal() {
+ this.setData({ showLoginModal: false })
+ },
+
+ toggleAgree() {
+ this.setData({ agreeProtocol: !this.data.agreeProtocol })
+ },
+
+ openUserProtocol() {
+ wx.navigateTo({ url: '/pages/agreement/agreement' })
+ },
+
+ openPrivacy() {
+ wx.navigateTo({ url: '/pages/privacy/privacy' })
+ },
+
+ // 从服务端刷新购买状态,避免登录后误用旧数据导致误解锁
+ // 【重构】微信登录(须先勾选同意协议,符合审核要求)
+ async handleWechatLogin() {
+ if (!this.data.agreeProtocol) {
+ wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+ return
+ }
+ try {
+ const result = await app.login()
+ if (!result) return
+
+ this.setData({ showLoginModal: false, agreeProtocol: false })
+ await this.onLoginSuccess()
+ wx.showToast({ title: '登录成功', icon: 'success' })
+
+ } catch (e) {
+ console.error('[Read] 登录失败:', e)
+ wx.showToast({ title: '登录失败,请重试', icon: 'none' })
+ }
+ },
+
+ // 【重构】手机号登录(标准流程)
+ async handlePhoneLogin(e) {
+ if (!e.detail.code) {
+ return this.handleWechatLogin()
+ }
+
+ try {
+ const result = await app.loginWithPhone(e.detail.code)
+ if (!result) return
+
+ this.setData({ showLoginModal: false })
+ await this.onLoginSuccess()
+ wx.showToast({ title: '登录成功', icon: 'success' })
+
+ } catch (e) {
+ console.error('[Read] 手机号登录失败:', e)
+ wx.showToast({ title: '登录失败', icon: 'none' })
+ }
+ },
+
+ // 【新增】登录成功后的标准处理流程
+ async onLoginSuccess() {
+ wx.showLoading({ title: '更新状态中...', mask: true })
+
+ try {
+ // 1. 刷新用户购买状态(从 orders 表拉取最新)
+ await accessManager.refreshUserPurchaseStatus()
+
+ // 2. 重新拉取免费列表、价格与用户推荐人状态
+ const userId = app.globalData.userInfo?.id
+ const [config, purchaseRes] = await Promise.all([
+ accessManager.fetchLatestConfig(),
+ userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
+ ])
+ const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
+ const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
+ const userDiscount = config.userDiscount ?? 5
+ const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
+ const hasReferralDiscount = hasReferral && userDiscount > 0
+ const displaySectionPrice = hasReferralDiscount
+ ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
+ : sectionPrice
+ const displayFullBookPrice = hasReferralDiscount
+ ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
+ : fullBookPrice
+ this.setData({
+ freeIds: config.freeChapters,
+ sectionPrice,
+ fullBookPrice,
+ userDiscount,
+ hasReferralDiscount,
+ showDiscountHint: userDiscount > 0,
+ displaySectionPrice,
+ displayFullBookPrice,
+ purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
+ })
+
+ // 3. 重新判断当前章节权限
+ const newAccessState = await accessManager.determineAccessState(
+ this.data.sectionId,
+ config.freeChapters
+ )
+ const canAccess = accessManager.canAccessFullContent(newAccessState)
+
+ this.setData({
+ accessState: newAccessState,
+ canAccess,
+ isLoggedIn: true,
+ showPaywall: !canAccess
+ })
+
+ // 4. 如果已解锁,重新加载内容并初始化阅读追踪
+ if (canAccess) {
+ await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
+ readingTracker.init(this.data.sectionId)
+ }
+
+ wx.hideLoading()
+
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[Read] 登录后更新状态失败:', e)
+ wx.showToast({ title: '状态更新失败,请重试', icon: 'none' })
+ }
+ },
+
+ // 购买章节 - 直接调起支付
+ async handlePurchaseSection() {
+ console.log('[Pay] 点击购买章节按钮')
+ wx.showLoading({ title: '处理中...', mask: true })
+
+ if (!this.data.isLoggedIn) {
+ wx.hideLoading()
+ console.log('[Pay] 用户未登录,显示登录弹窗')
+ this.setData({ showLoginModal: true })
+ return
+ }
+
+ const price = this.data.section?.price ?? this.data.sectionPrice ?? 1
+ console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price })
+ wx.hideLoading()
+ await this.processPayment('section', this.data.sectionId, price)
+ },
+
+ // 购买全书 - 直接调起支付
+ async handlePurchaseFullBook() {
+ console.log('[Pay] 点击购买全书按钮')
+ wx.showLoading({ title: '处理中...', mask: true })
+
+ if (!this.data.isLoggedIn) {
+ wx.hideLoading()
+ console.log('[Pay] 用户未登录,显示登录弹窗')
+ this.setData({ showLoginModal: true })
+ return
+ }
+
+ console.log('[Pay] 开始支付流程: 全书', { price: this.data.fullBookPrice })
+ wx.hideLoading()
+ await this.processPayment('fullbook', null, this.data.fullBookPrice)
+ },
+
+ // 处理支付 - 调用真实微信支付接口
+ async processPayment(type, sectionId, amount) {
+ console.log('[Pay] processPayment开始:', { type, sectionId, amount })
+
+ // 检查金额是否有效
+ if (!amount || amount <= 0) {
+ console.error('[Pay] 金额无效:', amount)
+ wx.showToast({ title: '价格信息错误', icon: 'none' })
+ return
+ }
+
+ // ✅ 从服务器查询是否已购买(基于 orders 表)
+ try {
+ wx.showLoading({ title: '检查购买状态...', mask: true })
+ const userId = app.globalData.userInfo?.id
+
+ if (userId) {
+ const checkRes = await app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`)
+
+ if (checkRes.success && checkRes.data) {
+ // 更新本地购买状态
+ app.globalData.hasFullBook = checkRes.data.hasFullBook
+ app.globalData.purchasedSections = checkRes.data.purchasedSections || []
+ app.globalData.sectionMidMap = checkRes.data.sectionMidMap || {}
+
+ // 检查是否已购买
+ if (type === 'section' && sectionId) {
+ if (checkRes.data.purchasedSections.includes(sectionId)) {
+ wx.hideLoading()
+ wx.showToast({ title: '已购买过此章节', icon: 'none' })
+ return
+ }
+ }
+
+ if (type === 'fullbook' && checkRes.data.hasFullBook) {
+ wx.hideLoading()
+ wx.showToast({ title: '已购买全书', icon: 'none' })
+ return
+ }
+ }
+ }
+ } catch (e) {
+ console.warn('[Pay] 查询购买状态失败,继续支付流程:', e)
+ // 查询失败不影响支付
+ }
+
+ this.setData({ isPaying: true })
+ wx.showLoading({ title: '正在发起支付...', mask: true })
+
+ try {
+ // 1. 先获取openId (支付必需)
+ let openId = app.globalData.openId || wx.getStorageSync('openId')
+
+ if (!openId) {
+ console.log('[Pay] 需要先获取openId,尝试静默获取')
+ wx.showLoading({ title: '获取支付凭证...', mask: true })
+ openId = await app.getOpenId()
+
+ if (!openId) {
+ // openId获取失败,但已登录用户可以使用用户ID替代
+ if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
+ console.log('[Pay] 使用用户ID作为替代')
+ openId = app.globalData.userInfo.id
+ } else {
+ wx.hideLoading()
+ wx.showModal({
+ title: '提示',
+ content: '需要登录后才能支付,请先登录',
+ showCancel: false
+ })
+ this.setData({ showLoginModal: true, isPaying: false })
+ return
+ }
+ }
+ }
+
+ console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' })
+ wx.showLoading({ title: '创建订单中...', mask: true })
+
+ // 2. 调用后端创建预支付订单
+ let paymentData = null
+
+ try {
+ // 获取章节完整名称用于支付描述
+ const sectionTitle = this.data.section?.title || sectionId
+ const description = type === 'fullbook'
+ ? '《一场Soul的创业实验》全书'
+ : `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
+
+ // 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
+ const referralCode = wx.getStorageSync('referral_code') || ''
+ const res = await app.request('/api/miniprogram/pay', {
+ method: 'POST',
+ data: {
+ openId,
+ productType: type,
+ productId: sectionId,
+ amount,
+ description,
+ userId: app.globalData.userInfo?.id || '',
+ referralCode: referralCode || undefined
+ }
+ })
+
+ console.log('[Pay] 创建订单响应:', res)
+
+ if (res.success && res.data?.payParams) {
+ paymentData = res.data.payParams
+ paymentData._orderSn = res.data.orderSn
+ console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
+ } else {
+ throw new Error(res.error || res.message || '创建订单失败')
+ }
+ } catch (apiError) {
+ console.error('[Pay] API创建订单失败:', apiError)
+ wx.hideLoading()
+ // 支付接口失败时,显示客服联系方式
+ wx.showModal({
+ title: '支付通道维护中',
+ content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
+ confirmText: '复制微信号',
+ cancelText: '稍后再说',
+ success: (res) => {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: '28533368',
+ success: () => {
+ wx.showToast({ title: '微信号已复制', icon: 'success' })
+ }
+ })
+ }
+ }
+ })
+ this.setData({ isPaying: false })
+ return
+ }
+
+ // 3. 调用微信支付
+ wx.hideLoading()
+ console.log('[Pay] 调起微信支付, paymentData:', paymentData)
+
+ try {
+ const orderSn = paymentData._orderSn
+ await this.callWechatPay(paymentData)
+
+ // 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
+ console.log('[Pay] 微信支付成功!')
+ await this.onPaymentSuccess(orderSn)
+
+ } catch (payErr) {
+ console.error('[Pay] 微信支付调起失败:', payErr)
+ if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
+ wx.showToast({ title: '已取消支付', icon: 'none' })
+ } else if (payErr.errMsg && payErr.errMsg.includes('requestPayment:fail')) {
+ // 支付失败,可能是参数错误或权限问题
+ wx.showModal({
+ title: '支付失败',
+ content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
+ confirmText: '复制微信号',
+ cancelText: '取消',
+ success: (res) => {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: '28533368',
+ success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
+ })
+ }
+ }
+ })
+ } else {
+ wx.showToast({ title: payErr.errMsg || '支付失败', icon: 'none' })
+ }
+ }
+
+ } catch (e) {
+ console.error('[Pay] 支付流程异常:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '支付出错,请重试', icon: 'none' })
+ } finally {
+ this.setData({ isPaying: false })
+ }
+ },
+
+ // 轮询订单状态,确认 paid 后刷新权限并解锁
+ async pollOrderUntilPaid(orderSn) {
+ const maxAttempts = 15
+ const interval = 800
+ for (let i = 0; i < maxAttempts; i++) {
+ try {
+ const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
+ if (r?.data?.status === 'paid') return true
+ } catch (_) {}
+ if (i < maxAttempts - 1) await this.sleep(interval)
+ }
+ return false
+ },
+
+ // 【新增】支付成功后的标准处理流程
+ async onPaymentSuccess(orderSn) {
+ wx.showLoading({ title: '确认购买中...', mask: true })
+
+ try {
+ // 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify)
+ if (orderSn) {
+ const paid = await this.pollOrderUntilPaid(orderSn)
+ if (!paid) {
+ console.warn('[Pay] 轮询超时,仍尝试刷新')
+ }
+ } else {
+ await this.sleep(1500)
+ }
+
+ // 2. 刷新用户购买状态
+ await accessManager.refreshUserPurchaseStatus()
+
+ // 3. 重新判断当前章节权限(应为 unlocked_purchased)
+ let newAccessState = await accessManager.determineAccessState(
+ this.data.sectionId,
+ this.data.freeIds
+ )
+
+ // 如果权限未生效,再重试一次(可能回调延迟)
+ if (newAccessState !== 'unlocked_purchased') {
+ console.log('[Pay] 权限未生效,1秒后重试...')
+ await this.sleep(1000)
+ newAccessState = await accessManager.determineAccessState(
+ this.data.sectionId,
+ this.data.freeIds
+ )
+ }
+
+ const canAccess = accessManager.canAccessFullContent(newAccessState)
+
+ this.setData({
+ accessState: newAccessState,
+ canAccess,
+ showPaywall: !canAccess
+ })
+
+ // 4. 重新加载全文
+ await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
+
+ // 5. 初始化阅读追踪
+ if (canAccess) {
+ readingTracker.init(this.data.sectionId)
+ }
+
+ wx.hideLoading()
+ wx.showToast({ title: '购买成功', icon: 'success' })
+
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[Pay] 支付后更新失败:', e)
+ wx.showModal({
+ title: '提示',
+ content: '购买成功,但内容加载失败,请返回重新进入',
+ showCancel: false
+ })
+ }
+ },
+
+ // ✅ 刷新用户购买状态(从服务器获取最新数据)
+ async refreshUserPurchaseStatus() {
+ try {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ console.warn('[Pay] 用户未登录,无法刷新购买状态')
+ return
+ }
+
+ // 调用专门的购买状态查询接口
+ const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`)
+
+ if (res.success && res.data) {
+ // 更新全局购买状态
+ app.globalData.hasFullBook = res.data.hasFullBook
+ app.globalData.purchasedSections = res.data.purchasedSections || []
+ app.globalData.sectionMidMap = res.data.sectionMidMap || {}
+ app.globalData.matchCount = res.data.matchCount ?? 0
+ app.globalData.matchQuota = res.data.matchQuota || null
+
+ // 更新用户信息中的购买记录
+ const userInfo = app.globalData.userInfo || {}
+ userInfo.hasFullBook = res.data.hasFullBook
+ userInfo.purchasedSections = res.data.purchasedSections || []
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+
+ console.log('[Pay] ✅ 购买状态已刷新:', {
+ hasFullBook: res.data.hasFullBook,
+ purchasedCount: res.data.purchasedSections.length,
+ matchCount: res.data.matchCount
+ })
+ }
+ } catch (e) {
+ console.error('[Pay] 刷新购买状态失败:', e)
+ // 刷新失败时不影响用户体验,只是记录日志
+ }
+ },
+
+ // 调用微信支付
+ callWechatPay(paymentData) {
+ return new Promise((resolve, reject) => {
+ wx.requestPayment({
+ timeStamp: paymentData.timeStamp,
+ nonceStr: paymentData.nonceStr,
+ package: paymentData.package,
+ signType: paymentData.signType || 'MD5',
+ paySign: paymentData.paySign,
+ success: resolve,
+ fail: reject
+ })
+ })
+ },
+
+ goToPrev() {
+ const s = this.data.prevSection
+ if (s) {
+ const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
+ wx.redirectTo({ url: `/pages/read/read?${q}` })
+ }
+ },
+
+ goToNext() {
+ const s = this.data.nextSection
+ if (s) {
+ const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
+ wx.redirectTo({ url: `/pages/read/read?${q}` })
+ }
+ },
+
+ // 跳转到推广中心
+ goToReferral() {
+ wx.navigateTo({ url: '/pages/referral/referral' })
+ },
+
+ // 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
+ async generatePoster() {
+ wx.showLoading({ title: '生成中...' })
+ this.setData({ showPosterModal: true, isGeneratingPoster: true })
+
+ const { section, contentParagraphs, sectionId, sectionMid } = this.data
+ const userInfo = app.globalData.userInfo
+ const userId = userInfo?.id || ''
+ const safeParagraphs = contentParagraphs || []
+
+ // 与 utils/scene 闭环:生成 scene 用 buildScene,扫码后用 parseScene 解析
+ let qrcodeTempPath = null
+ try {
+ const refVal = userId ? String(userId).slice(0, 12) : ''
+ const scene = buildScene({
+ mid: sectionMid || undefined,
+ id: sectionMid ? undefined : (sectionId || ''),
+ ref: refVal || undefined
+ })
+ const baseUrl = app.globalData.baseUrl || ''
+ const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
+ qrcodeTempPath = await new Promise((resolve) => {
+ wx.downloadFile({
+ url,
+ success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null),
+ fail: () => resolve(null)
+ })
+ })
+ } catch (e) {
+ console.log('[Poster] 获取小程序码失败,使用占位符')
+ }
+
+ const doDraw = () => {
+ try {
+ const ctx = wx.createCanvasContext('posterCanvas', this)
+ const width = 300
+ const height = 450
+
+ const grd = ctx.createLinearGradient(0, 0, 0, height)
+ grd.addColorStop(0, '#1a1a2e')
+ grd.addColorStop(1, '#16213e')
+ ctx.setFillStyle(grd)
+ ctx.fillRect(0, 0, width, height)
+
+ ctx.setFillStyle('#00CED1')
+ ctx.fillRect(0, 0, width, 4)
+
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(14)
+ ctx.fillText('📚 Soul创业派对', 20, 35)
+
+ ctx.setFontSize(18)
+ ctx.setFillStyle('#ffffff')
+ const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
+ const titleLines = this.wrapText(ctx, title, width - 40, 18)
+ let y = 70
+ titleLines.forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 26
+ })
+
+ ctx.setStrokeStyle('rgba(255,255,255,0.1)')
+ ctx.beginPath()
+ ctx.moveTo(20, y + 10)
+ ctx.lineTo(width - 20, y + 10)
+ ctx.stroke()
+
+ ctx.setFontSize(12)
+ ctx.setFillStyle('rgba(255,255,255,0.8)')
+ y += 30
+ const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150)
+ const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事'
+ const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12)
+ summaryLines.slice(0, 6).forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 20
+ })
+
+ ctx.setFillStyle('rgba(0,206,209,0.1)')
+ ctx.fillRect(0, height - 100, width, 100)
+
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(13)
+ ctx.fillText('长按识别小程序码', 20, height - 60)
+ ctx.setFillStyle('rgba(255,255,255,0.6)')
+ ctx.setFontSize(11)
+ ctx.fillText('长按小程序码阅读全文', 20, height - 38)
+
+ const drawQRCode = () => {
+ return new Promise((resolve) => {
+ if (qrcodeTempPath) {
+ ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70)
+ } else {
+ this.drawQRPlaceholder(ctx, width, height)
+ }
+ resolve()
+ })
+ }
+
+ drawQRCode().then(() => {
+ ctx.draw(true, () => {
+ wx.hideLoading()
+ this.setData({ isGeneratingPoster: false })
+ })
+ })
+ } catch (e) {
+ console.error('生成海报失败:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '生成失败', icon: 'none' })
+ this.setData({ showPosterModal: false, isGeneratingPoster: false })
+ }
+ }
+
+ setTimeout(doDraw, 400)
+ },
+
+ // 绘制小程序码占位符
+ drawQRPlaceholder(ctx, width, height) {
+ ctx.setFillStyle('#ffffff')
+ ctx.beginPath()
+ ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
+ ctx.fill()
+ ctx.setFillStyle('#00CED1')
+ ctx.setFontSize(9)
+ ctx.fillText('扫码', width - 57, height - 52)
+ ctx.fillText('阅读', width - 57, height - 40)
+ },
+
+ // 文字换行处理
+ wrapText(ctx, text, maxWidth, fontSize) {
+ const lines = []
+ let line = ''
+ for (let i = 0; i < text.length; i++) {
+ const testLine = line + text[i]
+ const metrics = ctx.measureText(testLine)
+ if (metrics.width > maxWidth && line) {
+ lines.push(line)
+ line = text[i]
+ } else {
+ line = testLine
+ }
+ }
+ if (line) lines.push(line)
+ return lines
+ },
+
+ // 关闭海报弹窗
+ closePosterModal() {
+ this.setData({ showPosterModal: false })
+ },
+
+ // 保存海报到相册:画布 300x450 兼容 iOS,导出 2 倍 600x900 提升清晰度(宽高比 2:3 不变)
+ savePoster() {
+ const width = 300
+ const height = 450
+ const exportScale = 2
+ wx.canvasToTempFilePath({
+ canvasId: 'posterCanvas',
+ destWidth: width * exportScale,
+ destHeight: height * exportScale,
+ fileType: 'png',
+ success: (res) => {
+ if (!res.tempFilePath) {
+ wx.showToast({ title: '生成图片失败', icon: 'none' })
+ return
+ }
+ wx.saveImageToPhotosAlbum({
+ filePath: res.tempFilePath,
+ success: () => {
+ wx.showToast({ title: '已保存到相册', icon: 'success' })
+ this.setData({ showPosterModal: false })
+ },
+ fail: (err) => {
+ console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
+ if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
+ wx.showModal({
+ title: '提示',
+ content: '需要相册权限才能保存海报',
+ confirmText: '去设置',
+ success: (sres) => {
+ if (sres.confirm) wx.openSetting()
+ }
+ })
+ } else {
+ wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
+ }
+ }
+ })
+ },
+ fail: (err) => {
+ console.error('[savePoster] canvasToTempFilePath fail:', err)
+ wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
+ }
+ }, this)
+ },
+
+ // 阻止冒泡
+ stopPropagation() {},
+
+ // 【新增】页面隐藏时上报阅读进度
+ onHide() {
+ readingTracker.onPageHide()
+ },
+
+ // 【新增】页面卸载时清理追踪器
+ onUnload() {
+ readingTracker.cleanup()
+ },
+
+ // 【新增】重试加载(当 accessState 为 error 时)
+ async handleRetry() {
+ wx.showLoading({ title: '重试中...', mask: true })
+
+ try {
+ const userId = app.globalData.userInfo?.id
+ const [config, purchaseRes] = await Promise.all([
+ accessManager.fetchLatestConfig(),
+ userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null)
+ ])
+ const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1
+ const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9
+ const userDiscount = config.userDiscount ?? 5
+ const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer)
+ const hasReferralDiscount = hasReferral && userDiscount > 0
+ const displaySectionPrice = hasReferralDiscount
+ ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100
+ : sectionPrice
+ const displayFullBookPrice = hasReferralDiscount
+ ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100
+ : fullBookPrice
+ this.setData({
+ freeIds: config.freeChapters,
+ sectionPrice,
+ fullBookPrice,
+ userDiscount,
+ hasReferralDiscount,
+ showDiscountHint: userDiscount > 0,
+ displaySectionPrice,
+ displayFullBookPrice,
+ purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0
+ })
+
+ // 重新判断权限
+ const newAccessState = await accessManager.determineAccessState(
+ this.data.sectionId,
+ config.freeChapters
+ )
+ const canAccess = accessManager.canAccessFullContent(newAccessState)
+
+ this.setData({
+ accessState: newAccessState,
+ canAccess,
+ showPaywall: !canAccess
+ })
+
+ // 重新加载内容
+ await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
+
+ // 如果有权限,初始化阅读追踪
+ if (canAccess) {
+ readingTracker.init(this.data.sectionId)
+ }
+
+ // 加载导航
+ this.loadNavigation(this.data.sectionId)
+
+ wx.hideLoading()
+ wx.showToast({ title: '加载成功', icon: 'success' })
+
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[Read] 重试失败:', e)
+ wx.showToast({ title: '重试失败,请检查网络', icon: 'none' })
+ }
+ },
+
+ // 工具:延迟
+ sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+ }
+})
diff --git a/miniprogram2/pages/read/read.js.backup b/miniprogram2/pages/read/read.js.backup
new file mode 100644
index 00000000..65ea2b60
--- /dev/null
+++ b/miniprogram2/pages/read/read.js.backup
@@ -0,0 +1,1055 @@
+/**
+ * Soul创业派对 - 阅读页
+ * 开发: 卡若
+ * 技术支持: 存客宝
+ */
+
+const app = getApp()
+
+Page({
+ data: {
+ // 系统信息
+ statusBarHeight: 44,
+ navBarHeight: 88,
+
+ // 章节信息
+ sectionId: '',
+ section: null,
+ partTitle: '',
+ chapterTitle: '',
+
+ // 内容
+ content: '',
+ previewContent: '',
+ contentParagraphs: [],
+ previewParagraphs: [],
+ loading: true,
+
+ // 用户状态
+ isLoggedIn: false,
+ hasFullBook: false,
+ canAccess: false,
+ purchasedCount: 0,
+
+ // 阅读进度
+ readingProgress: 0,
+ showPaywall: false,
+
+ // 上一篇/下一篇
+ prevSection: null,
+ nextSection: null,
+
+ // 价格
+ sectionPrice: 1,
+ fullBookPrice: 9.9,
+ totalSections: 62,
+
+ // 弹窗
+ showShareModal: false,
+ showLoginModal: false,
+ showPosterModal: false,
+ isPaying: false,
+ isGeneratingPoster: false,
+
+ // 免费章节
+ freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
+ },
+
+ onLoad(options) {
+ const { id, ref } = options
+
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ navBarHeight: app.globalData.navBarHeight,
+ sectionId: id
+ })
+
+ // 处理推荐码绑定
+ if (ref) {
+ console.log('[Read] 检测到推荐码:', ref)
+ wx.setStorageSync('referral_code', ref)
+ app.handleReferralCode({ query: { ref } })
+ }
+
+ // 先拉取免费章节配置再初始化,避免首帧误判免费/付费
+ const run = async () => {
+ await this.loadFreeChaptersConfig()
+ this.initSection(id)
+ }
+ run()
+ },
+
+ // 从后端加载免费章节配置
+ async loadFreeChaptersConfig() {
+ try {
+ const res = await app.request('/api/db/config')
+ if (res.success && res.freeChapters) {
+ this.setData({ freeIds: res.freeChapters })
+ console.log('[Read] 加载免费章节配置:', res.freeChapters)
+ }
+ } catch (e) {
+ console.log('[Read] 使用默认免费章节配置')
+ }
+ },
+
+ onPageScroll(e) {
+ // 计算阅读进度
+ const query = wx.createSelectorQuery()
+ query.select('.page').boundingClientRect()
+ query.exec((res) => {
+ if (res[0]) {
+ const scrollTop = e.scrollTop
+ const pageHeight = res[0].height - this.data.statusBarHeight - 200
+ const progress = pageHeight > 0 ? Math.min((scrollTop / pageHeight) * 100, 100) : 0
+ this.setData({ readingProgress: progress })
+ }
+ })
+ },
+
+ // 初始化章节:免费直接可看;付费则请求接口校验是否已购买
+ async initSection(id) {
+ this.setData({ loading: true })
+
+ try {
+ const section = this.getSectionInfo(id)
+ const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
+ const isFree = this.data.freeIds.includes(id)
+
+ let canAccess = isFree
+ let isPurchased = false
+
+ if (!isFree) {
+ if (!isLoggedIn || !app.globalData.userInfo?.id) {
+ canAccess = false
+ } else {
+ try {
+ const userId = app.globalData.userInfo.id
+ const res = await app.request(
+ `/api/user/check-purchased?userId=${encodeURIComponent(userId)}&type=section&productId=${encodeURIComponent(id)}`
+ )
+ if (res.success && res.data && res.data.isPurchased) {
+ isPurchased = true
+ canAccess = true
+ if (!purchasedSections.includes(id)) {
+ app.globalData.purchasedSections = [...(app.globalData.purchasedSections || []), id]
+ }
+ if (res.data.reason === 'has_full_book') {
+ app.globalData.hasFullBook = true
+ }
+ }
+ } catch (e) {
+ console.warn('[Read] 校验购买状态失败,保守处理为未购买:', e)
+ isPurchased = false
+ canAccess = false
+ }
+ }
+ }
+
+ const purchasedCount = (app.globalData.purchasedSections || []).length
+
+ this.setData({
+ section,
+ isLoggedIn,
+ hasFullBook: app.globalData.hasFullBook,
+ canAccess,
+ purchasedCount,
+ showPaywall: !canAccess
+ })
+
+ await this.loadContent(id)
+
+ if (canAccess) {
+ app.markSectionAsRead(id)
+ }
+
+ this.loadNavigation(id)
+
+ } catch (e) {
+ console.error('初始化章节失败:', e)
+ wx.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ this.setData({ loading: false })
+ }
+ },
+
+ // 获取章节信息
+ getSectionInfo(id) {
+ // 特殊章节
+ if (id === 'preface') {
+ return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 }
+ }
+ if (id === 'epilogue') {
+ return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 }
+ }
+ if (id.startsWith('appendix')) {
+ const appendixTitles = {
+ 'appendix-1': 'Soul派对房精选对话',
+ 'appendix-2': '创业者自检清单',
+ 'appendix-3': '本书提到的工具和资源'
+ }
+ return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
+ }
+
+ // 普通章节
+ return {
+ id: id,
+ title: this.getSectionTitle(id),
+ isFree: id === '1.1',
+ price: 1
+ }
+ },
+
+ // 获取章节标题
+ getSectionTitle(id) {
+ const titles = {
+ '1.1': '荷包:电动车出租的被动收入模式',
+ '1.2': '老墨:资源整合高手的社交方法',
+ '1.3': '笑声背后的MBTI',
+ '1.4': '人性的三角结构:利益、情感、价值观',
+ '1.5': '沟通差的问题:为什么你说的别人听不懂',
+ '2.1': '相亲故事:你以为找的是人,实际是在找模式',
+ '2.2': '找工作迷茫者:为什么简历解决不了人生',
+ '2.3': '撸运费险:小钱困住大脑的真实心理',
+ '2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力',
+ '2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒',
+ '3.1': '3000万流水如何跑出来(退税模式解析)',
+ '8.1': '流量杠杆:抖音、Soul、飞书',
+ '9.14': '大健康私域:一个月150万的70后'
+ }
+ return titles[id] || `章节 ${id}`
+ },
+
+ // 加载内容 - 三级降级方案:API → 本地缓存 → 备用API
+ async loadContent(id) {
+ const cacheKey = `chapter_${id}`
+
+ // 1. 优先从API获取
+ try {
+ const res = await this.fetchChapterWithTimeout(id, 5000)
+ if (res && res.content) {
+ this.setChapterContent(res)
+ // 成功后缓存到本地
+ wx.setStorageSync(cacheKey, res)
+ console.log('[Read] 从API加载成功:', id)
+ return
+ }
+ } catch (e) {
+ console.warn('[Read] API加载失败,尝试本地缓存:', e.message)
+ }
+
+ // 2. API失败,尝试从本地缓存读取
+ try {
+ const cached = wx.getStorageSync(cacheKey)
+ if (cached && cached.content) {
+ this.setChapterContent(cached)
+ console.log('[Read] 从本地缓存加载成功:', id)
+ // 后台静默刷新
+ this.silentRefresh(id)
+ return
+ }
+ } catch (e) {
+ console.warn('[Read] 本地缓存读取失败')
+ }
+
+ // 3. 都失败,显示加载中并持续重试
+ this.setData({
+ contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
+ previewParagraphs: ['章节内容加载中...']
+ })
+
+ // 延迟重试(最多3次)
+ this.retryLoadContent(id, 3)
+ },
+
+ // 带超时的章节请求
+ fetchChapterWithTimeout(id, timeout = 5000) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(new Error('请求超时'))
+ }, timeout)
+
+ app.request(`/api/book/chapter/${id}`)
+ .then(res => {
+ clearTimeout(timer)
+ resolve(res)
+ })
+ .catch(err => {
+ clearTimeout(timer)
+ reject(err)
+ })
+ })
+ },
+
+ // 设置章节内容
+ setChapterContent(res) {
+ const lines = res.content.split('\n').filter(line => line.trim())
+ const previewCount = Math.ceil(lines.length * 0.2)
+
+ this.setData({
+ content: res.content,
+ previewContent: lines.slice(0, previewCount).join('\n'),
+ contentParagraphs: lines,
+ previewParagraphs: lines.slice(0, previewCount),
+ partTitle: res.partTitle || '',
+ chapterTitle: res.chapterTitle || ''
+ })
+ },
+
+ // 静默刷新(后台更新缓存)
+ async silentRefresh(id) {
+ try {
+ const res = await this.fetchChapterWithTimeout(id, 10000)
+ if (res && res.content) {
+ wx.setStorageSync(`chapter_${id}`, res)
+ console.log('[Read] 后台缓存更新成功:', id)
+ }
+ } catch (e) {
+ // 静默失败不处理
+ }
+ },
+
+ // 重试加载
+ retryLoadContent(id, maxRetries, currentRetry = 0) {
+ if (currentRetry >= maxRetries) {
+ this.setData({
+ contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
+ previewParagraphs: ['内容加载失败']
+ })
+ return
+ }
+
+ setTimeout(async () => {
+ try {
+ const res = await this.fetchChapterWithTimeout(id, 8000)
+ if (res && res.content) {
+ this.setChapterContent(res)
+ wx.setStorageSync(`chapter_${id}`, res)
+ console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
+ return
+ }
+ } catch (e) {
+ console.warn('[Read] 重试失败,继续重试:', currentRetry + 1)
+ }
+ this.retryLoadContent(id, maxRetries, currentRetry + 1)
+ }, 2000 * (currentRetry + 1))
+ },
+
+
+ // 加载导航
+ loadNavigation(id) {
+ const sectionOrder = [
+ 'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
+ '2.1', '2.2', '2.3', '2.4', '2.5',
+ '3.1', '3.2', '3.3', '3.4',
+ '4.1', '4.2', '4.3', '4.4', '4.5',
+ '5.1', '5.2', '5.3', '5.4', '5.5',
+ '6.1', '6.2', '6.3', '6.4',
+ '7.1', '7.2', '7.3', '7.4', '7.5',
+ '8.1', '8.2', '8.3', '8.4', '8.5', '8.6',
+ '9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14',
+ '10.1', '10.2', '10.3', '10.4',
+ '11.1', '11.2', '11.3', '11.4', '11.5',
+ 'epilogue'
+ ]
+
+ const currentIndex = sectionOrder.indexOf(id)
+ const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
+ const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
+
+ this.setData({
+ prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
+ nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
+ })
+ },
+
+ // 返回
+ goBack() {
+ wx.navigateBack({
+ fail: () => wx.switchTab({ url: '/pages/chapters/chapters' })
+ })
+ },
+
+ // 分享弹窗
+ showShare() {
+ this.setData({ showShareModal: true })
+ },
+
+ closeShareModal() {
+ this.setData({ showShareModal: false })
+ },
+
+ // 复制链接
+ copyLink() {
+ const userInfo = app.globalData.userInfo
+ const referralCode = userInfo?.referralCode || ''
+ const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
+
+ wx.setClipboardData({
+ data: shareUrl,
+ success: () => {
+ wx.showToast({ title: '链接已复制', icon: 'success' })
+ this.setData({ showShareModal: false })
+ }
+ })
+ },
+
+ // 复制分享文案(朋友圈风格)
+ copyShareText() {
+ const { section } = this.data
+
+ const shareText = `🔥 刚看完这篇《${section?.title || 'Soul创业派对'}》,太上头了!
+
+62个真实商业案例,每个都是从0到1的实战经验。私域运营、资源整合、商业变现,干货满满。
+
+推荐给正在创业或想创业的朋友,搜"Soul创业派对"小程序就能看!
+
+#创业派对 #私域运营 #商业案例`
+
+ wx.setClipboardData({
+ data: shareText,
+ success: () => {
+ wx.showToast({ title: '文案已复制', icon: 'success' })
+ }
+ })
+ },
+
+ // 分享到微信 - 自动带分享人ID
+ onShareAppMessage() {
+ const { section, sectionId } = this.data
+ const userInfo = app.globalData.userInfo
+ const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
+
+ // 分享标题优化
+ const shareTitle = section?.title
+ ? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
+ : '📚 Soul创业派对 - 真实商业故事'
+
+ return {
+ title: shareTitle,
+ path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
+ imageUrl: '/assets/share-cover.png' // 可配置分享封面图
+ }
+ },
+
+ // 分享到朋友圈
+ onShareTimeline() {
+ const { section, sectionId } = this.data
+ const userInfo = app.globalData.userInfo
+ const referralCode = userInfo?.referralCode || ''
+
+ return {
+ title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
+ query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
+ }
+ },
+
+ // 显示登录弹窗
+ showLoginModal() {
+ this.setData({ showLoginModal: true })
+ },
+
+ closeLoginModal() {
+ this.setData({ showLoginModal: false })
+ },
+
+ // 从服务端刷新购买状态,避免登录后误用旧数据导致误解锁
+ async refreshPurchaseFromServer() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+ try {
+ const res = await app.request(`/api/user/purchase-status?userId=${encodeURIComponent(userId)}`)
+ if (res.success && res.data) {
+ app.globalData.hasFullBook = res.data.hasFullBook || false
+ app.globalData.purchasedSections = res.data.purchasedSections || []
+ wx.setStorageSync('userInfo', { ...(app.globalData.userInfo || {}), purchasedSections: app.globalData.purchasedSections, hasFullBook: app.globalData.hasFullBook })
+ }
+ } catch (e) {
+ console.warn('[Read] 刷新购买状态失败:', e)
+ }
+ },
+
+ // 微信登录(含:因付款弹窗发起的登录)
+ async handleWechatLogin() {
+ try {
+ const result = await app.login()
+ if (result) {
+ this.setData({ showLoginModal: false })
+ // 登录后必须重新向服务端拉取购买状态,并重新校验当前章节是否已购买,再决定是否解锁(避免误解锁)
+ await this.refreshPurchaseFromServer()
+ await this.recheckCurrentSectionAndRefresh()
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ }
+ } catch (e) {
+ wx.showToast({ title: '登录失败', icon: 'none' })
+ }
+ },
+
+ // 手机号登录(含:因付款弹窗发起的登录)
+ async handlePhoneLogin(e) {
+ if (!e.detail.code) {
+ return this.handleWechatLogin()
+ }
+
+ try {
+ const result = await app.loginWithPhone(e.detail.code)
+ if (result) {
+ this.setData({ showLoginModal: false })
+ // 登录后必须重新向服务端拉取购买状态,并重新校验当前章节是否已购买,再决定是否解锁(避免误解锁)
+ await this.refreshPurchaseFromServer()
+ await this.recheckCurrentSectionAndRefresh()
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ }
+ } catch (e) {
+ wx.showToast({ title: '登录失败', icon: 'none' })
+ }
+ },
+
+ // 登录后专用:重新向服务端校验当前章节是否已付费购买(或是否已改为免费),再刷新页面状态
+ async recheckCurrentSectionAndRefresh() {
+ const sectionId = this.data.sectionId
+ // 极端情况:用户登录后,当前章节可能刚被后台改为免费,先拉取最新免费列表再判断
+ await this.loadFreeChaptersConfig()
+ const isFree = this.data.freeIds.includes(sectionId)
+ if (isFree) {
+ this.setData({ isLoggedIn: true, canAccess: true, showPaywall: false })
+ await this.initSection(sectionId)
+ return
+ }
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ this.setData({ canAccess: false, showPaywall: true })
+ return
+ }
+ try {
+ const res = await app.request(
+ `/api/user/check-purchased?userId=${encodeURIComponent(userId)}&type=section&productId=${encodeURIComponent(sectionId)}`
+ )
+ const isPurchased = res.success && res.data && res.data.isPurchased
+ if (isPurchased) {
+ if (!(app.globalData.purchasedSections || []).includes(sectionId)) {
+ app.globalData.purchasedSections = [...(app.globalData.purchasedSections || []), sectionId]
+ }
+ if (res.data.reason === 'has_full_book') {
+ app.globalData.hasFullBook = true
+ }
+ }
+ this.setData({
+ isLoggedIn: true,
+ hasFullBook: app.globalData.hasFullBook,
+ canAccess: isPurchased,
+ purchasedCount: (app.globalData.purchasedSections || []).length,
+ showPaywall: !isPurchased
+ })
+ if (isPurchased) {
+ app.markSectionAsRead(sectionId)
+ }
+ await this.initSection(sectionId)
+ } catch (e) {
+ console.warn('[Read] 登录后校验当前章节购买状态失败,保守处理为未购买:', e)
+ this.setData({
+ isLoggedIn: true,
+ canAccess: false,
+ showPaywall: true
+ })
+ await this.initSection(sectionId)
+ }
+ },
+
+ // 购买章节 - 直接调起支付
+ async handlePurchaseSection() {
+ console.log('[Pay] 点击购买章节按钮')
+ wx.showLoading({ title: '处理中...', mask: true })
+
+ if (!this.data.isLoggedIn) {
+ wx.hideLoading()
+ console.log('[Pay] 用户未登录,显示登录弹窗')
+ this.setData({ showLoginModal: true })
+ return
+ }
+
+ const price = this.data.section?.price || 1
+ console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price })
+ wx.hideLoading()
+ await this.processPayment('section', this.data.sectionId, price)
+ },
+
+ // 购买全书 - 直接调起支付
+ async handlePurchaseFullBook() {
+ console.log('[Pay] 点击购买全书按钮')
+ wx.showLoading({ title: '处理中...', mask: true })
+
+ if (!this.data.isLoggedIn) {
+ wx.hideLoading()
+ console.log('[Pay] 用户未登录,显示登录弹窗')
+ this.setData({ showLoginModal: true })
+ return
+ }
+
+ console.log('[Pay] 开始支付流程: 全书', { price: this.data.fullBookPrice })
+ wx.hideLoading()
+ await this.processPayment('fullbook', null, this.data.fullBookPrice)
+ },
+
+ // 处理支付 - 调用真实微信支付接口
+ async processPayment(type, sectionId, amount) {
+ console.log('[Pay] processPayment开始:', { type, sectionId, amount })
+
+ // 检查金额是否有效
+ if (!amount || amount <= 0) {
+ console.error('[Pay] 金额无效:', amount)
+ wx.showToast({ title: '价格信息错误', icon: 'none' })
+ return
+ }
+
+ // ✅ 从服务器查询是否已购买(基于 orders 表)
+ try {
+ wx.showLoading({ title: '检查购买状态...', mask: true })
+ const userId = app.globalData.userInfo?.id
+
+ if (userId) {
+ const checkRes = await app.request(`/api/user/purchase-status?userId=${userId}`)
+
+ if (checkRes.success && checkRes.data) {
+ // 更新本地购买状态
+ app.globalData.hasFullBook = checkRes.data.hasFullBook
+ app.globalData.purchasedSections = checkRes.data.purchasedSections || []
+
+ // 检查是否已购买
+ if (type === 'section' && sectionId) {
+ if (checkRes.data.purchasedSections.includes(sectionId)) {
+ wx.hideLoading()
+ wx.showToast({ title: '已购买过此章节', icon: 'none' })
+ return
+ }
+ }
+
+ if (type === 'fullbook' && checkRes.data.hasFullBook) {
+ wx.hideLoading()
+ wx.showToast({ title: '已购买全书', icon: 'none' })
+ return
+ }
+ }
+ }
+ } catch (e) {
+ console.warn('[Pay] 查询购买状态失败,继续支付流程:', e)
+ // 查询失败不影响支付
+ }
+
+ this.setData({ isPaying: true })
+ wx.showLoading({ title: '正在发起支付...', mask: true })
+
+ try {
+ // 1. 先获取openId (支付必需)
+ let openId = app.globalData.openId || wx.getStorageSync('openId')
+
+ if (!openId) {
+ console.log('[Pay] 需要先获取openId,尝试静默获取')
+ wx.showLoading({ title: '获取支付凭证...', mask: true })
+ openId = await app.getOpenId()
+
+ if (!openId) {
+ // openId获取失败,但已登录用户可以使用用户ID替代
+ if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
+ console.log('[Pay] 使用用户ID作为替代')
+ openId = app.globalData.userInfo.id
+ } else {
+ wx.hideLoading()
+ wx.showModal({
+ title: '提示',
+ content: '需要登录后才能支付,请先登录',
+ showCancel: false
+ })
+ this.setData({ showLoginModal: true, isPaying: false })
+ return
+ }
+ }
+ }
+
+ console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' })
+ wx.showLoading({ title: '创建订单中...', mask: true })
+
+ // 2. 调用后端创建预支付订单
+ let paymentData = null
+
+ try {
+ // 获取章节完整名称用于支付描述
+ const sectionTitle = this.data.section?.title || sectionId
+ const description = type === 'fullbook'
+ ? '《一场Soul的创业实验》全书'
+ : `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
+
+ // 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),用于订单分销归属
+ const referralCode = wx.getStorageSync('referral_code') || ''
+ const res = await app.request('/api/miniprogram/pay', {
+ method: 'POST',
+ data: {
+ openId,
+ productType: type,
+ productId: sectionId,
+ amount,
+ description,
+ userId: app.globalData.userInfo?.id || '',
+ referralCode: referralCode || undefined
+ }
+ })
+
+ console.log('[Pay] 创建订单响应:', res)
+
+ if (res.success && res.data?.payParams) {
+ paymentData = res.data.payParams
+ console.log('[Pay] 获取支付参数成功:', paymentData)
+ } else {
+ throw new Error(res.error || res.message || '创建订单失败')
+ }
+ } catch (apiError) {
+ console.error('[Pay] API创建订单失败:', apiError)
+ wx.hideLoading()
+ // 支付接口失败时,显示客服联系方式
+ wx.showModal({
+ title: '支付通道维护中',
+ content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
+ confirmText: '复制微信号',
+ cancelText: '稍后再说',
+ success: (res) => {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: '28533368',
+ success: () => {
+ wx.showToast({ title: '微信号已复制', icon: 'success' })
+ }
+ })
+ }
+ }
+ })
+ this.setData({ isPaying: false })
+ return
+ }
+
+ // 3. 调用微信支付
+ wx.hideLoading()
+ console.log('[Pay] 调起微信支付, paymentData:', paymentData)
+
+ try {
+ await this.callWechatPay(paymentData)
+
+ // 4. 支付成功,刷新用户购买状态
+ console.log('[Pay] 微信支付成功!')
+ wx.showLoading({ title: '正在确认购买...', mask: true })
+
+ // 等待后端处理支付回调(1-3秒)
+ await new Promise(resolve => setTimeout(resolve, 2000))
+
+ // 重新获取用户信息(包含最新购买记录)
+ await this.refreshUserPurchaseStatus()
+
+ wx.hideLoading()
+ wx.showToast({ title: '购买成功', icon: 'success' })
+
+ // 5. 刷新页面
+ this.initSection(this.data.sectionId)
+ } catch (payErr) {
+ console.error('[Pay] 微信支付调起失败:', payErr)
+ if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
+ wx.showToast({ title: '已取消支付', icon: 'none' })
+ } else if (payErr.errMsg && payErr.errMsg.includes('requestPayment:fail')) {
+ // 支付失败,可能是参数错误或权限问题
+ wx.showModal({
+ title: '支付失败',
+ content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
+ confirmText: '复制微信号',
+ cancelText: '取消',
+ success: (res) => {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: '28533368',
+ success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
+ })
+ }
+ }
+ })
+ } else {
+ wx.showToast({ title: payErr.errMsg || '支付失败', icon: 'none' })
+ }
+ }
+
+ } catch (e) {
+ console.error('[Pay] 支付流程异常:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '支付出错,请重试', icon: 'none' })
+ } finally {
+ this.setData({ isPaying: false })
+ }
+ },
+
+ // ✅ 刷新用户购买状态(从服务器获取最新数据)
+ async refreshUserPurchaseStatus() {
+ try {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ console.warn('[Pay] 用户未登录,无法刷新购买状态')
+ return
+ }
+
+ // 调用专门的购买状态查询接口
+ const res = await app.request(`/api/user/purchase-status?userId=${userId}`)
+
+ if (res.success && res.data) {
+ // 更新全局购买状态
+ app.globalData.hasFullBook = res.data.hasFullBook
+ app.globalData.purchasedSections = res.data.purchasedSections || []
+
+ // 更新用户信息中的购买记录
+ const userInfo = app.globalData.userInfo || {}
+ userInfo.hasFullBook = res.data.hasFullBook
+ userInfo.purchasedSections = res.data.purchasedSections || []
+ app.globalData.userInfo = userInfo
+ wx.setStorageSync('userInfo', userInfo)
+
+ console.log('[Pay] ✅ 购买状态已刷新:', {
+ hasFullBook: res.data.hasFullBook,
+ purchasedCount: res.data.purchasedSections.length
+ })
+ }
+ } catch (e) {
+ console.error('[Pay] 刷新购买状态失败:', e)
+ // 刷新失败时不影响用户体验,只是记录日志
+ }
+ },
+
+ // 调用微信支付
+ callWechatPay(paymentData) {
+ return new Promise((resolve, reject) => {
+ wx.requestPayment({
+ timeStamp: paymentData.timeStamp,
+ nonceStr: paymentData.nonceStr,
+ package: paymentData.package,
+ signType: paymentData.signType || 'MD5',
+ paySign: paymentData.paySign,
+ success: resolve,
+ fail: reject
+ })
+ })
+ },
+
+ // 跳转到上一篇
+ goToPrev() {
+ if (this.data.prevSection) {
+ wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
+ }
+ },
+
+ // 跳转到下一篇
+ goToNext() {
+ if (this.data.nextSection) {
+ wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
+ }
+ },
+
+ // 跳转到推广中心
+ goToReferral() {
+ wx.navigateTo({ url: '/pages/referral/referral' })
+ },
+
+ // 生成海报
+ async generatePoster() {
+ wx.showLoading({ title: '生成中...' })
+ this.setData({ showPosterModal: true, isGeneratingPoster: true })
+
+ try {
+ const ctx = wx.createCanvasContext('posterCanvas', this)
+ const { section, contentParagraphs, sectionId } = this.data
+ const userInfo = app.globalData.userInfo
+ const userId = userInfo?.id || ''
+
+ // 获取小程序码(带推荐人参数)
+ let qrcodeImage = null
+ try {
+ const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
+ const qrRes = await app.request('/api/miniprogram/qrcode', {
+ method: 'POST',
+ data: { scene, page: 'pages/read/read', width: 280 }
+ })
+ if (qrRes.success && qrRes.image) {
+ qrcodeImage = qrRes.image
+ }
+ } catch (e) {
+ console.log('[Poster] 获取小程序码失败,使用占位符')
+ }
+
+ // 海报尺寸 300x450
+ const width = 300
+ const height = 450
+
+ // 背景渐变
+ const grd = ctx.createLinearGradient(0, 0, 0, height)
+ grd.addColorStop(0, '#1a1a2e')
+ grd.addColorStop(1, '#16213e')
+ ctx.setFillStyle(grd)
+ ctx.fillRect(0, 0, width, height)
+
+ // 顶部装饰条
+ ctx.setFillStyle('#00CED1')
+ ctx.fillRect(0, 0, width, 4)
+
+ // 标题区域
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(14)
+ ctx.fillText('📚 Soul创业派对', 20, 35)
+
+ // 章节标题
+ ctx.setFontSize(18)
+ ctx.setFillStyle('#ffffff')
+ const title = section?.title || '精彩内容'
+ const titleLines = this.wrapText(ctx, title, width - 40, 18)
+ let y = 70
+ titleLines.forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 26
+ })
+
+ // 分隔线
+ ctx.setStrokeStyle('rgba(255,255,255,0.1)')
+ ctx.beginPath()
+ ctx.moveTo(20, y + 10)
+ ctx.lineTo(width - 20, y + 10)
+ ctx.stroke()
+
+ // 内容摘要
+ ctx.setFontSize(12)
+ ctx.setFillStyle('rgba(255,255,255,0.8)')
+ y += 30
+ const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
+ const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
+ summaryLines.slice(0, 6).forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 20
+ })
+
+ // 底部区域背景
+ ctx.setFillStyle('rgba(0,206,209,0.1)')
+ ctx.fillRect(0, height - 100, width, 100)
+
+ // 左侧提示文字
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(13)
+ ctx.fillText('长按识别小程序码', 20, height - 60)
+ ctx.setFillStyle('rgba(255,255,255,0.6)')
+ ctx.setFontSize(11)
+ ctx.fillText('长按小程序码阅读全文', 20, height - 38)
+
+ // 绘制小程序码或占位符
+ const drawQRCode = () => {
+ return new Promise((resolve) => {
+ if (qrcodeImage) {
+ // 下载base64图片并绘制
+ const fs = wx.getFileSystemManager()
+ const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
+ const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
+
+ fs.writeFile({
+ filePath,
+ data: base64Data,
+ encoding: 'base64',
+ success: () => {
+ ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
+ resolve()
+ },
+ fail: () => {
+ this.drawQRPlaceholder(ctx, width, height)
+ resolve()
+ }
+ })
+ } else {
+ this.drawQRPlaceholder(ctx, width, height)
+ resolve()
+ }
+ })
+ }
+
+ await drawQRCode()
+
+ ctx.draw(true, () => {
+ wx.hideLoading()
+ this.setData({ isGeneratingPoster: false })
+ })
+ } catch (e) {
+ console.error('生成海报失败:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '生成失败', icon: 'none' })
+ this.setData({ showPosterModal: false, isGeneratingPoster: false })
+ }
+ },
+
+ // 绘制小程序码占位符
+ drawQRPlaceholder(ctx, width, height) {
+ ctx.setFillStyle('#ffffff')
+ ctx.beginPath()
+ ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
+ ctx.fill()
+ ctx.setFillStyle('#00CED1')
+ ctx.setFontSize(9)
+ ctx.fillText('扫码', width - 57, height - 52)
+ ctx.fillText('阅读', width - 57, height - 40)
+ },
+
+ // 文字换行处理
+ wrapText(ctx, text, maxWidth, fontSize) {
+ const lines = []
+ let line = ''
+ for (let i = 0; i < text.length; i++) {
+ const testLine = line + text[i]
+ const metrics = ctx.measureText(testLine)
+ if (metrics.width > maxWidth && line) {
+ lines.push(line)
+ line = text[i]
+ } else {
+ line = testLine
+ }
+ }
+ if (line) lines.push(line)
+ return lines
+ },
+
+ // 关闭海报弹窗
+ closePosterModal() {
+ this.setData({ showPosterModal: false })
+ },
+
+ // 保存海报到相册
+ savePoster() {
+ wx.canvasToTempFilePath({
+ canvasId: 'posterCanvas',
+ success: (res) => {
+ wx.saveImageToPhotosAlbum({
+ filePath: res.tempFilePath,
+ success: () => {
+ wx.showToast({ title: '已保存到相册', icon: 'success' })
+ this.setData({ showPosterModal: false })
+ },
+ fail: (err) => {
+ if (err.errMsg.includes('auth deny')) {
+ wx.showModal({
+ title: '提示',
+ content: '需要相册权限才能保存海报',
+ confirmText: '去设置',
+ success: (res) => {
+ if (res.confirm) {
+ wx.openSetting()
+ }
+ }
+ })
+ } else {
+ wx.showToast({ title: '保存失败', icon: 'none' })
+ }
+ }
+ })
+ },
+ fail: () => {
+ wx.showToast({ title: '生成图片失败', icon: 'none' })
+ }
+ }, this)
+ },
+
+ // 阻止冒泡
+ stopPropagation() {}
+})
diff --git a/miniprogram2/pages/read/read.json b/miniprogram2/pages/read/read.json
new file mode 100644
index 00000000..d182eac9
--- /dev/null
+++ b/miniprogram2/pages/read/read.json
@@ -0,0 +1,9 @@
+{
+ "usingComponents": {
+ "icon": "/components/icon/icon"
+ },
+ "enablePullDownRefresh": false,
+ "backgroundTextStyle": "light",
+ "backgroundColor": "#000000",
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/read/read.wxml b/miniprogram2/pages/read/read.wxml
new file mode 100644
index 00000000..37375214
--- /dev/null
+++ b/miniprogram2/pages/read/read.wxml
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ←
+
+
+ {{partTitle}}
+ {{chapterTitle}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+ 上一篇
+ {{prevSection.title}}
+
+
+
+
+ 下一篇
+
+ {{nextSection.title}}
+ →
+
+
+
+ 已是最后一篇 🎉
+
+
+
+
+
+
+
+
+ 🖼️
+ 生成海报
+
+
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+ 🔒
+ 登录后继续阅读
+ 已阅读20%,登录后查看完整内容
+
+
+ 立即登录
+
+
+
+
+
+
+
+ 上一篇
+ 章节 {{prevSection.id}}
+
+
+
+
+ 下一篇
+
+ {{nextSection.title}}
+ →
+
+
+
+ 已是最后一篇 🎉
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+ 🔒
+ 解锁完整内容
+ 已阅读20%,购买后继续阅读
+
+
+
+
+
+ 购买本章
+
+ ¥{{section && section.price != null ? section.price : sectionPrice}}
+ ¥{{displaySectionPrice}}
+ 省{{userDiscount}}%
+
+
+ ¥{{section && section.price != null ? section.price : sectionPrice}}
+ 好友链接立省{{userDiscount}}%
+
+ ¥{{section && section.price != null ? section.price : sectionPrice}}
+
+
+
+
+
+ ✨
+ 解锁全部 {{totalSections}} 章
+
+
+ ¥{{fullBookPrice || 9.9}}
+ ¥{{hasReferralDiscount ? displayFullBookPrice : (fullBookPrice || 9.9)}}
+ {{hasReferralDiscount ? '省' + userDiscount + '%' : '省82%'}}
+
+
+
+
+ 分享给好友一起学习,还能赚取佣金
+
+
+
+
+
+
+ 上一篇
+ 章节 {{prevSection.id}}
+
+
+
+
+ 下一篇
+
+ {{nextSection.title}}
+ →
+
+
+
+ 已是最后一篇 🎉
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+ ⚠️
+ 网络异常
+ 无法确认权限,请检查网络后重试
+
+
+ 重新加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💾
+ 保存到相册
+
+
+
+ 长按海报可直接分享到微信
+
+
+
+
+
+
+ ✕
+ 🔐
+ 登录 Soul创业派对
+ 登录后可购买章节、解锁更多内容
+
+
+
+
+ {{agreeProtocol ? '✓' : ''}}
+ 我已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+
+
+
+
+
+ 支付处理中...
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram2/pages/read/read.wxss b/miniprogram2/pages/read/read.wxss
new file mode 100644
index 00000000..b7e15a41
--- /dev/null
+++ b/miniprogram2/pages/read/read.wxss
@@ -0,0 +1,1009 @@
+/**
+ * Soul创业实验 - 阅读页样式
+ * 1:1还原Web版本UI
+ */
+
+.page {
+ min-height: 100vh;
+ background: #000000;
+}
+
+/* ===== 阅读进度条 ===== */
+.progress-bar-fixed {
+ position: fixed;
+ left: 0;
+ right: 0;
+ height: 4rpx;
+ background: #1c1c1e;
+ z-index: 200;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
+ transition: width 0.15s ease;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(40rpx);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.nav-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 24rpx;
+ height: 88rpx;
+}
+
+.nav-back, .nav-right-placeholder {
+ width: 72rpx;
+ height: 72rpx;
+ flex-shrink: 0;
+}
+
+.nav-back {
+ border-radius: 50%;
+ background: #1c1c1e;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nav-right-placeholder {
+ /* 占位保持标题居中 */
+}
+
+.back-arrow {
+ font-size: 36rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.nav-info {
+ flex: 1;
+ text-align: center;
+ padding: 0 16rpx;
+}
+
+.nav-part {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.4);
+ display: block;
+}
+
+.nav-chapter {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nav-placeholder {
+ width: 100%;
+}
+
+/* ===== 阅读内容 ===== */
+.read-content {
+ max-width: 750rpx;
+ margin: 0 auto;
+ padding: 48rpx 40rpx 200rpx;
+}
+
+/* ===== 章节标题 ===== */
+.chapter-header {
+ margin-bottom: 48rpx;
+}
+
+.chapter-meta {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 24rpx;
+}
+
+.chapter-id {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #00CED1;
+ background: rgba(0, 206, 209, 0.1);
+ padding: 8rpx 24rpx;
+ border-radius: 32rpx;
+}
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ padding: 6rpx 16rpx;
+ min-width: 80rpx;
+ border-radius: 8rpx;
+ box-sizing: border-box;
+ text-align: center;
+}
+
+.tag-free {
+ background: rgba(0, 206, 209, 0.1);
+ color: #00CED1;
+}
+
+.chapter-title {
+ font-size: 44rpx;
+ font-weight: 700;
+ color: #ffffff;
+ line-height: 1.4;
+}
+
+/* ===== 加载状态 ===== */
+.loading-state {
+ display: flex;
+ flex-direction: column;
+ gap: 32rpx;
+}
+
+.skeleton {
+ height: 32rpx;
+ background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
+ background-size: 200% 100%;
+ animation: skeleton-loading 1.5s ease-in-out infinite;
+ border-radius: 8rpx;
+}
+
+.skeleton-1 { width: 75%; }
+.skeleton-2 { width: 90%; }
+.skeleton-3 { width: 65%; }
+.skeleton-4 { width: 85%; }
+.skeleton-5 { width: 70%; }
+
+@keyframes skeleton-loading {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+/* ===== 文章内容 ===== */
+.article {
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 34rpx;
+ line-height: 1.9;
+}
+
+.paragraph {
+ margin-bottom: 48rpx;
+ text-align: justify;
+}
+
+.preview {
+ position: relative;
+}
+
+/* ===== 渐变遮罩 ===== */
+.fade-mask {
+ position: absolute;
+ bottom: 0;
+ left: -40rpx;
+ right: -40rpx;
+ height: 300rpx;
+ background: linear-gradient(to top, #000000 0%, transparent 100%);
+ pointer-events: none;
+}
+
+/* ===== 付费墙 ===== */
+.paywall {
+ position: relative;
+ z-index: 10;
+ margin-top: 48rpx;
+ padding: 48rpx;
+ background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
+ border-radius: 32rpx;
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+}
+
+.paywall-icon {
+ width: 128rpx;
+ height: 128rpx;
+ margin: 0 auto 32rpx;
+ background: rgba(0, 206, 209, 0.1);
+ border-radius: 32rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 64rpx;
+}
+
+.paywall-title {
+ font-size: 40rpx;
+ font-weight: 600;
+ color: #ffffff;
+ text-align: center;
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.paywall-desc {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ text-align: center;
+ display: block;
+ margin-bottom: 48rpx;
+}
+
+/* ===== 购买选项 ===== */
+.purchase-options {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.purchase-btn {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 28rpx 32rpx;
+ border-radius: 24rpx;
+}
+
+.purchase-section {
+ background: #2c2c2e;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.purchase-fullbook {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.3);
+}
+
+.purchase-section .btn-label {
+ font-size: 28rpx;
+ color: #ffffff;
+}
+
+.purchase-section .btn-price {
+ font-size: 28rpx;
+ font-weight: 600;
+}
+
+.purchase-section .btn-price-row {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.btn-original-price {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+ text-decoration: line-through;
+}
+
+.btn-discount-tag {
+ font-size: 22rpx;
+ color: #00CED1;
+}
+
+.brand-color {
+ color: #00CED1;
+}
+
+.purchase-fullbook .btn-left {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.purchase-fullbook .btn-sparkle {
+ font-size: 28rpx;
+}
+
+.purchase-fullbook .btn-label {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+}
+
+.purchase-fullbook .btn-right {
+ text-align: right;
+}
+
+.purchase-fullbook .btn-price {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.purchase-fullbook .btn-discount {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.7);
+ margin-left: 8rpx;
+}
+
+.paywall-tip {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ text-align: center;
+ display: block;
+}
+
+/* ===== 章节导航 ===== */
+.chapter-nav {
+ margin-top: 96rpx;
+ padding-top: 64rpx;
+ border-top: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.nav-buttons {
+ display: flex;
+ gap: 24rpx;
+ margin-bottom: 48rpx;
+}
+
+.nav-btn {
+ flex: 1;
+ padding: 24rpx;
+ border-radius: 24rpx;
+ max-width: 48%;
+}
+
+.nav-btn-placeholder {
+ flex: 1;
+ max-width: 48%;
+}
+
+.nav-prev {
+ background: #1c1c1e;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+}
+
+.nav-next {
+ background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.1) 100%);
+ border: 2rpx solid rgba(0, 206, 209, 0.2);
+}
+
+.nav-end {
+ background: #1c1c1e;
+ border: 2rpx solid rgba(255, 255, 255, 0.05);
+ text-align: center;
+}
+
+.nav-disabled {
+ opacity: 0.5;
+}
+
+.btn-label {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.5);
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.nav-next .btn-label {
+ color: #00CED1;
+}
+
+.btn-title {
+ font-size: 24rpx;
+ color: #ffffff;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.btn-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.btn-arrow {
+ font-size: 24rpx;
+ color: #00CED1;
+ flex-shrink: 0;
+ margin-left: 8rpx;
+}
+
+.btn-end-text {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 分享操作区 ===== */
+.action-section {
+ margin-top: 48rpx;
+}
+
+.action-row-inline {
+ display: flex;
+ gap: 16rpx;
+}
+
+.action-btn-inline {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8rpx;
+ padding: 24rpx 16rpx;
+ border-radius: 16rpx;
+ border: none;
+ background: transparent;
+ line-height: normal;
+ box-sizing: border-box;
+}
+
+.action-btn-inline::after {
+ border: none;
+}
+
+.btn-share-inline {
+ background: rgba(7, 193, 96, 0.15);
+ border: 2rpx solid rgba(7, 193, 96, 0.3);
+}
+
+.btn-poster-inline {
+ background: rgba(255, 215, 0, 0.15);
+ border: 2rpx solid rgba(255, 215, 0, 0.3);
+}
+
+
+.action-icon-small {
+ font-size: 28rpx;
+}
+
+.action-text-small {
+ font-size: 24rpx;
+ color: #ffffff;
+ font-weight: 500;
+}
+
+/* ===== 推广提示区 ===== */
+.promo-section {
+ margin-top: 32rpx;
+}
+
+.promo-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 32rpx;
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%);
+ border: 2rpx solid rgba(255, 215, 0, 0.2);
+ border-radius: 24rpx;
+}
+
+.promo-left {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+}
+
+.promo-icon {
+ width: 80rpx;
+ height: 80rpx;
+ background: rgba(255, 215, 0, 0.2);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36rpx;
+}
+
+.promo-info {
+ flex: 1;
+}
+
+.promo-title {
+ font-size: 30rpx;
+ color: #ffffff;
+ font-weight: 600;
+ display: block;
+}
+
+.promo-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+ display: block;
+ margin-top: 8rpx;
+}
+
+.promo-right {
+ padding-left: 20rpx;
+}
+
+.promo-arrow {
+ font-size: 32rpx;
+ color: #FFD700;
+}
+
+/* ===== 弹窗 ===== */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(20rpx);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ width: 100%;
+ max-width: 750rpx;
+ background: #1c1c1e;
+ border-radius: 48rpx 48rpx 0 0;
+ padding: 48rpx;
+ padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
+ animation: slideUp 0.3s ease;
+}
+
+@keyframes slideUp {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 32rpx;
+}
+
+.modal-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.modal-close {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 分享弹窗 ===== */
+.share-link-box {
+ padding: 32rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+ border-radius: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.link-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.link-url {
+ font-size: 26rpx;
+ color: #00CED1;
+ display: block;
+ word-break: break-all;
+ font-family: monospace;
+}
+
+.link-tip {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ display: block;
+ margin-top: 16rpx;
+}
+
+.share-buttons {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.share-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16rpx;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 24rpx;
+ border: none;
+ line-height: normal;
+}
+
+.share-btn::after {
+ border: none;
+}
+
+.share-btn-icon {
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40rpx;
+}
+
+.icon-copy {
+ background: rgba(0, 206, 209, 0.2);
+}
+
+.icon-wechat {
+ background: rgba(7, 193, 96, 0.2);
+ color: #07C160;
+ font-size: 32rpx;
+}
+
+.icon-poster {
+ background: rgba(255, 215, 0, 0.2);
+}
+
+.share-btn-text {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 支付弹窗 ===== */
+.payment-info {
+ padding: 24rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 24rpx;
+ margin-bottom: 32rpx;
+}
+
+.payment-type {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.payment-amount {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.amount-label {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.amount-value {
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #00CED1;
+}
+
+.payment-methods {
+ margin-bottom: 32rpx;
+}
+
+.method-item {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ padding: 24rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 16rpx;
+ border: 2rpx solid transparent;
+}
+
+.method-active {
+ border-color: #07C160;
+}
+
+.method-icon {
+ width: 48rpx;
+ height: 48rpx;
+ background: #07C160;
+ color: #ffffff;
+ font-size: 24rpx;
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.method-name {
+ flex: 1;
+ font-size: 28rpx;
+ color: #ffffff;
+}
+
+.method-check {
+ color: #07C160;
+ font-size: 28rpx;
+}
+
+.btn-primary {
+ width: 100%;
+ padding: 28rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #ffffff;
+ font-size: 32rpx;
+ font-weight: 600;
+ border-radius: 24rpx;
+ text-align: center;
+ margin-bottom: 16rpx;
+}
+
+.payment-notice {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.3);
+ text-align: center;
+ display: block;
+}
+
+/* ===== 登录提示 ===== */
+.login-prompt {
+ margin-top: 32rpx;
+}
+
+.login-btn {
+ padding: 28rpx;
+ background: rgba(255, 255, 255, 0.1);
+ border: 2rpx solid rgba(255, 255, 255, 0.2);
+ border-radius: 24rpx;
+ text-align: center;
+}
+
+.login-btn-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* ===== 登录弹窗 ===== */
+.login-modal {
+ padding: 48rpx 32rpx;
+ text-align: center;
+}
+
+.login-icon {
+ font-size: 80rpx;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.login-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.login-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.5);
+ display: block;
+ margin-bottom: 48rpx;
+}
+
+.btn-wechat {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 28rpx;
+ background: #07C160;
+ color: #ffffff;
+ font-size: 30rpx;
+ font-weight: 600;
+ border-radius: 24rpx;
+ margin-bottom: 20rpx;
+ border: none;
+}
+
+.btn-wechat::after {
+ border: none;
+}
+
+.btn-wechat-icon {
+ width: 40rpx;
+ height: 40rpx;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 8rpx;
+ font-size: 24rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn-phone {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ padding: 28rpx;
+ background: rgba(255, 255, 255, 0.05);
+ color: #ffffff;
+ font-size: 30rpx;
+ border-radius: 24rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.1);
+}
+
+.btn-phone::after {
+ border: none;
+}
+
+.btn-phone-icon {
+ font-size: 32rpx;
+}
+
+.login-agree-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ margin-top: 32rpx;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+.agree-checkbox {
+ width: 32rpx;
+ height: 32rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.5);
+ border-radius: 6rpx;
+ margin-right: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ color: #fff;
+ flex-shrink: 0;
+}
+.agree-checked {
+ background: #00CED1;
+ border-color: #00CED1;
+}
+.agree-text { color: rgba(255, 255, 255, 0.6); }
+.agree-link {
+ color: #00CED1;
+ text-decoration: underline;
+ padding: 0 4rpx;
+}
+.btn-wechat-disabled { opacity: 0.6; }
+
+/* ===== 支付中加载 ===== */
+.loading-box {
+ background: rgba(0, 0, 0, 0.8);
+ border-radius: 24rpx;
+ padding: 48rpx 64rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.loading-spinner {
+ width: 64rpx;
+ height: 64rpx;
+ border: 4rpx solid rgba(255, 255, 255, 0.2);
+ border-top-color: #00CED1;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* ===== 海报弹窗 ===== */
+.poster-modal {
+ padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
+}
+
+.poster-preview {
+ display: flex;
+ justify-content: center;
+ margin: 32rpx 0;
+ padding: 24rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 24rpx;
+}
+
+.poster-canvas {
+ border-radius: 16rpx;
+ box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
+}
+
+/* 分享卡片 canvas:离屏绘制,不展示给用户 */
+.share-card-canvas {
+ position: fixed;
+ left: -600px;
+ top: 0;
+ z-index: -1;
+}
+
+.poster-actions {
+ display: flex;
+ gap: 24rpx;
+ margin-bottom: 24rpx;
+}
+
+.poster-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ padding: 28rpx;
+ border-radius: 24rpx;
+ font-size: 30rpx;
+ font-weight: 500;
+}
+
+.btn-save {
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ color: #ffffff;
+}
+
+.btn-icon {
+ font-size: 32rpx;
+}
+
+.poster-tip {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+ text-align: center;
+ display: block;
+}
+
+/* ===== 右下角悬浮分享按钮 ===== */
+.fab-share {
+ position: fixed;
+ right: 32rpx;
+ width:70rpx!important;
+ bottom: calc(120rpx + env(safe-area-inset-bottom));
+ height: 70rpx;
+ border-radius: 60rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
+ padding: 0;
+ margin: 0;
+ border: none;
+ z-index: 9999;
+ display:flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.fab-share::after {
+ border: none;
+}
+
+.fab-share:active {
+ transform: scale(0.95);
+ box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
+}
+
+.fab-icon {
+ padding:16rpx;
+ width: 50rpx;
+ height: 50rpx;
+ display: block;
+}
+
diff --git a/miniprogram2/pages/referral/earnings-detail-styles.wxss b/miniprogram2/pages/referral/earnings-detail-styles.wxss
new file mode 100644
index 00000000..a64f5b59
--- /dev/null
+++ b/miniprogram2/pages/referral/earnings-detail-styles.wxss
@@ -0,0 +1,182 @@
+/* ===================================
+ 收益明细卡片样式 - 重构版
+ 创建时间:2026-02-04
+ 说明:修复布局错乱问题,优化文本显示
+ =================================== */
+
+/* 收益明细卡片容器 */
+.earnings-detail-card {
+ background: rgba(28, 28, 30, 0.8);
+ backdrop-filter: blur(40rpx);
+ border: 2rpx solid rgba(255,255,255,0.1);
+ border-radius: 32rpx;
+ overflow: hidden;
+ margin-bottom: 24rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* 卡片头部 */
+.detail-header {
+ padding: 40rpx 40rpx 24rpx;
+ border-bottom: 2rpx solid rgba(255,255,255,0.05);
+}
+
+.detail-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #fff;
+}
+
+/* 列表容器 */
+.detail-list {
+ max-height: 480rpx;
+ overflow-y: auto;
+ padding: 16rpx 0;
+}
+
+/* ===================================
+ 收益明细列表项 - 核心样式
+ =================================== */
+
+/* 列表项容器 - 使用flex布局 */
+.earnings-detail-card .detail-item {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ padding: 24rpx 40rpx;
+ background: transparent;
+ border-bottom: 2rpx solid rgba(255,255,255,0.03);
+ transition: background 0.3s;
+}
+
+.earnings-detail-card .detail-item:last-child {
+ border-bottom: none;
+}
+
+.earnings-detail-card .detail-item:active {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+/* 头像容器 - 固定宽度,不收缩 */
+.earnings-detail-card .detail-avatar-wrap {
+ width: 88rpx;
+ height: 88rpx;
+ flex-shrink: 0;
+}
+
+.earnings-detail-card .detail-avatar {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ border: 2rpx solid rgba(56, 189, 172, 0.2);
+ display: block;
+}
+
+.earnings-detail-card .detail-avatar-text {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+/* 详细信息容器 - 占据剩余空间,允许收缩 */
+.earnings-detail-card .detail-content {
+ flex: 1;
+ min-width: 0; /* 关键:允许flex子元素收缩到比内容更小 */
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+}
+
+/* 顶部行:昵称 + 金额 */
+.earnings-detail-card .detail-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16rpx;
+}
+
+/* 买家昵称 - 允许收缩,显示省略号 */
+.earnings-detail-card .detail-buyer {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #ffffff;
+ flex: 1;
+ min-width: 0; /* 关键:允许收缩 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* 佣金金额 - 固定宽度,不收缩 */
+.earnings-detail-card .detail-amount {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: #38bdac;
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+/* 商品信息行:书名 + 章节 */
+.earnings-detail-card .detail-product {
+ display: flex;
+ align-items: baseline;
+ gap: 4rpx;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+ min-width: 0; /* 关键:允许收缩 */
+ overflow: hidden;
+}
+
+/* 书名 - 限制最大宽度,显示省略号 */
+.earnings-detail-card .detail-book {
+ color: rgba(255, 255, 255, 0.7);
+ font-weight: 500;
+ max-width: 50%; /* 限制书名最多占一半宽度 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-shrink: 1;
+}
+
+/* 章节 - 占据剩余空间,显示省略号 */
+.earnings-detail-card .detail-chapter {
+ color: rgba(255, 255, 255, 0.5);
+ flex: 1;
+ min-width: 0; /* 关键:允许收缩 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* 时间信息 */
+.earnings-detail-card .detail-time {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* ===================================
+ 响应式优化
+ =================================== */
+
+/* 小屏幕优化 */
+@media (max-width: 375px) {
+ .earnings-detail-card .detail-buyer {
+ font-size: 26rpx;
+ }
+
+ .earnings-detail-card .detail-amount {
+ font-size: 30rpx;
+ }
+
+ .earnings-detail-card .detail-book {
+ max-width: 45%;
+ }
+}
diff --git a/miniprogram2/pages/referral/referral-temp.wxss b/miniprogram2/pages/referral/referral-temp.wxss
new file mode 100644
index 00000000..7c3edc4d
--- /dev/null
+++ b/miniprogram2/pages/referral/referral-temp.wxss
@@ -0,0 +1,379 @@
+/* ???????? - 1:1??Web?? */
+.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
+
+/* ??? */
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-left { display: flex; gap: 16rpx; align-items: center; }
+.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
+.nav-right-placeholder { width: 144rpx; }
+.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+
+.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
+
+/* ?????? */
+.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
+.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
+.banner-content { flex: 1; }
+.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
+.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
+
+/* ???? - ?? Next.js */
+.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
+.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
+.earnings-main { position: relative; }
+.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
+.earnings-left { display: flex; align-items: center; gap: 16rpx; }
+.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
+.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
+.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
+.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
+.earnings-right { text-align: right; }
+.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
+.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
+
+.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
+.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
+
+/* ???? - ?? Next.js 4??? */
+.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
+.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
+.stat-value.orange { color: #FFA500; }
+.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
+
+/* ????? */
+.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
+.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
+.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* ???? - ?? Next.js */
+.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
+.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
+.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
+.rules-list { padding-left: 8rpx; }
+.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
+.rule-item .gold { color: #FFD700; font-weight: 500; }
+.rule-item .brand { color: #00CED1; font-weight: 500; }
+.rule-item .orange { color: #FFA500; font-weight: 500; }
+
+/* ?????? - ?? Next.js */
+.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-title { display: flex; align-items: center; gap: 12rpx; }
+.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
+.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* Tab?? */
+.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
+.tab-item.tab-active { color: #00CED1; }
+.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
+
+/* ???? */
+.binding-list { max-height: 640rpx; overflow-y: auto; }
+.empty-state { padding: 80rpx 0; text-align: center; }
+.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
+.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-item:last-child { border-bottom: none; }
+.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
+.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
+.user-info { flex: 1; }
+.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.user-status { text-align: right; }
+.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
+.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
+.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
+.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
+.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
+
+/* ????? - ?? Next.js */
+.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
+.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
+.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
+.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
+.invite-tip .gold { color: #FFD700; }
+.invite-tip .brand { color: #00CED1; }
+
+/* ?????? - ?? Next.js */
+.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.detail-list { max-height: 480rpx; overflow-y: auto; }
+
+.detail-item:last-child { border-bottom: none; }
+.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
+.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.detail-info { flex: 1; }
+.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
+
+/* ???? - ?? Next.js */
+.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
+.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
+.share-item::after { border: none; }
+.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
+.share-icon.poster { background: rgba(103,58,183,0.2); }
+.share-icon.wechat { background: rgba(7,193,96,0.2); }
+.share-icon.link { background: rgba(158,158,158,0.2); }
+.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
+.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
+.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
+.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-info { flex: 1; text-align: left; }
+.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
+.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
+.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
+
+/* ?????????????? + ???? + ???????? */
+/* ???????? backdrop-filter??????????????? */
+.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
+
+.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
+.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
+
+/* ???? */
+.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
+.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
+
+/* ???? */
+/* ???????? filter: blur ??????????????? + ???? */
+.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
+.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
+.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
+.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
+
+/* ???? */
+.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
+.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
+.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
+.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
+
+/* ?? */
+.poster-title { margin-bottom: 8rpx; }
+.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
+.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
+.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
+
+/* ???? */
+.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
+.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
+.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
+.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
+.poster-stat-gold { color: #FFD700; }
+.poster-stat-brand { color: #00CED1; }
+.poster-stat-pink { color: #E91E63; }
+
+/* ?? */
+.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
+.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
+
+/* ??? */
+.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
+.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
+.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
+.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
+
+/* ??? */
+.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
+.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
+.poster-discount-highlight { color: #00CED1; font-weight: 800; }
+
+/* ??? */
+.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
+.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
+.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
+.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
+
+/* ??????? */
+.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
+.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
+.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
+
+
+
+/* ????- ?? Next.js */
+.empty-earnings { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 64rpx 40rpx; text-align: center; margin-bottom: 24rpx; }
+.empty-icon-wrapper { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(28, 28, 30, 0.8); display: flex; align-items: center; justify-content: center; margin: 0 auto 32rpx; }
+.empty-gift-icon { width: 64rpx; height: 64rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.empty-title { font-size: 30rpx; font-weight: 500; color: #fff; display: block; margin-bottom: 16rpx; }
+.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; line-height: 1.5; }
+
+
+ / * = = = = = TrGm? ? = = = = = * /
+ . l o a d i n g - o v e r l a y {
+ p o s i t i o n : f i x e d ;
+ t o p : 0 ;
+ l e f t : 0 ;
+ r i g h t : 0 ;
+ b o t t o m : 0 ;
+ b a c k g r o u n d : r g b a ( 0 , 0 , 0 , 0 . 7 ) ;
+ b a c k d r o p - f i l t e r : b l u r ( 1 0 r p x ) ;
+ z - i n d e x : 9 9 9 ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ }
+
+ . l o a d i n g - c o n t e n t {
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ }
+
+ . l o a d i n g - s p i n n e r {
+ w i d t h : 8 0 r p x ;
+ h e i g h t : 8 0 r p x ;
+ b o r d e r : 6 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ b o r d e r - t o p - c o l o r : # 3 8 b d a c ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ a n i m a t i o n : s p i n 1 s l i n e a r i n f i n i t e ;
+ }
+
+ @ k e y f r a m e s s p i n {
+ 0 % { t r a n s f o r m : r o t a t e ( 0 d e g ) ; }
+ 1 0 0 % { t r a n s f o r m : r o t a t e ( 3 6 0 d e g ) ; }
+ }
+
+ . l o a d i n g - t e x t {
+ f o n t - s i z e : 2 8 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 8 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ }
+
+ . c o n t e n t - l o a d i n g {
+ o p a c i t y : 0 . 3 ;
+ p o i n t e r - e v e n t s : n o n e ;
+ }
+
+ / * = = = = = ARlē^|op]͓\!} = = = = = * /
+ . d e t a i l - i t e m {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ p a d d i n g : 2 4 r p x ;
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ;
+ b o r d e r - r a d i u s : 1 6 r p x ;
+ m a r g i n - b o t t o m : 1 6 r p x ;
+ t r a n s i t i o n : a l l 0 . 3 s ;
+ }
+
+ . d e t a i l - i t e m : a c t i v e {
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 5 ) ;
+ }
+
+ . d e t a i l - a v a t a r - w r a p {
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a v a t a r {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b o r d e r : 2 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ }
+
+ . d e t a i l - a v a t a r - t e x t {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ f o n t - s i z e : 3 6 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # f f f f f f ;
+ }
+
+ . d e t a i l - c o n t e n t {
+ f l e x : 1 ;
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ g a p : 8 r p x ;
+ m i n - w i d t h : 0 ;
+ }
+
+ . d e t a i l - t o p {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ;
+ g a p : 1 6 r p x ;
+ }
+
+ . d e t a i l - b u y e r {
+ f o n t - s i z e : 2 8 r p x ;
+ f o n t - w e i g h t : 5 0 0 ;
+ c o l o r : # f f f f f f ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a m o u n t {
+ f o n t - s i z e : 3 2 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # 3 8 b d a c ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - p r o d u c t {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ f o n t - s i z e : 2 4 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - b o o k {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - c h a p t e r {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - t i m e {
+ f o n t - s i z e : 2 2 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ;
+ }
+
+ / * = = = ARlē^|͓\!}m2? = = = * /
+ . d e t a i l - i t e m { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; g a p : 2 4 r p x ; p a d d i n g : 2 4 r p x ; b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ; b o r d e r - r a d i u s : 1 6 r p x ; m a r g i n - b o t t o m : 1 6 r p x ; }
+ . d e t a i l - a v a t a r - w r a p { f l e x - s h r i n k : 0 ; }
+ . d e t a i l - a v a t a r { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; }
+ . d e t a i l - a v a t a r - t e x t { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ; d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : c e n t e r ; f o n t - s i z e : 3 6 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # f f f f f f ; }
+ . d e t a i l - c o n t e n t { f l e x : 1 ; d i s p l a y : f l e x ; f l e x - d i r e c t i o n : c o l u m n ; g a p : 8 r p x ; m i n - w i d t h : 0 ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - t o p { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ; g a p : 1 6 r p x ; }
+ . d e t a i l - b u y e r { f o n t - s i z e : 2 8 r p x ; f o n t - w e i g h t : 5 0 0 ; c o l o r : # f f f f f f ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; }
+ . d e t a i l - a m o u n t { f o n t - s i z e : 3 2 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # 3 8 b d a c ; f l e x - s h r i n k : 0 ; }
+ . d e t a i l - p r o d u c t { d i s p l a y : f l e x ; a l i g n - i t e m s : b a s e l i n e ; f o n t - s i z e : 2 4 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - b o o k { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ; f o n t - w e i g h t : 5 0 0 ; m a x - w i d t h : 2 0 0 r p x ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; f l e x - s h r i n k : 1 ; }
+ . d e t a i l - c h a p t e r { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; m a r g i n - l e f t : 4 r p x ; }
+ . d e t a i l - t i m e { f o n t - s i z e : 2 2 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ; }
+
+
diff --git a/miniprogram2/pages/referral/referral.js b/miniprogram2/pages/referral/referral.js
new file mode 100644
index 00000000..d5bd3a5c
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.js
@@ -0,0 +1,856 @@
+/**
+ * Soul创业派对 - 分销中心页
+ *
+ * 可见数据:
+ * - 绑定用户数(当前有效绑定)
+ * - 通过链接进的人数(总访问量)
+ * - 带来的付款人数(已转化购买)
+ * - 收益统计(90%归分发者)
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ isLoggedIn: false,
+ userInfo: null,
+
+ // === 核心可见数据 ===
+ bindingCount: 0, // 绑定用户数(当前有效)
+ visitCount: 0, // 通过链接进的人数
+ paidCount: 0, // 带来的付款人数
+ unboughtCount: 0, // 待购买人数(绑定但未付款)
+ expiredCount: 0, // 已过期人数
+
+ // === 收益数据 ===
+ totalCommission: 0, // 累计佣金总额(所有获得的佣金)
+ availableEarnings: 0, // 可提现金额(未申请提现的佣金)- 字符串格式用于显示
+ availableEarningsNum: 0, // 可提现金额 - 数字格式用于判断
+ pendingWithdrawAmount: 0, // 待审核金额(已申请提现但未审核)
+ withdrawnEarnings: 0, // 已提现金额
+ earnings: 0, // 已结算收益(保留兼容)
+ pendingEarnings: 0, // 待结算收益(保留兼容)
+ shareRate: 90, // 分成比例(90%),从 referral/data 或 config 获取
+ minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取
+ withdrawFee: 5, // 提现手续费%,从 referral/data 获取
+ bindingDays: 30, // 绑定期天数,从 referral/data 获取
+ userDiscount: 5, // 好友购买优惠%,从 referral/data 获取
+ hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置)
+
+ // === 统计数据 ===
+ referralCount: 0, // 总推荐人数
+ expiringCount: 0, // 即将过期人数
+
+ // 邀请码
+ referralCode: '',
+
+ // 绑定用户列表
+ showBindingList: true,
+ activeTab: 'active',
+ activeBindings: [],
+ convertedBindings: [],
+ expiredBindings: [],
+ currentBindings: [],
+ totalBindings: 0,
+
+ // 收益明细
+ earningsDetails: [],
+
+ // 海报
+ showPosterModal: false,
+ isGeneratingPoster: false,
+ posterQrSrc: '',
+ posterQrFilePath: '',
+ posterReferralLink: '',
+ posterNickname: '',
+ posterNicknameInitial: '',
+ posterCaseCount: 62,
+
+ },
+
+ onLoad() {
+ this.setData({ statusBarHeight: app.globalData.statusBarHeight })
+ this.initData()
+ },
+
+ onShow() {
+ // 从设置页返回时同步微信号绑定状态,便于提现按钮立即更新
+ const hasWechatId = !!(app.globalData.userInfo?.wechat || app.globalData.userInfo?.wechatId || wx.getStorageSync('user_wechat'))
+ this.setData({ hasWechatId })
+ this.initData()
+ },
+
+ // 初始化数据
+ async initData() {
+ const { isLoggedIn, userInfo } = app.globalData
+ if (isLoggedIn && userInfo) {
+ // 显示加载提示
+ wx.showLoading({
+ title: '加载中...',
+ mask: true // 防止触摸穿透
+ })
+
+ // 生成邀请码
+ const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
+
+ console.log('[Referral] 开始加载分销数据,userId:', userInfo.id)
+
+ // 从API获取真实数据
+ let realData = null
+ try {
+ // app.request 第一个参数是 URL 字符串(会自动拼接 baseUrl)
+ const res = await app.request('/api/miniprogram/referral/data?userId=' + userInfo.id)
+ console.log('[Referral] API返回:', JSON.stringify(res).substring(0, 200))
+
+ if (res && res.success && res.data) {
+ realData = res.data
+ console.log('[Referral] ✅ 获取推广数据成功')
+ console.log('[Referral] - bindingCount:', realData.bindingCount)
+ console.log('[Referral] - paidCount:', realData.paidCount)
+ console.log('[Referral] - earnings:', realData.earnings)
+ console.log('[Referral] - expiringCount:', realData.stats?.expiringCount)
+ } else {
+ console.log('[Referral] ❌ API返回格式错误:', res?.error || 'unknown')
+ }
+ } catch (e) {
+ console.log('[Referral] ❌ API调用失败:', e.message || e)
+ console.log('[Referral] 错误详情:', e)
+ }
+
+ // 使用真实数据或默认值
+ let activeBindings = realData?.activeUsers || []
+ let convertedBindings = realData?.convertedUsers || []
+ let expiredBindings = realData?.expiredUsers || []
+
+ console.log('[Referral] activeBindings:', activeBindings.length)
+ console.log('[Referral] convertedBindings:', convertedBindings.length)
+ console.log('[Referral] expiredBindings:', expiredBindings.length)
+
+ // 计算即将过期的数量(7天内)
+ const expiringCount = realData?.stats?.expiringCount || activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
+
+ console.log('[Referral] expiringCount:', expiringCount)
+
+ // 计算各类统计
+ const bindingCount = realData?.bindingCount || activeBindings.length
+ const paidCount = realData?.paidCount || convertedBindings.length
+ const expiredCount = realData?.expiredCount || expiredBindings.length
+ const unboughtCount = bindingCount - paidCount // 绑定中但未付款的
+
+ // 格式化用户数据
+ const formatUser = (user, type) => {
+ const formatted = {
+ id: user.id,
+ nickname: user.nickname || '用户' + (user.id || '').slice(-4),
+ avatar: user.avatar,
+ status: type,
+ daysRemaining: user.daysRemaining || 0,
+ bindingDate: user.bindingDate ? this.formatDate(user.bindingDate) : '--',
+ expiryDate: user.expiryDate ? this.formatDate(user.expiryDate) : '--',
+ commission: (user.commission || 0).toFixed(2),
+ orderAmount: (user.orderAmount || 0).toFixed(2),
+ purchaseCount: user.purchaseCount || 0,
+ conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
+ }
+ console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
+ return formatted
+ }
+
+ // 格式化金额(保留两位小数)
+ const formatMoney = (num) => {
+ return typeof num === 'number' ? num.toFixed(2) : '0.00'
+ }
+
+ // ✅ 可提现金额 = 累计佣金 - 已提现金额 - 待审核金额,且不低于 0(防止数据不同步时出现负数)
+ const totalCommissionNum = realData?.totalCommission || 0
+ const withdrawnNum = realData?.withdrawnEarnings || 0
+ const pendingWithdrawNum = realData?.pendingWithdrawAmount || 0
+ const availableEarningsNum = Math.max(0, totalCommissionNum - withdrawnNum - pendingWithdrawNum)
+ const minWithdrawAmount = realData?.minWithdrawAmount || 10
+
+ console.log('=== [Referral] 收益计算(完整版)===')
+ console.log('累计佣金 (totalCommission):', totalCommissionNum)
+ console.log('已提现金额 (withdrawnEarnings):', withdrawnNum)
+ console.log('待审核金额 (pendingWithdrawAmount):', pendingWithdrawNum)
+ console.log('可提现金额 = 累计 - 已提现 - 待审核 =', totalCommissionNum, '-', withdrawnNum, '-', pendingWithdrawNum, '=', availableEarningsNum)
+ console.log('最低提现金额 (minWithdrawAmount):', minWithdrawAmount)
+ console.log('按钮判断:', availableEarningsNum, '>=', minWithdrawAmount, '=', availableEarningsNum >= minWithdrawAmount)
+ console.log('✅ 按钮应该:', availableEarningsNum >= minWithdrawAmount ? '🟢 启用(绿色)' : '⚫ 禁用(灰色)')
+
+ const hasWechatId = !!(userInfo?.wechat || userInfo?.wechatId || wx.getStorageSync('user_wechat'))
+ this.setData({
+ isLoggedIn: true,
+ userInfo,
+ hasWechatId,
+
+ // 核心可见数据
+ bindingCount,
+ visitCount: realData?.visitCount || 0,
+ paidCount,
+ unboughtCount: expiringCount, // "即将过期"显示的是 expiringCount
+ expiredCount,
+
+ // 收益数据 - 格式化为两位小数
+ totalCommission: formatMoney(totalCommissionNum),
+ availableEarnings: formatMoney(availableEarningsNum), // ✅ 使用计算后的可提现金额
+ availableEarningsNum: availableEarningsNum, // ✅ 数字格式用于按钮判断
+ pendingWithdrawAmount: formatMoney(pendingWithdrawNum),
+ withdrawnEarnings: formatMoney(realData?.withdrawnEarnings || 0),
+ earnings: formatMoney(realData?.earnings || 0),
+ pendingEarnings: formatMoney(realData?.pendingEarnings || 0),
+ shareRate: realData?.shareRate ?? 90,
+ minWithdrawAmount: minWithdrawAmount,
+ withdrawFee: realData?.withdrawFee ?? 5,
+ bindingDays: realData?.bindingDays ?? 30,
+ userDiscount: realData?.userDiscount ?? 5,
+
+ // 统计
+ referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
+ expiringCount,
+
+ referralCode,
+ activeBindings: activeBindings.map(u => formatUser(u, 'active')),
+ convertedBindings: convertedBindings.map(u => formatUser(u, 'converted')),
+ expiredBindings: expiredBindings.map(u => formatUser(u, 'expired')),
+ currentBindings: activeBindings.map(u => formatUser(u, 'active')),
+ totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length,
+
+ // 收益明细
+ earningsDetails: (realData?.earningsDetails || []).map(item => {
+ // 解析商品描述,获取书名和章节
+ const productInfo = this.parseProductDescription(item.description, item.productType)
+
+ return {
+ id: item.id,
+ productType: item.productType,
+ bookTitle: productInfo.bookTitle,
+ chapterTitle: productInfo.chapterTitle,
+ commission: (item.commission || 0).toFixed(2),
+ payTime: item.payTime ? this.formatDate(item.payTime) : '--',
+ buyerNickname: item.buyerNickname || '用户',
+ buyerAvatar: item.buyerAvatar
+ }
+ })
+ })
+
+
+ console.log('[Referral] ✅ 数据设置完成')
+ console.log('[Referral] - 绑定中:', this.data.bindingCount)
+ console.log('[Referral] - 即将过期:', this.data.expiringCount)
+ console.log('[Referral] - 收益:', this.data.earnings)
+
+ console.log('=== [Referral] 按钮状态验证 ===')
+ console.log('累计佣金 (totalCommission):', this.data.totalCommission)
+ console.log('待审核金额 (pendingWithdrawAmount):', this.data.pendingWithdrawAmount)
+ console.log('可提现金额 (availableEarnings 显示):', this.data.availableEarnings)
+ console.log('可提现金额 (availableEarningsNum 判断):', this.data.availableEarningsNum, typeof this.data.availableEarningsNum)
+ console.log('最低提现金额 (minWithdrawAmount):', this.data.minWithdrawAmount, typeof this.data.minWithdrawAmount)
+ console.log('按钮启用条件:', this.data.availableEarningsNum, '>=', this.data.minWithdrawAmount, '=', this.data.availableEarningsNum >= this.data.minWithdrawAmount)
+ console.log('✅ 最终结果: 按钮应该', this.data.availableEarningsNum >= this.data.minWithdrawAmount ? '🟢 启用' : '⚫ 禁用')
+
+ // 隐藏加载提示
+ wx.hideLoading()
+ } else {
+ // 未登录时也隐藏loading
+ this.setData({ isLoading: false })
+ }
+ },
+
+ // 切换Tab
+ switchTab(e) {
+ const tab = e.currentTarget.dataset.tab
+ let currentBindings = []
+
+ if (tab === 'active') {
+ currentBindings = this.data.activeBindings
+ } else if (tab === 'converted') {
+ currentBindings = this.data.convertedBindings
+ } else {
+ currentBindings = this.data.expiredBindings
+ }
+
+ this.setData({ activeTab: tab, currentBindings })
+ },
+
+ // 切换绑定列表显示
+ toggleBindingList() {
+ this.setData({ showBindingList: !this.data.showBindingList })
+ },
+
+ // 复制邀请链接
+ copyLink() {
+ const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
+ wx.setClipboardData({
+ data: link,
+ success: () => wx.showToast({ title: '链接已复制', icon: 'success' })
+ })
+ },
+
+ // 生成推广海报 - 1:1 对齐 Next.js 设计
+ async generatePoster() {
+ wx.showLoading({ title: '生成中...', mask: true })
+ this.setData({ showPosterModal: true, isGeneratingPoster: true })
+
+ try {
+ const { referralCode, userInfo } = this.data
+ const nickname = userInfo?.nickname || '用户'
+ const scene = `ref=${referralCode}`
+
+ console.log('[Poster] 请求小程序码, scene:', scene)
+
+ // 调用后端接口生成「小程序码」(官方 getwxacodeunlimit),不再使用 H5 二维码
+ const res = await app.request('/api/miniprogram/qrcode', {
+ method: 'POST',
+ data: {
+ scene, // ref=XXXX
+ page: 'pages/index/index',
+ width: 280,
+ },
+ })
+
+ // 接口返回 { success, image: "data:image/png;base64,...", scene }
+ const imageData = res?.image || res?.data?.image
+ if (!res || !res.success || !imageData) {
+ console.error('[Poster] 生成小程序码失败:', res)
+ throw new Error(res?.error || res?.message || '生成小程序码失败')
+ }
+
+ // 小程序 image 组件支持 base64 格式,直接使用;同时写入本地供预览用
+ const base64Str = String(imageData).trim()
+ const fs = wx.getFileSystemManager()
+ const filePath = `${wx.env.USER_DATA_PATH}/poster_qrcode_${Date.now()}.png`
+ const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, '')
+
+ await new Promise((resolve, reject) => {
+ fs.writeFile({
+ filePath,
+ data: base64Data,
+ encoding: 'base64',
+ success: () => resolve(true),
+ fail: (err) => {
+ console.error('[Poster] 小程序码写入本地失败:', err)
+ reject(err)
+ },
+ })
+ })
+
+ // 优先用 base64 直接显示(兼容性更好);预览时用本地路径
+ this.setData({
+ posterQrSrc: base64Str,
+ posterQrFilePath: filePath,
+ posterReferralLink: '', // 小程序版本不再使用 H5 链接
+ posterNickname: nickname,
+ posterNicknameInitial: (nickname || '用').charAt(0),
+ isGeneratingPoster: false
+ })
+ wx.hideLoading()
+ } catch (e) {
+ console.error('[Poster] 生成二维码失败:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '生成失败', icon: 'none' })
+ this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterQrFilePath: '', posterReferralLink: '' })
+ }
+ },
+
+ // 绘制数据卡片
+ drawDataCard(ctx, x, y, width, height, value, label, color) {
+ // 卡片背景
+ ctx.setFillStyle('rgba(255,255,255,0.05)')
+ this.drawRoundRect(ctx, x, y, width, height, 8)
+ ctx.setStrokeStyle('rgba(255,255,255,0.1)')
+ ctx.setLineWidth(1)
+ ctx.stroke()
+
+ // 数值
+ ctx.setFillStyle(color)
+ ctx.setFontSize(24)
+ ctx.setTextAlign('center')
+ ctx.fillText(value, x + width / 2, y + 24)
+
+ // 标签
+ ctx.setFillStyle('rgba(255,255,255,0.5)')
+ ctx.setFontSize(10)
+ ctx.fillText(label, x + width / 2, y + 40)
+ },
+
+ // 绘制圆角矩形
+ drawRoundRect(ctx, x, y, width, height, radius) {
+ ctx.beginPath()
+ ctx.moveTo(x + radius, y)
+ ctx.lineTo(x + width - radius, y)
+ ctx.arc(x + width - radius, y + radius, radius, -Math.PI / 2, 0)
+ ctx.lineTo(x + width, y + height - radius)
+ ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2)
+ ctx.lineTo(x + radius, y + height)
+ ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI)
+ ctx.lineTo(x, y + radius)
+ ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5)
+ ctx.closePath()
+ ctx.fill()
+ },
+
+ // 光晕(替代 createRadialGradient):用同心圆叠加模拟模糊
+ // centerX/centerY: 圆心坐标;radius: 最大半径;rgb: [r,g,b];maxAlpha: 最内层透明度
+ drawGlow(ctx, centerX, centerY, radius, rgb, maxAlpha = 0.10) {
+ const steps = 14
+ for (let i = steps; i >= 1; i--) {
+ const r = (radius * i) / steps
+ const alpha = (maxAlpha * i) / steps
+ ctx.setFillStyle(`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`)
+ ctx.beginPath()
+ ctx.arc(centerX, centerY, r, 0, Math.PI * 2)
+ ctx.fill()
+ }
+ },
+
+ // 绘制二维码(支持Base64和URL两种格式)
+ async drawQRCode(ctx, qrcodeImage, x, y, size) {
+ return new Promise((resolve) => {
+ if (!qrcodeImage) {
+ console.log('[Poster] 无二维码数据,绘制占位符')
+ this.drawQRPlaceholder(ctx, x, y, size)
+ resolve()
+ return
+ }
+
+ // 判断是Base64还是URL
+ if (qrcodeImage.startsWith('data:image') || !qrcodeImage.startsWith('http')) {
+ // Base64格式(小程序码)
+ console.log('[Poster] 绘制Base64二维码')
+ const fs = wx.getFileSystemManager()
+ const filePath = `${wx.env.USER_DATA_PATH}/qrcode_promo_${Date.now()}.png`
+ const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
+
+ fs.writeFile({
+ filePath,
+ data: base64Data,
+ encoding: 'base64',
+ success: () => {
+ console.log('[Poster] ✅ Base64写入成功')
+ ctx.drawImage(filePath, x, y, size, size)
+ resolve()
+ },
+ fail: (err) => {
+ console.error('[Poster] ❌ Base64写入失败:', err)
+ this.drawQRPlaceholder(ctx, x, y, size)
+ resolve()
+ }
+ })
+ } else {
+ // URL格式(第三方二维码)
+ console.log('[Poster] 下载在线二维码:', qrcodeImage)
+ wx.downloadFile({
+ url: qrcodeImage,
+ success: (res) => {
+ if (res.statusCode === 200) {
+ console.log('[Poster] ✅ 二维码下载成功')
+ ctx.drawImage(res.tempFilePath, x, y, size, size)
+ resolve()
+ } else {
+ console.error('[Poster] ❌ 二维码下载失败, status:', res.statusCode)
+ this.drawQRPlaceholder(ctx, x, y, size)
+ resolve()
+ }
+ },
+ fail: (err) => {
+ console.error('[Poster] ❌ 二维码下载失败:', err)
+ this.drawQRPlaceholder(ctx, x, y, size)
+ resolve()
+ }
+ })
+ }
+ })
+ },
+
+ // 绘制小程序码占位符
+ drawQRPlaceholder(ctx, x, y, size) {
+ // 绘制占位符方框
+ ctx.setFillStyle('rgba(200,200,200,0.3)')
+ this.drawRoundRect(ctx, x, y, size, size, 8)
+
+ ctx.setFillStyle('#00CED1')
+ ctx.setFontSize(11)
+ ctx.setTextAlign('center')
+ ctx.fillText('小程序码', x + size / 2, y + size / 2)
+ },
+
+ // 关闭海报弹窗
+ closePosterModal() {
+ this.setData({ showPosterModal: false })
+ },
+
+ // 保存海报
+ savePoster() {
+ const { posterQrSrc } = this.data
+ if (!posterQrSrc) {
+ wx.showToast({ title: '二维码未生成', icon: 'none' })
+ return
+ }
+
+ wx.showLoading({ title: '保存中...', mask: true })
+ wx.downloadFile({
+ url: posterQrSrc,
+ success: (res) => {
+ if (res.statusCode !== 200) {
+ wx.hideLoading()
+ wx.showToast({ title: '下载失败', icon: 'none' })
+ return
+ }
+
+ wx.saveImageToPhotosAlbum({
+ filePath: res.tempFilePath,
+ success: () => {
+ wx.hideLoading()
+ wx.showToast({ title: '已保存到相册', icon: 'success' })
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ if (String(err.errMsg || '').includes('auth deny')) {
+ wx.showModal({
+ title: '提示',
+ content: '需要相册权限才能保存二维码',
+ confirmText: '去设置',
+ success: (r) => {
+ if (r.confirm) wx.openSetting()
+ }
+ })
+ return
+ }
+ wx.showToast({ title: '保存失败', icon: 'none' })
+ }
+ })
+ },
+ fail: () => {
+ wx.hideLoading()
+ wx.showToast({ title: '下载失败', icon: 'none' })
+ }
+ })
+ },
+
+ // 预览二维码
+ previewPosterQr() {
+ const { posterQrSrc, posterQrFilePath } = this.data
+ const url = posterQrFilePath || posterQrSrc
+ if (!url) return
+ wx.previewImage({ urls: [url] })
+ },
+
+ // 阻止冒泡
+ stopPropagation() {},
+
+ // 提现 - 直接到微信零钱
+ async handleWithdraw() {
+ const availableEarnings = this.data.availableEarningsNum || 0
+ const minWithdrawAmount = this.data.minWithdrawAmount || 10
+ const hasWechatId = this.data.hasWechatId
+
+ if (availableEarnings <= 0) {
+ wx.showToast({ title: '暂无可提现收益', icon: 'none' })
+ return
+ }
+ if (availableEarnings < minWithdrawAmount) {
+ wx.showToast({ title: `满${minWithdrawAmount}元可提现`, icon: 'none' })
+ return
+ }
+
+ // 未绑定微信号时引导去设置
+ if (!hasWechatId) {
+ wx.showModal({
+ title: '请先绑定微信号',
+ content: '提现需先绑定微信号,便于到账核对。请到「设置」中绑定后再提现。',
+ confirmText: '去绑定',
+ cancelText: '取消',
+ success: (res) => {
+ if (res.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
+ }
+ })
+ return
+ }
+
+ const withdrawFee = this.data.withdrawFee ?? 5
+ const actualAmount = withdrawFee > 0
+ ? Math.round(availableEarnings * (1 - withdrawFee / 100) * 100) / 100
+ : availableEarnings
+ const feeText = withdrawFee > 0
+ ? `\n扣除 ${withdrawFee}% 手续费后,实际到账 ¥${actualAmount.toFixed(2)}`
+ : ''
+ wx.showModal({
+ title: '确认提现',
+ content: `申请提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱${feeText}`,
+ confirmText: '立即提现',
+ success: async (res) => {
+ if (!res.confirm) return
+ const tmplId = app.globalData.withdrawSubscribeTmplId
+ if (tmplId && tmplId.length > 10) {
+ wx.requestSubscribeMessage({
+ tmplIds: [tmplId],
+ success: () => { this.doWithdraw(availableEarnings) },
+ fail: () => { this.doWithdraw(availableEarnings) }
+ })
+ } else {
+ await this.doWithdraw(availableEarnings)
+ }
+ }
+ })
+ },
+
+ // 跳转提现记录页
+ goToWithdrawRecords() {
+ wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
+ },
+
+ // 执行提现
+ async doWithdraw(amount) {
+ wx.showLoading({ title: '提现中...' })
+
+ try {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ wx.hideLoading()
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+
+ const res = await app.request('/api/miniprogram/withdraw', {
+ method: 'POST',
+ data: { userId, amount }
+ })
+
+ wx.hideLoading()
+
+ if (res.success) {
+ wx.showModal({
+ title: '提现申请已提交 ✅',
+ content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
+ showCancel: false,
+ confirmText: '知道了'
+ })
+
+ // 刷新数据(此时待审核金额会增加,可提现金额会减少)
+ this.initData()
+ } else {
+ if (res.needBind || res.needBindWechat) {
+ wx.showModal({
+ title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
+ content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
+ confirmText: '去绑定',
+ success: (modalRes) => {
+ if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
+ }
+ })
+ } else {
+ wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
+ }
+ }
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[Referral] 提现失败:', e)
+ wx.showToast({ title: '提现失败,请重试', icon: 'none' })
+ }
+ },
+
+ // 显示通知
+ showNotification() {
+ wx.showToast({ title: '暂无新消息', icon: 'none' })
+ },
+
+ // 显示设置
+ showSettings() {
+ wx.showActionSheet({
+ itemList: ['自动提现设置', '收益通知设置'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ this.showAutoWithdrawSettings()
+ } else {
+ this.showNotificationSettings()
+ }
+ }
+ })
+ },
+
+ // 自动提现设置
+ async showAutoWithdrawSettings() {
+ const app = getApp()
+ const { userInfo } = app.globalData
+
+ if (!userInfo) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+
+ // 获取当前设置
+ let autoWithdrawEnabled = wx.getStorageSync(`autoWithdraw_${userInfo.id}`) || false
+ let autoWithdrawThreshold = wx.getStorageSync(`autoWithdrawThreshold_${userInfo.id}`) || this.data.minWithdrawAmount || 10
+
+ wx.showModal({
+ title: '自动提现设置',
+ content: `当前状态:${autoWithdrawEnabled ? '已开启' : '已关闭'}\n自动提现阈值:¥${autoWithdrawThreshold}\n\n开启后,当可提现金额达到阈值时将自动发起提现申请。`,
+ confirmText: autoWithdrawEnabled ? '关闭' : '开启',
+ cancelText: '修改阈值',
+ success: (res) => {
+ if (res.confirm) {
+ // 切换开关
+ this.toggleAutoWithdraw(!autoWithdrawEnabled, autoWithdrawThreshold)
+ } else if (res.cancel) {
+ // 修改阈值
+ this.setAutoWithdrawThreshold(autoWithdrawEnabled, autoWithdrawThreshold)
+ }
+ }
+ })
+ },
+
+ // 切换自动提现开关
+ toggleAutoWithdraw(enabled, threshold) {
+ const app = getApp()
+ const { userInfo } = app.globalData
+
+ wx.setStorageSync(`autoWithdraw_${userInfo.id}`, enabled)
+
+ wx.showToast({
+ title: enabled ? '自动提现已开启' : '自动提现已关闭',
+ icon: 'success'
+ })
+
+ // 如果开启,检查当前金额是否达到阈值
+ if (enabled && this.data.availableEarningsNum >= threshold) {
+ wx.showModal({
+ title: '提示',
+ content: `当前可提现金额¥${this.data.availableEarnings}已达到阈值¥${threshold},是否立即提现?`,
+ success: (res) => {
+ if (res.confirm) {
+ this.handleWithdraw()
+ }
+ }
+ })
+ }
+ },
+
+ // 设置自动提现阈值
+ setAutoWithdrawThreshold(currentEnabled, currentThreshold) {
+ const minAmount = this.data.minWithdrawAmount || 10
+
+ wx.showModal({
+ title: '设置提现阈值',
+ content: `请输入自动提现金额阈值(最低¥${minAmount})`,
+ editable: true,
+ placeholderText: currentThreshold.toString(),
+ success: (res) => {
+ if (res.confirm && res.content) {
+ const threshold = parseFloat(res.content)
+
+ if (isNaN(threshold) || threshold < minAmount) {
+ wx.showToast({
+ title: `请输入不小于¥${minAmount}的金额`,
+ icon: 'none'
+ })
+ return
+ }
+
+ const app = getApp()
+ const { userInfo } = app.globalData
+
+ wx.setStorageSync(`autoWithdrawThreshold_${userInfo.id}`, threshold)
+
+ wx.showToast({
+ title: `阈值已设置为¥${threshold}`,
+ icon: 'success'
+ })
+
+ // 重新显示设置界面
+ setTimeout(() => {
+ this.showAutoWithdrawSettings()
+ }, 1500)
+ }
+ }
+ })
+ },
+
+ // 收益通知设置
+ showNotificationSettings() {
+ const app = getApp()
+ const { userInfo } = app.globalData
+
+ if (!userInfo) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+
+ // 获取当前设置
+ let notifyEnabled = wx.getStorageSync(`earningsNotify_${userInfo.id}`) !== false // 默认开启
+
+ wx.showModal({
+ title: '收益通知设置',
+ content: `当前状态:${notifyEnabled ? '已开启' : '已关闭'}\n\n开启后,将在有新收益时收到小程序通知提醒。`,
+ confirmText: notifyEnabled ? '关闭通知' : '开启通知',
+ success: (res) => {
+ if (res.confirm) {
+ const newState = !notifyEnabled
+ wx.setStorageSync(`earningsNotify_${userInfo.id}`, newState)
+
+ wx.showToast({
+ title: newState ? '收益通知已开启' : '收益通知已关闭',
+ icon: 'success'
+ })
+
+ // 如果开启,请求通知权限
+ if (newState) {
+ wx.requestSubscribeMessage({
+ tmplIds: [''] // 需要配置模板ID
+ }).catch(() => {
+ // 用户拒绝授权,不影响功能
+ })
+ }
+ }
+ }
+ })
+ },
+
+ // 分享 - 带自己的邀请码(与 app.getMyReferralCode 一致)
+ onShareAppMessage() {
+ const app = getApp()
+ const ref = app.getMyReferralCode() || this.data.referralCode
+ return {
+ title: 'Soul创业派对 - 来自派对房的真实商业故事',
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
+ }
+ },
+
+ goBack() {
+ wx.navigateBack()
+ },
+
+ // 解析商品描述,获取书名和章节
+ parseProductDescription(description, productType) {
+ if (!description) {
+ return {
+ bookTitle: '未知商品',
+ chapterTitle: ''
+ }
+ }
+
+ // 匹配格式:《书名》- 章节名
+ const match = description.match(/《(.+?)》(?:\s*-\s*(.+))?/)
+
+ if (match) {
+ return {
+ bookTitle: match[1] || '未知书籍',
+ chapterTitle: match[2] || (productType === 'fullbook' ? '全书购买' : '')
+ }
+ }
+
+ // 如果匹配失败,直接返回原始描述
+ return {
+ bookTitle: description.split('-')[0] || description,
+ chapterTitle: description.split('-')[1] || ''
+ }
+ },
+
+ // 格式化日期
+ formatDate(dateStr) {
+ if (!dateStr) return '--'
+ const d = new Date(dateStr)
+ const month = (d.getMonth() + 1).toString().padStart(2, '0')
+ const day = d.getDate().toString().padStart(2, '0')
+ return `${month}-${day}`
+ }
+})
diff --git a/miniprogram2/pages/referral/referral.json b/miniprogram2/pages/referral/referral.json
new file mode 100644
index 00000000..64f02792
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "enableShareAppMessage": true,
+ "enableShareTimeline": true
+}
diff --git a/miniprogram2/pages/referral/referral.wxml b/miniprogram2/pages/referral/referral.wxml
new file mode 100644
index 00000000..a300cee5
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.wxml
@@ -0,0 +1,307 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 分销中心
+
+
+
+
+
+
+
+
+
+
+
+ {{expiringCount}} 位用户绑定即将过期
+ {{bindingDays}}天内未付款将解除绑定关系
+
+
+
+
+
+
+
+
+
+ {{availableEarningsNum < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
+
+ 提现将扣除 {{withdrawFee}}% 手续费
+ 为便于提现到账,请先到「设置」中绑定微信号
+ 查看提现记录
+
+
+
+
+
+
+ {{bindingCount}}
+ 绑定中
+
+
+ {{paidCount}}
+ 已付款
+
+
+ {{unboughtCount}}
+ 即将过期
+
+
+ {{referralCount}}
+ 总邀请
+
+
+
+
+
+
+
+ • 好友通过你的链接购买,立享{{userDiscount}}%优惠
+ • 好友成功付款后,你获得 {{shareRate}}% 收益
+ • 绑定期{{bindingDays}}天,期满未付款自动解除
+
+
+
+
+
+
+
+
+
+
+ 绑定中 ({{activeBindings.length}})
+ 已付款 ({{convertedBindings.length}})
+ 已过期 ({{expiredBindings.length}})
+
+
+
+
+
+
+ 👤
+ 暂无用户
+
+
+
+
+
+ ✓
+ ⏰
+ {{item.nickname[0] || '用'}}
+
+
+ {{item.nickname || '匿名用户'}}
+ 绑定于 {{item.bindingDate}}
+
+
+
+ +¥{{item.commission}}
+ 已购{{item.purchaseCount || 1}}次
+
+
+ 已过期
+ {{item.expiryDate}}
+
+
+
+ {{item.daysRemaining}}天
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 生成推广海报
+ 一键生成精美海报分享
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.buyerNickname.charAt(0)}}
+
+
+
+
+
+
+ {{item.buyerNickname}}
+ +¥{{item.commission}}
+
+
+ {{item.bookTitle}}
+ - {{item.chapterTitle}}
+
+ {{item.payTime}}
+
+
+
+
+
+
+
+
+
+
+
+ ✕
+
+
+
+
+
+
+
+
+
+
+
+ 真实商业案例
+ 每日更新
+
+
+
+
+ 一场SOUL的
+ 创业实验场
+
+ 来自Soul派对房的真实商业故事
+
+
+
+
+ {{posterCaseCount}}
+ 真实案例
+
+
+ {{userDiscount}}%
+ 好友优惠
+
+
+ {{shareRate}}%
+ 你的收益
+
+
+
+
+
+ 人性观察
+ 行业揭秘
+ 赚钱逻辑
+ 创业复盘
+ 资源对接
+
+
+
+
+
+ {{posterNicknameInitial}}
+
+ {{posterNickname}} 推荐你来读
+
+
+
+
+ 通过我的链接购买,立省{{userDiscount}}%
+
+
+
+
+
+
+
+ 长按识别 · 立即试读
+ 邀请码: {{referralCode}}
+
+
+
+
+
+
+
+
diff --git a/miniprogram2/pages/referral/referral.wxss b/miniprogram2/pages/referral/referral.wxss
new file mode 100644
index 00000000..9474b400
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.wxss
@@ -0,0 +1,376 @@
+/* ???????? - 1:1??Web?? */
+.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
+
+/* ??? */
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-left { display: flex; gap: 16rpx; align-items: center; }
+.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
+.nav-right-placeholder { width: 144rpx; }
+.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+
+.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
+
+/* ?????? */
+.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
+.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
+.banner-content { flex: 1; }
+.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
+.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
+
+/* ???? - ?? Next.js */
+.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
+.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
+.earnings-main { position: relative; }
+.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
+.earnings-left { display: flex; align-items: center; gap: 16rpx; }
+.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
+.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
+.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
+.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
+.earnings-right { text-align: right; }
+.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
+.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
+
+.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
+.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
+.withdraw-fee-tip { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; text-align: center; }
+.wechat-tip { display: block; font-size: 24rpx; color: rgba(255,165,0,0.9); margin-top: 16rpx; text-align: center; }
+.withdraw-records-link { display: block; margin-top: 16rpx; text-align: center; font-size: 26rpx; color: #00CED1; }
+
+/* ???? - ?? Next.js 4??? */
+.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
+.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
+.stat-value.orange { color: #FFA500; }
+.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
+
+/* ????? */
+.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
+.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
+.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* ???? - ?? Next.js */
+.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
+.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
+.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
+.rules-list { padding-left: 8rpx; }
+.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
+.rule-item .gold { color: #FFD700; font-weight: 500; }
+.rule-item .brand { color: #00CED1; font-weight: 500; }
+.rule-item .orange { color: #FFA500; font-weight: 500; }
+
+/* ?????? - ?? Next.js */
+.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-title { display: flex; align-items: center; gap: 12rpx; }
+.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
+.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* Tab?? */
+.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
+.tab-item.tab-active { color: #00CED1; }
+.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
+
+/* ???? */
+.binding-list { max-height: 640rpx; overflow-y: auto; }
+.empty-state { padding: 80rpx 0; text-align: center; }
+.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
+.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+/* ?????? - ??? */
+.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); gap: 24rpx; }
+.binding-item:last-child { border-bottom: none; }
+
+/* ?? */
+.user-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; color: #00CED1; flex-shrink: 0; }
+.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
+
+/* ???? */
+.user-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
+.user-name { font-size: 28rpx; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+/* ???? */
+.user-status { flex-shrink: 0; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; min-width: 100rpx; }
+.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; white-space: nowrap; }
+.status-order { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
+.status-time { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
+
+/* ???? */
+.status-tag { font-size: 24rpx; font-weight: 600; padding: 6rpx 16rpx; border-radius: 16rpx; white-space: nowrap; }
+.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
+.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
+.status-tag.tag-gray { background: rgba(158,158,158,0.2); color: #9E9E9E; }
+
+/* ????? - ?? Next.js */
+.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
+.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
+.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
+.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
+.invite-tip .gold { color: #FFD700; }
+.invite-tip .brand { color: #00CED1; }
+
+/* ?????? - ??? */
+.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.detail-list { max-height: 480rpx; overflow-y: auto; padding: 16rpx 0; }
+
+/* ??????? */
+.earnings-detail-card .detail-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx 40rpx; background: transparent; border-bottom: 2rpx solid rgba(255,255,255,0.03); }
+.earnings-detail-card .detail-item:last-child { border-bottom: none; }
+.earnings-detail-card .detail-item:active { background: rgba(255, 255, 255, 0.05); }
+
+/* ???? */
+.earnings-detail-card .detail-avatar-wrap { width: 88rpx; height: 88rpx; flex-shrink: 0; }
+.earnings-detail-card .detail-avatar { width: 100%; height: 100%; border-radius: 50%; border: 2rpx solid rgba(56, 189, 172, 0.2); }
+.earnings-detail-card .detail-avatar-text { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #38bdac 0%, #2da396 100%); display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: #ffffff; }
+
+/* ???? */
+.earnings-detail-card .detail-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
+.earnings-detail-card .detail-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
+.earnings-detail-card .detail-buyer { font-size: 28rpx; font-weight: 500; color: #ffffff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.earnings-detail-card .detail-amount { font-size: 32rpx; font-weight: 700; color: #38bdac; flex-shrink: 0; white-space: nowrap; }
+
+/* ???? */
+.earnings-detail-card .detail-product { display: flex; align-items: baseline; gap: 4rpx; font-size: 24rpx; color: rgba(255, 255, 255, 0.6); min-width: 0; overflow: hidden; }
+.earnings-detail-card .detail-book { color: rgba(255, 255, 255, 0.7); font-weight: 500; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.earnings-detail-card .detail-chapter { color: rgba(255, 255, 255, 0.5); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.earnings-detail-card .detail-time { font-size: 22rpx; color: rgba(255, 255, 255, 0.4); }
+
+/* ???? - ?? Next.js */
+.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
+.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
+.share-item::after { border: none; }
+.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
+.share-icon.poster { background: rgba(103,58,183,0.2); }
+.share-icon.wechat { background: rgba(7,193,96,0.2); }
+.share-icon.link { background: rgba(158,158,158,0.2); }
+.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
+.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
+.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
+.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-info { flex: 1; text-align: left; }
+.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
+.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
+.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
+
+/* ?????????????? + ???? + ???????? */
+/* ???????? backdrop-filter??????????????? */
+.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
+
+.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
+.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
+
+/* ???? */
+.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
+.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
+
+/* ???? */
+/* ???????? filter: blur ??????????????? + ???? */
+.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
+.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
+.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
+.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
+
+/* ???? */
+.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
+.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
+.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
+.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
+
+/* ?? */
+.poster-title { margin-bottom: 8rpx; }
+.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
+.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
+.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
+
+/* ???? */
+.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
+.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
+.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
+.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
+.poster-stat-gold { color: #FFD700; }
+.poster-stat-brand { color: #00CED1; }
+.poster-stat-pink { color: #E91E63; }
+
+/* ?? */
+.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
+.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
+
+/* ??? */
+.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
+.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
+.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
+.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
+
+/* ??? */
+.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
+.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
+.poster-discount-highlight { color: #00CED1; font-weight: 800; }
+
+/* ??? */
+.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
+.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
+.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
+.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
+
+/* ??????? */
+.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
+.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
+.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
+
+
+
+/* 空状态 - 暂无收益 */
+.empty-earnings {
+ background: linear-gradient(160deg, rgba(28, 28, 30, 0.95) 0%, rgba(15, 33, 55, 0.9) 100%);
+ backdrop-filter: blur(40rpx);
+ border: 2rpx solid rgba(0, 206, 209, 0.15);
+ border-radius: 32rpx;
+ padding: 80rpx 48rpx;
+ margin-bottom: 24rpx;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+}
+.empty-earnings::before {
+ content: '';
+ position: absolute;
+ top: -80rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 320rpx;
+ height: 160rpx;
+ background: radial-gradient(ellipse at center, rgba(0, 206, 209, 0.08) 0%, transparent 70%);
+ pointer-events: none;
+}
+.empty-earnings-inner {
+ position: relative;
+ z-index: 1;
+}
+.empty-earnings-icon-wrap {
+ position: relative;
+ width: 160rpx;
+ height: 160rpx;
+ margin: 0 auto 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.empty-earnings-glow {
+ position: absolute;
+ inset: -20rpx;
+ background: radial-gradient(circle, rgba(0, 206, 209, 0.2) 0%, rgba(255, 215, 0, 0.06) 50%, transparent 70%);
+ border-radius: 50%;
+}
+.empty-earnings-icon {
+ width: 96rpx;
+ height: 96rpx;
+ position: relative;
+ z-index: 1;
+ filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
+}
+.empty-earnings-title {
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #fff;
+ display: block;
+ margin-bottom: 20rpx;
+ letter-spacing: 1rpx;
+}
+.empty-earnings-desc {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.65);
+ display: block;
+ line-height: 1.6;
+ margin-bottom: 12rpx;
+}
+.empty-earnings-rate {
+ display: inline-block;
+ font-size: 52rpx;
+ font-weight: 800;
+ color: #00CED1;
+ letter-spacing: 4rpx;
+ text-shadow: 0 0 40rpx rgba(0, 206, 209, 0.4);
+}
+.empty-earnings-hint {
+ font-size: 24rpx;
+ color: rgba(255, 215, 0, 0.85);
+ display: block;
+ margin-top: 8rpx;
+ font-weight: 500;
+}
+
+
+/* ===== Loading 遮罩(备用) ===== */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.7);
+ backdrop-filter: blur(10rpx);
+ z-index: 999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.loading-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24rpx;
+}
+.loading-spinner {
+ width: 80rpx;
+ height: 80rpx;
+ border: 6rpx solid rgba(56,189,172,0.2);
+ border-top-color: #38bdac;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+.loading-text {
+ font-size: 28rpx;
+ color: rgba(255,255,255,0.8);
+ font-weight: 500;
+}
+.content-loading {
+ opacity: 0.3;
+ pointer-events: none;
+}
+
+/* ===== 收益明细独立块 ===== */
+.detail-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.02); border-radius: 16rpx; margin-bottom: 16rpx; transition: all 0.3s; }
+.detail-item:active { background: rgba(255,255,255,0.05); }
+.detail-avatar-wrap { flex-shrink: 0; }
+.detail-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; border: 2rpx solid rgba(56,189,172,0.2); }
+.detail-avatar-text { width: 88rpx; height: 88rpx; border-radius: 50%; background: linear-gradient(135deg, #38bdac 0%, #2da396 100%); display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: #ffffff; }
+.detail-content { flex: 1; display: flex; flex-direction: column; gap: 8rpx; min-width: 0; }
+.detail-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
+.detail-buyer { font-size: 28rpx; font-weight: 500; color: #ffffff; flex-shrink: 0; }
+.detail-amount { font-size: 32rpx; font-weight: 700; color: #38bdac; flex-shrink: 0; }
+.detail-product { display: flex; align-items: center; font-size: 24rpx; color: rgba(255,255,255,0.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.detail-book { color: rgba(255,255,255,0.7); font-weight: 500; }
+.detail-chapter { color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.detail-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
diff --git a/miniprogram2/pages/referral/referral.wxss.backup b/miniprogram2/pages/referral/referral.wxss.backup
new file mode 100644
index 00000000..3c9e7c32
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.wxss.backup
@@ -0,0 +1,379 @@
+/* ???????? - 1:1??Web?? */
+.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
+
+/* ??? */
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-left { display: flex; gap: 16rpx; align-items: center; }
+.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
+.nav-right-placeholder { width: 144rpx; }
+.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+
+.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
+
+/* ?????? */
+.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
+.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
+.banner-content { flex: 1; }
+.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
+.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
+
+/* ???? - ?? Next.js */
+.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
+.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
+.earnings-main { position: relative; }
+.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
+.earnings-left { display: flex; align-items: center; gap: 16rpx; }
+.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
+.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
+.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
+.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
+.earnings-right { text-align: right; }
+.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
+.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
+
+.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
+.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
+
+/* ???? - ?? Next.js 4??? */
+.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
+.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
+.stat-value.orange { color: #FFA500; }
+.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
+
+/* ????? */
+.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
+.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
+.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* ???? - ?? Next.js */
+.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
+.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
+.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
+.rules-list { padding-left: 8rpx; }
+.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
+.rule-item .gold { color: #FFD700; font-weight: 500; }
+.rule-item .brand { color: #00CED1; font-weight: 500; }
+.rule-item .orange { color: #FFA500; font-weight: 500; }
+
+/* ?????? - ?? Next.js */
+.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-title { display: flex; align-items: center; gap: 12rpx; }
+.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
+.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* Tab?? */
+.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
+.tab-item.tab-active { color: #00CED1; }
+.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
+
+/* ???? */
+.binding-list { max-height: 640rpx; overflow-y: auto; }
+.empty-state { padding: 80rpx 0; text-align: center; }
+.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
+.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-item:last-child { border-bottom: none; }
+.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
+.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
+.user-info { flex: 1; }
+.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.user-status { text-align: right; }
+.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
+.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
+.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
+.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
+.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
+
+/* ????? - ?? Next.js */
+.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
+.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
+.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
+.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
+.invite-tip .gold { color: #FFD700; }
+.invite-tip .brand { color: #00CED1; }
+
+/* ?????? - ?? Next.js */
+.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.detail-list { max-height: 480rpx; overflow-y: auto; }
+.detail-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 40rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-item:last-child { border-bottom: none; }
+.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
+.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.detail-info { flex: 1; }
+.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
+
+/* ???? - ?? Next.js */
+.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
+.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
+.share-item::after { border: none; }
+.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
+.share-icon.poster { background: rgba(103,58,183,0.2); }
+.share-icon.wechat { background: rgba(7,193,96,0.2); }
+.share-icon.link { background: rgba(158,158,158,0.2); }
+.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
+.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
+.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
+.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-info { flex: 1; text-align: left; }
+.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
+.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
+.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
+
+/* ?????????????? + ???? + ???????? */
+/* ???????? backdrop-filter??????????????? */
+.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
+
+.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
+.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
+
+/* ???? */
+.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
+.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
+
+/* ???? */
+/* ???????? filter: blur ??????????????? + ???? */
+.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
+.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
+.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
+.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
+
+/* ???? */
+.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
+.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
+.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
+.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
+
+/* ?? */
+.poster-title { margin-bottom: 8rpx; }
+.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
+.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
+.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
+
+/* ???? */
+.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
+.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
+.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
+.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
+.poster-stat-gold { color: #FFD700; }
+.poster-stat-brand { color: #00CED1; }
+.poster-stat-pink { color: #E91E63; }
+
+/* ?? */
+.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
+.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
+
+/* ??? */
+.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
+.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
+.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
+.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
+
+/* ??? */
+.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
+.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
+.poster-discount-highlight { color: #00CED1; font-weight: 800; }
+
+/* ??? */
+.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
+.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
+.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
+.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
+
+/* ??????? */
+.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
+.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
+.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
+
+
+
+/* ????- ?? Next.js */
+.empty-earnings { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 64rpx 40rpx; text-align: center; margin-bottom: 24rpx; }
+.empty-icon-wrapper { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(28, 28, 30, 0.8); display: flex; align-items: center; justify-content: center; margin: 0 auto 32rpx; }
+.empty-gift-icon { width: 64rpx; height: 64rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.empty-title { font-size: 30rpx; font-weight: 500; color: #fff; display: block; margin-bottom: 16rpx; }
+.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; line-height: 1.5; }
+
+
+ / * = = = = = TrGm5 ? = = = = = * /
+ . l o a d i n g - o v e r l a y {
+ p o s i t i o n : f i x e d ;
+ t o p : 0 ;
+ l e f t : 0 ;
+ r i g h t : 0 ;
+ b o t t o m : 0 ;
+ b a c k g r o u n d : r g b a ( 0 , 0 , 0 , 0 . 7 ) ;
+ b a c k d r o p - f i l t e r : b l u r ( 1 0 r p x ) ;
+ z - i n d e x : 9 9 9 ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ }
+
+ . l o a d i n g - c o n t e n t {
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ }
+
+ . l o a d i n g - s p i n n e r {
+ w i d t h : 8 0 r p x ;
+ h e i g h t : 8 0 r p x ;
+ b o r d e r : 6 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ b o r d e r - t o p - c o l o r : # 3 8 b d a c ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ a n i m a t i o n : s p i n 1 s l i n e a r i n f i n i t e ;
+ }
+
+ @ k e y f r a m e s s p i n {
+ 0 % { t r a n s f o r m : r o t a t e ( 0 d e g ) ; }
+ 1 0 0 % { t r a n s f o r m : r o t a t e ( 3 6 0 d e g ) ; }
+ }
+
+ . l o a d i n g - t e x t {
+ f o n t - s i z e : 2 8 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 8 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ }
+
+ . c o n t e n t - l o a d i n g {
+ o p a c i t y : 0 . 3 ;
+ p o i n t e r - e v e n t s : n o n e ;
+ }
+
+ / * = = = = = ARlē^|op]͓\!} = = = = = * /
+ . d e t a i l - i t e m {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ p a d d i n g : 2 4 r p x ;
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ;
+ b o r d e r - r a d i u s : 1 6 r p x ;
+ m a r g i n - b o t t o m : 1 6 r p x ;
+ t r a n s i t i o n : a l l 0 . 3 s ;
+ }
+
+ . d e t a i l - i t e m : a c t i v e {
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 5 ) ;
+ }
+
+ . d e t a i l - a v a t a r - w r a p {
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a v a t a r {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b o r d e r : 2 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ }
+
+ . d e t a i l - a v a t a r - t e x t {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ f o n t - s i z e : 3 6 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # f f f f f f ;
+ }
+
+ . d e t a i l - c o n t e n t {
+ f l e x : 1 ;
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ g a p : 8 r p x ;
+ m i n - w i d t h : 0 ;
+ }
+
+ . d e t a i l - t o p {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ;
+ g a p : 1 6 r p x ;
+ }
+
+ . d e t a i l - b u y e r {
+ f o n t - s i z e : 2 8 r p x ;
+ f o n t - w e i g h t : 5 0 0 ;
+ c o l o r : # f f f f f f ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a m o u n t {
+ f o n t - s i z e : 3 2 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # 3 8 b d a c ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - p r o d u c t {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ f o n t - s i z e : 2 4 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - b o o k {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - c h a p t e r {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - t i m e {
+ f o n t - s i z e : 2 2 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ;
+ }
+
+ / * = = = ARlē^|͓\!}m2 = = = * /
+ . d e t a i l - i t e m { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; g a p : 2 4 r p x ; p a d d i n g : 2 4 r p x ; b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ; b o r d e r - r a d i u s : 1 6 r p x ; m a r g i n - b o t t o m : 1 6 r p x ; }
+ . d e t a i l - a v a t a r - w r a p { f l e x - s h r i n k : 0 ; }
+ . d e t a i l - a v a t a r { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; }
+ . d e t a i l - a v a t a r - t e x t { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ; d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : c e n t e r ; f o n t - s i z e : 3 6 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # f f f f f f ; }
+ . d e t a i l - c o n t e n t { f l e x : 1 ; d i s p l a y : f l e x ; f l e x - d i r e c t i o n : c o l u m n ; g a p : 8 r p x ; m i n - w i d t h : 0 ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - t o p { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ; g a p : 1 6 r p x ; }
+ . d e t a i l - b u y e r { f o n t - s i z e : 2 8 r p x ; f o n t - w e i g h t : 5 0 0 ; c o l o r : # f f f f f f ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; }
+ . d e t a i l - a m o u n t { f o n t - s i z e : 3 2 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # 3 8 b d a c ; f l e x - s h r i n k : 0 ; }
+ . d e t a i l - p r o d u c t { d i s p l a y : f l e x ; a l i g n - i t e m s : b a s e l i n e ; f o n t - s i z e : 2 4 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - b o o k { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ; f o n t - w e i g h t : 5 0 0 ; m a x - w i d t h : 2 0 0 r p x ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; f l e x - s h r i n k : 1 ; }
+ . d e t a i l - c h a p t e r { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; m a r g i n - l e f t : 4 r p x ; }
+ . d e t a i l - t i m e { f o n t - s i z e : 2 2 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ; }
+
+
\ No newline at end of file
diff --git a/miniprogram2/pages/referral/referral.wxss.broken b/miniprogram2/pages/referral/referral.wxss.broken
new file mode 100644
index 00000000..3c9e7c32
--- /dev/null
+++ b/miniprogram2/pages/referral/referral.wxss.broken
@@ -0,0 +1,379 @@
+/* ???????? - 1:1??Web?? */
+.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
+
+/* ??? */
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-left { display: flex; gap: 16rpx; align-items: center; }
+.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
+.nav-right-placeholder { width: 144rpx; }
+.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+
+.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
+
+/* ?????? */
+.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
+.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
+.banner-content { flex: 1; }
+.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
+.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
+
+/* ???? - ?? Next.js */
+.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
+.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
+.earnings-main { position: relative; }
+.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
+.earnings-left { display: flex; align-items: center; gap: 16rpx; }
+.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
+.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
+.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
+.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
+.earnings-right { text-align: right; }
+.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
+.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
+
+.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
+.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
+
+/* ???? - ?? Next.js 4??? */
+.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
+.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
+.stat-value.orange { color: #FFA500; }
+.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
+
+/* ????? */
+.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
+.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
+.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* ???? - ?? Next.js */
+.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
+.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
+.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
+.rules-list { padding-left: 8rpx; }
+.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
+.rule-item .gold { color: #FFD700; font-weight: 500; }
+.rule-item .brand { color: #00CED1; font-weight: 500; }
+.rule-item .orange { color: #FFA500; font-weight: 500; }
+
+/* ?????? - ?? Next.js */
+.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-title { display: flex; align-items: center; gap: 12rpx; }
+.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
+.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+
+/* Tab?? */
+.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
+.tab-item.tab-active { color: #00CED1; }
+.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
+
+/* ???? */
+.binding-list { max-height: 640rpx; overflow-y: auto; }
+.empty-state { padding: 80rpx 0; text-align: center; }
+.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
+.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.binding-item:last-child { border-bottom: none; }
+.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
+.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
+.user-info { flex: 1; }
+.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.user-status { text-align: right; }
+.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
+.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
+.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
+.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
+.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
+.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
+
+/* ????? - ?? Next.js */
+.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
+.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
+.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
+.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
+.invite-tip .gold { color: #FFD700; }
+.invite-tip .brand { color: #00CED1; }
+
+/* ?????? - ?? Next.js */
+.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
+.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
+.detail-list { max-height: 480rpx; overflow-y: auto; }
+.detail-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 40rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.detail-item:last-child { border-bottom: none; }
+.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
+.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
+.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
+.detail-info { flex: 1; }
+.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
+.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
+
+/* ???? - ?? Next.js */
+.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
+.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
+.share-item::after { border: none; }
+.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
+.share-icon.poster { background: rgba(103,58,183,0.2); }
+.share-icon.wechat { background: rgba(7,193,96,0.2); }
+.share-icon.link { background: rgba(158,158,158,0.2); }
+.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
+.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
+.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
+.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-info { flex: 1; text-align: left; }
+.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
+.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
+.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
+
+/* ?????????????? + ???? + ???????? */
+/* ???????? backdrop-filter??????????????? */
+.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
+
+.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
+.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
+
+/* ???? */
+.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
+.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
+
+/* ???? */
+/* ???????? filter: blur ??????????????? + ???? */
+.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
+.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
+.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
+.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
+
+/* ???? */
+.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
+.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
+.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
+.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
+
+/* ?? */
+.poster-title { margin-bottom: 8rpx; }
+.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
+.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
+.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
+
+/* ???? */
+.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
+.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
+.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
+.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
+.poster-stat-gold { color: #FFD700; }
+.poster-stat-brand { color: #00CED1; }
+.poster-stat-pink { color: #E91E63; }
+
+/* ?? */
+.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
+.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
+
+/* ??? */
+.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
+.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
+.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
+.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
+
+/* ??? */
+.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
+.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
+.poster-discount-highlight { color: #00CED1; font-weight: 800; }
+
+/* ??? */
+.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
+.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
+.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
+.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
+
+/* ??????? */
+.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
+.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
+.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
+
+
+
+/* ????- ?? Next.js */
+.empty-earnings { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 64rpx 40rpx; text-align: center; margin-bottom: 24rpx; }
+.empty-icon-wrapper { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(28, 28, 30, 0.8); display: flex; align-items: center; justify-content: center; margin: 0 auto 32rpx; }
+.empty-gift-icon { width: 64rpx; height: 64rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
+.empty-title { font-size: 30rpx; font-weight: 500; color: #fff; display: block; margin-bottom: 16rpx; }
+.empty-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; line-height: 1.5; }
+
+
+ / * = = = = = TrGm5 ? = = = = = * /
+ . l o a d i n g - o v e r l a y {
+ p o s i t i o n : f i x e d ;
+ t o p : 0 ;
+ l e f t : 0 ;
+ r i g h t : 0 ;
+ b o t t o m : 0 ;
+ b a c k g r o u n d : r g b a ( 0 , 0 , 0 , 0 . 7 ) ;
+ b a c k d r o p - f i l t e r : b l u r ( 1 0 r p x ) ;
+ z - i n d e x : 9 9 9 ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ }
+
+ . l o a d i n g - c o n t e n t {
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ }
+
+ . l o a d i n g - s p i n n e r {
+ w i d t h : 8 0 r p x ;
+ h e i g h t : 8 0 r p x ;
+ b o r d e r : 6 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ b o r d e r - t o p - c o l o r : # 3 8 b d a c ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ a n i m a t i o n : s p i n 1 s l i n e a r i n f i n i t e ;
+ }
+
+ @ k e y f r a m e s s p i n {
+ 0 % { t r a n s f o r m : r o t a t e ( 0 d e g ) ; }
+ 1 0 0 % { t r a n s f o r m : r o t a t e ( 3 6 0 d e g ) ; }
+ }
+
+ . l o a d i n g - t e x t {
+ f o n t - s i z e : 2 8 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 8 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ }
+
+ . c o n t e n t - l o a d i n g {
+ o p a c i t y : 0 . 3 ;
+ p o i n t e r - e v e n t s : n o n e ;
+ }
+
+ / * = = = = = ARlē^|op]͓\!} = = = = = * /
+ . d e t a i l - i t e m {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ g a p : 2 4 r p x ;
+ p a d d i n g : 2 4 r p x ;
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ;
+ b o r d e r - r a d i u s : 1 6 r p x ;
+ m a r g i n - b o t t o m : 1 6 r p x ;
+ t r a n s i t i o n : a l l 0 . 3 s ;
+ }
+
+ . d e t a i l - i t e m : a c t i v e {
+ b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 5 ) ;
+ }
+
+ . d e t a i l - a v a t a r - w r a p {
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a v a t a r {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b o r d e r : 2 r p x s o l i d r g b a ( 5 6 , 1 8 9 , 1 7 2 , 0 . 2 ) ;
+ }
+
+ . d e t a i l - a v a t a r - t e x t {
+ w i d t h : 8 8 r p x ;
+ h e i g h t : 8 8 r p x ;
+ b o r d e r - r a d i u s : 5 0 % ;
+ b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ;
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : c e n t e r ;
+ f o n t - s i z e : 3 6 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # f f f f f f ;
+ }
+
+ . d e t a i l - c o n t e n t {
+ f l e x : 1 ;
+ d i s p l a y : f l e x ;
+ f l e x - d i r e c t i o n : c o l u m n ;
+ g a p : 8 r p x ;
+ m i n - w i d t h : 0 ;
+ }
+
+ . d e t a i l - t o p {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ;
+ g a p : 1 6 r p x ;
+ }
+
+ . d e t a i l - b u y e r {
+ f o n t - s i z e : 2 8 r p x ;
+ f o n t - w e i g h t : 5 0 0 ;
+ c o l o r : # f f f f f f ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - a m o u n t {
+ f o n t - s i z e : 3 2 r p x ;
+ f o n t - w e i g h t : 7 0 0 ;
+ c o l o r : # 3 8 b d a c ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - p r o d u c t {
+ d i s p l a y : f l e x ;
+ a l i g n - i t e m s : c e n t e r ;
+ f o n t - s i z e : 2 4 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - b o o k {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ;
+ f o n t - w e i g h t : 5 0 0 ;
+ f l e x - s h r i n k : 0 ;
+ }
+
+ . d e t a i l - c h a p t e r {
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ;
+ o v e r f l o w : h i d d e n ;
+ t e x t - o v e r f l o w : e l l i p s i s ;
+ w h i t e - s p a c e : n o w r a p ;
+ }
+
+ . d e t a i l - t i m e {
+ f o n t - s i z e : 2 2 r p x ;
+ c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ;
+ }
+
+ / * = = = ARlē^|͓\!}m2 = = = * /
+ . d e t a i l - i t e m { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; g a p : 2 4 r p x ; p a d d i n g : 2 4 r p x ; b a c k g r o u n d : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 0 2 ) ; b o r d e r - r a d i u s : 1 6 r p x ; m a r g i n - b o t t o m : 1 6 r p x ; }
+ . d e t a i l - a v a t a r - w r a p { f l e x - s h r i n k : 0 ; }
+ . d e t a i l - a v a t a r { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; }
+ . d e t a i l - a v a t a r - t e x t { w i d t h : 8 8 r p x ; h e i g h t : 8 8 r p x ; b o r d e r - r a d i u s : 5 0 % ; b a c k g r o u n d : l i n e a r - g r a d i e n t ( 1 3 5 d e g , # 3 8 b d a c 0 % , # 2 d a 3 9 6 1 0 0 % ) ; d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : c e n t e r ; f o n t - s i z e : 3 6 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # f f f f f f ; }
+ . d e t a i l - c o n t e n t { f l e x : 1 ; d i s p l a y : f l e x ; f l e x - d i r e c t i o n : c o l u m n ; g a p : 8 r p x ; m i n - w i d t h : 0 ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - t o p { d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : s p a c e - b e t w e e n ; g a p : 1 6 r p x ; }
+ . d e t a i l - b u y e r { f o n t - s i z e : 2 8 r p x ; f o n t - w e i g h t : 5 0 0 ; c o l o r : # f f f f f f ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; }
+ . d e t a i l - a m o u n t { f o n t - s i z e : 3 2 r p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # 3 8 b d a c ; f l e x - s h r i n k : 0 ; }
+ . d e t a i l - p r o d u c t { d i s p l a y : f l e x ; a l i g n - i t e m s : b a s e l i n e ; f o n t - s i z e : 2 4 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 6 ) ; o v e r f l o w : h i d d e n ; }
+ . d e t a i l - b o o k { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 7 ) ; f o n t - w e i g h t : 5 0 0 ; m a x - w i d t h : 2 0 0 r p x ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; f l e x - s h r i n k : 1 ; }
+ . d e t a i l - c h a p t e r { c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 5 ) ; f l e x : 1 ; o v e r f l o w : h i d d e n ; t e x t - o v e r f l o w : e l l i p s i s ; w h i t e - s p a c e : n o w r a p ; m a r g i n - l e f t : 4 r p x ; }
+ . d e t a i l - t i m e { f o n t - s i z e : 2 2 r p x ; c o l o r : r g b a ( 2 5 5 , 2 5 5 , 2 5 5 , 0 . 4 ) ; }
+
+
\ No newline at end of file
diff --git a/miniprogram2/pages/scan/scan.js b/miniprogram2/pages/scan/scan.js
new file mode 100644
index 00000000..8b6c42e2
--- /dev/null
+++ b/miniprogram2/pages/scan/scan.js
@@ -0,0 +1,157 @@
+/**
+ * Soul创业派对 - 扫码解析页
+ * 扫描二维码/条形码,展示解析内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ // 最近一次解析结果
+ lastResult: null,
+ scanType: '',
+ charSet: '',
+ // 小程序码解析(路径、参数)
+ parsedPath: null,
+ parsedQuery: [],
+ canNavigate: false,
+ // 历史记录
+ history: []
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ this.loadHistory()
+ },
+
+ loadHistory() {
+ try {
+ const history = wx.getStorageSync('scanHistory') || []
+ this.setData({ history })
+ } catch (e) {
+ console.log('加载扫码历史失败:', e)
+ }
+ },
+
+ saveToHistory(result, scanType, charSet) {
+ const item = { result, scanType, charSet, time: new Date().toLocaleString() }
+ let history = wx.getStorageSync('scanHistory') || []
+ history = [item, ...history].slice(0, 10)
+ wx.setStorageSync('scanHistory', history)
+ this.setData({ history })
+ },
+
+ // 解析小程序码内容:path?key=val 或 path
+ parseMiniProgramCode(result) {
+ if (!result || typeof result !== 'string') return { path: null, query: [], canNavigate: false }
+ const idx = result.indexOf('?')
+ let path = idx >= 0 ? result.slice(0, idx) : result
+ const qs = idx >= 0 ? result.slice(idx + 1) : ''
+ path = path.replace(/^\//, '').trim()
+ const query = []
+ if (qs) {
+ qs.split('&').forEach(pair => {
+ const eq = pair.indexOf('=')
+ const k = eq >= 0 ? pair.slice(0, eq) : pair
+ const v = eq >= 0 ? pair.slice(eq + 1) : ''
+ try {
+ if (k) query.push({ key: decodeURIComponent(k), value: decodeURIComponent(v) })
+ } catch (_) {
+ if (k) query.push({ key: k, value: v })
+ }
+ })
+ }
+ const isMiniProgramPath = /^pages\/[\w-]+\/[\w-]+$/.test(path)
+ return { path: path || null, query, canNavigate: isMiniProgramPath }
+ },
+
+ // 发起扫码(支持小程序码)
+ doScan() {
+ wx.scanCode({
+ onlyFromCamera: false,
+ scanType: ['qrCode', 'barCode'],
+ success: (res) => {
+ const { result, scanType, charSet } = res
+ const parsed = this.parseMiniProgramCode(result)
+ this.setData({
+ lastResult: result,
+ scanType: scanType || '未知',
+ charSet: charSet || '',
+ parsedPath: parsed.path,
+ parsedQuery: parsed.query,
+ canNavigate: parsed.canNavigate
+ })
+ this.saveToHistory(result, scanType, charSet)
+ },
+ fail: (err) => {
+ if (err.errMsg && err.errMsg.includes('cancel')) {
+ return
+ }
+ wx.showToast({ title: err.errMsg || '扫码失败', icon: 'none' })
+ }
+ })
+ },
+
+ // 复制内容
+ copyResult() {
+ const { lastResult } = this.data
+ if (!lastResult) {
+ wx.showToast({ title: '暂无解析内容', icon: 'none' })
+ return
+ }
+ wx.setClipboardData({
+ data: lastResult,
+ success: () => wx.showToast({ title: '已复制', icon: 'success' })
+ })
+ },
+
+ // 清空当前结果
+ clearResult() {
+ this.setData({
+ lastResult: null, scanType: '', charSet: '',
+ parsedPath: null, parsedQuery: [], canNavigate: false
+ })
+ },
+
+ // 打开解析出的小程序页面
+ openParsedPage() {
+ const { parsedPath, parsedQuery } = this.data
+ if (!parsedPath || !this.data.canNavigate) {
+ wx.showToast({ title: '非本小程序页面', icon: 'none' })
+ return
+ }
+ const queryStr = parsedQuery.length
+ ? '?' + parsedQuery.map(q => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join('&')
+ : ''
+ const url = `/${parsedPath}${queryStr}`
+ const tabPages = ['pages/index/index', 'pages/chapters/chapters', 'pages/match/match', 'pages/my/my']
+ if (tabPages.includes(parsedPath)) {
+ wx.switchTab({ url: `/${parsedPath}` })
+ } else {
+ wx.navigateTo({ url })
+ }
+ },
+
+ // 清空历史
+ clearHistory() {
+ wx.setStorageSync('scanHistory', [])
+ this.setData({ history: [], lastResult: null, scanType: '', charSet: '' })
+ wx.showToast({ title: '已清空', icon: 'success' })
+ },
+
+ // 点击历史项复制
+ onHistoryItemTap(e) {
+ const result = e.currentTarget.dataset.result
+ if (!result) return
+ wx.setClipboardData({
+ data: result,
+ success: () => wx.showToast({ title: '已复制', icon: 'success' })
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ }
+})
diff --git a/miniprogram2/pages/scan/scan.json b/miniprogram2/pages/scan/scan.json
new file mode 100644
index 00000000..88880e06
--- /dev/null
+++ b/miniprogram2/pages/scan/scan.json
@@ -0,0 +1 @@
+{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"扫码解析"}
diff --git a/miniprogram2/pages/scan/scan.wxml b/miniprogram2/pages/scan/scan.wxml
new file mode 100644
index 00000000..a58da57c
--- /dev/null
+++ b/miniprogram2/pages/scan/scan.wxml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+ ←
+
+ 扫码解析
+
+
+
+
+
+
+
+
+ 📷
+ 扫描小程序码 / 二维码
+
+
+
+
+
+
+
+
+
+ 路径
+ {{parsedPath}}
+
+
+ {{q.key}}
+ {{q.value}}
+
+
+
+ 类型: {{scanType}}
+ 字符集: {{charSet}}
+
+
+ {{lastResult}}
+
+
+
+
+
+ 📷
+ 点击上方按钮扫码
+ 支持小程序码、二维码、条形码
+
+
+
+
+
+
+
+ {{item.result}}
+ {{item.time}}
+
+
+
+
+
diff --git a/miniprogram2/pages/scan/scan.wxss b/miniprogram2/pages/scan/scan.wxss
new file mode 100644
index 00000000..1e2c30a4
--- /dev/null
+++ b/miniprogram2/pages/scan/scan.wxss
@@ -0,0 +1,248 @@
+/* 扫码解析页样式 */
+.page {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #0a0a0a 0%, #111111 100%);
+}
+
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(10, 10, 10, 0.95);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+}
+
+.nav-content {
+ display: flex;
+ align-items: center;
+ padding: 8rpx 24rpx;
+ height: 88rpx;
+}
+
+.back-btn {
+ width: 60rpx;
+ height: 60rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.back-icon {
+ font-size: 40rpx;
+ color: #00CED1;
+}
+
+.nav-title {
+ flex: 1;
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #fff;
+ text-align: center;
+ margin-right: 60rpx;
+}
+
+.main-content {
+ padding: 24rpx;
+}
+
+/* 扫码按钮 */
+.scan-action {
+ padding: 60rpx 0;
+}
+
+.scan-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.15) 100%);
+ border: 2rpx solid rgba(0, 206, 209, 0.4);
+ border-radius: 32rpx;
+ padding: 80rpx 48rpx;
+}
+
+.scan-icon {
+ font-size: 80rpx;
+ margin-bottom: 24rpx;
+}
+
+.scan-text {
+ font-size: 32rpx;
+ color: #00CED1;
+ font-weight: 500;
+}
+
+/* 解析结果卡片 */
+.result-card {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 24rpx;
+ padding: 32rpx;
+ margin-top: 32rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.08);
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.result-label {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ font-weight: 500;
+}
+
+.result-actions {
+ display: flex;
+ gap: 24rpx;
+}
+
+.action-btn {
+ font-size: 26rpx;
+ color: #00CED1;
+ padding: 8rpx 20rpx;
+}
+
+.action-btn.secondary {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* 小程序码解析:路径+参数 */
+.parsed-section {
+ background: rgba(0, 206, 209, 0.08);
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 24rpx;
+ border: 1rpx solid rgba(0, 206, 209, 0.2);
+}
+
+.parsed-row {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: 12rpx;
+}
+
+.parsed-row:last-child {
+ margin-bottom: 0;
+}
+
+.parsed-label {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+ min-width: 120rpx;
+ flex-shrink: 0;
+}
+
+.parsed-value {
+ font-size: 26rpx;
+ color: #00CED1;
+ word-break: break-all;
+}
+
+.result-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16rpx;
+ margin-bottom: 20rpx;
+}
+
+.meta-item {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.result-content {
+ max-height: 400rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 16rpx;
+ padding: 24rpx;
+}
+
+.result-text {
+ font-size: 28rpx;
+ color: #fff;
+ line-height: 1.6;
+ word-break: break-all;
+ white-space: pre-wrap;
+}
+
+/* 无结果提示 */
+.empty-tip {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 60rpx 0;
+}
+
+.empty-icon {
+ font-size: 80rpx;
+ margin-bottom: 20rpx;
+ opacity: 0.5;
+}
+
+.empty-text {
+ font-size: 30rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 8rpx;
+}
+
+.empty-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* 扫码历史 */
+.history-section {
+ margin-top: 48rpx;
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.history-title {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.clear-history {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.history-item {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 16rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.06);
+}
+
+.history-content {
+ font-size: 26rpx;
+ color: #fff;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.history-time {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 8rpx;
+ display: block;
+}
diff --git a/miniprogram2/pages/search/search.js b/miniprogram2/pages/search/search.js
new file mode 100644
index 00000000..d9b0a4ca
--- /dev/null
+++ b/miniprogram2/pages/search/search.js
@@ -0,0 +1,118 @@
+/**
+ * Soul创业派对 - 章节搜索页
+ * 搜索章节标题和内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ keyword: '',
+ results: [],
+ loading: false,
+ searched: false,
+ total: 0,
+ // 热门搜索关键词
+ hotKeywords: ['私域', '电商', '流量', '赚钱', '创业', 'Soul', '抖音', '变现'],
+ // 热门章节推荐
+ hotChapters: [
+ { id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', part: '真实的人' },
+ { id: '9.12', title: '美业整合:一个人的公司如何月入十万', tag: '热门', part: '真实的赚钱' },
+ { id: '3.1', title: '3000万流水如何跑出来', tag: '热门', part: '真实的行业' },
+ { id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', part: '真实的赚钱' },
+ { id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', tag: '最新', part: '真实的赚钱' }
+ ]
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ // 加载热门章节
+ this.loadHotChapters()
+ },
+
+ // 加载热门章节(从服务器获取点击量高的章节)
+ async loadHotChapters() {
+ try {
+ const res = await app.request('/api/miniprogram/book/hot')
+ if (res && res.success && res.chapters?.length > 0) {
+ this.setData({ hotChapters: res.chapters })
+ }
+ } catch (e) {
+ console.log('加载热门章节失败,使用默认数据')
+ }
+ },
+
+ // 输入关键词
+ onInput(e) {
+ this.setData({ keyword: e.detail.value })
+ },
+
+ // 清空搜索
+ clearSearch() {
+ this.setData({
+ keyword: '',
+ results: [],
+ searched: false,
+ total: 0
+ })
+ },
+
+ // 点击热门关键词
+ onHotKeyword(e) {
+ const keyword = e.currentTarget.dataset.keyword
+ this.setData({ keyword })
+ this.doSearch()
+ },
+
+ // 执行搜索
+ async doSearch() {
+ const { keyword } = this.data
+ if (!keyword || keyword.trim().length < 1) {
+ wx.showToast({ title: '请输入搜索关键词', icon: 'none' })
+ return
+ }
+
+ this.setData({ loading: true, searched: true })
+
+ try {
+ const res = await app.request(`/api/miniprogram/book/search?q=${encodeURIComponent(keyword.trim())}`)
+
+ if (res && res.success) {
+ this.setData({
+ results: res.results || [],
+ total: res.total || 0
+ })
+ } else {
+ this.setData({ results: [], total: 0 })
+ }
+ } catch (e) {
+ console.error('搜索失败:', e)
+ wx.showToast({ title: '搜索失败', icon: 'none' })
+ this.setData({ results: [], total: 0 })
+ } finally {
+ this.setData({ loading: false })
+ }
+ },
+
+ goToRead(e) {
+ const id = e.currentTarget.dataset.id
+ const mid = e.currentTarget.dataset.mid
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ // 返回上一页
+ goBack() {
+ wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 搜索',
+ path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search'
+ }
+ }
+})
diff --git a/miniprogram2/pages/search/search.json b/miniprogram2/pages/search/search.json
new file mode 100644
index 00000000..877955df
--- /dev/null
+++ b/miniprogram2/pages/search/search.json
@@ -0,0 +1,5 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "搜索"
+}
diff --git a/miniprogram2/pages/search/search.wxml b/miniprogram2/pages/search/search.wxml
new file mode 100644
index 00000000..6a9e4df7
--- /dev/null
+++ b/miniprogram2/pages/search/search.wxml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+ ←
+
+
+ 🔍
+
+ ×
+
+ 搜索
+
+
+
+
+
+
+
+
+ 热门搜索
+
+ {{item}}
+
+
+
+
+
+ 热门章节
+
+
+ {{index + 1}}
+
+ {{item.title}}
+ {{item.part}}
+
+ {{item.tag}}
+
+
+
+
+
+
+
+
+
+ 搜索中...
+
+
+
+
+
+
+
+
+
+ {{item.title}}
+ {{item.part}}
+
+ {{item.matchedContent}}
+
+ →
+
+
+
+
+
+
+ 🔍
+ 未找到相关章节
+ 换个关键词试试
+
+
+
+
diff --git a/miniprogram2/pages/search/search.wxss b/miniprogram2/pages/search/search.wxss
new file mode 100644
index 00000000..aa56b19b
--- /dev/null
+++ b/miniprogram2/pages/search/search.wxss
@@ -0,0 +1,335 @@
+/* 章节搜索页样式 */
+.page {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #0a0a0a 0%, #111111 100%);
+}
+
+/* 导航栏 */
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(10, 10, 10, 0.95);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+}
+
+.nav-content {
+ display: flex;
+ align-items: center;
+ padding: 8rpx 24rpx;
+ height: 88rpx;
+}
+
+.back-btn {
+ width: 60rpx;
+ height: 60rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.back-icon {
+ font-size: 40rpx;
+ color: #00CED1;
+}
+
+.search-input-wrap {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background: rgba(255,255,255,0.08);
+ border-radius: 40rpx;
+ padding: 0 24rpx;
+ height: 64rpx;
+ margin: 0 16rpx;
+}
+
+.search-icon-small {
+ font-size: 28rpx;
+ margin-right: 12rpx;
+}
+
+.search-input {
+ flex: 1;
+ font-size: 28rpx;
+ color: #fff;
+}
+
+.search-input::placeholder {
+ color: rgba(255,255,255,0.4);
+}
+
+.clear-btn {
+ width: 40rpx;
+ height: 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: rgba(255,255,255,0.5);
+}
+
+.search-btn {
+ font-size: 28rpx;
+ color: #00CED1;
+ padding: 0 16rpx;
+}
+
+/* 主内容 */
+.main-content {
+ padding: 24rpx;
+}
+
+/* 热门搜索 */
+.hot-section {
+ padding: 24rpx 0;
+}
+
+.section-title {
+ font-size: 28rpx;
+ color: rgba(255,255,255,0.6);
+ margin-bottom: 24rpx;
+ display: block;
+}
+
+.hot-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20rpx;
+}
+
+.hot-tag {
+ background: rgba(0, 206, 209, 0.15);
+ color: #00CED1;
+ padding: 16rpx 32rpx;
+ border-radius: 32rpx;
+ font-size: 28rpx;
+ border: 1rpx solid rgba(0, 206, 209, 0.3);
+}
+
+/* 热门章节 */
+.hot-chapters {
+ padding: 32rpx 0;
+ margin-top: 16rpx;
+}
+
+.chapter-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.chapter-item {
+ display: flex;
+ align-items: center;
+ background: rgba(255,255,255,0.05);
+ border-radius: 20rpx;
+ padding: 24rpx;
+ gap: 20rpx;
+}
+
+.chapter-rank {
+ width: 48rpx;
+ height: 48rpx;
+ background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 26rpx;
+ font-weight: 600;
+ color: #000;
+ flex-shrink: 0;
+}
+
+.chapter-item:nth-child(1) .chapter-rank { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); }
+.chapter-item:nth-child(2) .chapter-rank { background: linear-gradient(135deg, #C0C0C0 0%, #A9A9A9 100%); }
+.chapter-item:nth-child(3) .chapter-rank { background: linear-gradient(135deg, #CD7F32 0%, #8B4513 100%); }
+
+.chapter-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.chapter-title {
+ font-size: 28rpx;
+ color: #fff;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chapter-part {
+ font-size: 22rpx;
+ color: rgba(255,255,255,0.4);
+ margin-top: 6rpx;
+ display: block;
+}
+
+.chapter-tag {
+ padding: 8rpx 16rpx;
+ border-radius: 16rpx;
+ font-size: 22rpx;
+ flex-shrink: 0;
+}
+
+.chapter-tag.tag-free { background: rgba(76, 175, 80, 0.2); color: #4CAF50; }
+.chapter-tag.tag-hot { background: rgba(255, 87, 34, 0.2); color: #FF5722; }
+.chapter-tag.tag-new { background: rgba(233, 30, 99, 0.2); color: #E91E63; }
+
+/* 搜索结果 */
+.results-section {
+ padding: 16rpx 0;
+}
+
+.results-header {
+ margin-bottom: 24rpx;
+}
+
+.results-count {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.5);
+}
+
+.results-list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+}
+
+.result-item {
+ background: rgba(255,255,255,0.05);
+ border-radius: 24rpx;
+ padding: 28rpx;
+ position: relative;
+ border: 1rpx solid rgba(255,255,255,0.08);
+}
+
+.result-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+}
+
+.result-chapter {
+ font-size: 24rpx;
+ color: #00CED1;
+ font-weight: 500;
+}
+
+.result-tags {
+ display: flex;
+ gap: 12rpx;
+}
+
+.tag {
+ font-size: 20rpx;
+ padding: 6rpx 16rpx;
+ border-radius: 20rpx;
+}
+
+.tag-match {
+ background: rgba(147, 112, 219, 0.2);
+ color: #9370DB;
+}
+
+.tag-free {
+ background: rgba(76, 175, 80, 0.2);
+ color: #4CAF50;
+}
+
+.result-title {
+ font-size: 30rpx;
+ color: #fff;
+ font-weight: 500;
+ line-height: 1.5;
+ display: block;
+ margin-bottom: 8rpx;
+}
+
+.result-part {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.5);
+ display: block;
+}
+
+.result-content {
+ margin-top: 16rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx solid rgba(255,255,255,0.1);
+}
+
+.content-preview {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.6);
+ line-height: 1.6;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.result-arrow {
+ position: absolute;
+ right: 28rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 32rpx;
+ color: rgba(255,255,255,0.3);
+}
+
+/* 加载状态 */
+.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100rpx 0;
+}
+
+.loading-spinner {
+ width: 60rpx;
+ height: 60rpx;
+ border: 4rpx solid rgba(0, 206, 209, 0.3);
+ border-top-color: #00CED1;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-text {
+ margin-top: 24rpx;
+ font-size: 28rpx;
+ color: rgba(255,255,255,0.5);
+}
+
+/* 空状态 */
+.empty-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100rpx 0;
+}
+
+.empty-icon {
+ font-size: 80rpx;
+ margin-bottom: 24rpx;
+}
+
+.empty-text {
+ font-size: 32rpx;
+ color: rgba(255,255,255,0.6);
+ margin-bottom: 12rpx;
+}
+
+.empty-hint {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.4);
+}
diff --git a/miniprogram2/pages/settings/settings.js b/miniprogram2/pages/settings/settings.js
new file mode 100644
index 00000000..922390bf
--- /dev/null
+++ b/miniprogram2/pages/settings/settings.js
@@ -0,0 +1,446 @@
+/**
+ * Soul创业派对 - 设置页
+ * 账号绑定功能
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ isLoggedIn: false,
+ userInfo: null,
+ version: '1.0.0',
+
+ // 绑定信息
+ phoneNumber: '',
+ wechatId: '',
+ alipayAccount: '',
+ address: '',
+
+ // 自动提现(默认开启)
+ autoWithdrawEnabled: true,
+
+ // 绑定弹窗
+ showBindModal: false,
+ bindType: '', // phone | wechat | alipay
+ bindValue: ''
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight,
+ isLoggedIn: app.globalData.isLoggedIn,
+ userInfo: app.globalData.userInfo
+ })
+ this.loadBindingInfo()
+ },
+
+ onShow() {
+ this.loadBindingInfo()
+ },
+
+ // 加载绑定信息
+ loadBindingInfo() {
+ const { userInfo, isLoggedIn } = app.globalData
+ if (isLoggedIn && userInfo) {
+ // 从本地存储或用户信息中获取绑定数据
+ const phoneNumber = wx.getStorageSync('user_phone') || userInfo.phone || ''
+ const wechatId = wx.getStorageSync('user_wechat') || userInfo.wechatId || userInfo.wechat || ''
+ const alipayAccount = wx.getStorageSync('user_alipay') || userInfo.alipay || ''
+ const address = wx.getStorageSync('user_address') || userInfo.address || ''
+ // 默认开启自动提现
+ const autoWithdrawEnabled = wx.getStorageSync('auto_withdraw_enabled') !== false
+
+ this.setData({
+ isLoggedIn: true,
+ userInfo,
+ phoneNumber,
+ wechatId,
+ alipayAccount,
+ address,
+ autoWithdrawEnabled
+ })
+ }
+ },
+
+ // 收货地址已改为「地址管理」页(goToAddresses)
+
+ // 切换自动提现
+ async toggleAutoWithdraw(e) {
+ const enabled = e.detail.value
+
+ // 检查是否绑定了支付方式
+ if (enabled && !this.data.wechatId && !this.data.alipayAccount) {
+ wx.showToast({ title: '请先绑定微信号或支付宝', icon: 'none' })
+ this.setData({ autoWithdrawEnabled: false })
+ return
+ }
+
+ // 开启时需要确认
+ if (enabled) {
+ wx.showModal({
+ title: '开启自动提现',
+ content: `收益将自动打款到您的${this.data.alipayAccount ? '支付宝' : '微信'}账户,确认开启吗?`,
+ success: async (res) => {
+ if (res.confirm) {
+ this.setData({ autoWithdrawEnabled: true })
+ wx.setStorageSync('auto_withdraw_enabled', true)
+
+ // 同步到服务器
+ try {
+ await app.request('/api/miniprogram/user/update', {
+ method: 'POST',
+ data: {
+ userId: app.globalData.userInfo?.id,
+ autoWithdraw: true,
+ withdrawAccount: this.data.alipayAccount || this.data.wechatId
+ }
+ })
+ } catch (e) {
+ console.log('同步自动提现设置失败', e)
+ }
+
+ wx.showToast({ title: '已开启自动提现', icon: 'success' })
+ } else {
+ this.setData({ autoWithdrawEnabled: false })
+ }
+ }
+ })
+ } else {
+ this.setData({ autoWithdrawEnabled: false })
+ wx.setStorageSync('auto_withdraw_enabled', false)
+ wx.showToast({ title: '已关闭自动提现', icon: 'success' })
+ }
+ },
+
+ // 绑定手机号
+ bindPhone() {
+ this.setData({
+ showBindModal: true,
+ bindType: 'phone',
+ bindValue: ''
+ })
+ },
+
+ // 微信号输入
+ onWechatInput(e) {
+ this.setData({ wechatId: e.detail.value })
+ },
+
+ // 保存微信号
+ async saveWechat() {
+ const { wechatId } = this.data
+ if (!wechatId || wechatId.length < 6) return
+
+ wx.setStorageSync('user_wechat', wechatId)
+
+ // 更新用户信息
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.wechat = wechatId
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+
+ // 同步到服务器
+ try {
+ await app.request('/api/miniprogram/user/update', {
+ method: 'POST',
+ data: {
+ userId: app.globalData.userInfo?.id,
+ wechat: wechatId
+ }
+ })
+ wx.showToast({ title: '微信号已保存', icon: 'success' })
+ } catch (e) {
+ console.log('保存微信号失败', e)
+ }
+ },
+
+ // 输入绑定值
+ onBindInput(e) {
+ let value = e.detail.value
+ if (this.data.bindType === 'phone') {
+ value = value.replace(/\D/g, '').slice(0, 11)
+ }
+ this.setData({ bindValue: value })
+ },
+
+ // 确认绑定
+ confirmBind() {
+ const { bindType, bindValue } = this.data
+
+ if (!bindValue) {
+ wx.showToast({ title: '请输入内容', icon: 'none' })
+ return
+ }
+
+ // 验证
+ if (bindType === 'phone' && !/^1[3-9]\d{9}$/.test(bindValue)) {
+ wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
+ return
+ }
+
+ if (bindType === 'wechat' && bindValue.length < 6) {
+ wx.showToast({ title: '微信号至少6位', icon: 'none' })
+ return
+ }
+
+ if (bindType === 'alipay' && !bindValue.includes('@') && !/^1[3-9]\d{9}$/.test(bindValue)) {
+ wx.showToast({ title: '请输入正确的支付宝账号', icon: 'none' })
+ return
+ }
+
+ // 保存绑定信息到本地
+ if (bindType === 'phone') {
+ wx.setStorageSync('user_phone', bindValue)
+ this.setData({ phoneNumber: bindValue })
+ } else if (bindType === 'wechat') {
+ wx.setStorageSync('user_wechat', bindValue)
+ this.setData({ wechatId: bindValue })
+ } else if (bindType === 'alipay') {
+ wx.setStorageSync('user_alipay', bindValue)
+ this.setData({ alipayAccount: bindValue })
+ }
+
+ // 同步到服务器
+ this.syncProfileToServer()
+
+ this.setData({ showBindModal: false })
+ wx.showToast({ title: '绑定成功', icon: 'success' })
+ },
+
+ // 同步资料到服务器
+ async syncProfileToServer() {
+ try {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+
+ const res = await app.request('/api/miniprogram/user/profile', {
+ method: 'POST',
+ data: {
+ userId,
+ phone: this.data.phoneNumber || undefined,
+ wechatId: this.data.wechatId || undefined
+ }
+ })
+
+ if (res.success) {
+ console.log('[Settings] 资料同步成功')
+ // 更新本地用户信息
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.phone = this.data.phoneNumber
+ app.globalData.userInfo.wechatId = this.data.wechatId
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+ }
+ } catch (e) {
+ console.log('[Settings] 资料同步失败:', e)
+ }
+ },
+
+ // 获取微信头像(新版授权)
+ async getWechatAvatar() {
+ try {
+ const res = await wx.getUserProfile({
+ desc: '用于完善会员资料'
+ })
+
+ if (res.userInfo) {
+ const { nickName, avatarUrl: tempAvatarUrl } = res.userInfo
+
+ wx.showLoading({ title: '上传中...', mask: true })
+
+ // 1. 先上传图片到服务器
+ console.log('[Settings] 开始上传头像:', tempAvatarUrl)
+
+ const uploadRes = await new Promise((resolve, reject) => {
+ wx.uploadFile({
+ url: app.globalData.baseUrl + '/api/miniprogram/upload',
+ filePath: tempAvatarUrl,
+ name: 'file',
+ formData: {
+ folder: 'avatars'
+ },
+ success: (uploadResult) => {
+ try {
+ const data = JSON.parse(uploadResult.data)
+ if (data.success) {
+ resolve(data)
+ } else {
+ reject(new Error(data.error || '上传失败'))
+ }
+ } catch (err) {
+ reject(new Error('解析响应失败'))
+ }
+ },
+ fail: (err) => {
+ reject(err)
+ }
+ })
+ })
+
+ // 2. 获取上传后的完整URL
+ const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
+ console.log('[Settings] 头像上传成功:', avatarUrl)
+
+ // 3. 更新本地
+ this.setData({
+ userInfo: {
+ ...this.data.userInfo,
+ nickname: nickName,
+ avatar: avatarUrl
+ }
+ })
+
+ // 4. 同步到服务器数据库
+ const userId = app.globalData.userInfo?.id
+ if (userId) {
+ await app.request('/api/miniprogram/user/profile', {
+ method: 'POST',
+ data: { userId, nickname: nickName, avatar: avatarUrl }
+ })
+ }
+
+ // 5. 更新全局
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.nickname = nickName
+ app.globalData.userInfo.avatar = avatarUrl
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+
+ wx.hideLoading()
+ wx.showToast({ title: '头像更新成功', icon: 'success' })
+ }
+ } catch (e) {
+ wx.hideLoading()
+ console.error('[Settings] 获取头像失败:', e)
+ wx.showToast({
+ title: e.message || '获取头像失败',
+ icon: 'none'
+ })
+ }
+ },
+
+ // 一键获取微信手机号(button组件回调)
+ async onGetPhoneNumber(e) {
+ console.log('[Settings] 获取手机号回调:', e.detail)
+
+ if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+ wx.showToast({ title: '授权失败', icon: 'none' })
+ return
+ }
+
+ try {
+ // 需要将code发送到服务器解密获取手机号
+ const code = e.detail.code
+ if (!code) {
+ // 如果没有code,弹出手动输入
+ this.bindPhone()
+ return
+ }
+
+ wx.showLoading({ title: '获取中...', mask: true })
+
+ // 调用服务器解密手机号(传入userId以便同步到数据库)
+ const userId = app.globalData.userInfo?.id
+ const res = await app.request('/api/miniprogram/phone', {
+ method: 'POST',
+ data: { code, userId }
+ })
+
+ wx.hideLoading()
+
+ if (res.success && res.phoneNumber) {
+ wx.setStorageSync('user_phone', res.phoneNumber)
+ this.setData({ phoneNumber: res.phoneNumber })
+
+ // 更新用户信息
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.phone = res.phoneNumber
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+
+ // 同步到服务器
+ this.syncProfileToServer()
+
+ wx.showToast({ title: '手机号绑定成功', icon: 'success' })
+ } else {
+ // 获取失败,弹出手动输入
+ this.bindPhone()
+ }
+ } catch (e) {
+ wx.hideLoading()
+ console.log('[Settings] 获取手机号失败:', e)
+ // 获取失败,弹出手动输入
+ this.bindPhone()
+ }
+ },
+
+ // 关闭绑定弹窗
+ closeBindModal() {
+ this.setData({ showBindModal: false })
+ },
+
+ // 清除缓存
+ clearCache() {
+ wx.showModal({
+ title: '清除缓存',
+ content: '确定要清除本地缓存吗?',
+ success: (res) => {
+ if (res.confirm) {
+ // 保留登录信息,只清除其他缓存
+ const token = wx.getStorageSync('token')
+ const userInfo = wx.getStorageSync('userInfo')
+ wx.clearStorageSync()
+ if (token) wx.setStorageSync('token', token)
+ if (userInfo) wx.setStorageSync('userInfo', userInfo)
+ wx.showToast({ title: '缓存已清除', icon: 'success' })
+ }
+ }
+ })
+ },
+
+ // 退出登录
+ handleLogout() {
+ wx.showModal({
+ title: '退出登录',
+ content: '确定要退出登录吗?',
+ success: (res) => {
+ if (res.confirm) {
+ app.logout()
+ this.setData({
+ isLoggedIn: false,
+ userInfo: null,
+ phoneNumber: '',
+ wechatId: '',
+ alipayAccount: ''
+ })
+ wx.showToast({ title: '已退出登录', icon: 'success' })
+ setTimeout(() => wx.navigateBack(), 1500)
+ }
+ }
+ })
+ },
+
+ // 联系客服 - 跳转到Soul派对房
+ contactService() {
+ wx.showToast({ title: '请在Soul派对房联系客服', icon: 'none' })
+ },
+
+ // 阻止冒泡
+ stopPropagation() {},
+
+ goBack() { wx.navigateBack() },
+
+ // 跳转到地址管理页
+ goToAddresses() {
+ wx.navigateTo({ url: '/pages/addresses/addresses' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 设置',
+ path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
+ }
+ }
+})
diff --git a/miniprogram2/pages/settings/settings.json b/miniprogram2/pages/settings/settings.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram2/pages/settings/settings.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/settings/settings.wxml b/miniprogram2/pages/settings/settings.wxml
new file mode 100644
index 00000000..2338a79b
--- /dev/null
+++ b/miniprogram2/pages/settings/settings.wxml
@@ -0,0 +1,146 @@
+
+
+
+
+ ‹
+
+ 设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📱
+
+ 手机号
+ {{phoneNumber || '未绑定'}}
+
+
+
+ ✓
+
+
+
+
+
+
+
+ 💬
+
+ 微信号
+
+
+
+
+ ✓
+
+
+
+
+
+
+ 📍
+
+ 收货地址
+ 管理收货地址,用于发货与邮寄
+
+
+
+ 管理
+
+
+
+
+
+
+
+
+
+
+
+ 开启自动提现
+
+
+
+
+
+ 提现方式
+ 微信零钱
+
+
+ 提现账户
+ {{wechatId}}
+
+ 收益将在每笔订单完成后自动打款
+
+
+
+
+
+
+ 提示:绑定微信号才能使用提现功能
+
+
+ 退出登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{bindType === 'phone' ? '绑定手机号后可用于找伙伴匹配' : bindType === 'wechat' ? '绑定微信号后可用于找伙伴匹配和好友添加' : '绑定支付宝后可用于提现收益'}}
+
+
+
+ 确认绑定
+
+
+
+
+
diff --git a/miniprogram2/pages/settings/settings.wxss b/miniprogram2/pages/settings/settings.wxss
new file mode 100644
index 00000000..5e7a4254
--- /dev/null
+++ b/miniprogram2/pages/settings/settings.wxss
@@ -0,0 +1,114 @@
+/* 设置页样式 */
+.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
+
+/* 导航栏 */
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
+.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); font-weight: 300; }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
+.nav-placeholder { width: 64rpx; }
+
+.content { padding: 24rpx; }
+
+/* 账号绑定卡片 */
+.bind-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
+.card-header { display: flex; align-items: flex-start; gap: 16rpx; margin-bottom: 24rpx; padding-bottom: 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.card-icon { font-size: 40rpx; }
+.card-title-wrap { flex: 1; }
+.card-title { font-size: 30rpx; font-weight: 600; color: #fff; display: block; }
+.card-desc { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
+
+.bind-list { display: flex; flex-direction: column; gap: 24rpx; }
+.bind-item { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0; }
+.bind-left { display: flex; align-items: center; gap: 20rpx; }
+.bind-icon { width: 72rpx; height: 72rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; }
+.bind-icon.phone-icon { background: rgba(0,206,209,0.2); }
+.bind-icon.wechat-icon { background: rgba(158,158,158,0.2); }
+.bind-icon.alipay-icon { background: rgba(158,158,158,0.2); }
+.bind-info { display: flex; flex-direction: column; gap: 4rpx; flex: 1; }
+.bind-label { font-size: 28rpx; color: #fff; font-weight: 500; }
+.bind-value { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.address-text { max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bind-icon.address-icon { background: rgba(255,165,0,0.2); }
+.required { color: #FF6B6B; font-size: 24rpx; }
+.bind-input { font-size: 24rpx; color: #00CED1; background: transparent; padding: 8rpx 0; }
+.bind-right { display: flex; align-items: center; }
+.bind-check { color: #00CED1; font-size: 32rpx; }
+.bind-btn { color: #00CED1; font-size: 26rpx; }
+.bind-manage { color: #00CED1; font-size: 26rpx; }
+.brand-color { color: #00CED1; }
+
+/* 一键获取手机号按钮 */
+.get-phone-btn {
+ padding: 12rpx 24rpx;
+ background: rgba(0,206,209,0.2);
+ border: 2rpx solid rgba(0,206,209,0.3);
+ border-radius: 16rpx;
+ font-size: 24rpx;
+ color: #00CED1;
+ line-height: normal;
+}
+.get-phone-btn::after { border: none; }
+
+/* 自动提现卡片 */
+.auto-withdraw-card { margin-top: 24rpx; }
+.auto-withdraw-content { padding-top: 16rpx; }
+.withdraw-switch-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16rpx 0;
+}
+.switch-label { font-size: 28rpx; color: #fff; }
+.withdraw-info {
+ background: rgba(0,206,209,0.1);
+ border-radius: 16rpx;
+ padding: 20rpx;
+ margin-top: 16rpx;
+}
+.info-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 8rpx 0;
+}
+.info-label { font-size: 26rpx; color: rgba(255,255,255,0.6); }
+.info-value { font-size: 26rpx; color: #00CED1; }
+.withdraw-tip {
+ display: block;
+ font-size: 22rpx;
+ color: rgba(255,255,255,0.4);
+ margin-top: 12rpx;
+ text-align: center;
+}
+
+/* 提现提示 */
+.tip-banner { background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; }
+.tip-text { font-size: 24rpx; color: #FFA500; line-height: 1.5; }
+
+/* 设置组 */
+.settings-group { background: #1c1c1e; border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; }
+.settings-item { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
+.settings-item:last-child { border-bottom: none; }
+.item-left { display: flex; align-items: center; gap: 16rpx; }
+.item-icon { font-size: 36rpx; }
+.item-title { font-size: 28rpx; color: #fff; }
+.item-arrow { font-size: 28rpx; color: rgba(255,255,255,0.3); }
+.item-value { font-size: 26rpx; color: rgba(255,255,255,0.5); }
+
+/* 退出登录按钮 */
+.logout-btn { margin-top: 48rpx; padding: 28rpx; background: rgba(244,67,54,0.1); border: 2rpx solid rgba(244,67,54,0.3); border-radius: 24rpx; text-align: center; font-size: 28rpx; color: #F44336; }
+
+/* 弹窗 - 简洁大气风格 */
+.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(20rpx); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 48rpx; }
+.modal-content { width: 100%; max-width: 640rpx; background: linear-gradient(180deg, #1c1c1e 0%, #0d0d0d 100%); border-radius: 40rpx; overflow: hidden; border: 2rpx solid rgba(255,255,255,0.08); }
+.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 40rpx 40rpx 24rpx; }
+.modal-title { font-size: 36rpx; font-weight: 700; color: #fff; }
+.modal-close { width: 64rpx; height: 64rpx; background: rgba(255,255,255,0.08); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: rgba(255,255,255,0.5); }
+.modal-body { padding: 16rpx 40rpx 48rpx; }
+.input-wrapper { margin-bottom: 32rpx; }
+.form-input { width: 100%; padding: 32rpx 24rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; font-size: 32rpx; color: #fff; box-sizing: border-box; transition: all 0.2s; }
+.form-input:focus { border-color: rgba(0,206,209,0.5); background: rgba(0,206,209,0.05); }
+.input-placeholder { color: rgba(255,255,255,0.25); }
+.bind-tip { font-size: 24rpx; color: rgba(255,255,255,0.4); margin-bottom: 40rpx; display: block; line-height: 1.6; text-align: center; }
+.btn-primary { padding: 32rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 28rpx; }
+.btn-primary.btn-disabled { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.3); }
diff --git a/miniprogram2/pages/withdraw-records/withdraw-records.js b/miniprogram2/pages/withdraw-records/withdraw-records.js
new file mode 100644
index 00000000..9757efac
--- /dev/null
+++ b/miniprogram2/pages/withdraw-records/withdraw-records.js
@@ -0,0 +1,131 @@
+/**
+ * 提现记录 - 独立页面
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ list: [],
+ loading: true
+ },
+
+ onLoad() {
+ this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
+ this.loadRecords()
+ },
+
+ onShow() {
+ this.loadRecords()
+ },
+
+ async loadRecords() {
+ const userInfo = app.globalData.userInfo
+ if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
+ this.setData({ list: [], loading: false })
+ return
+ }
+ this.setData({ loading: true })
+ try {
+ const res = await app.request('/api/miniprogram/withdraw/records?userId=' + userInfo.id)
+ if (res && res.success && res.data && Array.isArray(res.data.list)) {
+ const list = (res.data.list || []).map(item => ({
+ id: item.id,
+ amount: (item.amount != null ? item.amount : 0).toFixed(2),
+ status: this.statusText(item.status),
+ statusRaw: item.status,
+ createdAt: (item.createdAt ?? item.created_at) ? this.formatDate(item.createdAt ?? item.created_at) : '--',
+ canReceive: !!item.canReceive
+ }))
+ this.setData({ list, loading: false })
+ } else {
+ this.setData({ list: [], loading: false })
+ }
+ } catch (e) {
+ console.log('[WithdrawRecords] 加载失败:', e)
+ this.setData({ list: [], loading: false })
+ }
+ },
+
+ statusText(status) {
+ const map = {
+ pending: '待审核',
+ pending_confirm: '待确认收款',
+ processing: '处理中',
+ success: '已到账',
+ failed: '已拒绝'
+ }
+ return map[status] || status || '--'
+ },
+
+ formatDate(dateStr) {
+ if (!dateStr) return '--'
+ const d = new Date(dateStr)
+ const y = d.getFullYear()
+ const m = (d.getMonth() + 1).toString().padStart(2, '0')
+ const day = d.getDate().toString().padStart(2, '0')
+ return `${y}-${m}-${day}`
+ },
+
+ goBack() {
+ wx.navigateBack()
+ },
+
+ async onReceiveTap(e) {
+ const id = e.currentTarget.dataset.id
+ if (!id) return
+ wx.showLoading({ title: '加载中...' })
+ try {
+ const res = await app.request('/api/miniprogram/withdraw/confirm-info?id=' + encodeURIComponent(id))
+ wx.hideLoading()
+ if (!res || !res.success || !res.data) {
+ wx.showToast({ title: res?.message || '获取领取信息失败', icon: 'none' })
+ return
+ }
+ const { mchId, appId, package: pkg } = res.data
+ if (!pkg || pkg === '') {
+ wx.showToast({
+ title: '打款已发起,请到微信零钱中查看',
+ icon: 'none',
+ duration: 2500
+ })
+ return
+ }
+ if (!wx.canIUse('requestMerchantTransfer')) {
+ wx.showToast({ title: '当前微信版本过低,请更新后重试', icon: 'none' })
+ return
+ }
+ wx.requestMerchantTransfer({
+ mchId: mchId || '',
+ appId: appId || wx.getAccountInfoSync().miniProgram.appId,
+ package: pkg,
+ success: (res) => {
+ if (res.errMsg === 'requestMerchantTransfer:ok') {
+ wx.showToast({ title: '已调起收款页', icon: 'success' })
+ this.loadRecords()
+ } else {
+ wx.showToast({ title: res.errMsg || '操作完成', icon: 'none' })
+ }
+ },
+ fail: (err) => {
+ if (err.errMsg && err.errMsg.indexOf('cancel') !== -1) {
+ wx.showToast({ title: '已取消', icon: 'none' })
+ } else {
+ wx.showToast({ title: err.errMsg || '调起失败', icon: 'none' })
+ }
+ }
+ })
+ } catch (e) {
+ wx.hideLoading()
+ wx.showToast({ title: '网络异常,请重试', icon: 'none' })
+ }
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 提现记录',
+ path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
+ }
+ }
+})
diff --git a/miniprogram2/pages/withdraw-records/withdraw-records.json b/miniprogram2/pages/withdraw-records/withdraw-records.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram2/pages/withdraw-records/withdraw-records.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram2/pages/withdraw-records/withdraw-records.wxml b/miniprogram2/pages/withdraw-records/withdraw-records.wxml
new file mode 100644
index 00000000..def185dc
--- /dev/null
+++ b/miniprogram2/pages/withdraw-records/withdraw-records.wxml
@@ -0,0 +1,25 @@
+
+
+ ←
+ 提现记录
+
+
+
+
+
+ 加载中...
+ 暂无提现记录
+
+
+
+ ¥{{item.amount}}
+ {{item.createdAt}}
+
+
+ {{item.status}}
+
+
+
+
+
+
diff --git a/miniprogram2/pages/withdraw-records/withdraw-records.wxss b/miniprogram2/pages/withdraw-records/withdraw-records.wxss
new file mode 100644
index 00000000..4de9e2bb
--- /dev/null
+++ b/miniprogram2/pages/withdraw-records/withdraw-records.wxss
@@ -0,0 +1,71 @@
+.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.9);
+ display: flex;
+ align-items: center;
+ padding: 0 24rpx;
+ height: 88rpx;
+}
+.nav-back {
+ color: #00CED1;
+ font-size: 36rpx;
+ padding: 16rpx;
+}
+.nav-title {
+ flex: 1;
+ text-align: center;
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #fff;
+}
+.nav-placeholder { width: 80rpx; }
+.nav-placeholder-bar { width: 100%; }
+
+.content {
+ padding: 32rpx;
+}
+.loading-tip, .empty {
+ text-align: center;
+ color: rgba(255,255,255,0.6);
+ font-size: 28rpx;
+ padding: 80rpx 0;
+}
+.list { }
+.item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 28rpx 0;
+ border-bottom: 2rpx solid rgba(255,255,255,0.06);
+}
+.item:last-child { border-bottom: none; }
+.item-left { display: flex; flex-direction: column; gap: 8rpx; }
+.item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 12rpx; }
+.amount { font-size: 32rpx; font-weight: 600; color: #fff; }
+.time { font-size: 24rpx; color: rgba(255,255,255,0.5); }
+.status { font-size: 26rpx; }
+.status.status-pending { color: #FFA500; }
+.status.status-processing { color: #4CAF50; }
+.status.status-pending_confirm { color: #4CAF50; }
+.status.status-success { color: #4CAF50; }
+.status.status-failed { color: rgba(255,255,255,0.5); }
+.btn-receive {
+ margin: 0;
+ padding: 0 24rpx;
+ height: 56rpx;
+ line-height: 56rpx;
+ font-size: 24rpx;
+ color: #00CED1;
+ background: transparent;
+ border: 2rpx solid #00CED1;
+ border-radius: 8rpx;
+}
+.btn-receive::after { border: none; }
diff --git a/miniprogram2/project.config.json b/miniprogram2/project.config.json
new file mode 100644
index 00000000..28e84ef2
--- /dev/null
+++ b/miniprogram2/project.config.json
@@ -0,0 +1,62 @@
+{
+ "compileType": "miniprogram",
+ "miniprogramRoot": "",
+ "projectname": "soul-startup",
+ "description": "Soul创业派对 - 来自派对房的真实商业故事",
+ "appid": "wxb8bbb2b10dec74aa",
+ "setting": {
+ "urlCheck": false,
+ "es6": true,
+ "enhance": true,
+ "postcss": true,
+ "preloadBackgroundData": false,
+ "minified": true,
+ "newFeature": true,
+ "coverView": true,
+ "nodeModules": false,
+ "autoAudits": false,
+ "showShadowRootInWxmlPanel": true,
+ "scopeDataCheck": false,
+ "uglifyFileName": false,
+ "checkInvalidKey": true,
+ "checkSiteMap": true,
+ "uploadWithSourceMap": true,
+ "compileHotReLoad": true,
+ "lazyloadPlaceholderEnable": false,
+ "useMultiFrameRuntime": true,
+ "useApiHook": true,
+ "useApiHostProcess": true,
+ "babelSetting": {
+ "ignore": [],
+ "disablePlugins": [],
+ "outputPath": ""
+ },
+ "enableEngineNative": false,
+ "useIsolateContext": true,
+ "userConfirmedBundleSwitch": false,
+ "packNpmManually": false,
+ "packNpmRelationList": [],
+ "minifyWXSS": true,
+ "disableUseStrict": false,
+ "minifyWXML": true,
+ "showES6CompileOption": false,
+ "useCompilerPlugins": false,
+ "ignoreUploadUnusedFiles": true,
+ "compileWorklet": false,
+ "localPlugins": false,
+ "condition": false,
+ "swc": false,
+ "disableSWC": true
+ },
+ "libVersion": "3.13.2",
+ "packOptions": {
+ "ignore": [],
+ "include": []
+ },
+ "condition": {},
+ "editorSetting": {
+ "tabIndent": "insertSpaces",
+ "tabSize": 2
+ },
+ "simulatorPluginLibVersion": {}
+}
\ No newline at end of file
diff --git a/miniprogram2/project.private.config.json b/miniprogram2/project.private.config.json
new file mode 100644
index 00000000..20414ec5
--- /dev/null
+++ b/miniprogram2/project.private.config.json
@@ -0,0 +1,36 @@
+{
+ "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
+ "projectname": "miniprogram",
+ "setting": {
+ "compileHotReLoad": true,
+ "urlCheck": false,
+ "coverView": true,
+ "lazyloadPlaceholderEnable": false,
+ "skylineRenderEnable": false,
+ "preloadBackgroundData": false,
+ "autoAudits": false,
+ "useApiHook": true,
+ "showShadowRootInWxmlPanel": true,
+ "useStaticServer": false,
+ "useLanDebug": false,
+ "showES6CompileOption": false,
+ "checkInvalidKey": true,
+ "ignoreDevUnusedFiles": true,
+ "bigPackageSizeSupport": false,
+ "useIsolateContext": true
+ },
+ "libVersion": "3.13.2",
+ "condition": {
+ "miniprogram": {
+ "list": [
+ {
+ "name": "我的",
+ "pathName": "pages/my/my",
+ "query": "",
+ "launchMode": "default",
+ "scene": null
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/miniprogram2/sitemap.json b/miniprogram2/sitemap.json
new file mode 100644
index 00000000..55d1d29e
--- /dev/null
+++ b/miniprogram2/sitemap.json
@@ -0,0 +1,7 @@
+{
+ "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
+ "rules": [{
+ "action": "allow",
+ "page": "*"
+ }]
+}
diff --git a/miniprogram2/utils/chapterAccessManager.js b/miniprogram2/utils/chapterAccessManager.js
new file mode 100644
index 00000000..925375a3
--- /dev/null
+++ b/miniprogram2/utils/chapterAccessManager.js
@@ -0,0 +1,206 @@
+/**
+ * 章节权限管理器
+ * 统一管理章节权限判断、状态流转、异常处理
+ */
+
+const app = getApp()
+
+class ChapterAccessManager {
+ constructor() {
+ this.accessStates = {
+ UNKNOWN: 'unknown',
+ FREE: 'free',
+ LOCKED_NOT_LOGIN: 'locked_not_login',
+ LOCKED_NOT_PURCHASED: 'locked_not_purchased',
+ UNLOCKED_PURCHASED: 'unlocked_purchased',
+ ERROR: 'error'
+ }
+ }
+
+ /**
+ * 拉取最新配置(免费章节列表、价格等)
+ */
+ async fetchLatestConfig() {
+ try {
+ const res = await app.request('/api/miniprogram/config', { timeout: 3000 })
+ if (res.success && res.freeChapters) {
+ return {
+ freeChapters: res.freeChapters,
+ prices: res.prices || { section: 1, fullbook: 9.9 },
+ userDiscount: (typeof res.userDiscount === 'number' ? res.userDiscount : 5)
+ }
+ }
+ } catch (e) {
+ console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
+ }
+
+ return {
+ freeChapters: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
+ prices: { section: 1, fullbook: 9.9 },
+ userDiscount: 5
+ }
+ }
+
+ /**
+ * 判断章节是否免费
+ */
+ isFreeChapter(sectionId, freeList) {
+ return freeList.includes(sectionId)
+ }
+
+ /**
+ * 【核心方法】确定章节权限状态
+ * @param {string} sectionId - 章节ID
+ * @param {Array} freeList - 免费章节列表
+ * @returns {Promise} accessState
+ */
+ async determineAccessState(sectionId, freeList) {
+ try {
+ // 1. 检查是否免费
+ if (this.isFreeChapter(sectionId, freeList)) {
+ console.log('[AccessManager] 免费章节:', sectionId)
+ return this.accessStates.FREE
+ }
+
+ // 2. 检查是否登录
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ console.log('[AccessManager] 未登录,需要登录:', sectionId)
+ return this.accessStates.LOCKED_NOT_LOGIN
+ }
+
+ // 3. 请求服务端校验是否已购买(带重试)
+ const res = await this.requestWithRetry(
+ `/api/miniprogram/user/check-purchased?userId=${encodeURIComponent(userId)}&type=section&productId=${encodeURIComponent(sectionId)}`,
+ { timeout: 5000 },
+ 2 // 最多重试2次
+ )
+
+ if (res.success && res.data?.isPurchased) {
+ console.log('[AccessManager] 已购买:', sectionId, res.data.reason)
+
+ // 同步更新本地缓存(仅用于展示,不作权限依据)
+ this.syncLocalCache(sectionId, res.data)
+
+ return this.accessStates.UNLOCKED_PURCHASED
+ }
+
+ console.log('[AccessManager] 未购买:', sectionId)
+ return this.accessStates.LOCKED_NOT_PURCHASED
+
+ } catch (error) {
+ console.error('[AccessManager] 权限判断失败:', error)
+ // 网络/服务端错误 → 保守策略:返回错误状态
+ return this.accessStates.ERROR
+ }
+ }
+
+ /**
+ * 带重试的请求
+ */
+ async requestWithRetry(url, options = {}, maxRetries = 3) {
+ let lastError = null
+
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ const res = await app.request(url, options)
+ return res
+ } catch (e) {
+ lastError = e
+ console.warn(`[AccessManager] 第 ${i+1} 次请求失败:`, url, e.message)
+
+ // 如果不是最后一次,等待后重试(指数退避)
+ if (i < maxRetries - 1) {
+ await this.sleep(1000 * (i + 1))
+ }
+ }
+ }
+
+ throw lastError
+ }
+
+ /**
+ * 同步更新本地购买缓存(仅用于展示,不作权限依据)
+ */
+ syncLocalCache(sectionId, purchaseData) {
+ if (purchaseData.reason === 'has_full_book') {
+ app.globalData.hasFullBook = true
+ }
+
+ if (!app.globalData.purchasedSections.includes(sectionId)) {
+ app.globalData.purchasedSections = [...app.globalData.purchasedSections, sectionId]
+ }
+
+ // 更新 storage
+ const userInfo = app.globalData.userInfo || {}
+ userInfo.hasFullBook = app.globalData.hasFullBook
+ userInfo.purchasedSections = app.globalData.purchasedSections
+ wx.setStorageSync('userInfo', userInfo)
+ }
+
+ /**
+ * 刷新用户购买状态(从 orders 表拉取最新)
+ */
+ async refreshUserPurchaseStatus() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+
+ try {
+ const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
+
+ if (res.success && res.data) {
+ app.globalData.hasFullBook = res.data.hasFullBook || false
+ app.globalData.purchasedSections = res.data.purchasedSections || []
+ app.globalData.sectionMidMap = res.data.sectionMidMap || {}
+ app.globalData.matchCount = res.data.matchCount ?? 0
+ app.globalData.matchQuota = res.data.matchQuota || null
+
+ const userInfo = app.globalData.userInfo || {}
+ userInfo.hasFullBook = res.data.hasFullBook
+ userInfo.purchasedSections = res.data.purchasedSections
+ wx.setStorageSync('userInfo', userInfo)
+
+ console.log('[AccessManager] 购买状态已刷新:', {
+ hasFullBook: res.data.hasFullBook,
+ purchasedCount: res.data.purchasedSections.length,
+ matchCount: res.data.matchCount
+ })
+ }
+ } catch (e) {
+ console.error('[AccessManager] 刷新购买状态失败:', e)
+ }
+ }
+
+ /**
+ * 获取状态对应的用户提示文案
+ */
+ getStateMessage(accessState) {
+ const messages = {
+ [this.accessStates.UNKNOWN]: '加载中...',
+ [this.accessStates.FREE]: '免费阅读',
+ [this.accessStates.LOCKED_NOT_LOGIN]: '登录后继续阅读',
+ [this.accessStates.LOCKED_NOT_PURCHASED]: '购买后继续阅读',
+ [this.accessStates.UNLOCKED_PURCHASED]: '已解锁',
+ [this.accessStates.ERROR]: '网络异常,请重试'
+ }
+ return messages[accessState] || '未知状态'
+ }
+
+ /**
+ * 判断是否可访问全文
+ */
+ canAccessFullContent(accessState) {
+ return [this.accessStates.FREE, this.accessStates.UNLOCKED_PURCHASED].includes(accessState)
+ }
+
+ /**
+ * 工具:延迟
+ */
+ sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+ }
+}
+
+// 导出单例
+const accessManager = new ChapterAccessManager()
+export default accessManager
diff --git a/miniprogram2/utils/payment.js b/miniprogram2/utils/payment.js
new file mode 100644
index 00000000..9e048d1a
--- /dev/null
+++ b/miniprogram2/utils/payment.js
@@ -0,0 +1,211 @@
+// miniprogram/utils/payment.js
+// 微信支付工具类
+
+const app = getApp()
+
+/**
+ * 发起微信支付
+ * @param {Object} options - 支付选项
+ * @param {String} options.orderId - 订单ID
+ * @param {Number} options.amount - 支付金额(元)
+ * @param {String} options.description - 商品描述
+ * @param {Function} options.success - 成功回调
+ * @param {Function} options.fail - 失败回调
+ */
+function wxPay(options) {
+ const { orderId, amount, description, success, fail } = options
+
+ wx.showLoading({
+ title: '正在支付...',
+ mask: true
+ })
+
+ // 1. 调用后端创建支付订单
+ wx.request({
+ url: `${app.globalData.apiBase}/payment/create`,
+ method: 'POST',
+ header: {
+ 'Authorization': `Bearer ${wx.getStorageSync('token')}`
+ },
+ data: {
+ orderId,
+ amount,
+ description,
+ paymentMethod: 'wechat'
+ },
+ success: (res) => {
+ wx.hideLoading()
+
+ if (res.statusCode === 200) {
+ const paymentData = res.data
+
+ // 2. 调起微信支付
+ wx.requestPayment({
+ timeStamp: paymentData.timeStamp,
+ nonceStr: paymentData.nonceStr,
+ package: paymentData.package,
+ signType: paymentData.signType || 'RSA',
+ paySign: paymentData.paySign,
+ success: (payRes) => {
+ console.log('支付成功', payRes)
+
+ // 3. 通知后端支付成功
+ notifyPaymentSuccess(orderId, paymentData.prepayId)
+
+ wx.showToast({
+ title: '支付成功',
+ icon: 'success',
+ duration: 2000
+ })
+
+ success && success(payRes)
+ },
+ fail: (payErr) => {
+ console.error('支付失败', payErr)
+
+ if (payErr.errMsg.indexOf('cancel') !== -1) {
+ wx.showToast({
+ title: '支付已取消',
+ icon: 'none'
+ })
+ } else {
+ wx.showToast({
+ title: '支付失败',
+ icon: 'none'
+ })
+ }
+
+ fail && fail(payErr)
+ }
+ })
+ } else {
+ wx.showToast({
+ title: res.data.message || '创建订单失败',
+ icon: 'none'
+ })
+ fail && fail(res)
+ }
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ console.error('请求失败', err)
+
+ wx.showToast({
+ title: '网络请求失败',
+ icon: 'none'
+ })
+
+ fail && fail(err)
+ }
+ })
+}
+
+/**
+ * 通知后端支付成功
+ * @param {String} orderId
+ * @param {String} prepayId
+ */
+function notifyPaymentSuccess(orderId, prepayId) {
+ wx.request({
+ url: `${app.globalData.apiBase}/payment/notify`,
+ method: 'POST',
+ header: {
+ 'Authorization': `Bearer ${wx.getStorageSync('token')}`
+ },
+ data: {
+ orderId,
+ prepayId,
+ status: 'success'
+ },
+ success: (res) => {
+ console.log('支付通知成功', res)
+ },
+ fail: (err) => {
+ console.error('支付通知失败', err)
+ }
+ })
+}
+
+/**
+ * 查询订单状态
+ * @param {String} orderId
+ * @param {Function} callback
+ */
+function queryOrderStatus(orderId, callback) {
+ wx.request({
+ url: `${app.globalData.apiBase}/payment/query`,
+ method: 'GET',
+ header: {
+ 'Authorization': `Bearer ${wx.getStorageSync('token')}`
+ },
+ data: { orderId },
+ success: (res) => {
+ if (res.statusCode === 200) {
+ callback && callback(true, res.data)
+ } else {
+ callback && callback(false, null)
+ }
+ },
+ fail: () => {
+ callback && callback(false, null)
+ }
+ })
+}
+
+/**
+ * 购买完整电子书
+ * @param {Function} success
+ * @param {Function} fail
+ */
+function purchaseFullBook(success, fail) {
+ // 计算动态价格:9.9 + (天数 * 1元)
+ const basePrice = 9.9
+ const startDate = new Date('2025-01-01') // 书籍上架日期
+ const today = new Date()
+ const daysPassed = Math.floor((today - startDate) / (1000 * 60 * 60 * 24))
+ const currentPrice = basePrice + daysPassed
+
+ const orderId = `ORDER_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+
+ wxPay({
+ orderId,
+ amount: currentPrice,
+ description: 'Soul派对·创业实验 完整版',
+ success: (res) => {
+ // 更新本地购买状态
+ updatePurchaseStatus(true)
+ success && success(res)
+ },
+ fail
+ })
+}
+
+/**
+ * 更新购买状态
+ * @param {Boolean} isPurchased
+ */
+function updatePurchaseStatus(isPurchased) {
+ const userInfo = app.getUserInfo()
+ if (userInfo) {
+ userInfo.isPurchased = isPurchased
+ wx.setStorageSync('userInfo', userInfo)
+ app.globalData.userInfo = userInfo
+ }
+}
+
+/**
+ * 检查是否已购买
+ * @returns {Boolean}
+ */
+function checkPurchaseStatus() {
+ const userInfo = app.getUserInfo()
+ return userInfo ? userInfo.isPurchased : false
+}
+
+module.exports = {
+ wxPay,
+ queryOrderStatus,
+ purchaseFullBook,
+ checkPurchaseStatus,
+ updatePurchaseStatus
+}
diff --git a/miniprogram2/utils/readingTracker.js b/miniprogram2/utils/readingTracker.js
new file mode 100644
index 00000000..a52fb896
--- /dev/null
+++ b/miniprogram2/utils/readingTracker.js
@@ -0,0 +1,246 @@
+/**
+ * 阅读进度追踪器
+ * 记录阅读进度、时长、是否读完,支持断点续读
+ */
+
+const app = getApp()
+
+class ReadingTracker {
+ constructor() {
+ this.activeTracker = null
+ this.reportInterval = null
+ }
+
+ /**
+ * 初始化阅读追踪
+ */
+ init(sectionId) {
+ // 清理旧的追踪器
+ this.cleanup()
+
+ this.activeTracker = {
+ sectionId,
+ startTime: Date.now(),
+ lastScrollTime: Date.now(),
+ totalDuration: 0,
+ maxProgress: 0,
+ lastPosition: 0,
+ isCompleted: false,
+ completedAt: null,
+ scrollTimer: null
+ }
+
+ console.log('[ReadingTracker] 初始化追踪:', sectionId)
+
+ // 恢复上次阅读位置
+ this.restoreLastPosition(sectionId)
+
+ // 开始定期上报(每30秒)
+ this.startProgressReport()
+ }
+
+ /**
+ * 恢复上次阅读位置(断点续读)
+ */
+ restoreLastPosition(sectionId) {
+ try {
+ const progressData = wx.getStorageSync('reading_progress') || {}
+ const lastProgress = progressData[sectionId]
+
+ if (lastProgress && lastProgress.lastPosition > 100) {
+ setTimeout(() => {
+ wx.pageScrollTo({
+ scrollTop: lastProgress.lastPosition,
+ duration: 300
+ })
+
+ wx.showToast({
+ title: `继续阅读 (${lastProgress.progress}%)`,
+ icon: 'none',
+ duration: 2000
+ })
+ }, 500)
+ }
+ } catch (e) {
+ console.warn('[ReadingTracker] 恢复位置失败:', e)
+ }
+ }
+
+ /**
+ * 更新阅读进度(由页面滚动事件调用)
+ */
+ updateProgress(scrollInfo) {
+ if (!this.activeTracker) return
+
+ const { scrollTop, scrollHeight, clientHeight } = scrollInfo
+ const totalScrollable = scrollHeight - clientHeight
+
+ if (totalScrollable <= 0) return
+
+ const progress = Math.min(100, Math.round((scrollTop / totalScrollable) * 100))
+
+ // 更新最大进度
+ if (progress > this.activeTracker.maxProgress) {
+ this.activeTracker.maxProgress = progress
+ this.activeTracker.lastPosition = scrollTop
+ this.saveProgressLocal()
+
+ console.log('[ReadingTracker] 进度更新:', progress + '%')
+ }
+
+ // 检查是否读完(≥90%)
+ if (progress >= 90 && !this.activeTracker.isCompleted) {
+ this.checkCompletion()
+ }
+ }
+
+ /**
+ * 检查是否读完(需要停留3秒)
+ */
+ async checkCompletion() {
+ if (!this.activeTracker || this.activeTracker.isCompleted) return
+
+ // 等待3秒,确认用户真的读到底部
+ await this.sleep(3000)
+
+ if (this.activeTracker && this.activeTracker.maxProgress >= 90 && !this.activeTracker.isCompleted) {
+ this.activeTracker.isCompleted = true
+ this.activeTracker.completedAt = Date.now()
+
+ console.log('[ReadingTracker] 阅读完成:', this.activeTracker.sectionId)
+
+ // 标记已读(app.js 里的已读章节列表)
+ app.markSectionAsRead(this.activeTracker.sectionId)
+
+ // 立即上报完成状态
+ await this.reportProgressToServer(true)
+
+ // 触发埋点
+ this.trackEvent('chapter_completed', {
+ sectionId: this.activeTracker.sectionId,
+ duration: this.activeTracker.totalDuration
+ })
+
+ wx.showToast({
+ title: '已完成阅读',
+ icon: 'success',
+ duration: 1500
+ })
+ }
+ }
+
+ /**
+ * 保存进度到本地
+ */
+ saveProgressLocal() {
+ if (!this.activeTracker) return
+
+ try {
+ const progressData = wx.getStorageSync('reading_progress') || {}
+ progressData[this.activeTracker.sectionId] = {
+ progress: this.activeTracker.maxProgress,
+ lastPosition: this.activeTracker.lastPosition,
+ lastOpenAt: Date.now()
+ }
+ wx.setStorageSync('reading_progress', progressData)
+ } catch (e) {
+ console.warn('[ReadingTracker] 保存本地进度失败:', e)
+ }
+ }
+
+ /**
+ * 开始定期上报
+ */
+ startProgressReport() {
+ // 每30秒上报一次
+ this.reportInterval = setInterval(() => {
+ this.reportProgressToServer(false)
+ }, 30000)
+ }
+
+ /**
+ * 上报进度到服务端
+ */
+ async reportProgressToServer(isCompletion = false) {
+ if (!this.activeTracker) return
+
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+
+ // 计算本次上报的时长
+ const now = Date.now()
+ const duration = Math.round((now - this.activeTracker.lastScrollTime) / 1000)
+ this.activeTracker.totalDuration += duration
+ this.activeTracker.lastScrollTime = now
+
+ try {
+ await app.request('/api/miniprogram/user/reading-progress', {
+ method: 'POST',
+ data: {
+ userId,
+ sectionId: this.activeTracker.sectionId,
+ progress: this.activeTracker.maxProgress,
+ duration: this.activeTracker.totalDuration,
+ status: this.activeTracker.isCompleted ? 'completed' : 'reading',
+ completedAt: this.activeTracker.completedAt
+ }
+ })
+
+ if (isCompletion) {
+ console.log('[ReadingTracker] 完成状态已上报')
+ }
+ } catch (e) {
+ console.warn('[ReadingTracker] 上报进度失败,下次重试:', e)
+ }
+ }
+
+ /**
+ * 页面隐藏/卸载时调用(立即上报)
+ */
+ onPageHide() {
+ if (this.activeTracker) {
+ this.reportProgressToServer(false)
+ }
+ }
+
+ /**
+ * 清理追踪器
+ */
+ cleanup() {
+ if (this.reportInterval) {
+ clearInterval(this.reportInterval)
+ this.reportInterval = null
+ }
+
+ if (this.activeTracker) {
+ this.reportProgressToServer(false)
+ this.activeTracker = null
+ }
+ }
+
+ /**
+ * 获取当前章节的阅读进度(用于展示)
+ */
+ getCurrentProgress() {
+ return this.activeTracker ? this.activeTracker.maxProgress : 0
+ }
+
+ /**
+ * 数据埋点(可对接统计平台)
+ */
+ trackEvent(eventName, eventData) {
+ console.log('[Analytics]', eventName, eventData)
+ // TODO: 接入微信小程序数据助手 / 第三方统计
+ }
+
+ /**
+ * 工具:延迟
+ */
+ sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+ }
+}
+
+// 导出单例
+const readingTracker = new ReadingTracker()
+export default readingTracker
diff --git a/miniprogram2/utils/scene.js b/miniprogram2/utils/scene.js
new file mode 100644
index 00000000..a36808e7
--- /dev/null
+++ b/miniprogram2/utils/scene.js
@@ -0,0 +1,45 @@
+/**
+ * 小程序码 scene 参数统一编解码(海报生成 ↔ 扫码解析闭环)
+ * 官方以 options.scene 接收扫码参数;后端生成码时会把 & 转为 _,故解析时同时支持 & 和 _
+ * scene 同时可带两个参数:章节标识(mid/id) + 推荐人(ref)
+ */
+
+const SEP = '_' // 生成时统一用 _,与微信实际存储一致,且不占 32 字符限制
+
+/**
+ * 编码:生成海报/分享时组 scene 字符串(同时带 mid或id + ref)
+ * @param {{ mid?: number, id?: string, ref?: string }} opts
+ * @returns {string} 如 "mid=1_ref=ogpTW5fmXR" 或 "id=1.1_ref=xxx"
+ */
+function buildScene(opts) {
+ const parts = []
+ if (opts.mid != null && opts.mid !== '') parts.push(`mid=${opts.mid}`)
+ if (opts.id) parts.push(`id=${opts.id}`)
+ if (opts.ref) parts.push(`ref=${opts.ref}`)
+ return parts.join(SEP)
+}
+
+/**
+ * 解码:从 options.scene 解析出 mid、id、ref(支持 & 或 _ 分隔)
+ * @param {string} sceneStr 原始 scene(可能未 decodeURIComponent)
+ * @returns {{ mid: number, id: string, ref: string }}
+ */
+function parseScene(sceneStr) {
+ const res = { mid: 0, id: '', ref: '' }
+ if (!sceneStr || typeof sceneStr !== 'string') return res
+ const decoded = decodeURIComponent(String(sceneStr)).trim()
+ const parts = decoded.split(/[&_]/)
+ for (const part of parts) {
+ const eq = part.indexOf('=')
+ if (eq > 0) {
+ const k = part.slice(0, eq)
+ const v = part.slice(eq + 1)
+ if (k === 'mid') res.mid = parseInt(v, 10) || 0
+ if (k === 'id' && v) res.id = v
+ if (k === 'ref' && v) res.ref = v
+ }
+ }
+ return res
+}
+
+module.exports = { buildScene, parseScene }
diff --git a/miniprogram2/utils/util.js b/miniprogram2/utils/util.js
new file mode 100644
index 00000000..855e96fd
--- /dev/null
+++ b/miniprogram2/utils/util.js
@@ -0,0 +1,182 @@
+/**
+ * Soul创业实验 - 工具函数
+ */
+
+// 格式化时间
+const formatTime = date => {
+ const year = date.getFullYear()
+ const month = date.getMonth() + 1
+ const day = date.getDate()
+ const hour = date.getHours()
+ const minute = date.getMinutes()
+ const second = date.getSeconds()
+
+ return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
+}
+
+const formatNumber = n => {
+ n = n.toString()
+ return n[1] ? n : `0${n}`
+}
+
+// 格式化日期
+const formatDate = date => {
+ const year = date.getFullYear()
+ const month = date.getMonth() + 1
+ const day = date.getDate()
+ return `${year}-${formatNumber(month)}-${formatNumber(day)}`
+}
+
+// 格式化金额
+const formatMoney = (amount, decimals = 2) => {
+ return Number(amount).toFixed(decimals)
+}
+
+// 防抖函数
+const debounce = (fn, delay = 300) => {
+ let timer = null
+ return function (...args) {
+ if (timer) clearTimeout(timer)
+ timer = setTimeout(() => {
+ fn.apply(this, args)
+ }, delay)
+ }
+}
+
+// 节流函数
+const throttle = (fn, delay = 300) => {
+ let last = 0
+ return function (...args) {
+ const now = Date.now()
+ if (now - last >= delay) {
+ fn.apply(this, args)
+ last = now
+ }
+ }
+}
+
+// 生成唯一ID
+const generateId = () => {
+ return 'id_' + Date.now().toString(36) + Math.random().toString(36).substr(2)
+}
+
+// 检查手机号格式
+const isValidPhone = phone => {
+ return /^1[3-9]\d{9}$/.test(phone)
+}
+
+// 检查微信号格式
+const isValidWechat = wechat => {
+ return wechat && wechat.length >= 6 && wechat.length <= 20
+}
+
+// 深拷贝
+const deepClone = obj => {
+ if (obj === null || typeof obj !== 'object') return obj
+ if (obj instanceof Date) return new Date(obj)
+ if (obj instanceof Array) return obj.map(item => deepClone(item))
+ if (obj instanceof Object) {
+ const copy = {}
+ Object.keys(obj).forEach(key => {
+ copy[key] = deepClone(obj[key])
+ })
+ return copy
+ }
+}
+
+// 获取URL参数
+const getQueryParams = url => {
+ const params = {}
+ const queryString = url.split('?')[1]
+ if (queryString) {
+ queryString.split('&').forEach(pair => {
+ const [key, value] = pair.split('=')
+ params[decodeURIComponent(key)] = decodeURIComponent(value || '')
+ })
+ }
+ return params
+}
+
+// 存储操作
+const storage = {
+ get(key) {
+ try {
+ return wx.getStorageSync(key)
+ } catch (e) {
+ console.error('获取存储失败:', e)
+ return null
+ }
+ },
+ set(key, value) {
+ try {
+ wx.setStorageSync(key, value)
+ return true
+ } catch (e) {
+ console.error('设置存储失败:', e)
+ return false
+ }
+ },
+ remove(key) {
+ try {
+ wx.removeStorageSync(key)
+ return true
+ } catch (e) {
+ console.error('删除存储失败:', e)
+ return false
+ }
+ },
+ clear() {
+ try {
+ wx.clearStorageSync()
+ return true
+ } catch (e) {
+ console.error('清除存储失败:', e)
+ return false
+ }
+ }
+}
+
+// 显示Toast
+const showToast = (title, icon = 'none', duration = 2000) => {
+ wx.showToast({ title, icon, duration })
+}
+
+// 显示Loading
+const showLoading = (title = '加载中...') => {
+ wx.showLoading({ title, mask: true })
+}
+
+// 隐藏Loading
+const hideLoading = () => {
+ wx.hideLoading()
+}
+
+// 显示确认框
+const showConfirm = (title, content) => {
+ return new Promise((resolve) => {
+ wx.showModal({
+ title,
+ content,
+ success: res => resolve(res.confirm)
+ })
+ })
+}
+
+module.exports = {
+ formatTime,
+ formatDate,
+ formatMoney,
+ formatNumber,
+ debounce,
+ throttle,
+ generateId,
+ isValidPhone,
+ isValidWechat,
+ deepClone,
+ getQueryParams,
+ storage,
+ showToast,
+ showLoading,
+ hideLoading,
+ showConfirm
+}