diff --git a/.cursor/agent/开发助理/evolution/2026-03-16-交互习惯分析.md b/.cursor/agent/开发助理/evolution/2026-03-16-交互习惯分析.md new file mode 100644 index 00000000..bd17ed65 --- /dev/null +++ b/.cursor/agent/开发助理/evolution/2026-03-16-交互习惯分析.md @@ -0,0 +1,91 @@ +# 用户交互习惯分析(基于 agent-transcripts 抽样) + +> 乘风发起,读取 97 个 agent 会话记录,抽样分析后总结。用于优化 agent 响应策略与 Skill 设计。 + +--- + +## 一、角色与触发词使用习惯 + +| 触发词/角色 | 使用场景 | 期望动作 | +|-------------|----------|----------| +| **乘风** | 开会、同步进度、协调开发、总结经验 | 老板分身主持,协调各角色 | +| **开会** | 需求评审、方案讨论、进度同步 | 按 team-meeting SKILL 主持多角色会议 | +| **吸收经验** | 功能完成、讨论完毕 | 经验入库 + Skill 升级 + 同步需求文档 | +| **橙子 / 小橙** | 记录、同步文档、会议收尾 | 文档同步、纪要生成、索引更新 | +| **加个需求:xxx** | 新增功能 | 产品经理三端分析 → 功能规划 → 指派 | +| **变更完成 / 检查一下** | 代码改完 | 过 change-checklist 三端关联检查 | + +--- + +## 二、表达方式偏好 + +### 1. 直接点名角色 +- 如:「小程序工程师」「后端工程师」「管理端工程师」「测试工程师」 +- 期望:agent 按该角色 Skill 执行,不混用其他端逻辑 + +### 2. @ 文件引用 +- 如:@new-soul、@scripts、@soul-admin、@开发文档 +- 期望:agent 读取并理解引用内容,作为上下文执行任务 + +### 3. 任务导向、先分析再实现 +- 典型句式:「帮我对比」「整理出迁移清单」「分析有没有逻辑盲点」「从经营角度看看」 +- 期望:先出方案/文档,再写代码;不直接动手改 + +### 4. 图片辅助反馈 +- 经常截图 + 文字描述问题(如「@匹配有问题」「弹窗太大」「封面图要长这样」) +- 期望:agent 结合截图理解 UI/交互问题,给出针对性修改 + +--- + +## 三、工作流程偏好 + +| 阶段 | 习惯 | 说明 | +|------|------|------| +| **需求** | 以需求驱动、以界面定需求 | 不凭空加功能,需求与实现可追溯 | +| **分析** | 先对比、出方案、写文档 | 迁移清单、逻辑盲点、技术协调、经营建议 | +| **实现** | 小步迭代、可读性优先 | 函数单一职责,避免深层嵌套 | +| **验收** | 测试 → 吸收经验 → 同步文档 | 闭环:经验入库、Skill 升级、需求汇总更新 | +| **会议** | 开会 → 各角色发言 → 决议 → 橙子总结 | 会议结束触发收尾流程 | + +--- + +## 四、沟通风格 + +- **简洁**:如「帮我处理」「帮我测试一下」「修复」 +- **追问细节**:如「界面有什么变化」「有逻辑盲点吗」「从经营角度看看」 +- **强调合理性**:如「产品经理要根据实际情况判断,不能随意增加管理列表」 +- **确认后执行**:如「先帮我对比」「整理出迁移清单」——先出结果再决定是否实现 + +--- + +## 五、技术偏好(Soul 项目) + +- **三端隔离**:小程序只调 `/api/miniprogram/*`,管理端只调 `/api/admin/*`、`/api/db/*` +- **存客宝对接**:Person、LinkTag、ckb_api_key、planType、sceneId、status 等参数约定 +- **测试规范**:scripts/test 目录,miniapp / web / process 分类,pytest + requests,运行前显式提示测试环境 + +--- + +## 六、反馈与确认习惯 + +| 用户表达 | 触发动作 | +|----------|----------| +| 「搞定了」「可以了」「解决了」 | 经验自动收集(老板分身-索引.mdc) | +| 「会议结束」「散会」 | 助理橙子会议收尾 | +| 「吸收经验」「同步到需求文档」 | 经验入库 + 需求汇总更新 | +| 「不要记录」「不用沉淀」 | 不触发经验入库 | + +--- + +## 七、Agent 响应建议 + +1. **先理解再动手**:用户说「帮我xxx」时,先确认范围、出方案,再写代码 +2. **按角色执行**:用户点名角色时,必须 Read 对应 Skill,按规范执行 +3. **文档先行**:迁移、重构类任务,先出清单/分析文档,再实现 +4. **闭环收尾**:功能完成时主动提示「可以说吸收经验同步到文档」 +5. **图片理解**:用户发截图时,结合截图分析问题,不只看文字 + +--- + +**来源**:agent-transcripts 抽样(约 15 个会话),结合经验清单与项目索引交叉验证。 +**适用**:开发助理、老板分身、各角色 agent 的响应策略优化。 diff --git a/.cursor/agent/开发助理/evolution/索引.md b/.cursor/agent/开发助理/evolution/索引.md index 937f5ede..39773d58 100644 --- a/.cursor/agent/开发助理/evolution/索引.md +++ b/.cursor/agent/开发助理/evolution/索引.md @@ -4,3 +4,4 @@ | 日期 | 摘要 | 文件 | |------|------|------| +| 2026-03-16 | 用户交互习惯分析(基于 agent-transcripts) | 2026-03-16-交互习惯分析.md | diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md index 641b6dce..a9fcf77f 100644 --- a/.cursor/agent/开发助理/经验清单.md +++ b/.cursor/agent/开发助理/经验清单.md @@ -47,6 +47,7 @@ | 2026-03-16 | 软件测试 | 目录约定 | testing SKILL §5 | scripts/test/process:流程测试,跨端多接口串联(下单→支付→分润等) | | 2026-03-16 | 软件测试 | 配置约定 | testing SKILL | pytest 架构、配置从 soul-api/.env* 读取、SOUL_TEST_ENV 必显;运行前报告头部显示测试环境,避免误测正式库 | | 2026-03-16 | 小程序 | 最佳实践 | miniprogram-dev SKILL §10 | 编辑资料页分享名片:转发/朋友圈特殊处理,Canvas 绘制 5:4 封面,标题「昵称+为您分享名片」,路径 member-detail | +| 2026-03-16 | 开发助理 | 交互习惯分析 | - | 乘风读取 agent-transcripts 抽样分析:角色触发词、表达方式、工作流程、沟通风格、技术偏好、Agent 响应建议 | --- diff --git a/.cursor/agent/开发助理/项目索引/助理橙子.md b/.cursor/agent/开发助理/项目索引/助理橙子.md index cf429d6f..a903edec 100644 --- a/.cursor/agent/开发助理/项目索引/助理橙子.md +++ b/.cursor/agent/开发助理/项目索引/助理橙子.md @@ -18,9 +18,10 @@ | 2026-02-28 | .cursor 按 cursor标准模板 重构:agent 目录、config、evolution.py、meeting | 已完成 | | 2026-03-11 | 会议收尾:开发团队对齐业务逻辑与以界面定需求;纪要生成、各角色经验入库、项目索引与会议索引更新 | 已完成 | | 2026-03-16 | 乘风发起例行开发进度同步 | 已完成 | +| 2026-03-16 | 乘风读取 agent-transcripts 分析交互习惯,总结经验并吸收 | 已完成 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-03-16 +**最后更新**:2026-03-16(交互习惯分析) diff --git a/.cursor/rules/soul-project-boundary.mdc b/.cursor/rules/soul-project-boundary.mdc index bfe6d8c5..849c25f2 100644 --- a/.cursor/rules/soul-project-boundary.mdc +++ b/.cursor/rules/soul-project-boundary.mdc @@ -61,5 +61,6 @@ alwaysApply: true | 开个会、开会、团队会议、乘风开会、需求评审、方案讨论、大家一起讨论 | `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md`(老板分身/乘风主持) | | 会议结束、散会、会开完了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md`(会议收尾) | | **加个需求**、加个需求:xxx | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md`(产品经理三端分析 → 功能规划 → 指派) | +| **新版分析**、版本对比、迁移分析、甲方代码分析、快速分析新版、抽取需求 | `e:\Gongsi\Mycontent\.cursor\skills\new-version-analyze\SKILL.md`(新版快速分析 → 差异清单 → 接口冲突 → 迁移迭代) | **注意**:「必须 Read」= 使用 Read 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。 diff --git a/.cursor/skills/new-version-analyze/SKILL.md b/.cursor/skills/new-version-analyze/SKILL.md new file mode 100644 index 00000000..85863261 --- /dev/null +++ b/.cursor/skills/new-version-analyze/SKILL.md @@ -0,0 +1,284 @@ +--- +description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,逻辑不完善、接口不规范、存在逻辑冲突时使用。快速分析、体验评估、逻辑补齐、抽取需求在稳定版迭代。Use when 新版分析、版本对比、迁移分析、甲方代码分析、快速分析新版、new-soul 分析. +--- +# SKILL - 新版快速分析(甲方代码迁移) + +> 针对「甲方/第三方用 AI 写的新版本,逻辑不完善、接口不规范、存在逻辑冲突」的场景,快速分析、抽取需求、在稳定版迭代。 + +--- + +## 1. 何时使用本 Skill + +| 触发词 | 场景 | +|--------|------| +| **新版分析**、**版本对比**、**迁移分析** | 需要系统对比 new-soul 与稳定版 | +| **甲方代码分析**、**快速分析新版** | 对方改了代码,逻辑不完善、接口不规范 | +| **@new-soul 分析**、**抽取需求** | 从新版抽取需求,在稳定版更新迭代 | + +**典型困境**: +- 对方有改接口,但不符合规范(路由、响应格式、鉴权) +- 存在逻辑冲突(如余额消费不写 orders、分润规则不统一) +- 纯界面需求变更,底层逻辑未考虑 +- **稳定版也有新增功能,甲方版本并未更新** → 功能对齐与取舍需明确 +- 需要在短时间内补齐完整逻辑 + +--- + +## 2. 核心原则 + +| 原则 | 说明 | +|------|------| +| **稳定版为基准** | 稳定版是生产环境,逻辑已验证;新版只做增量补齐 | +| **接口不以新版为准** | 新版接口编写必然很多没考虑(路由、鉴权、事务、分润、幂等)。**接口设计必须以稳定版规范为准**,按 api-dev SKILL 重新设计,不照搬新版实现 | +| **界面当需求,逻辑自己写** | 对方界面/交互可参考,业务逻辑按规范重写 | +| **先分析再实现** | 先出清单、方案、冲突表,再写代码 | +| **规范优先** | 接口不符合规范的一律按 api-dev/miniprogram-dev 修正 | +| **最小功能迁移** | 按最小功能单元迁移,**每个功能迁移后都能完整运行**;界面修改可先迁,大逻辑排后 | + +**接口处理方式**:新版接口仅作「能力参考」(知道要什么),实现时在稳定版 soul-api 中**按规范从零设计**,不直接复用新版代码。 + +--- + +## 2.5 保护区域(迁移时禁止动或慎动) + +**以下模块是稳定版核心逻辑,新需求迁移时务必谨慎,不得影响:** + +| 保护区域 | 说明 | 处理方式 | +|----------|------|----------| +| **文章详情 @/# 标签** | @某人、#链接标签、contentParser、onMentionTap、onLinkTagTap、存客宝对接 | **禁止动**。迁移时不得修改 read 页的 @/# 解析与点击逻辑 | +| **分销** | 推荐码绑定、分润计算、referral 相关接口与订单关联 | **禁止动**。新增功能(如余额消费)若涉及购买,必须与分销逻辑兼容,不得覆盖或冲突 | +| **支付** | 微信支付、pay 接口、PayNotify 回调、订单创建与状态 | **禁止动**。余额支付等新增支付方式需在现有支付流程上**扩展**,不得替换或破坏原有逻辑 | + +**迁移前检查**:若新版改动涉及 read 页正文、contentParser、ckb/lead、referral、pay、orders,必须**逐行对比**,确认不覆盖保护区域。有冲突时以稳定版为准。 + +--- + +## 2.6 迁移前必做:需求评审 + +**迁移前必须先做需求评审,评审通过后再开始迁移。** + +| 步骤 | 动作 | 产出 | +|------|------|------| +| 1 | 需求评审 | 召集评审,明确迁移范围与优先级 | +| 2 | 列出功能点 | 逐项列出:新增功能、修改功能、移除功能 | +| 3 | 列出样式变更 | 逐项列出:布局、配色、文案、交互变化 | +| 4 | 逐一确认 | 每个功能点、每项样式变更经确认后再迁 | +| 5 | 开始迁移 | 评审通过后,按确认清单逐项迁移 | + +**产出文档**:`开发文档/新版迁移-需求评审清单.md`(功能点 + 样式变更表,含确认状态) + +--- + +## 2.7 迁移顺序(最小功能 + 可运行优先) + +| 顺序 | 类型 | 说明 | +|------|------|------| +| **先迁** | 界面修改 | 纯 WXML/WXSS/布局、文案、样式,不涉及新接口或新逻辑 → 可直接迁移 | +| **后迁** | 大逻辑 | 涉及新接口、DB、事务、支付、分润等 → 排后,逐个迁移 | + +**原则**: +- 按**最小功能**拆:一个功能 = 一个可独立运行、可验证的单元 +- 每次迁移后**必须能完整运行**:迁完即测,不积压半成品 +- 界面优先:先迁界面类,快速见效;大逻辑逐个排期,降低风险 + +--- + +## 3. 功能对齐与取舍(必做) + +**背景**:稳定版有新增功能,甲方版本未同步;甲方版本也有新功能。需先做**双向对齐**,再决定取舍。 + +### 3.0 功能三向分类 + +| 分类 | 说明 | 取舍原则 | +|------|------|----------| +| **仅稳定版有** | 你已开发,甲方未更新 | **保留**,不因迁移而删除 | +| **仅新版有** | 甲方新增,稳定版无 | 按需求评估:有价值则迁(界面当需求,逻辑重写);无价值则弃 | +| **两者共有** | 同一功能,实现不同 | **以稳定版为准**;若新版交互更好,可只迁界面/交互,逻辑仍用稳定版 | + +### 3.0.1 取舍决策表(产出) + +| 功能 | 分类 | 取舍 | 理由 | +|------|------|------|------| +| 链接人与事 | 仅稳定版有 | 保留 | 已上线,甲方无 | +| 钱包/余额 | 仅新版有 | 迁移 | 有业务价值,按规范重写 | +| 首页精选展开 | 两者共有 | 迁界面、逻辑用稳定版 | 新版交互可参考,数据源用 book/recommended 或 hot | + +**产出**:在功能差异清单中增加「分类」「取舍」「理由」三列。 + +--- + +## 4. 分析流程(五步) + +### 4.1 第一步:快速摸底(1~2 小时) + +**动作**:**双向对比** new-soul 与稳定版,建立差异清单(含功能三向分类) + +| 对比维度 | 产出 | +|----------|------| +| **页面** | 新增/移除/改版页面列表 | +| **接口** | 新增/修改/删除的 API 列表 | +| **字段** | 请求/响应/DB 字段差异 | +| **功能分类** | 仅稳定版有 / 仅新版有 / 两者共有 | +| **标注** | 每个变更:✅ 可用 / ⚠️ 不完整 / ❌ 逻辑错误 | + +**产出文档**:`开发文档/新版迁移-功能差异清单.md`(可复用已有迁移文档补充) + +--- + +### 4.2 第二步:逻辑分层检查 + +对每个功能按三层过一遍,避免漏改: + +``` +┌─────────────────────────────────────┐ +│ 界面层:WXML/WXSS/JS、事件、数据绑定 │ ← 对方常只改这里 +├─────────────────────────────────────┤ +│ 接口层:调哪个 API、入参、返回、错误 │ ← 易漏:参数、鉴权、响应格式 +├─────────────────────────────────────┤ +│ 数据层:DB 表、字段、事务、幂等 │ ← 易漏:orders、分润、状态机 +└─────────────────────────────────────┘ +``` + +**检查项**: +- 界面改了,接口是否已提供且规范? +- 接口改了,DB/事务是否已同步? +- 是否有逻辑冲突(如 consume 不写 orders)? + +--- + +### 4.3 第三步:体验评估 + +| 维度 | 检查点 | +|------|--------| +| **交互** | 加载态、空态、错误态是否完整 | +| **反馈** | 操作成功/失败是否有提示 | +| **边界** | 未登录、余额不足、网络失败等处理 | +| **一致性** | 与稳定版其他页面的风格、文案是否统一 | + +**产出**:在差异清单中增加「体验评估」列 + +--- + +### 4.4 第四步:接口规范与冲突清单 + +**默认立场**:新版接口**不照搬**。从新版只提取「需要什么能力」,在稳定版按 api-dev 规范**重新设计**。 + +**接口规范检查**(以 api-dev SKILL 为准): + +| 维度 | 稳定版约定 | 检查 | +|------|------------|------| +| 路由分组 | miniprogram / admin / db | 小程序是否只调 miniprogram | +| 响应格式 | `{ success, data, message }` | 是否统一 | +| 鉴权 | Bearer token、openId | 是否按约定 | +| 命名 | REST、资源名 | 是否统一 | + +**逻辑冲突识别**: + +| 冲突类型 | 示例 | 处理原则 | +|----------|------|----------| +| 数据不一致 | 余额扣了不写 orders | 同一事务内补齐 | +| 规则不统一 | 微信支付有分润、余额消费没有 | 统一规则 | +| 字段语义冲突 | 同一字段不同含义 | 定死语义,全项目统一 | +| 幂等缺失 | 回调重复执行 | 加幂等(订单号去重) | + +**产出文档**:`开发文档/新版迁移-接口规范与冲突清单.md` + +--- + +### 4.5 第五步:抽取需求,排期迭代 + +**动作**: +1. 从差异清单中抽取「可迁移需求」 +2. 排除:技术债、规则不清、与稳定版冲突的部分 +3. 按**最小功能**拆分,保证每个任务迁移后能完整运行 +4. 排期顺序:**界面修改优先** → 大逻辑排后;P0(逻辑不通)→ P1(功能缺失)→ P2(优化) +5. 写入需求汇总,形成迁移任务清单 + +**产出**: +- `开发文档/1、需求/需求汇总.md` 追加需求 +- `开发文档/新版迁移-开发方案与清单.md` 或等价迁移清单 + +--- + +## 5. 产出物模板 + +### 5.1 功能差异清单(表格) + +| 功能 | 分类 | 取舍 | 页面 | 接口 | 数据 | 甲方实现 | 体验 | 迁移动作 | +|------|------|------|------|------|------|----------|------|----------| +| 链接人与事 | 仅稳定版有 | 保留 | - | - | - | - | - | 不删 | +| 钱包 | 仅新版有 | 迁移 | wallet | balance/* | user_balance | ⚠️ 不完整 | 待补空态 | 迁界面+补 consume 写 orders | + +### 5.2 接口规范与冲突清单(表格) + +| 接口 | 甲方实现 | 规范要求 | 冲突说明 | 处理 | +|------|----------|----------|----------|------| +| POST balance/consume | 只扣余额 | 扣余额+写 orders | check-purchased 判未购买 | 重写 consume | + +### 5.3 需求评审清单(迁移前必产出) + +| 类型 | 项 | 说明 | 确认 | +|------|-----|------|------| +| 功能点 | 钱包页 | 新增余额、充值、交易记录 | ☐ | +| 功能点 | 我的页余额入口 | 第 4 项统计,点击进 wallet | ☐ | +| 样式变更 | 首页精选展开 | 默认 3 条,可展开更多 | ☐ | +| 样式变更 | Banner 按钮文案 | 「开始阅读」→「点击阅读」 | ☐ | + +**确认**:每个项经评审确认后再迁;未确认不迁。 + +### 5.4 功能闭环 Checklist(每功能必过) + +``` +□ 界面:页面、交互、数据绑定 +□ 接口:API 存在、参数正确、响应格式规范 +□ 数据:DB/事务/幂等 +□ 边界:未登录、余额不足、网络失败 +□ 三端:小程序+后端+管理端(如需)是否都改到 +□ 保护区域:未动 @/#、分销、支付 核心逻辑;若涉及则在原逻辑上扩展 +``` + +--- + +## 6. 与其它 Skill 的衔接 + +| 阶段 | 衔接 Skill | +|------|------------| +| 分析完成,开始实现 | **change-checklist**:变更完成必过三端关联检查 | +| 实现完成,经验沉淀 | **assistant-doc-sync**:吸收经验、同步需求文档 | +| 新增需求需三端规划 | **product-manager**:加个需求 → 三端分析 → 指派 | +| 跨端功能开发 | **role-flow-control**:后端先行 → 小程序 → 管理端 | +| 需求评审 | **team-meeting**:开会、需求评审 → 各角色发言 → 形成决议 | + +--- + +## 7. 执行顺序(单次分析) + +1. **Read 本 Skill** 完整内容 +2. **功能对齐与取舍**:先做三向分类(仅稳定版有/仅新版有/两者共有),产出取舍决策表 +3. **快速摸底**:双向对比 new-soul 与稳定版,产出/更新功能差异清单(含分类、取舍列) +4. **逻辑分层**:每个功能过三层(界面/接口/数据) +5. **体验评估**:补充空态、错误态、边界处理 +6. **接口规范与冲突**:产出接口规范与冲突清单 +7. **抽取需求**:写入需求汇总,形成迁移任务清单 +8. **需求评审(迁移前必做)**:列出功能点 + 样式变更,逐一确认,产出评审清单 +9. **回复用户**:给出分析摘要 + 文档路径 + 建议执行顺序;**迁移须在需求评审通过后开始** + +--- + +## 8. 文档写入位置 + +| 文档 | 路径 | +|------|------| +| 功能差异清单 | `开发文档/新版迁移-功能差异清单.md` | +| 接口规范与冲突 | `开发文档/新版迁移-接口规范与冲突清单.md` | +| 迁移方案/清单 | `开发文档/新版迁移-开发方案与清单.md` 或 `新版功能迁移到稳定版方案.md` | +| **需求评审清单** | `开发文档/新版迁移-需求评审清单.md`(功能点 + 样式变更,含确认状态) | +| 需求汇总 | `开发文档/1、需求/需求汇总.md` | + +若已有同名文档,在其基础上**追加或更新**,不重复创建。 + +--- + +## 9. 一句话总结 + +**迁移前必做需求评审**:列出功能点 + 样式变更,逐一确认后再迁。先做功能对齐与取舍,评审通过后按最小功能迁移;界面修改先迁、大逻辑排后;@/#、分销、支付为保护区域;用五步分析产出差异清单、接口冲突表、评审清单,再按 checklist 逐功能闭环。 diff --git a/miniprogram/app.js b/miniprogram/app.js index 045c2dc5..53a7282e 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -9,8 +9,8 @@ App({ globalData: { // API 基础地址(切换环境时注释/取消注释) // baseUrl: 'https://soulapi.quwanzhi.com', - // baseUrl: 'http://localhost:8080', // 本地调试 - baseUrl: 'https://souldev.quwanzhi.com', // 测试环境 + baseUrl: 'http://localhost:8080', // 本地调试 + // baseUrl: 'https://souldev.quwanzhi.com', // 测试环境 // 小程序配置 - 真实AppID diff --git a/miniprogram/app.json b/miniprogram/app.json index 49f1b392..dcbbf6c6 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -16,13 +16,16 @@ "pages/addresses/addresses", "pages/addresses/edit", "pages/withdraw-records/withdraw-records", + "pages/wallet/wallet", "pages/vip/vip", "pages/member-detail/member-detail", "pages/mentors/mentors", "pages/mentor-detail/mentor-detail", "pages/profile-show/profile-show", "pages/profile-edit/profile-edit", - "pages/avatar-nickname/avatar-nickname" + "pages/avatar-nickname/avatar-nickname", + "pages/gift-pay/detail", + "pages/gift-pay/list" ], "window": { "backgroundTextStyle": "light", @@ -58,7 +61,10 @@ ] }, "usingComponents": {}, - "navigateToMiniProgramAppIdList": [], + "navigateToMiniProgramAppIdList": [ + "wx6489c26045912fe1", + "wx3d15ed02e98b04e3" + ], "__usePrivacyCheck__": true, "lazyCodeLoading": "requiredComponents", "style": "v2", diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js index a53c1dc2..7e24bc5d 100644 --- a/miniprogram/pages/chapters/chapters.js +++ b/miniprogram/pages/chapters/chapters.js @@ -40,7 +40,10 @@ Page({ ], // 每日新增章节(懒加载后暂无,可后续用 latest-chapters 补充) - dailyChapters: [] + dailyChapters: [], + + // book/parts 加载中 + partsLoading: true }, onLoad() { @@ -55,16 +58,40 @@ Page({ }, // 懒加载:仅拉取篇章列表 + totalSections + fixedSections + // 优先 book/parts,404 或失败时降级为 all-chapters 推导 async loadParts() { + this.setData({ partsLoading: true }) try { - const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true }) - if (!res?.success) { - this.setData({ bookData: [], totalSections: 0 }) - return + let res + try { + res = await app.request({ url: '/api/miniprogram/book/parts', silent: true }) + } catch (e) { + console.log('[Chapters] book/parts 失败,降级 all-chapters:', e?.message || e) + res = null + } + let parts = [] + let totalSections = 0 + let fixedSections = [] + if (res?.success && Array.isArray(res.parts) && res.parts.length > 0) { + parts = res.parts + totalSections = res.totalSections ?? 0 + fixedSections = res.fixedSections || [] + } else { + // 降级:从 all-chapters 推导 parts + const allRes = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true }) + const list = (allRes?.data || allRes?.chapters || []) + totalSections = list.length + const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase() + const exclude = (c) => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录') + const partMap = new Map() + list.filter(exclude).forEach(c => { + const pid = c.partId || c.part_id || 'default' + const ptitle = c.partTitle || c.part_title || '未分类' + if (!partMap.has(pid)) partMap.set(pid, { id: pid, title: ptitle, subtitle: '', chapterCount: 0 }) + partMap.get(pid).chapterCount++ + }) + parts = Array.from(partMap.values()) } - const parts = res.parts || [] - const totalSections = res.totalSections ?? 0 - const fixedSections = res.fixedSections || [] const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'] const fixedMap = {} fixedSections.forEach(f => { fixedMap[f.id] = f.mid }) @@ -87,11 +114,12 @@ Page({ totalSections, fixedSectionsMap: fixedMap, appendixList, - _loadedChapters: {} + _loadedChapters: {}, + partsLoading: false }) } catch (e) { console.log('[Chapters] 加载篇章失败:', e) - this.setData({ bookData: [], totalSections: 0 }) + this.setData({ bookData: [], totalSections: 0, partsLoading: false }) } }, diff --git a/miniprogram/pages/chapters/chapters.wxml b/miniprogram/pages/chapters/chapters.wxml index 7a6f0b6d..996d0e81 100644 --- a/miniprogram/pages/chapters/chapters.wxml +++ b/miniprogram/pages/chapters/chapters.wxml @@ -17,8 +17,14 @@ + + + + 加载目录中... + + - + 📚 @@ -33,7 +39,7 @@ - + diff --git a/miniprogram/pages/chapters/chapters.wxss b/miniprogram/pages/chapters/chapters.wxss index 3e5586a9..7ae8ac8b 100644 --- a/miniprogram/pages/chapters/chapters.wxss +++ b/miniprogram/pages/chapters/chapters.wxss @@ -75,6 +75,34 @@ width: 100%; } +/* ===== 目录加载中 ===== */ +.parts-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 0; + gap: 24rpx; +} + +.parts-loading-spinner { + width: 64rpx; + height: 64rpx; + border: 6rpx solid rgba(255, 255, 255, 0.1); + border-top-color: #00CED1; + border-radius: 50%; + animation: parts-spin 0.8s linear infinite; +} + +.parts-loading-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +@keyframes parts-spin { + to { transform: rotate(360deg); } +} + /* ===== 书籍信息卡 ===== */ .book-info-card { display: flex; diff --git a/miniprogram/pages/gift-pay/detail.js b/miniprogram/pages/gift-pay/detail.js new file mode 100644 index 00000000..8f3ab2af --- /dev/null +++ b/miniprogram/pages/gift-pay/detail.js @@ -0,0 +1,114 @@ +/** + * Soul创业派对 - 代付详情页 + * 好友打开后看到订单信息,点击「帮他付款」完成代付 + */ +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + requestSn: '', + detail: null, + loading: true, + paying: false + }, + + onLoad(options) { + this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) + const requestSn = (options.requestSn || '').trim() + if (!requestSn) { + wx.showToast({ title: '代付链接无效', icon: 'none' }) + setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 1500) + return + } + this.setData({ requestSn }) + this.loadDetail() + }, + + async loadDetail() { + const { requestSn } = this.data + if (!requestSn) return + this.setData({ loading: true }) + try { + const res = await app.request(`/api/miniprogram/gift-pay/detail?requestSn=${encodeURIComponent(requestSn)}`) + if (res && res.success) { + this.setData({ detail: res, loading: false }) + } else { + this.setData({ loading: false }) + wx.showToast({ title: res?.error || '加载失败', icon: 'none' }) + } + } catch (e) { + this.setData({ loading: false }) + wx.showToast({ title: '加载失败', icon: 'none' }) + } + }, + + async doPay() { + if (!app.globalData.isLoggedIn || !app.globalData.userInfo) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => wx.switchTab({ url: '/pages/my/my' }), 1500) + return + } + const openId = app.globalData.openId || '' + if (!openId) { + wx.showToast({ title: '请先完成微信授权', icon: 'none' }) + return + } + const { requestSn, detail } = this.data + if (!requestSn || !detail) return + + this.setData({ paying: true }) + wx.showLoading({ title: '创建订单中...', mask: true }) + try { + const res = await app.request({ + url: '/api/miniprogram/gift-pay/pay', + method: 'POST', + data: { + requestSn, + openId, + userId: app.globalData.userInfo?.id || '' + } + }) + wx.hideLoading() + if (!res || !res.success || !res.data?.payParams) { + throw new Error(res?.error || '创建订单失败') + } + const payParams = res.data.payParams + payParams._orderSn = res.data.orderSn + + await new Promise((resolve, reject) => { + wx.requestPayment({ + ...payParams, + signType: payParams.signType || 'MD5', + success: resolve, + fail: reject + }) + }) + + wx.showToast({ title: '代付成功', icon: 'success' }) + this.setData({ paying: false }) + setTimeout(() => { + wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) }) + }, 1500) + } catch (e) { + this.setData({ paying: false }) + if (e.errMsg && e.errMsg.includes('cancel')) { + wx.showToast({ title: '已取消支付', icon: 'none' }) + } else { + wx.showToast({ title: e.message || e.error || '支付失败', icon: 'none' }) + } + } + }, + + goBack() { + app.goBackOrToHome() + }, + + onShareAppMessage() { + const { requestSn } = this.data + return { + title: '好友请你帮忙代付 - Soul创业派对', + path: requestSn ? `/pages/gift-pay/detail?requestSn=${requestSn}` : '/pages/gift-pay/detail' + } + } +}) diff --git a/miniprogram/pages/gift-pay/detail.json b/miniprogram/pages/gift-pay/detail.json new file mode 100644 index 00000000..b16ef3e3 --- /dev/null +++ b/miniprogram/pages/gift-pay/detail.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/miniprogram/pages/gift-pay/detail.wxml b/miniprogram/pages/gift-pay/detail.wxml new file mode 100644 index 00000000..8ca99450 --- /dev/null +++ b/miniprogram/pages/gift-pay/detail.wxml @@ -0,0 +1,52 @@ + + + + + + + + + 帮他付款 + + + + + + + + + + 加载中... + + + + + + 代付订单 + {{detail.initiatorNickname || '好友'}} 请你帮忙付款 + + + + 商品 + {{detail.description || '-'}} + + + 金额 + ¥{{detail.amount ? detail.amount.toFixed(2) : '0.00'}} + + + + + 付款后,{{detail.initiatorNickname || '好友'}}将获得对应权益 + + + + + + 代付请求不存在或已处理 + + + + diff --git a/miniprogram/pages/gift-pay/detail.wxss b/miniprogram/pages/gift-pay/detail.wxss new file mode 100644 index 00000000..69c07d09 --- /dev/null +++ b/miniprogram/pages/gift-pay/detail.wxss @@ -0,0 +1,160 @@ +/* Soul创业派对 - 代付详情页 */ +.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); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24rpx; + height: 88rpx; +} + +.nav-back { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + background: #1c1c1e; + display: flex; + align-items: center; + justify-content: center; +} + +.back-arrow { + font-size: 36rpx; + color: rgba(255, 255, 255, 0.8); +} + +.nav-title { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.content { + padding: 32rpx; +} + +.loading-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 0; +} + +.loading-spinner { + width: 48rpx; + height: 48rpx; + border: 4rpx solid rgba(0, 206, 209, 0.3); + border-top-color: #00CED1; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 24rpx; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +.card { + background: #1c1c1e; + border-radius: 24rpx; + overflow: hidden; + margin-bottom: 32rpx; +} + +.card-header { + padding: 32rpx; + border-bottom: 1rpx solid rgba(255, 255, 255, 0.06); +} + +.card-title { + display: block; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 8rpx; +} + +.initiator { + font-size: 34rpx; + font-weight: 600; + color: #fff; +} + +.card-body { + padding: 32rpx; +} + +.row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.row:last-child { + margin-bottom: 0; +} + +.label { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +.value { + font-size: 28rpx; + color: #fff; +} + +.amount-row .amount { + font-size: 40rpx; + font-weight: 700; + color: #00CED1; +} + +.tips { + padding: 0 8rpx 32rpx; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.4); +} + +.pay-btn { + width: 100%; + height: 96rpx; + line-height: 96rpx; + background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); + color: #fff; + font-size: 32rpx; + font-weight: 600; + border-radius: 48rpx; + border: none; +} + +.pay-btn[disabled] { + opacity: 0.6; +} + +.empty { + text-align: center; + padding: 120rpx 0; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} diff --git a/miniprogram/pages/gift-pay/list.js b/miniprogram/pages/gift-pay/list.js new file mode 100644 index 00000000..9c5a7856 --- /dev/null +++ b/miniprogram/pages/gift-pay/list.js @@ -0,0 +1,89 @@ +/** + * Soul创业派对 - 我的代付 + * Tab: 我发起的 / 我帮付的 + */ +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + tab: 'requests', + requests: [], + payments: [], + loading: false + }, + + onLoad() { + this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) + this.loadData() + }, + + onShow() { + if (this.data.requests.length > 0 || this.data.payments.length > 0) { + this.loadData() + } + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab || 'requests' + this.setData({ tab }) + this.loadData() + }, + + async loadData() { + const userId = app.globalData.userInfo?.id || '' + if (!userId) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + this.setData({ loading: true }) + try { + if (this.data.tab === 'requests') { + const res = await app.request(`/api/miniprogram/gift-pay/my-requests?userId=${encodeURIComponent(userId)}`) + this.setData({ requests: (res && res.list) || [], loading: false }) + } else { + const res = await app.request(`/api/miniprogram/gift-pay/my-payments?userId=${encodeURIComponent(userId)}`) + this.setData({ payments: (res && res.list) || [], loading: false }) + } + } catch (e) { + this.setData({ loading: false }) + } + }, + + async cancelRequest(e) { + const requestSn = e.currentTarget.dataset.sn + if (!requestSn) return + const ok = await new Promise(r => { + wx.showModal({ title: '取消代付', content: '确定取消该代付请求?', success: res => r(res.confirm) }) + }) + if (!ok) return + try { + const res = await app.request({ + url: '/api/miniprogram/gift-pay/cancel', + method: 'POST', + data: { requestSn, userId: app.globalData.userInfo?.id } + }) + if (res && res.success) { + wx.showToast({ title: '已取消', icon: 'success' }) + this.loadData() + } else { + wx.showToast({ title: res?.error || '取消失败', icon: 'none' }) + } + } catch (e) { + wx.showToast({ title: '取消失败', icon: 'none' }) + } + }, + + shareRequest(e) { + const requestSn = e.currentTarget.dataset.sn + wx.showToast({ title: '请点击右上角「...」分享给好友', icon: 'none', duration: 2500 }) + }, + + goBack() { + app.goBackOrToHome() + }, + + onShareAppMessage() { + return { title: '我的代付 - Soul创业派对', path: '/pages/gift-pay/list' } + } +}) diff --git a/miniprogram/pages/gift-pay/list.json b/miniprogram/pages/gift-pay/list.json new file mode 100644 index 00000000..b16ef3e3 --- /dev/null +++ b/miniprogram/pages/gift-pay/list.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/miniprogram/pages/gift-pay/list.wxml b/miniprogram/pages/gift-pay/list.wxml new file mode 100644 index 00000000..27d852be --- /dev/null +++ b/miniprogram/pages/gift-pay/list.wxml @@ -0,0 +1,64 @@ + + + + + + + + + 我的代付 + + + + + + + 我发起的 + 我帮付的 + + + + + + + 加载中... + + + + + 暂无发起的代付 + + + + + {{item.description}} + ¥{{item.amount}} + + + {{item.status === 'pending' ? '待支付' : item.status === 'paid' ? '已支付' : item.status === 'cancelled' ? '已取消' : '已过期'}} + + 分享 + 取消 + + + + + + + + 暂无帮付记录 + + + + + {{item.description}} + ¥{{item.amount}} + + + {{item.status === 'paid' ? '已支付' : item.status}} + + + + + + diff --git a/miniprogram/pages/gift-pay/list.wxss b/miniprogram/pages/gift-pay/list.wxss new file mode 100644 index 00000000..5fabe6a5 --- /dev/null +++ b/miniprogram/pages/gift-pay/list.wxss @@ -0,0 +1,160 @@ +/* Soul创业派对 - 我的代付 */ +.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); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24rpx; + height: 88rpx; +} + +.nav-back { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + background: #1c1c1e; + display: flex; + align-items: center; + justify-content: center; +} + +.back-arrow { + font-size: 36rpx; + color: rgba(255, 255, 255, 0.8); +} + +.nav-title { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.tabs { + display: flex; + padding: 24rpx 32rpx; + gap: 24rpx; + background: #000; +} + +.tab { + flex: 1; + text-align: center; + padding: 20rpx; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); + border-radius: 12rpx; + background: #1c1c1e; +} + +.tab.active { + color: #00CED1; + background: rgba(0, 206, 209, 0.15); +} + +.content { + padding: 0 32rpx 32rpx; +} + +.loading-box { + display: flex; + flex-direction: column; + align-items: center; + padding: 80rpx 0; +} + +.loading-spinner { + width: 48rpx; + height: 48rpx; + border: 4rpx solid rgba(0, 206, 209, 0.3); + border-top-color: #00CED1; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 24rpx; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +.empty { + text-align: center; + padding: 80rpx 0; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.4); +} + +.card { + background: #1c1c1e; + border-radius: 16rpx; + padding: 24rpx 32rpx; + margin-bottom: 24rpx; +} + +.card-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12rpx; +} + +.card-row:last-child { + margin-bottom: 0; +} + +.desc { + font-size: 28rpx; + color: #fff; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.amount { + font-size: 32rpx; + font-weight: 600; + color: #00CED1; + margin-left: 16rpx; +} + +.status { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.5); +} + +.status.paid { + color: #00CED1; +} + +.actions { + display: flex; + gap: 24rpx; +} + +.action-text { + font-size: 26rpx; + color: #00CED1; +} + +.action-text.cancel { + color: rgba(255, 255, 255, 0.5); +} diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 411efa11..5e20ddb7 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -47,8 +47,9 @@ Page({ superMembers: [], superMembersLoading: true, - // 最新新增章节 + // 最新新增章节(完整列表 + 展示列表,用于展开/折叠) latestChapters: [], + displayLatestChapters: [], // 篇章数(从 bookData 计算) partCount: 0, @@ -58,7 +59,13 @@ Page({ // 链接卡若 - 留资弹窗 showLeadModal: false, - leadPhone: '' + leadPhone: '', + + // 展开状态(首页精选/最新) + featuredExpanded: false, + latestExpanded: false, + featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表 + featuredExpandedLoading: false }, onLoad(options) { @@ -180,7 +187,25 @@ Page({ } } catch (e) { console.log('[Index] book/recommended 失败:', e) } - // 兜底:无 recommended 时从 all-chapters 按更新时间取前3 + // 兜底:无 recommended 时从 book/hot 取前3 + if (featured.length === 0) { + try { + const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true }) + const hotList = (hotRes && hotRes.data) ? hotRes.data : [] + if (hotList.length > 0) { + const tagMap = ['热门', '推荐', '精选'] + featured = hotList.slice(0, 3).map((s, i) => ({ + id: s.id || s.section_id, + mid: s.mid ?? s.MID ?? 0, + title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '', + part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(), + tag: tagMap[i] || '精选', + tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec' + })) + this.setData({ featuredSections: featured }) + } + } catch (e) { console.log('[Index] book/hot 兜底失败:', e) } + } if (featured.length === 0) { const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true }) const chapters = (res && res.data) || (res && res.chapters) || [] @@ -482,6 +507,50 @@ Page({ wx.switchTab({ url: '/pages/match/match' }) }, + // 精选推荐:展开/折叠 + async toggleFeaturedExpanded() { + if (this.data.featuredExpandedLoading) return + if (this.data.featuredExpanded) { + const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections + this.setData({ featuredExpanded: false, featuredSections: collapsed }) + return + } + if (this.data.featuredSectionsFull.length > 0) { + this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull }) + return + } + this.setData({ featuredExpandedLoading: true }) + try { + const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true }) + const list = (res && res.data) ? res.data : [] + const tagMap = ['热门', '推荐', '精选'] + const full = list.map((s, i) => ({ + id: s.id || s.section_id, + mid: s.mid ?? s.MID ?? 0, + title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '', + part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(), + tag: tagMap[i % 3] || '精选', + tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec' + })) + this.setData({ + featuredSectionsFull: full, + featuredSections: full, + featuredExpanded: true, + featuredExpandedLoading: false + }) + } catch (e) { + console.log('[Index] 加载精选更多失败:', e) + this.setData({ featuredExpandedLoading: false }) + } + }, + + // 最新新增:展开/折叠(默认 5 条,点击展开剩余) + toggleLatestExpanded() { + const expanded = !this.data.latestExpanded + const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5) + this.setData({ latestExpanded: expanded, displayLatestChapters: display }) + }, + // 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章 async loadLatestChapters() { try { @@ -491,7 +560,7 @@ Page({ const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录') const latest = list .filter(exclude) - .slice(0, 10) + .slice(0, 20) .map(c => { const d = new Date(c.updatedAt || c.updated_at || Date.now()) const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || '' @@ -504,7 +573,8 @@ Page({ dateStr: `${d.getMonth() + 1}/${d.getDate()}` } }) - this.setData({ latestChapters: latest }) + const display = this.data.latestExpanded ? latest : latest.slice(0, 5) + this.setData({ latestChapters: latest, displayLatestChapters: display }) } catch (e) { console.log('[Index] 加载最新新增失败:', e) } }, diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 4fdb7202..50b9f5a4 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -52,6 +52,19 @@ + + + + 阅读进度 + 已读 {{readCount}}/{{totalSections}} + + + + + + + + @@ -91,10 +104,14 @@ - + 精选推荐 + + {{featuredExpandedLoading ? '加载中...' : (featuredExpanded ? '收起' : '展开更多')}} + {{featuredExpanded ? '▲' : '▼'}} + - + 最新新增 - - +{{latestChapters.length}} + + + +{{latestChapters.length}} + + + {{latestExpanded ? '收起' : '展开更多'}} + {{latestExpanded ? '▲' : '▼'}} + - + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index 32bad3f9..8116b63c 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -688,6 +688,12 @@ margin-bottom: 32rpx; } +.section-header-right { + display: flex; + align-items: center; + gap: 16rpx; +} + .daily-badge-wrap { display: inline-flex; align-items: center; diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index f78bc31b..0f2f1c72 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -76,6 +76,9 @@ Page({ // 设置入口:开发版、体验版显示 showSettingsEntry: false, + + // 我的余额 + walletBalanceText: '--', }, onLoad() { @@ -148,6 +151,7 @@ Page({ this.loadMyEarnings() this.loadPendingConfirm() this.loadVipStatus() + this.loadWalletBalance() } else { const guestReadCount = app.getReadCount() this.setData({ @@ -762,8 +766,10 @@ Page({ const routes = { orders: '/pages/purchases/purchases', + giftPay: '/pages/gift-pay/list', referral: '/pages/referral/referral', withdrawRecords: '/pages/withdraw-records/withdraw-records', + wallet: '/pages/wallet/wallet', about: '/pages/about/about', settings: '/pages/settings/settings' } @@ -848,6 +854,18 @@ Page({ } catch (e) { console.log('[My] VIP查询失败', e) } }, + async loadWalletBalance() { + const userId = app.globalData.userInfo?.id + if (!userId) return + try { + const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }) + if (res?.success && res.data) { + const balance = res.data.balance || 0 + this.setData({ walletBalanceText: balance.toFixed(2) }) + } + } catch (e) { console.log('[My] 余额查询失败', e) } + }, + // 头像点击:已登录弹出选项(微信头像 / 相册) onAvatarTap() { if (!this.data.isLoggedIn) { this.showLogin(); return } @@ -915,6 +933,12 @@ Page({ wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }, + // 进入个人资料展示页(enhanced_professional_profile),展示页内可再进编辑 + goToProfileShow() { + if (!this.data.isLoggedIn) { this.showLogin(); return } + wx.navigateTo({ url: '/pages/profile-show/profile-show' }) + }, + async handleWithdraw() { if (!this.data.isLoggedIn) { this.showLogin(); return } const amount = parseFloat(this.data.pendingEarnings) diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index ef2ad90f..3d2c2177 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -34,7 +34,13 @@ {{userInfo.nickname || '点击设置昵称'}} - {{isVip ? '会员中心' : '成为会员'}} + + + + 编辑 + + {{isVip ? '会员中心' : '成为会员'}} + 会员 @@ -57,6 +63,10 @@ {{earnings === '-' ? '--' : earnings}} 我的收益 + + {{walletBalanceText}} + 我的余额 + @@ -147,6 +157,13 @@ + + + + 我的代付 + + + diff --git a/miniprogram/pages/my/my.wxss b/miniprogram/pages/my/my.wxss index 6d43a7da..57f9fa4a 100644 --- a/miniprogram/pages/my/my.wxss +++ b/miniprogram/pages/my/my.wxss @@ -59,6 +59,10 @@ .vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); } .profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; } .profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; } +.profile-name-actions { display: flex; align-items: center; gap: 16rpx; flex-shrink: 0; } +.profile-edit-btn { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 16rpx; background: rgba(255,255,255,0.08); border-radius: 12rpx; } +.profile-edit-icon { width: 28rpx; height: 28rpx; opacity: 0.7; } +.profile-edit-text { font-size: 24rpx; color: rgba(255,255,255,0.7); } .user-name { font-size: 44rpx; font-weight: bold; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; @@ -178,6 +182,8 @@ .icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; } .icon-gray { background: rgba(156,163,175,0.15); } .icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; } +.icon-gold { background: rgba(200,161,70,0.2); } +.icon-gold .menu-icon-img { width: 32rpx; height: 32rpx; } .menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; } .menu-arrow { font-size: 36rpx; color: #9CA3AF; } diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 5a744771..b6ad9eaf 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -65,6 +65,9 @@ Page({ // 弹窗 showShareModal: false, + showGiftShareModal: false, + shareMode: '', // 'gift' = 代付分享,onShareAppMessage 返回 gift-pay/detail + giftRequestSn: '', // 代付请求号,分享时用 showLoginModal: false, agreeProtocol: false, showPosterModal: false, @@ -72,7 +75,10 @@ Page({ isGeneratingPoster: false, // 章节 mid(扫码/海报分享用,便于分享 path 带 mid) - sectionMid: null + sectionMid: null, + + // 余额(用于余额支付) + walletBalance: 0, }, async onLoad(options) { @@ -88,12 +94,16 @@ Page({ }).catch(() => {}) } - // 支持 scene(扫码)、mid、id、ref + // 支持 scene(扫码)、mid、id、ref、gift(代付) const sceneStr = (options && options.scene) || '' const parsed = parseScene(sceneStr) const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0) let id = options.id || parsed.id || app.globalData.initialSectionId const ref = options.ref || parsed.ref + const isGift = options.gift === '1' || options.gift === 'true' + if (isGift && ref) { + wx.setStorageSync('gift_for_ref', ref) // 代付模式:好友打开后,购买时传 giftFor(后端待支持) + } if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid if (app.globalData.initialSectionId) delete app.globalData.initialSectionId @@ -647,6 +657,55 @@ Page({ this.setData({ showShareModal: false }) }, + // 代付分享弹窗:创建代付请求后分享到代付页面 + async showGiftShareModal() { + if (!app.globalData.userInfo?.id) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + const { sectionId, sectionMid } = this.data + const productId = sectionId || '' + if (!productId) { + wx.showToast({ title: '章节信息异常', icon: 'none' }) + return + } + wx.showLoading({ title: '创建代付请求...', mask: true }) + try { + const res = await app.request({ + url: '/api/miniprogram/gift-pay/create', + method: 'POST', + data: { + userId: app.globalData.userInfo.id, + productType: 'section', + productId + } + }) + wx.hideLoading() + if (res && res.success && res.requestSn) { + this.setData({ showGiftShareModal: true, giftRequestSn: res.requestSn }) + } else { + wx.showToast({ title: res?.error || '创建失败', icon: 'none' }) + } + } catch (e) { + wx.hideLoading() + wx.showToast({ title: '创建失败', icon: 'none' }) + } + }, + + closeGiftShareModal() { + this.setData({ showGiftShareModal: false }) + }, + + // 分享给好友(代付):引导用户点右上角,分享到代付详情页 + shareGiftToFriend() { + this.setData({ shareMode: 'gift', showGiftShareModal: false }) + wx.showToast({ + title: '请点击右上角「...」→ 发送给好友', + icon: 'none', + duration: 2500 + }) + }, + // 复制链接 copyLink() { const userInfo = app.globalData.userInfo @@ -682,17 +741,27 @@ Page({ }) }, - // 分享到微信 - 自动带分享人ID;优先用 mid(扫码/海报闭环),无则用 id + // 分享到微信 - 自动带分享人ID;shareMode=gift 时分享到代付详情页 onShareAppMessage() { - const { section, sectionId, sectionMid } = this.data + const { section, sectionId, sectionMid, shareMode, giftRequestSn } = this.data const ref = app.getMyReferralCode() const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}` - const shareTitle = section?.title + let path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}` + let title = section?.title ? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}` : '📚 Soul创业派对 - 真实商业故事' + if (shareMode === 'gift' && giftRequestSn) { + path = `/pages/gift-pay/detail?requestSn=${giftRequestSn}` + title = '好友请你帮忙代付 - Soul创业派对' + this.setData({ shareMode: '', giftRequestSn: '' }) + } else { + title = section?.title + ? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}` + : '📚 Soul创业派对 - 真实商业故事' + } return { - title: shareTitle, - path: ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}` + title, + path // 不设置 imageUrl,使用当前阅读页截图作为分享卡片中间图片 } }, @@ -706,16 +775,21 @@ Page({ }) }, - // 分享到朋友圈:带文章标题,过长时截断(朋友圈卡片标题显示有限) + // 分享到朋友圈:带文章标题,过长时截断;shareMode=gift 时 query 带 gift=1 onShareTimeline() { - const { section, sectionId, sectionMid, chapterTitle } = this.data + const { section, sectionId, sectionMid, chapterTitle, shareMode } = this.data const ref = app.getMyReferralCode() const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}` + let query = ref ? `${q}&ref=${ref}` : q + if (shareMode === 'gift' && ref) { + query = `${q}&ref=${ref}&gift=1` + this.setData({ shareMode: '' }) + } const articleTitle = (section?.title || chapterTitle || '').trim() const title = articleTitle ? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle) : 'Soul创业派对 - 真实商业故事' - return { title, query: ref ? `${q}&ref=${ref}` : q } + return { title, query } }, // 显示登录弹窗(每次打开协议未勾选,符合审核要求) @@ -926,6 +1000,39 @@ Page({ wx.showLoading({ title: '正在发起支付...', mask: true }) try { + // 0. 尝试余额支付(若余额足够) + const userId = app.globalData.userInfo?.id + const referralCode = wx.getStorageSync('referral_code') || '' + if (userId) { + try { + const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }) + const balance = balanceRes?.data?.balance || 0 + if (balance >= amount) { + const productId = type === 'section' ? sectionId : (type === 'fullbook' ? 'fullbook' : '') + const consumeRes = await app.request({ + url: '/api/miniprogram/balance/consume', + method: 'POST', + data: { + userId, + productType: type, + productId: type === 'section' ? sectionId : (type === 'fullbook' ? 'fullbook' : 'vip_annual'), + amount, + referralCode: referralCode || undefined + } + }) + if (consumeRes?.success) { + wx.hideLoading() + this.setData({ isPaying: false }) + wx.showToast({ title: '购买成功', icon: 'success' }) + await this.onPaymentSuccess() + return + } + } + } catch (e) { + console.warn('[Pay] 余额支付失败,改用微信支付:', e) + } + } + // 1. 先获取openId (支付必需) let openId = app.globalData.openId || wx.getStorageSync('openId') diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml index 84f8181b..409efc69 100644 --- a/miniprogram/pages/read/read.wxml +++ b/miniprogram/pages/read/read.wxml @@ -93,6 +93,10 @@ 🖼️ 生成海报 + + 🎁 + 代付分享 + @@ -187,6 +191,11 @@ 分享给好友一起学习,还能赚取佣金 + + + 🎁 + 找好友代付 + @@ -289,6 +298,23 @@ + + + + + diff --git a/miniprogram/pages/read/read.wxss b/miniprogram/pages/read/read.wxss index f7cd9da3..4f28b279 100644 --- a/miniprogram/pages/read/read.wxss +++ b/miniprogram/pages/read/read.wxss @@ -586,6 +586,48 @@ color: rgba(255, 255, 255, 0.6); } +/* ===== 代付分享 ===== */ +.btn-gift-inline { + /* 与 btn-share-inline 同风格 */ +} +.gift-share-row { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin-top: 24rpx; + padding: 20rpx; + background: rgba(255, 215, 0, 0.08); + border-radius: 24rpx; + border: 1rpx solid rgba(255, 215, 0, 0.2); +} +.gift-share-icon { font-size: 32rpx; } +.gift-share-text { font-size: 28rpx; color: #FFD700; } +.share-modal-desc { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + display: block; + margin-bottom: 32rpx; + line-height: 1.5; +} +.share-modal-actions { + display: flex; + gap: 24rpx; +} +.share-modal-btn { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + padding: 32rpx 24rpx; + background: rgba(255, 255, 255, 0.06); + border-radius: 24rpx; + border: 1rpx solid rgba(255, 255, 255, 0.1); +} +.share-modal-btn .btn-icon { font-size: 48rpx; } +.share-modal-btn text:last-child { font-size: 26rpx; color: rgba(255, 255, 255, 0.8); } + /* ===== 分享弹窗 ===== */ .share-link-box { padding: 32rpx; diff --git a/miniprogram/pages/search/search.js b/miniprogram/pages/search/search.js index 7e2bb727..cf5b9538 100644 --- a/miniprogram/pages/search/search.js +++ b/miniprogram/pages/search/search.js @@ -36,7 +36,7 @@ Page({ // 加载热门章节(从服务器获取点击量高的章节) async loadHotChapters() { try { - const res = await app.request('/api/miniprogram/book/hot') + const res = await app.request('/api/miniprogram/book/hot?limit=50') const list = (res && res.data) || (res && res.chapters) || [] if (list.length > 0) { const hotChapters = list.map((c, i) => ({ diff --git a/miniprogram/pages/vip/vip.js b/miniprogram/pages/vip/vip.js index dfc1a29e..ee101c8f 100644 --- a/miniprogram/pages/vip/vip.js +++ b/miniprogram/pages/vip/vip.js @@ -84,7 +84,37 @@ Page({ } } this.setData({ purchasing: true }) + const amount = this.data.price try { + // 0. 尝试余额支付(若余额足够) + const referralCode = wx.getStorageSync('referral_code') || '' + try { + const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }) + const balance = balanceRes?.data?.balance || 0 + if (balance >= amount) { + const consumeRes = await app.request({ + url: '/api/miniprogram/balance/consume', + method: 'POST', + data: { + userId, + productType: 'vip', + productId: 'vip_annual', + amount, + referralCode: referralCode || undefined + } + }) + if (consumeRes?.success) { + this.setData({ purchasing: false }) + wx.showToast({ title: 'VIP开通成功', icon: 'success' }) + await this._onVipPaymentSuccess() + return + } + } + } catch (e) { + console.warn('[VIP] 余额支付失败,改用微信支付:', e) + } + + // 1. 微信支付 const payRes = await app.request('/api/miniprogram/pay', { method: 'POST', data: { @@ -92,7 +122,7 @@ Page({ userId, productType: 'vip', productId: 'vip_annual', - amount: this.data.price, + amount, description: '卡若创业派对VIP年度会员(365天)' } }) diff --git a/miniprogram/pages/wallet/wallet.js b/miniprogram/pages/wallet/wallet.js new file mode 100644 index 00000000..c62ffbcb --- /dev/null +++ b/miniprogram/pages/wallet/wallet.js @@ -0,0 +1,134 @@ +const app = getApp() +const { trackClick } = require('../../utils/trackClick') + +Page({ + data: { + statusBarHeight: 44, + balance: 0, + balanceText: '0.00', + transactions: [], + loading: true, + rechargeAmounts: [10, 30, 50, 100], + selectedAmount: 30, + }, + + onLoad() { + this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) + this.loadBalance() + this.loadTransactions() + }, + + async loadBalance() { + if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return + const userId = app.globalData.userInfo.id + try { + const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }) + if (res && res.data) { + this.setData({ + balance: res.data.balance || 0, + balanceText: (res.data.balance || 0).toFixed(2), + loading: false, + }) + } + } catch (e) { + this.setData({ loading: false }) + } + }, + + async loadTransactions() { + if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return + const userId = app.globalData.userInfo.id + try { + const res = await app.request({ url: `/api/miniprogram/balance/transactions?userId=${userId}`, silent: true }) + if (res && res.data) { + const list = (res.data || []).map(t => ({ + ...t, + amountText: Math.abs(t.amount || 0).toFixed(2), + amountSign: (t.amount || 0) >= 0 ? '+' : '-', + description: t.type === 'recharge' ? '充值' : t.type === 'consume' ? '阅读消费' : t.type === 'refund' ? '退款' : '其他', + createdAt: t.createdAt ? new Date(t.createdAt).toLocaleString('zh-CN') : '--', + })) + this.setData({ transactions: list }) + } + } catch (e) { + console.warn('[Wallet] load transactions failed', e) + } + }, + + selectAmount(e) { + trackClick('wallet', 'tab_click', '选择金额' + (e.currentTarget.dataset.amount || '')) + this.setData({ selectedAmount: parseInt(e.currentTarget.dataset.amount) }) + }, + + async handleRecharge() { + trackClick('wallet', 'btn_click', '充值') + if (!app.globalData.isLoggedIn || !app.globalData.userInfo) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + const userId = app.globalData.userInfo.id + const amount = this.data.selectedAmount + + let openId = app.globalData.openId + if (!openId) { + openId = await app.getOpenId() + } + if (!openId) { + wx.showToast({ title: '获取支付凭证失败,请重新登录', icon: 'none', duration: 2500 }) + return + } + + wx.showLoading({ title: '创建订单...' }) + try { + const res = await app.request({ + url: '/api/miniprogram/balance/recharge', + method: 'POST', + data: { userId, amount } + }) + wx.hideLoading() + if (res && res.data && res.data.orderSn) { + const payRes = await app.request({ + url: '/api/miniprogram/pay', + method: 'POST', + data: { + openId: openId, + productType: 'balance_recharge', + productId: res.data.orderSn, + amount: amount, + description: `余额充值 ¥${amount}`, + userId: userId, + } + }) + const params = (payRes && payRes.data && payRes.data.payParams) ? payRes.data.payParams : (payRes && payRes.payParams ? payRes.payParams : null) + if (params) { + wx.requestPayment({ + ...params, + success: async () => { + await app.request({ + url: '/api/miniprogram/balance/recharge/confirm', + method: 'POST', + data: { orderSn: res.data.orderSn } + }) + wx.showToast({ title: '充值成功', icon: 'success' }) + this.loadBalance() + this.loadTransactions() + }, + fail: () => { + wx.showToast({ title: '支付取消', icon: 'none' }) + } + }) + } else { + wx.showToast({ title: payRes?.error || '创建支付失败', icon: 'none' }) + } + } + } catch (e) { + wx.hideLoading() + console.error('[Wallet] recharge error', e) + wx.showToast({ title: '充值失败:' + (e.message || e.errMsg || '网络异常'), icon: 'none', duration: 3000 }) + } + }, + + goBack() { + wx.navigateBack() + }, +}) diff --git a/miniprogram/pages/wallet/wallet.json b/miniprogram/pages/wallet/wallet.json new file mode 100644 index 00000000..ad7e247e --- /dev/null +++ b/miniprogram/pages/wallet/wallet.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的余额", + "navigationStyle": "custom" +} diff --git a/miniprogram/pages/wallet/wallet.wxml b/miniprogram/pages/wallet/wallet.wxml new file mode 100644 index 00000000..df505830 --- /dev/null +++ b/miniprogram/pages/wallet/wallet.wxml @@ -0,0 +1,78 @@ + + + + + + + 我的余额 + + + + + + + 当前余额 + ¥{{balanceText}} + 充值后可直接用于解锁付费内容,消费记录会展示在下方。 + + + 加载中... + + + + + + 选择充值金额 + 当前已选 ¥{{selectedAmount}} + + + + + ¥{{item}} + + + + + {{selectedAmount === item ? '已选中,点击充值' : '点击选择此金额'}} + + + + + + 充值 + + + + + 充值/消费记录 + 按时间倒序显示 + + + + + 💰 + 🎁 + ↩️ + 📖 + + + + {{item.description}} + {{item.createdAt || '--'}} + + {{item.amountSign}}¥{{item.amountText}} + + + + 暂无充值或消费记录 + + + + + diff --git a/miniprogram/pages/wallet/wallet.wxss b/miniprogram/pages/wallet/wallet.wxss new file mode 100644 index 00000000..e1ea463a --- /dev/null +++ b/miniprogram/pages/wallet/wallet.wxss @@ -0,0 +1,256 @@ +/* Soul创业派对 - 我的余额 - 深色主题 */ +.page { + min-height: 100vh; + background: #0a0a0a; + padding-bottom: 64rpx; +} + +.nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: rgba(10, 10, 10, 0.95); + 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; +} +.nav-placeholder-block { + width: 100%; +} + +.balance-card { + margin: 24rpx 24rpx 32rpx; + background: linear-gradient(135deg, #1c1c1e 0%, rgba(56, 189, 172, 0.15) 100%); + border-radius: 32rpx; + padding: 40rpx 32rpx; + border: 2rpx solid rgba(56, 189, 172, 0.2); +} +.balance-main { + min-height: 220rpx; + display: flex; + flex-direction: column; + justify-content: center; +} +.balance-label { + display: block; + font-size: 26rpx; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 8rpx; +} +.balance-value { + font-size: 72rpx; + font-weight: 700; + color: #38bdac; + letter-spacing: 2rpx; +} +.balance-tip { + margin-top: 18rpx; + font-size: 24rpx; + line-height: 1.7; + color: rgba(255, 255, 255, 0.58); +} +.balance-skeleton { + padding: 40rpx 0; +} +.skeleton-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.4); +} + +.section { + margin: 0 24rpx 32rpx; +} +.section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16rpx; + margin-bottom: 20rpx; +} +.section-title { + font-size: 28rpx; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} +.section-note { + font-size: 22rpx; + color: rgba(255, 255, 255, 0.45); +} + +.amount-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20rpx; +} +.amount-card { + padding: 26rpx 24rpx; + background: linear-gradient(180deg, #19191b 0%, #151517 100%); + border-radius: 28rpx; + border: 2rpx solid rgba(255, 255, 255, 0.1); + box-sizing: border-box; +} +.amount-card-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} +.amount-card-value { + font-size: 40rpx; + font-weight: 700; + color: rgba(255, 255, 255, 0.9); +} +.amount-card-desc { + display: block; + font-size: 22rpx; + color: rgba(255, 255, 255, 0.46); +} +.amount-card-check { + width: 34rpx; + height: 34rpx; + border-radius: 50%; + border: 2rpx solid rgba(255, 255, 255, 0.18); + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} +.amount-card-check-active { + border-color: #38bdac; + background: rgba(56, 189, 172, 0.18); +} +.amount-card-check-dot { + width: 14rpx; + height: 14rpx; + border-radius: 50%; + background: #38bdac; +} +.amount-card-active { + background: linear-gradient(180deg, rgba(56, 189, 172, 0.22) 0%, rgba(15, 30, 29, 0.95) 100%); + border-color: rgba(56, 189, 172, 0.95); + box-shadow: 0 0 0 2rpx rgba(56, 189, 172, 0.1); +} +.amount-card-active .amount-card-value { + color: #52d8c7; +} +.amount-card-active .amount-card-desc { + color: rgba(213, 255, 250, 0.72); +} + +.action-row { + display: flex; + gap: 24rpx; + margin: 0 24rpx 40rpx; +} +.btn { + flex: 1; + height: 88rpx; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 30rpx; + font-weight: 600; +} +.btn-recharge { + background: #38bdac; + color: #0a0a0a; +} + +.transactions { + background: #1c1c1e; + border-radius: 24rpx; + overflow: hidden; + border: 2rpx solid rgba(255, 255, 255, 0.04); +} +.tx-item { + display: flex; + align-items: center; + padding: 28rpx 32rpx; + border-bottom: 2rpx solid rgba(255, 255, 255, 0.04); +} +.tx-item:last-child { + border-bottom: none; +} +.tx-icon { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + margin-right: 24rpx; +} +.tx-icon.recharge { + background: rgba(56, 189, 172, 0.2); +} +.tx-icon.gift { + background: rgba(255, 215, 0, 0.15); +} +.tx-icon.refund { + background: rgba(255, 255, 255, 0.1); +} +.tx-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4rpx; +} +.tx-desc { + font-size: 28rpx; + color: #fff; +} +.tx-time { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.45); +} +.tx-amount { + font-size: 30rpx; + font-weight: 600; +} +.tx-amount-plus { + color: #38bdac; +} +.tx-amount-minus { + color: rgba(255, 255, 255, 0.6); +} +.tx-empty { + padding: 60rpx; + text-align: center; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.4); + background: #1c1c1e; + border-radius: 24rpx; +} + +.bottom-space { + height: 48rpx; +} diff --git a/miniprogram/utils/trackClick.js b/miniprogram/utils/trackClick.js new file mode 100644 index 00000000..332bdef1 --- /dev/null +++ b/miniprogram/utils/trackClick.js @@ -0,0 +1,25 @@ +const app = getApp() + +/** + * 全局按钮/标签点击埋点 + * @param {string} module 模块:home|chapters|read|my|vip|wallet|match|referral|search|settings|about + * @param {string} action 行为:tab_click|btn_click|nav_click|card_click|link_click 等 + * @param {string} target 目标标识:按钮文案、章节ID、标签名等 + * @param {object} [extra] 附加数据 + */ +function trackClick(module, action, target, extra) { + const userId = app.globalData.userInfo?.id || '' + app.request({ + url: '/api/miniprogram/track', + method: 'POST', + data: { + userId: userId || undefined, + action, + target, + extraData: Object.assign({ module, page: module }, extra || {}) + }, + silent: true + }).catch(() => {}) +} + +module.exports = { trackClick } diff --git a/soul-admin/src/components/modules/user/UserDetailModal.tsx b/soul-admin/src/components/modules/user/UserDetailModal.tsx index cc655d14..3a568037 100644 --- a/soul-admin/src/components/modules/user/UserDetailModal.tsx +++ b/soul-admin/src/components/modules/user/UserDetailModal.tsx @@ -104,6 +104,7 @@ export function UserDetailModal({ const [user, setUser] = useState(null) const [tracks, setTracks] = useState([]) const [referrals, setReferrals] = useState([]) + const [balanceData, setBalanceData] = useState<{ balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } | null>(null) const [loading, setLoading] = useState(false) const [syncing, setSyncing] = useState(false) const [saving, setSaving] = useState(false) @@ -121,7 +122,6 @@ export function UserDetailModal({ // 设成超级个体(VIP) const [vipForm, setVipForm] = useState({ isVip: false, vipExpireDate: '', vipRole: '', vipName: '', vipProject: '', vipContact: '', vipBio: '' }) const [vipRoles, setVipRoles] = useState<{ id: number; name: string }[]>([]) - const [vipSaving, setVipSaving] = useState(false) // 用户资料完善(神射手) const [sssLoading, setSssLoading] = useState(false) @@ -194,6 +194,13 @@ export function UserDetailModal({ ) if (refData?.success && refData.referrals) setReferrals(refData.referrals) } catch { setReferrals([]) } + try { + const balData = await get<{ success?: boolean; data?: { balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } }>( + `/api/admin/users/${encodeURIComponent(userId)}/balance`, + ) + if (balData?.success && balData.data) setBalanceData(balData.data) + else setBalanceData(null) + } catch { setBalanceData(null) } } catch (e) { console.error('Load user detail error:', e) } finally { @@ -222,6 +229,10 @@ export function UserDetailModal({ async function handleSave() { if (!user) return + if (vipForm.isVip && !vipForm.vipExpireDate.trim()) { + toast.error('开启 VIP 请填写有效到期日') + return + } setSaving(true) try { const payload: Record = { @@ -229,6 +240,14 @@ export function UserDetailModal({ phone: editPhone || undefined, nickname: editNickname || undefined, tags: JSON.stringify(editTags), + // 超级个体/VIP 相关字段一并保存 + isVip: vipForm.isVip, + vipExpireDate: vipForm.isVip ? vipForm.vipExpireDate : undefined, + vipRole: vipForm.vipRole || undefined, + vipName: vipForm.vipName || undefined, + vipProject: vipForm.vipProject || undefined, + vipContact: vipForm.vipContact || undefined, + vipBio: vipForm.vipBio || undefined, } const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload) if (data?.success) { @@ -268,27 +287,6 @@ export function UserDetailModal({ } catch { toast.error('修改失败') } finally { setPasswordSaving(false) } } - async function handleSaveVip() { - if (!user) return - if (vipForm.isVip && !vipForm.vipExpireDate.trim()) { toast.error('开启 VIP 请填写有效到期日'); return } - setVipSaving(true) - try { - const payload = { - id: user.id, - isVip: vipForm.isVip, - vipExpireDate: vipForm.isVip ? vipForm.vipExpireDate : undefined, - vipRole: vipForm.vipRole || undefined, - vipName: vipForm.vipName || undefined, - vipProject: vipForm.vipProject || undefined, - vipContact: vipForm.vipContact || undefined, - vipBio: vipForm.vipBio || undefined, - } - const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload) - if (data?.success) { toast.success('VIP 设置已保存'); loadUserDetail(); onUserUpdated?.() } - else toast.error('保存失败: ' + (data?.error || '')) - } catch { toast.error('保存失败') } finally { setVipSaving(false) } - } - // 用户资料完善查询(支持多维度) async function handleSSSQuery() { if (!sssQueryPhone && !sssQueryOpenId && !sssQueryWechatId) { @@ -512,7 +510,7 @@ export function UserDetailModal({ )} -
+

推荐人数

{user.referralCount ?? 0}

@@ -523,6 +521,12 @@ export function UserDetailModal({ ¥{(user.pendingEarnings ?? 0).toFixed(2)}

+
+

当前余额

+

+ ¥{(balanceData?.balance ?? 0).toFixed(2)} +

+

创建时间

@@ -609,14 +613,33 @@ export function UserDetailModal({ onChange={(e) => setVipForm((f) => ({ ...f, vipName: e.target.value }))} />

- +
+ + setVipForm((f) => ({ ...f, vipProject: e.target.value }))} + /> +
+
+ + setVipForm((f) => ({ ...f, vipContact: e.target.value }))} + /> +
+
+ + setVipForm((f) => ({ ...f, vipBio: e.target.value }))} + /> +
@@ -797,20 +820,32 @@ export function UserDetailModal({ - {/* 存客宝标签 */} - {user.ckbTags && ( -
-
- - 存客宝标签 + {/* 存客宝标签(与用户标签共用 ckb_tags,兼容 JSON 与逗号分隔) */} + {(() => { + const raw = user.tags || user.ckbTags || '' + let arr: string[] = [] + try { + const parsed = typeof raw === 'string' ? JSON.parse(raw || '[]') : [] + arr = Array.isArray(parsed) ? parsed : (typeof raw === 'string' ? raw.split(',') : []) + } catch { + arr = typeof raw === 'string' ? raw.split(',') : [] + } + const tags = arr.map((t) => String(t).trim()).filter(Boolean) + if (tags.length === 0) return null + return ( +
+
+ + 存客宝标签 +
+
+ {tags.map((tag, i) => ( + {tag} + ))} +
-
- {(typeof user.ckbTags === 'string' ? user.ckbTags.split(',') : []).map((tag, i) => ( - {tag.trim()} - ))} -
-
- )} + ) + })()} {/* ===== 用户旅程(原行为轨迹)===== */} diff --git a/soul-admin/src/pages/dashboard/DashboardPage.tsx b/soul-admin/src/pages/dashboard/DashboardPage.tsx index ba3dc2c5..505a80ad 100644 --- a/soul-admin/src/pages/dashboard/DashboardPage.tsx +++ b/soul-admin/src/pages/dashboard/DashboardPage.tsx @@ -181,6 +181,10 @@ export function DashboardPage() { const formatOrderProduct = (p: OrderRow) => { const type = p.productType || '' const desc = p.description || '' + if (type === 'balance_recharge') { + const amount = typeof p.amount === 'number' ? p.amount.toFixed(2) : parseFloat(String(p.amount || '0')).toFixed(2) + return { title: `余额充值 ¥${amount}`, subtitle: '余额充值' } + } if (desc) { if (type === 'section' && desc.includes('章节')) { if (desc.includes('-')) { @@ -197,6 +201,9 @@ export function DashboardPage() { if (type === 'fullbook' || desc.includes('全书')) { return { title: '《一场Soul的创业实验》', subtitle: '全书购买' } } + if (type === 'vip' || desc.includes('VIP')) { + return { title: '超级个体开通费用', subtitle: '超级个体' } + } if (type === 'match' || desc.includes('伙伴')) { return { title: '找伙伴匹配', subtitle: '功能服务' } } @@ -207,6 +214,7 @@ export function DashboardPage() { } if (type === 'section') return { title: `章节 ${p.productId || ''}`, subtitle: '单章购买' } if (type === 'fullbook') return { title: '《一场Soul的创业实验》', subtitle: '全书购买' } + if (type === 'vip') return { title: '超级个体开通费用', subtitle: '超级个体' } if (type === 'match') return { title: '找伙伴匹配', subtitle: '功能服务' } return { title: '未知商品', subtitle: type || '其他' } } diff --git a/soul-admin/src/pages/distribution/DistributionPage.tsx b/soul-admin/src/pages/distribution/DistributionPage.tsx index 42ce5615..391eaeed 100644 --- a/soul-admin/src/pages/distribution/DistributionPage.tsx +++ b/soul-admin/src/pages/distribution/DistributionPage.tsx @@ -838,6 +838,14 @@ export function DistributionPage() {

{(() => { const type = order.productType || order.type + const desc = order.description || '' + const pid = String(order.productId || order.sectionId || '') + const isVip = type === 'vip' || desc.includes('VIP') || desc.toLowerCase().includes('vip') || pid.toLowerCase().includes('vip') + if (type === 'balance_recharge') { + const amount = typeof order.amount === 'number' ? order.amount.toFixed(2) : parseFloat(String(order.amount || '0')).toFixed(2) + return `余额充值 ¥${amount}` + } + if (isVip) return '超级个体开通费用' if (type === 'fullbook') return `${order.bookName || '《底层逻辑》'} - 全本` if (type === 'match') return '匹配次数购买' @@ -847,6 +855,11 @@ export function DistributionPage() {

{(() => { const type = order.productType || order.type + const desc = order.description || '' + const pid = String(order.productId || order.sectionId || '') + const isVip = type === 'vip' || desc.includes('VIP') || desc.toLowerCase().includes('vip') || pid.toLowerCase().includes('vip') + if (type === 'balance_recharge') return '余额充值' + if (isVip) return '超级个体' if (type === 'fullbook') return '全书解锁' if (type === 'match') return '功能权益' return order.chapterTitle || '单章购买' @@ -860,9 +873,11 @@ export function DistributionPage() { {order.paymentMethod === 'wechat' ? '微信支付' - : order.paymentMethod === 'alipay' - ? '支付宝' - : order.paymentMethod || '微信支付'} + : order.paymentMethod === 'balance' + ? '余额支付' + : order.paymentMethod === 'alipay' + ? '支付宝' + : order.paymentMethod || '微信支付'} {order.status === 'refunded' ? ( diff --git a/soul-admin/src/pages/orders/OrdersPage.tsx b/soul-admin/src/pages/orders/OrdersPage.tsx index b038c047..92b011ec 100644 --- a/soul-admin/src/pages/orders/OrdersPage.tsx +++ b/soul-admin/src/pages/orders/OrdersPage.tsx @@ -41,6 +41,9 @@ interface Purchase { productType?: string description?: string refundReason?: string + giftPayRequestId?: string + payerUserId?: string + payerNickname?: string } interface UsersItem { @@ -114,6 +117,10 @@ export function OrdersPage() { const formatProduct = (order: Purchase) => { const type = order.productType || order.type || '' const desc = order.description || '' + if (type === 'balance_recharge') { + const amount = Number(order.amount || 0).toFixed(2) + return { name: `余额充值 ¥${amount}`, type: '余额充值' } + } if (desc) { if (type === 'section' && desc.includes('章节')) { if (desc.includes('-')) { @@ -128,7 +135,7 @@ export function OrdersPage() { return { name: '《一场Soul的创业实验》', type: '全书购买' } } if (type === 'vip' || desc.includes('VIP')) { - return { name: 'VIP年度会员', type: 'VIP' } + return { name: '超级个体开通费用', type: '超级个体' } } if (type === 'match' || desc.includes('伙伴')) { return { name: '找伙伴匹配', type: '功能服务' } @@ -137,7 +144,7 @@ export function OrdersPage() { } if (type === 'section') return { name: `章节 ${order.productId || order.sectionId || ''}`, type: '单章' } if (type === 'fullbook') return { name: '《一场Soul的创业实验》', type: '全书' } - if (type === 'vip') return { name: 'VIP年度会员', type: 'VIP' } + if (type === 'vip') return { name: '超级个体开通费用', type: '超级个体' } if (type === 'match') return { name: '找伙伴匹配', type: '功能' } return { name: '未知商品', type: type || '其他' } } @@ -182,7 +189,7 @@ export function OrdersPage() { getUserPhone(p.userId), product.name, Number(p.amount || 0).toFixed(2), - p.paymentMethod === 'wechat' ? '微信支付' : p.paymentMethod === 'alipay' ? '支付宝' : p.paymentMethod || '微信支付', + p.paymentMethod === 'wechat' ? '微信支付' : p.paymentMethod === 'balance' ? '余额支付' : p.paymentMethod === 'alipay' ? '支付宝' : p.paymentMethod || '微信支付', p.status === 'refunded' ? '已退款' : p.status === 'paid' || p.status === 'completed' ? '已完成' : p.status === 'pending' || p.status === 'created' ? '待支付' : '已失败', p.status === 'refunded' && p.refundReason ? p.refundReason : '-', p.referrerEarnings ? Number(p.referrerEarnings).toFixed(2) : '-', @@ -305,8 +312,18 @@ export function OrdersPage() {

-

{getUserNickname(purchase)}

+

+ {getUserNickname(purchase)} + {purchase.payerUserId && ( + + 代付 + + )} +

{getUserPhone(purchase.userId)}

+ {purchase.payerUserId && purchase.payerNickname && ( +

代付人:{purchase.payerNickname}

+ )}
@@ -315,7 +332,7 @@ export function OrdersPage() { {product.name} {(purchase.productType || purchase.type) === 'vip' && ( - VIP + 超级个体 )}

@@ -328,9 +345,11 @@ export function OrdersPage() { {purchase.paymentMethod === 'wechat' ? '微信支付' - : purchase.paymentMethod === 'alipay' - ? '支付宝' - : purchase.paymentMethod || '微信支付'} + : purchase.paymentMethod === 'balance' + ? '余额支付' + : purchase.paymentMethod === 'alipay' + ? '支付宝' + : purchase.paymentMethod || '微信支付'} {purchase.status === 'refunded' ? ( @@ -363,7 +382,8 @@ export function OrdersPage() { {new Date(purchase.createdAt).toLocaleString('zh-CN')} - {(purchase.status === 'paid' || purchase.status === 'completed') && ( + {(purchase.status === 'paid' || purchase.status === 'completed') && + purchase.paymentMethod !== 'balance' && (