Compare commits
34 Commits
main
...
yongpxu-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a272c92d8 | ||
|
|
4a105204cc | ||
|
|
01f9e216da | ||
|
|
e6df2b78be | ||
|
|
c594bfb7e2 | ||
|
|
0cc8e2b270 | ||
|
|
99d65dc664 | ||
|
|
c84c53515e | ||
|
|
b1db1efeb8 | ||
|
|
2645212d47 | ||
|
|
b2b753d566 | ||
|
|
7399e392aa | ||
|
|
bfd7610527 | ||
|
|
c78a0d531f | ||
|
|
5a88e87654 | ||
|
|
8abd0280b7 | ||
|
|
78d39ed8a5 | ||
|
|
aec0c59b09 | ||
|
|
83a420f1b9 | ||
|
|
f08c82c441 | ||
|
|
73d56a225a | ||
|
|
51d38cff0f | ||
|
|
e905fac69d | ||
|
|
5b5e01e9b5 | ||
|
|
37be3034ec | ||
|
|
0dd6a1021d | ||
|
|
7cd8ffdd26 | ||
|
|
34d284c129 | ||
|
|
0ce86645b1 | ||
|
|
5010e2dfba | ||
|
|
35e18a7ccb | ||
|
|
e391115938 | ||
|
|
29c8ca96a2 | ||
|
|
3cc33c7d7a |
6
.cursor/agent/产品经理/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 产品经理 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档
|
||||
|
||||
- **要点**:以可验收功能点为最小同步单位;主需求与《以界面定需求》为验收真源;跨端规则变更须同时更新需求清单状态与优先级。
|
||||
- **待办**:核对 2026-04-02 以来实现与文档差异;vip-members/融合一期验收口径在需求文档中单点定稿。
|
||||
7
.cursor/agent/产品经理/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 产品经理 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:同步开发需求
|
||||
|
||||
- **需求清单**:在 `开发文档/1、需求/需求汇总.md` 维护编号行(SYNC / LOOP / MAINT 等),状态与责任角色与纪要待办一致。
|
||||
- **验收绑定**:功能完成 = 实现可合并 + **需求表或项目索引当日一行**;跨端规则须在需求或运营与变更中单点写清优先级。
|
||||
- **管理端必要性**:沿用产品 Skill — 纯文案/纯视觉可不建管理端;**配置/开关/审核/统计** 必须显式在需求表标「管理端入口」避免壳功能。
|
||||
@@ -6,3 +6,5 @@
|
||||
| 2026-03-05 | 文章详情@某人高亮与一键加好友验收标准与待确认 | [2026-03-05.md](./2026-03-05.md) |
|
||||
| 2026-03-10 | 管理端迁移 Mycontent-temp:主导航收敛与隐藏页面入口承载策略 | [2026-03-10.md](./2026-03-10.md) |
|
||||
| 2026-03-24 | 开发进度同步会议:文档同步原则、项目索引补齐 | [2026-03-24.md](./2026-03-24.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档会议:验收与需求真源、功能完成定义 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:需求汇总表维护、验收与管理端必要性 | [2026-04-14.md](./2026-04-14.md) |
|
||||
|
||||
6
.cursor/agent/后端工程师/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 后端工程师 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档
|
||||
|
||||
- **要点**:接口变更须写明路由组(miniprogram/admin/db)、路径、字段与错误码;表结构/迁移在部署文档或临时需求池留痕;历史待办按 P0/P1 标注与依赖方。
|
||||
- **待办**:router/ossConfig、vip-members 等待办在文档中优先级化。
|
||||
6
.cursor/agent/后端工程师/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 后端工程师 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:接口与需求文档同步
|
||||
|
||||
- 新增/变更接口须在需求或运营摘要中写明 **路由组(miniprogram/admin/db)**、路径、关键字段与错误码;迁移写在 `开发文档/8、部署/` 或临时需求池(若存在)。
|
||||
- **P0/P1 待办**(router、ossConfig、vip-members 等)在 `需求汇总.md` 已单列 BACKEND-P0 行,合并前更新状态与依赖方。
|
||||
13
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝工作台资源对接接口测试.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 存客宝工作台「join·资源对接」接口测试误报失败
|
||||
|
||||
## 现象
|
||||
|
||||
管理端 `CKBConfigPanel` 接口测试对 `POST /api/ckb/join` 传 `type=investor`、`userId=admin_test`。`CKBJoin` 对资源对接在非空 `userId` 时校验 `userHasContentPurchase`,`admin_test` 非真实用户且无订单 → 返回 `success:false`、`CONTENT_PURCHASE_REQUIRED`,与其它 join 子项不一致。
|
||||
|
||||
## 处理
|
||||
|
||||
在 `internal/handler/ckb.go` 中:**当 `userId` 去空格后为 `admin_test` 时跳过购课校验**(该串仅工作台连通性测试使用)。真实用户 ID 仍为 UUID,不受影响。
|
||||
|
||||
## 注意
|
||||
|
||||
勿将真实用户 ID 设为 `admin_test`;若后续收紧,可改为管理端专用 Header 或仅 `/api/db` 代理测试接口。
|
||||
37
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝计划Key列表与重推一致.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 存客宝:计划 Key 列表展示与重推实发 apiKey 一致
|
||||
|
||||
## 问题现象
|
||||
|
||||
- 管理端「推广中心 / 获客列表」**计划Key** 列显示为某条计划(如 `2y4v5-…`)。
|
||||
- 点 **重推** 时,实际请求 `ckbapi…/v1/api/scenarios` 的 `apiKey` 却是全局 `getCkbLeadApiKey()`(如 `IPQ9s-…`),存客宝返回 `无效的apiKey` / 401。
|
||||
|
||||
## 根因
|
||||
|
||||
1. **两套解析链**:`GET /api/db/ckb-leads?mode=contact` 对 `planApiKey` 做多层兜底(`plan_api_key` → action 默认 → `target_person_id`/`persons.ckb_api_key` → index_link 全局人物);旧 **`retryOneLeadRecord`** 在 `plan_api_key` 为空时**直接** `getCkbLeadApiKey()`,且曾用 `params.targetUserId` 覆盖却**未读表字段 `target_person_id`**,甚至曾在非空快照上再被人物 key 覆盖。
|
||||
2. **展示与写请求分叉**:运营按界面计划 Key 排障,与抓包不一致,误判为「存客宝挂了」。
|
||||
|
||||
## 修复要点(已实现)
|
||||
|
||||
- 抽 **`resolveLeadPlanAPIKeyForRetry`**(命名保留历史;语义为「本条线索应采信的计划 apiKey」),顺序与列表原注释一致。
|
||||
- **`retryOneLeadRecord`** 仅通过该函数取 key;`target_person_id` 与 `params.targetUserId` 并用。
|
||||
- **`DBCKBLeadList` mode=contact** 循环内 **`planApiKey` 改为调用同一函数**,与重推、定时重试(同走 `retryOneLeadRecord`)长期同源,避免再分叉。
|
||||
- 调试用的 **`DBCKBLeadRetry` 终端打印、`pushLeadToCKB` URL 打印** 已按上线要求移除。
|
||||
|
||||
## 排查同类问题时的检查清单
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 是否存在「列表/详情一套、再请求另一套」 | 搜索同一业务字段在两处分别拼接 |
|
||||
| `plan_api_key` 空时行为 | 是否与人物、`action/source` 一致 |
|
||||
| 人物标识 | 表字段 `target_person_id` 与 JSON `targetUserId` 是否同时考虑 |
|
||||
| 重推与首次提交 | 重推当前为 **GET scenarios 简化参数**;若与首次 **POST JSON** 不一致,可能仍有个案失败(产品另行评估) |
|
||||
|
||||
## 关联代码
|
||||
|
||||
- `soul-api/internal/handler/ckb.go`:`resolveLeadPlanAPIKeyForRetry`、`retryOneLeadRecord`
|
||||
- `soul-api/internal/handler/db_ckb_leads.go`:`DBCKBLeadList` mode=contact 中 `planApiKey`
|
||||
|
||||
## 关联文档 / Skill
|
||||
|
||||
- `.cursor/skills/ckb-leads-api-test/SKILL.md`(Gotchas:planKey 与重推一致性)
|
||||
- `.cursor/skills/api-dev/SKILL.md`(Gotchas:展示与下游请求字段同源)
|
||||
@@ -10,3 +10,7 @@
|
||||
| 2026-03-16 | ParseAutoLinkContent data-label;存客宝 create planType/sceneId/status | [2026-03-16.md](./2026-03-16.md) |
|
||||
| 2026-03-17 | 代付 PayNotify beneficiaryUserID 权益归发起人;gift-pay detail 返回 initiatorUserId | [2026-03-17.md](./2026-03-17.md) |
|
||||
| 2026-03-24 | router 缺失四 handler:BookRanking、DBPersonPinnedToken、CKBPinnedPerson、AdminDashboardLeads | [2026-03-24.md](./2026-03-24.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:接口/迁移文档化与 P0/P1 标注 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:接口与需求表同步、BACKEND-P0 行 | [2026-04-14.md](./2026-04-14.md) |
|
||||
| 2026-05-08 | 存客宝获客列表计划Key与重推 apiKey:`resolveLeadPlanAPIKeyForRetry` 与列表同源,避免展示与抓包分叉 | [2026-05-08-存客宝计划Key列表与重推一致.md](./2026-05-08-存客宝计划Key列表与重推一致.md) |
|
||||
| 2026-05-08 | 存客宝工作台 join·资源对接测试:`userId=admin_test` 跳过购课校验,避免 CONTENT_PURCHASE_REQUIRED 误报 | [2026-05-08-存客宝工作台资源对接接口测试.md](./2026-05-08-存客宝工作台资源对接接口测试.md) |
|
||||
|
||||
6
.cursor/agent/团队/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 团队 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档(跨角色)
|
||||
|
||||
- **决议**:功能完成定义包含 **代码可合并状态 + 当日文档/项目索引一行摘要(含日期)**;跨端字段或接口变更除各角色文档外,须在团队 evolution 或会议纪要留契约摘要。
|
||||
- **关联**:详见 `agent/产品经理|后端工程师|管理端开发工程师|小程序开发工程师|软件测试/evolution/2026-04-13.md` 与 `.cursor/meeting/2026-04-13_按功能同步开发文档.md`。
|
||||
19
.cursor/agent/团队/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 团队 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:小程序优先闭环 + 需求同步
|
||||
|
||||
### 场景
|
||||
|
||||
评审「功能是否闭环」、区分界面写死与配置化、识别仅有样式无管理端入口、评估对其它模块的影响。
|
||||
|
||||
### 结论(团队约定)
|
||||
|
||||
1. **逆推顺序**:从小程序用户路径出发 → 数据来源(常量 / 本地 / `app.request`)→ `/api/miniprogram/*` 是否存在与错误语义 → 是否需要 **管理端或远程配置**(运营是否会改、是否与钱/风控相关)→ 是否影响支付/登录/分享/其它页 `onShow`。
|
||||
2. **写死 vs 配置化**:按 **变更频率、责任主体(运营/研发)、风险** 决策;需运行时开关/灰度时采用 **Feature Toggle** 思路(见业界 Martin Fowler *Feature Toggles*),避免永久堆砌 `if(flag)` 无退役计划。
|
||||
3. **壳功能信号**:有入口但无 API、仅有样式、管理端无对应配置且业务上明显需运营改的,标为 **技术债或假完成**,需求表状态不得标「已完成」。
|
||||
4. **跨模块同步**:契约线(字段/枚举/缓存)、流程线(支付/分销/scene)、权限配置线;变更须在 **需求汇总 / 运营与变更 / 纪要** 之一可追溯,并双写本文件或 `2026-04-13` 纪要中的契约摘要。
|
||||
5. **仓库需求真源**:本仓库已建 `开发文档/1、需求/需求汇总.md` 与 `开发文档/10、项目管理/运营与变更.md`,与 `.cursor/meeting/`、各角色 evolution 交叉引用。
|
||||
|
||||
### 同时影响
|
||||
|
||||
小程序、管理端、后端、测试、产品(文档与验收)。
|
||||
50
.cursor/agent/团队/evolution/2026-04-15.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 团队 经验记录 - 2026-04-15
|
||||
|
||||
## 联网吸收:优秀编程思维 → 智能体执行约束
|
||||
|
||||
> 以下为公开资料的**提炼与落地映射**,便于 Soul 多智能体(Cursor 角色)统一行为;**非**替代项目 boundary / Skill 全文。
|
||||
|
||||
### 1. 垂直切片(Vertical slicing)
|
||||
|
||||
- **含义**:按「用户可感知的一小条价值」贯穿 **展示 → 接口 → 数据/配置**,而不是长期只做「全后端再全前端」的水平分层半成品。
|
||||
- **对智能体**:拆任务时优先 **end-to-end 最小可演示**(例如:一条只读列表从真实 API 出数),再迭代字段与交互;避免单次会话产出大量未接线 UI。
|
||||
|
||||
**参考**:敏捷社区对 vertical slicing 的讨论(如 *The Art of the Vertical Slice* 等文章,关键词 *vertical slice user story*)。
|
||||
|
||||
### 2. 追踪弹(Tracer bullets)
|
||||
|
||||
- **出处思想**:《The Pragmatic Programmer》——用一发「能穿过各层的薄路径」验证架构是否通畅。
|
||||
- **与原型区别**:原型可丢;追踪弹是**进主分支的骨架**,后续在其上增厚。
|
||||
- **对智能体**:强制 **小步、可编译、可运行、可测** 的增量;先打通再美化,降低大模型「一次写太多未验证代码」的风险。延伸阅读:*Tracer Bullets: Keeping AI Slop Under Control*(`https://www.aihero.dev/tracer-bullets`)。
|
||||
|
||||
### 3. AI 编码与多智能体:验证优先
|
||||
|
||||
- **上下文优先**:先给目的、约束、边界、失败模式,再让生成代码(多篇 2024–2025 *AI coding best practices* 共性)。
|
||||
- **分块生成 + 人审合并**:大纲 → 分文件小改 → 跑测试/静态检查;不把未读 diff 直接合并。
|
||||
- **评估维度(借鉴 Agent 质量讨论)**:任务是否完成、工具/接口调用是否准确、是否引入多余依赖(参见 AWS 等「Agent 质量评估」类文章中的 TCR、工具准确率等提法)。
|
||||
|
||||
### 4. 工程文化:无责复盘与简单性
|
||||
|
||||
- **Blameless postmortem**:复盘关注**系统与流程**如何允许失误、行动项是否可验证,避免归罪个人;利于真实写入经验库(Google SRE Book — Postmortem Culture:`https://sre.google/sre-book/postmortem-culture/`)。
|
||||
- **简单性**:偏好无聊但清晰的实现;与 Feature Toggle 一样,**实验性分支要有退役计划**。
|
||||
|
||||
### 5. 映射到本仓库(Soul)
|
||||
|
||||
| 外部思维 | 本仓库落地 |
|
||||
|----------|------------|
|
||||
| 垂直切片 / 追踪弹 | 跨端任务走 `role-flow-control`:先契约与最小联调,再扩功能;纪要/需求表可追溯 |
|
||||
| 验证优先 | `change-checklist`、测试 Skill、`app.request` 与三端路由自检 |
|
||||
| 无责复盘 | `.cursor/meeting/*` 问题与作答区 + 各角色 `evolution` 按日沉淀 |
|
||||
| 简单性 | 禁止跨端乱调路由;避免无退役计划的永久 feature flag |
|
||||
|
||||
---
|
||||
|
||||
## 业务补齐(同日追加)
|
||||
|
||||
- **`app.request`**:`success:false` 与部分非 200 响应在 `reject` 的 `Error` 上挂载 **`response`**,与 `miniprogram-dev` Skill 及 `referral/my` 提现分支一致。
|
||||
- **导师咨询支付**:`productType=mentor_consultation`,`getStandardPrice` 校验预约单归属与金额;`activateOrderBenefits` + `PayNotify` 将 `mentor_consultations.status` 置 `paid`;**分销佣金**对导师单显式跳过(待产品另定规则)。
|
||||
- **资源对接 `investor`**:`CKBJoin` 未购章节时 **`success:false` + `errorCode: CONTENT_PURCHASE_REQUIRED`**,小程序 `match.js` 用 `e.response` 展示文案。
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-15
|
||||
@@ -11,3 +11,6 @@
|
||||
| 2026-03-16 | TipTap Mention 需 data-label,否则显示 token | [2026-03-16.md](./2026-03-16.md) |
|
||||
| 2026-03-17 | 代付美团式流程与权益归属约定:读页→代付页→分享;权益/分佣归发起人 | [2026-03-17.md](./2026-03-17.md) |
|
||||
| 2026-03-24 | 文档同步原则:实现变更后同步需求/运营与变更/项目索引 | [2026-03-24.md](./2026-03-24.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:功能完成=实现+索引;跨端契约双写 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 小程序优先闭环审计、写死vs配置化、需求表骨架与 role-flow §1.2 | [2026-04-14.md](./2026-04-14.md) |
|
||||
| 2026-04-15 | 联网:垂直切片、追踪弹、AI验证优先、无责复盘;role-flow §1.3 | [2026-04-15.md](./2026-04-15.md) |
|
||||
|
||||
6
.cursor/agent/小程序开发工程师/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 小程序开发工程师 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档
|
||||
|
||||
- **要点**:页面级记录 `/api/miniprogram/*`、scene、支付/登录、分享 singlePage 等行为变更;纯 UI 无接口变更时也注明,便于测试裁剪回归。
|
||||
- **待办**:主路径未记入文档的改动补索引一行。
|
||||
6
.cursor/agent/小程序开发工程师/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 小程序开发工程师 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:闭环与硬编码自检
|
||||
|
||||
- 新功能自检:**页面 → 数据从哪来**;若仅为写死展示,在索引注明「无接口/无配置」以免误判完成度。
|
||||
- 涉及 **scene、支付、登录、分享 singlePage** 的改动必须在 `需求汇总` 或项目索引可追溯,并评估对 **我的/阅读/代付** 等邻页的副作用。
|
||||
@@ -15,3 +15,5 @@
|
||||
| 2026-03-17 | 代付美团式:读页→代付页→分享;详情页双态(发起人/好友);目录 loading、最新新增 5 条折叠 | [2026-03-17.md](./2026-03-17.md) |
|
||||
| 2026-03-30 | 阅读页 `navigateToMiniProgram` 时 path 追加 `phone`(已登录且已绑定) | [2026-03-30.md](./2026-03-30.md) |
|
||||
| 2026-04-11 | 阅读付费墙三态框体统一、`shareTipLine` 与文末分享区一致、需求文档闭环 | [2026-04-11-阅读付费墙三态与营销句闭环.md](./2026-04-11-阅读付费墙三态与营销句闭环.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:页面级接口与分享行为、无接口变更亦标注 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:闭环与硬编码自检、邻页副作用 | [2026-04-14.md](./2026-04-14.md) |
|
||||
|
||||
6
.cursor/agent/开发助理/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 开发助理(橙子)经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:各智能体同步开发需求
|
||||
|
||||
- 触发:用户「各智能体吸收经验沉淀,同步开发需求」→ 执行 `assistant-doc-sync` §5 + 文档同步:补齐仓库根 `开发文档/` 骨架,写入 `需求汇总.md`、`运营与变更.md`。
|
||||
- 同步 `经验清单.md`、`智能体-技能-经验总索引.md`、各角色 `evolution/索引.md`、项目索引一行、`sync-log`;轻量升级 `role-flow-control` §1.2(小程序逆推闭环)。
|
||||
7
.cursor/agent/开发助理/evolution/2026-04-15.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 开发助理 经验记录 - 2026-04-15
|
||||
|
||||
## 联网吸收:编程思维写入智能体
|
||||
|
||||
- **触发**:用户要求联网吸收优秀编程思维并沉淀到智能体。
|
||||
- **动作**:检索垂直切片、追踪弹、AI 编码验证、SRE 无责复盘等公开论述;写入 `agent/团队/evolution/2026-04-15.md`;在 `role-flow-control/SKILL.md` 增加 **§1.3 智能体协作提要**;更新 `经验清单`、`智能体-技能-经验总索引`、团队/助理橙子项目索引、`sync-log`。
|
||||
- **注意**:外链为思想参考;执行顺序仍以本仓库 `.cursor/rules` 与各主 Skill 为准。
|
||||
@@ -6,3 +6,5 @@
|
||||
|------|------|------|
|
||||
| 2026-03-21 | MBTI 头像小程序全链路兜底 + 匹配接口回填 | 2026-03-21-MBTI头像C端全链路兜底.md |
|
||||
| 2026-03-16 | 用户交互习惯分析(基于 agent-transcripts) | 2026-03-16-交互习惯分析.md |
|
||||
| 2026-04-14 | 各智能体吸收经验沉淀:开发文档骨架、经验清单、role-flow §1.2 | 2026-04-14.md |
|
||||
| 2026-04-15 | 联网吸收编程思维:团队 evolution、role-flow §1.3、索引与 sync-log | 2026-04-15.md |
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
| 2026-03-17 | 后端、团队 | 架构/最佳实践 | api-dev SKILL | Redis 缓存:parts/hot/recommended/stats/config/章节 content;容灾回退 DB;OSS 上传;/health 返回 database/redis 状态 |
|
||||
| 2026-03-18 | 小程序、团队 | 业务规则/最佳实践 | - | 分享链路兼容好友/朋友圈 singlePage:单页模式能力降级(不支付/不自动领取),引导点击底部“前往小程序”进入完整版 |
|
||||
| 2026-03-18 | 产品、后端、管理端、测试 | 文档归档/需求口径 | - | 文档归档整理:以《以界面定需求》为基准,各角色重整“功能需求+验收口径+风险点”并写入各自经验库;补齐《项目落地推进表》 |
|
||||
| 2026-04-14 | 团队、产品、后端、管理端、小程序、测试、助理橙子 | 方法论/需求同步 | role-flow-control SKILL §1.2、assistant-doc-sync 文档树说明 | 吸收沉淀:小程序逆推闭环、写死vs配置、需求汇总+运营与变更骨架;各角色 evolution/2026-04-14.md |
|
||||
| 2026-04-15 | 团队、助理橙子 | 方法论(联网) | role-flow-control SKILL §1.3 | 垂直切片、追踪弹与 AI slop 控制、验证优先、Google SRE 无责复盘;详 `agent/团队/evolution/2026-04-15.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -63,4 +65,4 @@
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-04-15
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 项目总结
|
||||
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/2026-03-20-需求.md`(主需求清单),闭环结论见 `开发文档/10、项目管理/闭环与文档同步-2026-04-07.md`,临时需求/分析在 `临时需求池/`。
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/2026-03-20-需求.md`(主需求清单,若未入库见同目录 `需求汇总.md`),闭环结论见 `开发文档/10、项目管理/闭环与文档同步-2026-04-07.md`(若存在),临时需求/分析在 `临时需求池/`(按需创建)。
|
||||
|
||||
---
|
||||
|
||||
@@ -31,9 +31,11 @@ Soul 创业派对产品定位:面向创业者的社区/工具型小程序。
|
||||
| 2026-03-31 | 会议:超级个体列表与 @ 列表融合方案(先数据互链再单页);一期验收口径待拍板 | 待续 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;一期融合验收口径澄清、各端索引与需求 delta 跟进 | 进行中 |
|
||||
| 2026-04-07 | 开发文档与主需求交叉同步;闭环分析落盘(主路径成立、规则/风控与纪要问题区待闭环) | 已完成 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;功能完成=实现+文档/索引可追溯;待办见纪要问题区 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:新建 `开发文档/1、需求/需求汇总.md` 与 `开发文档/10、项目管理/运营与变更.md`;需求清单 SYNC/LOOP/MAINT/BACKEND 行 | 进行中 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-07
|
||||
**最后更新**:2026-04-14
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
| 2026-03-17 | 会议收尾:源码优化完成与测试流程定稿;纪要、经验入库、开发文档同步 | 已完成 |
|
||||
| 2026-04-02 | 会议收尾:工作进度与需求同步会;纪要、各角色 evolution、项目索引与会议索引更新 | 已完成 |
|
||||
| 2026-04-07 | 同步开发文档与主需求;新增《闭环与文档同步-2026-04-07》并更新索引、sync-log | 已完成 |
|
||||
| 2026-04-14 | 吸收经验:各角色 evolution/2026-04-14、开发文档骨架、经验清单、总索引、role-flow §1.2 | 已完成 |
|
||||
| 2026-04-15 | 联网吸收编程思维:团队 evolution/2026-04-15、role-flow §1.3、经验清单与 sync-log | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-07(文档同步与闭环分析)
|
||||
**最后更新**:2026-04-15
|
||||
|
||||
@@ -43,9 +43,12 @@ soul-api(Go + Gin + GORM + MySQL)提供三组路由:`/api/miniprogram/*`
|
||||
| 2026-03-24 | 会议:三端闭环评审;提现/CKB/匹配后端校验与 errorCode 为 P0/P1 | 已完成 |
|
||||
| 2026-03-31 | 会议:超级个体与 @ 列表融合;优先扩展 vip-members 聚合 Person 关联字段 | 待续 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;deploy 验证后提交;vip-members 聚合优先;router/ossConfig 历史待办跟进 | 进行中 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;接口/迁移变更须标路由组与 P0/P1;文档待补齐 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:`需求汇总.md` 增加 BACKEND-P0 行;接口说明与迁移落文档 | 进行中 |
|
||||
| 2026-04-15 | 业务补齐:`mentor_consultation` 支付计价/回调/权益;`CKBJoin` investor 显式失败;`getStandardPrice` 增加 buyerUserID | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-02
|
||||
**最后更新**:2026-04-15
|
||||
|
||||
@@ -34,9 +34,12 @@ Soul 创业派对全项目架构与约定:路由隔离(miniprogram/admin/db
|
||||
| 2026-03-31 | 会议:超级个体与 @ 列表融合;先数据与入口融合、路由边界不变 | 已完成 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;融合落地顺序与 deploy 验证门禁、问题与作答区待闭环 | 已完成 |
|
||||
| 2026-04-07 | 文档同步:需求与开发文档索引对齐;闭环结论:主路径成立、规则/风控待加固项见《闭环与文档同步-2026-04-07》 | 已完成 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;功能完成=实现+索引一行;跨端契约双写团队/纪要 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:小程序逆推闭环方法论;`role-flow-control` §1.2;开发文档需求真源 | 进行中 |
|
||||
| 2026-04-15 | 联网:垂直切片/追踪弹/验证优先/无责复盘 → `role-flow-control` §1.3、`团队/evolution/2026-04-15.md` | 已完成 |
|
||||
|
||||
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-07
|
||||
**最后更新**:2026-04-15
|
||||
|
||||
@@ -49,9 +49,13 @@
|
||||
| 2026-03-30 | 会议:文章详情 # 跳转关联小程序(如 MBTI)时 path 追加 `phone`;read.js 已实现 | 已完成 |
|
||||
| 2026-03-31 | 会议:超级个体与 @ 列表融合;C 端默认不改接口,仅回归 | 已完成 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;全局 app/request 类改动需主路径冒烟;融合仍以回归为主 | 已完成 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;页面级记 miniprogram 接口与分享行为;无接口变更亦标注 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:逆推闭环自检写入 evolution;LOOP 需求行 | 进行中 |
|
||||
| 2026-04-15 | 业务补齐:`app.js` 挂 `err.response`;导师详情微信支付;找伙伴 join 失败展示后端文案 | 已完成 |
|
||||
| 2026-05-09 | 我的页快捷入口:渐变面性 SVG 图标、`stat-icon-wrap` 圆形底托与卡片光效;开发文档运营与变更 / 当前小程序开发细则 §2.2.1 已同步 | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-02
|
||||
**最后更新**:2026-05-09
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
| 2026-03-24 | 会议:三端闭环评审;API 绕过负例与双入口提现回归待契约落地后执行 | 已完成 |
|
||||
| 2026-03-31 | 会议:超级个体与 @ 列表融合;回归清单含排序/@/绑定/Webhook 边界 | 待续 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;融合清单待联调后执行;deploy PR 增加 compose/health/smoke | 待续 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;以文档与索引为真源更新回归;契约未入文档可阻塞 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:LOOP 需求与逆推清单纳入回归裁剪依据 | 进行中 |
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-02
|
||||
**最后更新**:2026-04-14
|
||||
|
||||
@@ -48,11 +48,13 @@
|
||||
| 2026-03-24 | 会议:三端闭环评审;管理端与小程序配套整体齐全;类型债择机修 | 已完成 |
|
||||
| 2026-03-31 | 会议:超级个体与 @ 列表融合;UsersPage 表格加 @ 状态与跳转内容页 | 待续 |
|
||||
| 2026-04-02 | 会议:工作进度与需求同步;UsersPage 待接口字段就绪后联调;deploy 文档基址与后端对齐 | 进行中 |
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;页面粒度记 admin/db 依赖;类型债排期入文档 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:MAINT-ADMIN-TS 入需求表;配置入口与运营与变更联动 | 进行中 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-04-02
|
||||
**最后更新**:2026-04-14
|
||||
|
||||
> 注:soul-admin 构建仍有 DistributionPage Order.description 类型错误(与本次迁移无关),待修复。
|
||||
|
||||
@@ -7,25 +7,40 @@
|
||||
## 1. 经验索引(按角色)
|
||||
|
||||
- 助理橙子:`agent/开发助理/evolution/索引.md`
|
||||
- 2026-04-15:联网编程思维沉淀 + role-flow §1.3
|
||||
- 2026-04-14:各智能体吸收沉淀 + 开发文档骨架与 role-flow §1.2
|
||||
- 2026-03-21:MBTI 头像小程序全链路兜底 + 匹配接口回填
|
||||
- 2026-03-16:用户交互习惯分析(基于 agent-transcripts)
|
||||
- 小程序开发工程师:`agent/小程序开发工程师/evolution/索引.md`
|
||||
- 2026-04-14:吸收沉淀(闭环自检、邻页副作用)
|
||||
- 2026-04-13:按功能同步开发文档(页面级接口与分享、无接口变更标注)
|
||||
- 2026-03-30:阅读页 `navigateToMiniProgram` 时 path 追加 `phone`
|
||||
- 2026-03-17:代付美团式流程与目录 loading/折叠
|
||||
- 2026-03-16:编辑资料页分享名片(Canvas 封面)
|
||||
- 管理端开发工程师:`agent/管理端开发工程师/evolution/索引.md`
|
||||
- 2026-04-14:吸收沉淀(配置入口、MAINT-ADMIN-TS)
|
||||
- 2026-04-13:按功能同步开发文档(页面粒度 admin/db、字段对齐)
|
||||
- 2026-03-16:链接人与事(复制/删除 Dialog、planId/apiKey 列)
|
||||
- 2026-03-10:迁移 Mycontent-temp(主导航收敛/Settings 承载)
|
||||
- 2026-03-05:文章详情@某人(编辑页插入 @ 用户)
|
||||
- 后端工程师:`agent/后端工程师/evolution/索引.md`
|
||||
- 2026-05-08:存客宝计划 Key 列表与重推 apiKey 同源(resolveLeadPlanAPIKeyForRetry)
|
||||
- 2026-04-14:吸收沉淀(接口与需求表、BACKEND-P0)
|
||||
- 2026-04-13:按功能同步开发文档(接口/迁移文档化、P0/P1)
|
||||
- 2026-03-24:router 缺失四 handler 补齐(BookRanking 等)
|
||||
- 2026-03-17:PayNotify 权益归属发起人
|
||||
- 2026-03-14:内容排名算法修正(rank 分公式)
|
||||
- 产品经理:`agent/产品经理/evolution/索引.md`
|
||||
- 2026-04-14:吸收沉淀(需求汇总表、管理端必要性)
|
||||
- 2026-04-13:按功能同步开发文档会议(功能完成定义/验收真源)
|
||||
- 2026-03-24:开发进度同步会议(文档同步原则/项目索引补齐)
|
||||
- 2026-03-10:需求同步(管理端迁移入口策略)
|
||||
- 2026-03-05:文章详情@某人(验收标准与待确认)
|
||||
- 软件测试:`agent/软件测试/evolution/索引.md`
|
||||
- 2026-05-07:存客宝工作台「类型」join/match/lead 中文映射与开发文档入库
|
||||
- 2026-05-06:JWT 驱动 ckb-leads submitted 契约测试 Skill(脚本 + 工作台字段验收)
|
||||
- 2026-04-14:吸收沉淀(LOOP 逆推清单回归)
|
||||
- 2026-04-13:按功能同步开发文档(文档为真源、契约缺失可阻塞)
|
||||
- 2026-03-16:scripts/test/process 流程测试目录
|
||||
- 2026-03-05:文章详情@某人(用例、联调与回归点)
|
||||
- 2026-03-10:管理端迁移回归(鉴权/跳转可达性)
|
||||
@@ -33,6 +48,9 @@
|
||||
- 2026-03-20:挖矿病毒排查经验转化为 Skills
|
||||
- 2026-03-20:部署 API / 部署触发词(环境与脚本映射)
|
||||
- 团队(跨角色共享):`agent/团队/evolution/索引.md`
|
||||
- 2026-04-15:联网编程思维(垂直切片、追踪弹、验证优先、无责复盘)
|
||||
- 2026-04-14:小程序优先闭环、写死vs配置化、需求真源路径
|
||||
- 2026-04-13:按功能同步开发文档(实现+索引、跨端契约双写)
|
||||
- 2026-03-16:TipTap Mention 需 data-label
|
||||
- 2026-03-17:代付美团式流程与权益归属约定
|
||||
- 2026-03-24:文档同步原则(实现变更后同步需求/运营/索引)
|
||||
@@ -52,13 +70,15 @@
|
||||
- 触发:需求分析、需求文档、验收、加个需求
|
||||
- 测试(功能/回归/三端联调):`skills/testing/SKILL.md`
|
||||
- 触发:测试、测试用例、回归测试、QA
|
||||
- 存客宝获客列表 submitted + JWT 回归:`skills/ckb-leads-api-test/SKILL.md`
|
||||
- 触发:JWT 测试存客宝、ckb-leads submitted、获客列表字段验收、发 jwt 给你测、「加入/匹配提交」API、工作台 phone/wechatId
|
||||
- 助理橙子(文档同步/经验入库/会议结束/日志):`skills/assistant-doc-sync/SKILL.md`
|
||||
- 触发:小橙/橙子/橙橙/🍊、讨论完毕、吸收经验、保存进度、会议结束、出日志
|
||||
- 团队会议(多角色发言、决议、纪要):`skills/team-meeting/SKILL.md`
|
||||
- 触发:开个会、团队会议、需求评审、方案讨论
|
||||
- 变更关联检查(防漏改,改完必过):`skills/change-checklist/SKILL.md`
|
||||
- 触发:变更完成、检查一下、准备提交
|
||||
- 跨端协同流程(职责分工/联想补齐):`skills/role-flow-control/SKILL.md`
|
||||
- 跨端协同流程(职责分工/联想补齐/小程序逆推闭环/智能体协作提要):`skills/role-flow-control/SKILL.md`
|
||||
- 触发:跨端功能开发、三端联动优化
|
||||
- 三端架构分析/迁移拆解约束:`skills/three-tier-arch/SKILL.md`
|
||||
- 触发:拆解、迁移、框架选择、三端架构
|
||||
@@ -76,6 +96,8 @@
|
||||
- 触发:部署、服务器操作、SSH、devloy、master
|
||||
- 卡若创业派对运营(报表/切片/分发/飞书视频文字下载):`skills/karuo-party/SKILL.md`
|
||||
- 触发:运营报表、视频切片、多平台分发、飞书视频下载、派对运营
|
||||
- 飞书/Lark 群自定义机器人 Webhook(与 Soul 订单·获客推送契约一致,可迁移):`skills/feishu-webhook/SKILL.md`
|
||||
- 触发:飞书 webhook、自定义机器人、群通知、Lark bot、从 Soul 迁移通知
|
||||
|
||||
---
|
||||
|
||||
|
||||
6
.cursor/agent/管理端开发工程师/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 管理端开发工程师 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档
|
||||
|
||||
- **要点**:以页面/菜单为粒度同步至运营与变更或项目索引;标注所用 `/api/admin/*`、`/api/db/*`;与小程序共用字段写明来源。
|
||||
- **待办**:DistributionPage 类型债是否纳入排期,在文档或索引中闭环。
|
||||
6
.cursor/agent/管理端开发工程师/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 管理端开发工程师 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:配置入口与需求表
|
||||
|
||||
- 以 **页面/菜单** 为粒度在 `运营与变更.md` 或项目索引记录所用 `/api/admin/*`、`/api/db/*`;与小程序对齐的字段写明来源。
|
||||
- **MAINT-ADMIN-TS**:`DistributionPage` 等类型债已进 `需求汇总.md`,排期修复后更新状态为已完成。
|
||||
@@ -8,3 +8,5 @@
|
||||
| 2026-03-05 | 文章详情@某人:编辑页插入 @用户、保存约定 content 格式 | [2026-03-05.md](./2026-03-05.md) |
|
||||
| 2026-03-10 | 管理端迁移 Mycontent-temp 菜单/布局:主导航收敛、Settings Tab 承载 author/admin | [2026-03-10.md](./2026-03-10.md) |
|
||||
| 2026-03-16 | 链接人与事:table 布局、planId/apiKey 列、复制图标、删除 Dialog 弹窗 | [2026-03-16.md](./2026-03-16.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:页面粒度 admin/db 与字段对齐说明 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:配置入口与 MAINT-ADMIN-TS 需求行 | [2026-04-14.md](./2026-04-14.md) |
|
||||
|
||||
6
.cursor/agent/软件测试/evolution/2026-04-13.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 软件测试 经验记录 - 2026-04-13
|
||||
|
||||
## 会议:按功能同步开发文档
|
||||
|
||||
- **要点**:以更新后的开发文档与各角色项目索引为真源维护回归与联调清单;契约未入文档可标阻塞。
|
||||
- **待办**:依据各端索引更新融合与主路径用例;deploy/health/smoke 与项目管理文档交叉引用。
|
||||
6
.cursor/agent/软件测试/evolution/2026-04-14.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 软件测试 经验记录 - 2026-04-14
|
||||
|
||||
## 吸收沉淀:以文档为真源的回归
|
||||
|
||||
- **LOOP-20260414**:按「小程序优先逆推」清单编写/裁剪用例;文档与索引未更新的契约项可标 **阻塞—待文档** 并指向纪要问题区。
|
||||
- 主路径冒烟与 deploy/health/smoke 要求见 `运营与变更.md` 与历史纪要交叉引用。
|
||||
@@ -10,3 +10,7 @@
|
||||
| 2026-03-16 | scripts/test/process 流程测试:跨端多接口串联 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) |
|
||||
| 2026-03-16 | pytest 架构、配置从项目读取、运行前显示测试环境 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) |
|
||||
| 2026-03-16 | 测试用例归档规则:混合→process、纯端→miniapp/web;需求变更时更新 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:以文档与索引为真源、契约缺失可阻塞 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:LOOP 需求行与逆推清单回归 | [2026-04-14.md](./2026-04-14.md) |
|
||||
| 2026-05-08 | 获客列表计划Key与重推 apiKey 同源后可验收:同 id 下列展示与推送 key 一致 | [后端 2026-05-08](../../后端工程师/evolution/2026-05-08-存客宝计划Key列表与重推一致.md) |
|
||||
| 2026-05-07 | 存客宝工作台线索类型:join=加入、match=匹配、lead=留资;开发文档与 UI 中文映射 | [存客宝-工作台线索类型](../../../开发文档/6、后端/存客宝-工作台线索类型与中英文说明.md) |
|
||||
|
||||
108
.cursor/meeting/2026-04-13_按功能同步开发文档.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 会议纪要 - 2026-04-13 | 按功能同步开发文档
|
||||
|
||||
> 本文件由**助理橙子**在会议结束后自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **时间**:2026-04-13 10:00
|
||||
- **议题**:各角色依据本人负责的开发功能,同步开发文档(需求汇总、运营与变更、项目索引、临时需求池与接口契约说明)
|
||||
- **触发方式**:开个会 / 团队会议(文档同步专项)
|
||||
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员、乘风(主持)、助理橙子(书记)
|
||||
|
||||
---
|
||||
|
||||
## 各角色发言
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
需求侧以 `开发文档/1、需求/` 主需求与《以界面定需求》为验收基准。各端每完成一块可交付功能,须在 **需求清单/验收口径** 中对应更新状态;跨端规则(如 VIP、提现、@ 人)变更必须写清业务规则与优先级,避免「代码已上、文档仍旧」。
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
每新增或变更接口,须在实现合并前同步:`路由组(miniprogram/admin/db)`、路径、请求/响应关键字段、错误码约定;涉及表结构或迁移的,在 `开发文档/8、部署/` 或临时需求池技术分析中留痕。历史待办(如 router/ossConfig、vip-members 聚合)在文档中标注 **P0/P1** 与依赖方。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
管理端功能以页面/菜单为粒度,在 `开发文档/10、项目管理/运营与变更.md` 或需求汇总中追加「做了什么、调了哪些 `/api/admin/*` `/api/db/*`」;与小程序字段对齐处(列表、详情、配置项)写明字段来源,便于测试写用例。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
C 端以页面与核心流程为粒度同步:涉及 `/api/miniprogram/*` 的变更、scene、支付/登录分支、分享 singlePage 行为,须在开发文档或角色项目索引中可追溯;仅改 UI 也需注明「无接口变更」以便测试裁剪回归范围。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
文档同步后,测试以 **文档与索引为真源** 更新回归清单与联调顺序;三端契约变更若文档未更新,测试有权将项标为「阻塞—待文档」并在问题与作答区登记。
|
||||
|
||||
---
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- **【管理端开发工程师】回复【产品经理】**:界面定需求已存在,建议「功能完成」定义包含:代码合并 + 文档/索引一行摘要,避免遗漏。
|
||||
- **【后端开发】回复【小程序开发工程师】**:miniprogram 与 admin 路由隔离不变;文档中写清「仅供某端调用」可避免联调误用。
|
||||
- **【测试人员】回复【全员】**:deploy/health/smoke 与关键路径用例在 `开发文档/10、项目管理` 与测试索引中保持交叉引用,便于发版前自检。
|
||||
|
||||
---
|
||||
|
||||
## 会议决议
|
||||
|
||||
1. **按功能粒度同步**:以「可验收功能点」为最小单位,责任角色在合并前或合并当日更新:对应 `开发文档` 章节、`.cursor/agent/开发助理/项目索引/{角色}.md` 开发进度表一行(**必须含日期**)。
|
||||
2. **跨端变更双写**:影响两端的字段或接口,除本角色文档外,须在 `agent/团队/evolution/` 或会议纪要中留架构/契约摘要,并在相关角色索引中各记一行。
|
||||
3. **待确认项进表**:接口路径、错误码、验收口径未定的,一律落入本次会议「问题与作答区」,由责任角色补充作答后闭环。
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
| 责任角色 | 任务 | 优先级 | 截止建议 |
|
||||
|---------|------|--------|---------|
|
||||
| 产品经理 | 核对主需求与《以界面定需求》与各端近期实现差异,更新需求汇总状态 | 高 | 2026-04-20 |
|
||||
| 后端开发 | 将 router/ossConfig、vip-members 等待办在文档中标注优先级与依赖 | 中 | 2026-04-20 |
|
||||
| 管理端开发工程师 | soul-admin 近期页面/接口变更写入运营与变更或索引 | 中 | 2026-04-20 |
|
||||
| 小程序开发工程师 | 阅读/支付/分享等主路径若有未记文档的改动,补索引与说明 | 中 | 2026-04-20 |
|
||||
| 测试人员 | 依据更新后的文档刷新融合与主路径回归清单 | 中 | 2026-04-20 |
|
||||
|
||||
---
|
||||
|
||||
## 问题与作答区
|
||||
|
||||
> 会议中提出的待确认问题在此列出;作答区域供后续补充答案,便于追溯闭环。
|
||||
|
||||
| # | 问题 | 责任角色 | 作答 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 各端「最近一次合并到主分支」的功能清单是否与项目索引 2026-04-02 之后实际代码一致? | 各角色负责人 | (待补充) |
|
||||
| 2 | vip-members 聚合与超级个体/@ 融合一期验收口径是否已在需求文档中单点定稿? | 产品经理 | (待补充) |
|
||||
| 3 | DistributionPage `Order.description` 类型债是否纳入本迭代文档与修复排期? | 管理端开发工程师 | (待补充) |
|
||||
|
||||
---
|
||||
|
||||
## 各角色经验与业务理解更新
|
||||
|
||||
### 产品经理
|
||||
|
||||
- 文档同步与验收基准绑定:功能完成 = 实现 + 文档/索引可追溯。
|
||||
|
||||
### 后端开发
|
||||
|
||||
- 接口与迁移变更必须标明路由组与优先级,避免管理端/小程序误调。
|
||||
|
||||
### 管理端开发工程师
|
||||
|
||||
- 以页面为粒度记录 admin/db 依赖,字段与小程序对齐处显式写出。
|
||||
|
||||
### 小程序开发工程师
|
||||
|
||||
- 无接口变更的纯 UI 迭代也应在索引中标注,便于测试裁剪范围。
|
||||
|
||||
### 测试人员
|
||||
|
||||
- 以更新后的文档与索引为真源维护回归清单;文档缺失可阻塞验收。
|
||||
|
||||
### 团队共享
|
||||
|
||||
- 延续 2026-03-24 文档同步原则:实现变更后同步需求、运营与变更、各角色项目索引;跨端契约写入团队 evolution 或会议纪要。
|
||||
|
||||
---
|
||||
|
||||
*会议纪要由助理橙子生成 | 各角色经验已同步至 `agent/{角色}/evolution/2026-04-13.md`*
|
||||
@@ -85,3 +85,4 @@ YYYY-MM-DD_会议主题.md
|
||||
| 2026-03-30 | 文章详情 # 标签跳转 MBTI 小程序传手机号 | 产品、后端、管理端、小程序、测试 | [2026-03-30_文章详情跳转MBTI小程序传手机号.md](2026-03-30_文章详情跳转MBTI小程序传手机号.md) |
|
||||
| 2026-03-31 | 超级个体列表与 @ 列表融合方案 | 产品、后端、管理端、小程序、测试、团队 | [2026-03-31_超级个体与@列表融合.md](2026-03-31_超级个体与@列表融合.md) |
|
||||
| 2026-04-02 | 工作进度与需求同步会 | 产品、后端、管理端、小程序、测试、团队 | [2026-04-02_工作进度与需求同步会.md](2026-04-02_工作进度与需求同步会.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档 | 产品、后端、管理端、小程序、测试、团队 | [2026-04-13_按功能同步开发文档.md](2026-04-13_按功能同步开发文档.md) |
|
||||
|
||||
@@ -66,6 +66,7 @@ alwaysApply: true
|
||||
| 小橙、橙子、橙橙、🍊、讨论完毕、记录一下、记录、同步文档 | `.cursor/skills/assistant-doc-sync/SKILL.md` |
|
||||
| 吸收经验、升级 skills、记录经验、保存开发进度、更新项目索引、记录开发进度、任务完成、搞定了、完成了 | `.cursor/skills/assistant-doc-sync/SKILL.md` |
|
||||
| 跨端功能开发 | `.cursor/skills/role-flow-control/SKILL.md` |
|
||||
| JWT 测试存客宝获客列表、ckb-leads submitted、发 jwt 测接口、工作台「加入/匹配提交」字段验收 | `.cursor/skills/ckb-leads-api-test/SKILL.md` |
|
||||
| 变更完成、检查一下、准备提交 | `.cursor/skills/change-checklist/SKILL.md` |
|
||||
| 开个会、开会、团队会议、乘风开会、需求评审、方案讨论、大家一起讨论 | `.cursor/skills/team-meeting/SKILL.md`(老板分身/乘风主持) |
|
||||
| 会议结束、散会、会开完了 | `.cursor/skills/assistant-doc-sync/SKILL.md`(会议收尾) |
|
||||
|
||||
@@ -112,6 +112,7 @@ description: Trigger when 编辑 soul-api、后端、API、Go、Gin、GORM、新
|
||||
| 新增表/字段后未执行迁移 | 线上报错、字段缺失 | 编写 scripts/add-xxx.sql → 执行 `node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-xxx.sql` |
|
||||
| 新增 model 未 AutoMigrate | 表不存在、启动失败 | 在 `internal/database/database.go` 中 `AutoMigrate(&model.Xxx{})` |
|
||||
| 业务逻辑直接 `os.Getenv` | 配置分散、难维护 | 仅通过 `internal/config` 的 `Load()` 读环境变量 |
|
||||
| 管理端展示的「计划 Key / 状态」与再次调用第三方用的密钥 **分两处手写解析** | 线上列表对的、重推错的;排障永远对不上抓包 | **单一真源函数**:如存客宝线索 `resolveLeadPlanAPIKeyForRetry` 同时服务 `GET .../ckb-leads?mode=contact` 的 `planApiKey` 与 `retryOneLeadRecord`,禁止列表与重推各写一套顺序 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -157,13 +157,13 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
## 文档结构速查
|
||||
|
||||
```
|
||||
开发文档/
|
||||
开发文档/ # 仓库根目录下;若尚未克隆外部大文档,可先维护本骨架与摘要
|
||||
├── 1、需求/需求汇总.md # 需求清单、业务需求
|
||||
├── 8、部署/ # 技术方案、部署说明
|
||||
├── 8、部署/ # 技术方案、部署说明(按需创建)
|
||||
├── 10、项目管理/
|
||||
│ ├── 项目落地推进表.md # 里程碑、永平落地
|
||||
│ ├── 项目落地推进表.md # 里程碑、永平落地(按需创建)
|
||||
│ └── 运营与变更.md # 近期更新、变更记录
|
||||
临时需求池/ # 需求分析、技术分析、待办
|
||||
临时需求池/ # 需求分析、技术分析、待办(按需创建)
|
||||
.cursor/agent/ # 每角色 evolution 经验池
|
||||
├── 开发助理/evolution/、项目索引/{小程序,管理端,后端,产品,测试,助理橙子,团队}.md
|
||||
├── 小程序开发工程师/evolution/
|
||||
|
||||
@@ -6,6 +6,23 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
- 文档同步:小程序「我的」快捷入口 UI 变更写入 `开发文档/10、项目管理/运营与变更.md`、`开发文档/4、前端/当前小程序开发细则.md` §2.2.1;`项目索引/小程序.md` 开发进度追加
|
||||
|
||||
## 2026-04-15
|
||||
|
||||
- 吸收经验(联网):编程思维沉淀至 `agent/团队/evolution/2026-04-15.md`;升级 `role-flow-control` §1.3;`经验清单`、总索引、团队/助理橙子项目索引、sync-log 已更新
|
||||
- 业务补齐:`app.request` 失败体挂 `err.response`;导师咨询 `mentor_consultation` 支付全链路;`CKBJoin` investor 假成功改为显式失败;`需求汇总` 更新
|
||||
|
||||
## 2026-04-14
|
||||
|
||||
- 吸收经验:各智能体沉淀 + 同步开发需求;新建 `开发文档/1、需求/需求汇总.md`、`开发文档/10、项目管理/运营与变更.md`、`开发文档/README.md`;各角色 `evolution/2026-04-14.md` 与索引;`经验清单`、`智能体-技能-经验总索引`、项目索引七处;升级 `role-flow-control` §1.2、`assistant-doc-sync` 文档树说明
|
||||
|
||||
## 2026-04-13
|
||||
|
||||
- 会议收尾:按功能同步开发文档;纪要 `.cursor/meeting/2026-04-13_按功能同步开发文档.md`;各角色 evolution、项目索引(产品/后端/管理端/小程序/测试/团队)、meeting/README 已更新
|
||||
|
||||
## 2026-04-07
|
||||
|
||||
- 文档同步:开发文档索引、主需求 2026-03-20、需求索引、10 项目管理 README 与《闭环与文档同步-2026-04-07》交叉引用;项目索引助理橙子/团队追加进度
|
||||
|
||||
@@ -77,6 +77,9 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a
|
||||
| 找伙伴/匹配 | 匹配、展示 | match、ckb 等 miniprogram 接口 | 匹配配置、开关 |
|
||||
| 配置项 | 仅读取 | miniprogram/config 或 db/config | 配置编辑(admin/db) |
|
||||
| VIP/超级个体 | 展示列表、详情 | miniprogram/vip/members;db/users PUT、db/vip-roles | 用户列表「设置 VIP」弹窗、VIP 角色管理页 |
|
||||
| 超级个体发文(UGC+AI) | `super-article-editor`、阅读页 **`read`(`superArticleId`)**、`super-article-detail`(兼容重定向)、`super-article-mine` 等 | 表 `super_articles`;`GET/POST/DELETE` `/api/miniprogram/super/articles*`、`/super/articles/generate` | 当前无独立超管文章列表为常态时勿只挂 admin;**勿**与 book/章节阅读接口混改或合并列表;详情 UI 与章节共用 `read` 但 **数据模型仍隔离** |
|
||||
|
||||
**专项检查**:若本次变更涉及 `super_articles`、**`super-article-*` 页面** 或 `super/articles` 接口,再读 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md),确认未与书籍/章节、MBTI 的 `soul_articles` 混表或混接口。
|
||||
|
||||
每次做「某个功能」的变更时,问:
|
||||
|
||||
|
||||
91
.cursor/skills/ckb-leads-api-test/SKILL.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: soul-ckb-leads-api-test
|
||||
description: JWT 驱动的存客宝获客列表 API 回归。用户粘贴管理端 JWT 时立即执行脚本验证 GET /api/db/ckb-leads mode=submitted 是否返回 phone/wechatId。Use when JWT 测试存客宝、ckb-leads submitted、获客列表接口回归、工作台加入匹配提交字段验收、发 jwt 给你测一下。
|
||||
---
|
||||
|
||||
# 存客宝获客列表 submitted 接口 · JWT 一键测试
|
||||
|
||||
当用户**发来管理端 JWT**(或使用「发 JWT 给你测」「跑存客宝 submitted 接口」等意图)时:**先读本 Skill,再立刻执行下列测试流程**。无需反问「要不要测」。
|
||||
|
||||
---
|
||||
|
||||
## 角色
|
||||
|
||||
**软件测试**。只验证 API 契约与脚本结果,不替你改业务代码(若失败再转后端/前端)。
|
||||
|
||||
---
|
||||
|
||||
## 触发话术(任选)
|
||||
|
||||
- 「这是 JWT:`eyJ…`」「发 jwt 给你」「用我的 token 测一下」
|
||||
- 「存客宝工作台 submitted」「ckb-leads 接口回归」「手机号列 API 字段验收」
|
||||
|
||||
**信号**:消息里出现 **Bearer token 形态**(`eyJ` 开头的三段 JWT)或用户明确说已复制 **admin_token**。
|
||||
|
||||
---
|
||||
|
||||
## 立即执行的测试流程
|
||||
|
||||
1. **解析环境**
|
||||
- `SOUL_API_BASE`:优先用户消息中的 URL;否则读 `soul-admin/.env.development` 的 `VITE_API_BASE_URL`;再否则 `http://localhost:8080`。
|
||||
- 去掉末尾 `/`。
|
||||
|
||||
2. **提取 JWT**
|
||||
- 若用户粘贴 `Bearer xxx`,只取 `xxx`。
|
||||
- 整段为 `eyJ...` 则原样使用。
|
||||
|
||||
3. **运行脚本**(仓库根目录,需已 `pip install -r scripts/test/requirements-test.txt`)
|
||||
```bash
|
||||
# PowerShell 示例
|
||||
$env:ADMIN_TOKEN="<JWT>"
|
||||
$env:SOUL_API_BASE="<与上一步一致>"
|
||||
python scripts/test/soul_api/test_ckb_leads_submitted.py --page-size 20
|
||||
```
|
||||
- 解释退出码:**0** = 本页每条记录均含 `phone`、`wechatId` 键;**1** = 401、HTTP 错误或缺键(多为**未部署**含该字段的后端)。
|
||||
|
||||
4. **回复用户(中文)**
|
||||
- **环境**:用一句话写清测的是哪个 BASE URL(不写全 JWT)。
|
||||
- **结果**:通过 / 失败 + 摘录脚本输出要点(前几行样本、缺失字段条数)。
|
||||
- **建议**:失败且为缺键 → 提示部署含 `phone`/`wechatId` 的 `soul-api`;401 → token 失效或环境问题。
|
||||
|
||||
5. **安全**
|
||||
- **不要在回复中完整复述用户 JWT**。
|
||||
- 日志里如需引用,仅保留 `eyJ` 后前 **8** 字符 + `…`。
|
||||
- JWT 等价密码,勿写入 git、勿贴公开场合。
|
||||
|
||||
---
|
||||
|
||||
## 脚本真源与验收点
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 脚本路径 | `scripts/test/soul_api/test_ckb_leads_submitted.py` |
|
||||
| 请求 | `GET /api/db/ckb-leads?mode=submitted&page=1&pageSize=…`,头 `Authorization: Bearer <ADMIN_TOKEN>` |
|
||||
| 断言 | `records[]` **每条**均含键 `phone`、`wechatId`(值可为空) |
|
||||
| 背景 | 存客宝工作台「加入/匹配提交」表依赖这两字段;旧 API 未返回则全列「—」 |
|
||||
|
||||
---
|
||||
|
||||
## Gotchas(必看)
|
||||
|
||||
| 陷阱 | 后果 | 正确做法 |
|
||||
|------|------|----------|
|
||||
| 未设 `SOUL_API_BASE`,依赖 `scripts/test/config.py` 解析到正式域名 | **误测生产** | 显式与用户确认或读 `soul-admin/.env.development`,本地测必须指向本地端口 |
|
||||
| 把 JWT 写进会议纪要/需求文档 | **账号泄露** | 仅口头/私聊粘贴;复盘里写「已用临时 token 测过」不写原文 |
|
||||
| 脚本失败就认为一定是小程序 Bug | **误判** | 先区分「接口未返回字段」vs「库里真的无手机号」——前者是后端/部署,后者才可能追 C 端 |
|
||||
| 工作台「类型」列 join/match | **混淆** | join=**加入**(ckb/join),match=**匹配**(ckb/match);留资为 lead,见 `开发文档/6、后端/存客宝-工作台线索类型与中英文说明.md` |
|
||||
| **获客列表「计划Key」与重推 URL 里 `apiKey` 不一致** | 界面是人物/快照兜底后的展示,重推曾误用全局 `CKB_LEAD_API_KEY`,报「无效的apiKey」 | 后端已统一:`resolveLeadPlanAPIKeyForRetry` 同时用于 `mode=contact` 的 `planApiKey` 与 `POST .../ckb-leads/retry` 内部推送;验收时对比**同一条 id** 下列 key 与(临时)服务端日志/抓包是否同源 |
|
||||
| 控制台中文乱码 | 难以读输出 | Windows 可先 `chcp 65001` 或设 `PYTHONIOENCODING=utf-8` |
|
||||
|
||||
---
|
||||
|
||||
## 与其它 Skill 的关系
|
||||
|
||||
- 通用测试原则、环境横幅:`.cursor/skills/testing/SKILL.md`
|
||||
- 本 Skill 专治 **JWT + submitted 获客列表契约**;扩展更多 CKB 用例可走 `scripts/test/process/` pytest。
|
||||
|
||||
---
|
||||
|
||||
## 会话收尾
|
||||
|
||||
若项目要求卡若复盘块:本 Skill **不豁免**每条助手回复末尾的复盘格式;测试结果写入复盘「过程/总结」即可。
|
||||
112
.cursor/skills/feishu-webhook/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: feishu-custom-bot-webhook
|
||||
description: 飞书(及 Lark 国际版)群自定义机器人 Webhook 对接。与 Soul 订单/获客推送同契约;迁移到其他项目时复用 JSON 格式与分支规则。Use when 飞书 webhook、自定义机器人、群通知、Lark bot、从 Soul 迁移通知.
|
||||
---
|
||||
|
||||
# 飞书自定义机器人 Webhook(可迁移 Skill)
|
||||
|
||||
本 Skill 描述 **HTTP 自定义机器人** 向飞书群发消息的契约与落地方式。Soul 后端在「非企业微信 URL」时统一走 **飞书/Lark 文本消息** 格式;复制到其他语言或项目时,只需保留 **URL 判别 + JSON 结构 + POST application/json**。
|
||||
|
||||
**与 Open API 的区别**:自定义机器人只需 Webhook 地址与固定 JSON,**不需要** `app_id`/`app_secret`/tenant token。若你要调飞书开放平台 API(发卡片、@人、多维表格),不在本 Skill 范围。
|
||||
|
||||
---
|
||||
|
||||
## 1. 消息体契约(文本)
|
||||
|
||||
### 1.1 飞书 / Lark 自定义机器人
|
||||
|
||||
- **Content-Type**:`application/json`
|
||||
- **结构**:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": "第一行\n第二行"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Webhook URL 特征**:常见为 `https://open.feishu.cn/open-apis/bot/v2/hook/...` 或 `https://open.larksuite.com/open-apis/bot/v2/hook/...`(以飞书控制台生成的为准)。
|
||||
|
||||
### 1.2 企业微信群机器人(Soul 并存)
|
||||
|
||||
若 URL 包含 `qyapi.weixin.qq.com`,Soul 使用 **企业微信** 格式(字段名不同):
|
||||
|
||||
```json
|
||||
{
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": "第一行\n第二行"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**迁移时的分支规则**:用 URL 是否包含 `qyapi.weixin.qq.com` 区分两套 payload;其余一律按飞书格式发送(与 Soul `order_webhook.go`、`ckb.go` 一致)。
|
||||
|
||||
---
|
||||
|
||||
## 2. Soul 项目中的参考实现(迁移源码锚点)
|
||||
|
||||
| 能力 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 支付成功订单推送 + 失败重试 | `soul-api/internal/handler/order_webhook.go` | `pushPaidOrderWebhook`、`RetryPendingPaidOrderWebhooks`;订单表字段 `webhook_push_*` |
|
||||
| 获客线索推送 | `soul-api/internal/handler/ckb.go` | `sendLeadWebhook`、`loadLeadWebhookURL`;1h 内同用户去重 |
|
||||
| 定时补偿 | `soul-api/internal/handler/cron.go` | `CronRetryOrderWebhooks` → `/api/cron/retry-order-webhooks` |
|
||||
| 按超级个体配置 Webhook | `soul-api/internal/handler/vip_members_admin.go` | `DBVipMemberWebhookSet`;配置键 `super_individual_webhook_map` |
|
||||
|
||||
### 2.1 配置存储(`system_config`)
|
||||
|
||||
| `config_key` | 用途 |
|
||||
|--------------|------|
|
||||
| `order_paid_webhook_url` | 支付成功推送主地址(JSON 字符串存 URL) |
|
||||
| `ckb_lead_webhook_url` | 全局获客 Webhook;订单推送无主地址时的 fallback |
|
||||
| `super_individual_webhook_map` | `userId → webhookUrl` 的 JSON 对象,获客按对接人路由 |
|
||||
|
||||
### 2.2 小程序配置 JSON(上麦打赏等)
|
||||
|
||||
- 路径:`system_config` 中 `mp_config` → `mpUi.homePage.liveMicSchedule.paidWebhookUrl`
|
||||
- 仅当订单类型为 `link_karuo_tip` 且 `product_id` 形如 `live_mic|...` 时,**优先**使用该地址(见 `loadLiveMicTipPaidWebhookURL`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 迁移到其他项目的检查清单
|
||||
|
||||
1. **在飞书群添加自定义机器人**,复制 Webhook URL,仅保存在服务端配置或密钥管理,**不要**写进前端或公开仓库。
|
||||
2. **实现分支**:`qyapi` → 企微 JSON;否则 → 飞书 `msg_type` + `content.text`。
|
||||
3. **HTTP**:`POST`,`Content-Type: application/json`,body 为 UTF-8 JSON;检查响应状态码(Soul 对 `>=400` 视为失败)。
|
||||
4. **可靠性(可选)**:对关键业务(如支付通知)做「失败落库 + 定时重试」,避免仅 `go` 异步发送不感知失败。
|
||||
5. **限流与重复**:飞书对机器人有频率限制;获客类通知可按「用户 + 业务键」做短时间去重(Soul 为内存缓存 1h,进程重启会清空——迁移时可改为 Redis)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 最小验证(curl)
|
||||
|
||||
将 `YOUR_HOOK_URL` 换成真实 Webhook(勿提交到 git):
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "YOUR_HOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"迁移验证 $(date +%Y-%m-%d\\ %H:%M)\"}}"
|
||||
```
|
||||
|
||||
企微机器人请改用 `msgtype` / `text.content` 字段。
|
||||
|
||||
---
|
||||
|
||||
## 5. Gotchas(踩坑)
|
||||
|
||||
| 陷阱 | 后果 | 正确做法 |
|
||||
|------|------|----------|
|
||||
| 把企微的 `msgtype` 发到飞书 | 飞书返回错误,群无消息 | 严格按 URL 分支选 JSON |
|
||||
| 把飞书的 `msg_type` 发到企微 | 同上 | 同上 |
|
||||
| 误以为要飞书 Open API 凭证 | 过度设计 | 自定义机器人只需 Webhook |
|
||||
| 日志打印完整 Webhook URL | 泄露群推送密钥 | 只打域名或脱敏 |
|
||||
| 正文过长 | 可能被截断或拒收 | 控制单行长度;必要时分多条或改用卡片(需查飞书卡片文档,格式不同) |
|
||||
| 仅依赖异步 `go func` 且无重试 | 支付通知丢失 | 关键路径参考 Soul:`markOrderWebhookResult` + cron 补偿 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 `karuo-party` Skill 的关系
|
||||
|
||||
`karuo-party` 侧重运营侧(报表、飞书**云文档/视频**下载等,多依赖 Token)。本 Skill 仅覆盖 **群机器人 Webhook 文本推送**,与 Soul 订单/获客逻辑对齐,便于抽到任意后端项目。
|
||||
@@ -47,6 +47,7 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
- **支付**:下单/查单走 `POST/GET /api/miniprogram/pay`、回调由 soul-api 处理;支付前必须已有 openId(通过登录或 getOpenId 获得)。
|
||||
- **提现**:申请/记录/确认等走 `/api/miniprogram/withdraw/*`;订阅消息模板 ID 在 `app.globalData.withdrawSubscribeTmplId`。
|
||||
- **scene 编解码**:海报与扫码统一用 `utils/scene.js` 的 `buildScene`/`parseScene`,支持 mid、id、ref,分隔符与后端生成一致(如 `_`)。
|
||||
- **超级个体 AI 写文章**:数据落在 soul-api 表 `super_articles`,接口为 `/api/miniprogram/super/articles`(生成/发布/列表/详情/删除);编辑器 `super-article-editor`;**正文详情展示与书籍章节共用 `pages/read/read`,通过 query `superArticleId`(或 `saId`)进入动态模式**;`super-article-detail` 仅保留对 `read` 的兼容重定向。**数据模型与书籍章节接口仍分离**,勿混表混列表。详见 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -114,5 +115,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
- 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。
|
||||
- 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 user-select)。
|
||||
- 做编辑资料页分享名片时(遵循 §10)。
|
||||
- 做超级个体 **AI 写文章**、`super-article-editor`、**`read`(含 `superArticleId` 动态详情)** / `super-article-detail`(兼容跳转)时(遵循 §4 超级个体一条与 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md))。
|
||||
|
||||
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: soul-role-workflow
|
||||
description: Soul 创业派对开发协同流程。小程序开发工程师、管理端开发工程师、后端开发职责与协同。跨端功能开发、新增/优化时使用。Use when 跨端协同, 角色流程, 功能开发协作.
|
||||
description: Soul 创业派对开发协同流程。小程序/管理端/后端职责、垂直切片与追踪弹、智能体协作提要。跨端功能开发、新增/优化、AI 多角色执行时使用。Use when 跨端协同, 角色流程, 功能开发协作, 智能体编程思维.
|
||||
---
|
||||
# Soul 创业派对 - 角色流程控制 Skill
|
||||
|
||||
@@ -27,6 +27,30 @@ description: Soul 创业派对开发协同流程。小程序开发工程师、
|
||||
- 小程序:是否按接口/配置渲染而非硬编码。
|
||||
- 默认交付口径:**可配置、可运营、可联调、可验收**,否则视为未完成。
|
||||
|
||||
### 1.2 从小程序逆推闭环(文档 / 需求同步)
|
||||
|
||||
评审或收尾时优先从 **C 端页面** 逆推,避免「只有样式、无接口/无配置」被误判为完成:
|
||||
|
||||
1. **用户路径**:入口、前置条件(登录 / VIP / 资料)、失败提示是否可验收。
|
||||
2. **数据来源**:写死常量、`app.request`、本地缓存;若无后端/配置真源,须在 `开发文档/1、需求/需求汇总.md` 或当日项目索引标注「无接口/待配置」。
|
||||
3. **路由与契约**:仅调 `/api/miniprogram/*`;跨端字段变更须同步 **后端契约 + 管理端(若需运营配置)+ 需求表一行**。
|
||||
4. **配置化判断**:运营是否可能在线变更、是否涉款/风控/合规;需要运行时灰度/熔断的采用 **Feature Toggle** 思路并规划退役,避免永久 `if(flag)`。
|
||||
5. **回归影响**:支付、scene、分享 singlePage、邻页 `onShow`;测试以 **文档与索引为真源**,契约未入库可标阻塞。
|
||||
|
||||
详见:`agent/团队/evolution/2026-04-14.md`,需求清单:`开发文档/1、需求/需求汇总.md`。
|
||||
|
||||
### 1.3 智能体协作:联网沉淀的编程思维(提要)
|
||||
|
||||
> 外部资料作**思想库**;执行仍须遵守本仓库 **boundary** 与 **miniprogram / admin / db** 路由隔离。
|
||||
|
||||
1. **垂直切片(vertical slice)**:以「可演示的用户价值」为单位打通 UI→API→持久化/配置,避免长期在单一层级堆半成品(敏捷实践中对 vertical slicing 的共识:更快用事实替代猜测)。
|
||||
2. **追踪弹(tracer bullets,《The Pragmatic Programmer》)**:先落地**薄而端到端**、可进主分支的骨架路径,用真实反馈校准架构;与「用完即扔」的原型不同,追踪弹是后续扩展的基座。用于约束多智能体/大模型:**小步打通再扩面**,控制一次性大 diff(业界亦用该比喻讨论 AI 生成代码的验证节奏,如 aihero.dev 对 tracer bullets 与 AI slop 的论述)。
|
||||
3. **先验证再堆量(verify before build)**:上下文与约束写清 → 分块实现 → **测试或最小复现** 再合并;可用 AI 辅助生成边界/对抗用例,人做最终把关(多篇「AI coding best practices」强调 context-first 与系统化验证)。
|
||||
4. **无责复盘(blameless postmortem)**:故障与重大返工后,复盘聚焦**系统为何允许失败**、时间线与可验证改进行动,而非个人追责(参见 Google *Site Reliability Engineering*:Postmortem Culture — `https://sre.google/sre-book/postmortem-culture/`)。
|
||||
5. **简单性**:优先可读、可预测实现;临时开关/实验分支需**退役计划**,避免永久 `if(flag)` 分叉。
|
||||
|
||||
团队详述与引用:`agent/团队/evolution/2026-04-15.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 协同流程总览(线框图)
|
||||
|
||||
89
.cursor/skills/super-individual-article/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: soul-super-individual-article
|
||||
description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/super/articles、super-article-editor、与书籍/阅读及 MBTI soul_articles 不混用。Use when 超级个体, AI 写文章, super-article, super_articles, 超级个体发文.
|
||||
---
|
||||
# Soul 创业派对 - 超级个体 AI 写文章 Skill
|
||||
|
||||
在 **soul-api**、**miniprogram** 中编辑「超级个体发布文章」「AI 一键生成文章草稿」或 **`super_articles` 表**时必读。与 [soul-miniprogram-dev](../miniprogram-dev/SKILL.md)、[soul-api-dev](../api-dev/SKILL.md) 配合使用。
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品边界
|
||||
|
||||
- **谁能用**:`handler` 中通过 `miniprogramSuperIndividualPerson` 判断——**有效 VIP 且**在 `persons` 表已绑定人物(`persons.user_id`)视为超级个体,方可 **AI 生成、发布、列表/详情**(与客资/轨迹逻辑同源,见 `soul-api/internal/handler/miniprogram_my_super_crm.go`)。
|
||||
- **谁能看**:列表/详情仅对「作者本身是超级个体」的稿件可见;非超级个体用户发布的记录不应存在(发布接口已拦截)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据层:独立表,禁止与书籍/阅读混用
|
||||
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| **`super_articles`** | 超级个体在小程序中发布的 UGC 文章(含 AI 辅助生成的标题/正文落库后同等存储) |
|
||||
| 书籍/章节/ stitch 内容 | `read` 页、book/chapter 接口,**不写入** `super_articles` |
|
||||
|
||||
- **Model**:[soul-api/internal/model/super_article.go](soul-api/internal/model/super_article.go)(字段:`id`, `user_id`, `title`, `content`, `created_at`, `updated_at`)。
|
||||
- **AutoMigrate**:[soul-api/internal/database/database.go](soul-api/internal/database/database.go) 已注册 `SuperArticle`。
|
||||
- **禁止**:在章节列表、全站搜索、阅读进度等查询中把 `super_articles` 与书籍章节**合并**为同一套「文章」列表,除非产品明确要做一个「统一 Feed」(当前未做则在代码与接口层保持隔离)。
|
||||
|
||||
### 2.1 与 MBTI_wang 的 `soul_articles` 区别(不要合并设计)
|
||||
|
||||
| 维度 | MBTI_wang(子项目 / PHP) | Soul 主项目(本功能) |
|
||||
|------|---------------------------|------------------------|
|
||||
| 表/服务 | `soul_articles` + `SoulArticleService`,Soul 侧同步/推荐位 | **`super_articles`**,Go,仅超级个体 UGC + OpenAI 生成 |
|
||||
| 接口 | `/api/ai/articles/*`(如 recommended) | **`/api/miniprogram/super/articles*`** |
|
||||
| 用途 | 神仙 AI 引流、运营选品 | 个人发文、名片/会员详情跳转 |
|
||||
|
||||
两栈**不要**在 soul-api 里把 `soul_articles` 与 `super_articles` 混为一张表或一个 handler。
|
||||
|
||||
---
|
||||
|
||||
## 3. API 清单(soul-api,仅 miniprogram 组)
|
||||
|
||||
路由注册见 [soul-api/internal/router/router.go](soul-api/internal/router/router.go)(`miniprogram` 组)。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/miniprogram/super/articles/generate` | 提示词 `description` + 可选 `referenceText`(端上文本文件拼接)+ `materialUrls`(附件上传 URL,服务端安全域内抓取)+ `imageUrls`;四者至少一项;返回 `title`/`content` 草稿;需 OpenAI 兼容 Key/模型;契约见 `开发文档/5、接口/API接口完整文档.md` §1.3 |
|
||||
| POST | `/api/miniprogram/super/articles` | 发布;body:`userId`, `title`, `content`;失败时可能带 `forbidden: true` |
|
||||
| GET | `/api/miniprogram/super/articles?authorUserId=&page=&pageSize=` | 作者为超级个体时返回 ta 的文章列表(含 preview) |
|
||||
| GET | `/api/miniprogram/super/articles/:id` | 详情(作者须为超级个体,否则不可见) |
|
||||
|
||||
- **H5 分享页**(非 miniprogram 前缀):`GET /s/:id`,与小程序详情页展示需一致,见 [h5_super_article.go](soul-api/internal/handler/h5_super_article.go)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 小程序(miniprogram)
|
||||
|
||||
| 页面/逻辑 | 路径 |
|
||||
|-----------|------|
|
||||
| AI 写文 + 发布 | [miniprogram/pages/super-article-editor/](miniprogram/pages/super-article-editor/):多文件 `chooseMessageFile`;图片上传 `folder=article-images`,附件 `folder=book-attachments`,文本可读类型可本地 read 后并入 `referenceText`;生成 `POST .../super/articles/generate`(timeout 建议 ≥120s),发布 `POST .../super/articles`;统一 `utils/miniprogramUpload.js` → `POST /api/miniprogram/upload` |
|
||||
| **动态详情(正文展示)** | **`pages/read/read`**:`onLoad` 含 **`superArticleId`**(或 `saId`)时为超级个体详情模式(顶栏「文章详情」);正文/HTML、配图解析见 [miniprogram/utils/superArticleDetail.js](miniprogram/utils/superArticleDetail.js) |
|
||||
| 兼容旧链 | [miniprogram/pages/super-article-detail/](miniprogram/pages/super-article-detail/):仅 **`redirectTo`** → `read?superArticleId=` |
|
||||
| 我的动态列表 | [miniprogram/pages/super-article-mine/](miniprogram/pages/super-article-mine/) |
|
||||
| 入口 | [miniprogram/pages/my/my.js](miniprogram/pages/my/my.js) 中 **`superArticleMine`**、`showSuperArticleEntry`;进编辑器前会调 `GET /api/miniprogram/my/super-stats` 校验 `isSuperIndividual` |
|
||||
|
||||
- 所有请求**必须**走 `app.request` 与 **`/api/miniprogram/...` 前缀**(与 miniprogram-dev 一致)。
|
||||
- **数据仍独立**:`super_articles` 与书籍章节接口不合并;仅小程序 **UI** 共用 `read` 页面容器。
|
||||
|
||||
---
|
||||
|
||||
## 5. 管理端(soul-admin)
|
||||
|
||||
- **当前**:无针对 `super_articles` 的独立超管「文章 CRUD」列表为常态能力时,**不要**为发文功能误把接口只挂在 `admin/db` 而小程序又未挂 miniprogram 组。
|
||||
- 若以后增加运营审核/下架,应新增 **admin/db 专用接口 + 表字段**(如 `status`),仍保持与 book 内容表分离。
|
||||
|
||||
---
|
||||
|
||||
## 6. 变更时联调
|
||||
|
||||
- 改 handler、model、小程序任一端时,过 **[change-checklist](../change-checklist/SKILL.md)**:涉及 `super_articles` 或 `super-article-*` 时,确认未误改 **book/章节/阅读** 相关接口与列表逻辑。
|
||||
- 若新增字段:同步 **model、迁移脚本、miniprogram 展示/提交、H5 页** 是否需要一起改。
|
||||
|
||||
---
|
||||
|
||||
## 7. 何时激活本 Skill
|
||||
|
||||
- 改「超级个体」「AI 写文章」「super-article-editor」、**阅读页动态模式(`read` + `superArticleId`)**、`super-article-detail`(兼容重定向)或 `super_articles`。
|
||||
- 需区分 **本书章节**、**MBTI 引流文** 与 **超级个体发文** 时。
|
||||
- 设计草稿/审核/定时发等新能力前,先读本 Skill §2 与 §5,避免与现有表职责混淆。
|
||||
@@ -68,6 +68,12 @@ description: Soul 创业派对测试人员。功能测试、回归测试、三
|
||||
|
||||
---
|
||||
|
||||
## 5.1 专题:JWT 驱动 · 存客宝 submitted 获客列表契约
|
||||
|
||||
用户**粘贴管理端 JWT**并要求验证「加入/匹配提交」数据来源时:**必读** `.cursor/skills/ckb-leads-api-test/SKILL.md`,并执行脚本 `scripts/test/soul_api/test_ckb_leads_submitted.py`(`ADMIN_TOKEN` + `SOUL_API_BASE`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 产出与协同
|
||||
|
||||
| 产出 | 说明 |
|
||||
|
||||
@@ -12,9 +12,11 @@ const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
// 线上正式 API(真机/体验版/正式版)。永平仓库默认正式域;部署前核对见 .cursor/skills/security-server-ops/SKILL.md
|
||||
const API_BASE_URL_PROD = 'https://soulapi.quwanzhi.com'
|
||||
// const API_BASE_URL_PROD = 'http://127.0.0.1:9100'
|
||||
|
||||
// 开发者工具默认与正式一致;若需连测服可改为 https://souldev.quwanzhi.com 或本机 http://127.0.0.1:9100
|
||||
const API_BASE_URL_DEV = 'https://soulapi.quwanzhi.com'
|
||||
// const API_BASE_URL_DEV ='http://127.0.0.1:9100'
|
||||
|
||||
// 运行时自动判断:非开发工具(真机/体验版/正式版)强制用 PROD,避免 localhost 打进上传包导致白屏
|
||||
const _isDevTools = (typeof __wxConfig !== 'undefined' && __wxConfig.platform === 'devtools')
|
||||
@@ -33,6 +35,7 @@ App({
|
||||
appDisplayVersion: APP_DISPLAY_VERSION,
|
||||
|
||||
// API:仅使用代码常量手动切换,不再使用运行时开关
|
||||
// baseUrl: API_BASE_URL,
|
||||
baseUrl: API_BASE_URL,
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: DEFAULT_APP_ID,
|
||||
@@ -1026,8 +1029,11 @@ App({
|
||||
},
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
const rejectWithBody = (message) => {
|
||||
reject(new Error(message))
|
||||
/** 业务失败时挂 err.response,供页面读取 needBindWechat / errorCode 等(与 miniprogram-dev Skill 一致) */
|
||||
const rejectWithBody = (message, body) => {
|
||||
const err = new Error(message)
|
||||
if (body && typeof body === 'object') err.response = body
|
||||
reject(err)
|
||||
}
|
||||
if (res.statusCode === 200) {
|
||||
if (data && data.success === false) {
|
||||
@@ -1036,7 +1042,7 @@ App({
|
||||
this.logout()
|
||||
}
|
||||
showError(msg)
|
||||
rejectWithBody(msg)
|
||||
rejectWithBody(msg, data)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
@@ -1050,7 +1056,7 @@ App({
|
||||
}
|
||||
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
|
||||
showError(msg)
|
||||
rejectWithBody(msg)
|
||||
rejectWithBody(msg, data && typeof data === 'object' ? data : undefined)
|
||||
},
|
||||
fail: (err) => {
|
||||
const msg = (err && err.errMsg)
|
||||
@@ -1090,8 +1096,8 @@ App({
|
||||
}
|
||||
|
||||
const promise = this._requestOnce(url, options, silent).catch((err) => {
|
||||
const msg = (err && err.message) ? err.message : '网络异常,请重试'
|
||||
throw new Error(msg)
|
||||
if (err && err.message) throw err
|
||||
throw new Error('网络异常,请重试')
|
||||
})
|
||||
|
||||
if (method === 'GET') {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"pages/index/index",
|
||||
"pages/chapters/chapters",
|
||||
"pages/match/match",
|
||||
"pages/super-moments/super-moments",
|
||||
"pages/my/my",
|
||||
"pages/read/read",
|
||||
"pages/link-preview/link-preview",
|
||||
@@ -22,6 +23,9 @@
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/wallet/wallet",
|
||||
"pages/super-crm/super-crm",
|
||||
"pages/super-article-editor/super-article-editor",
|
||||
"pages/super-article-detail/super-article-detail",
|
||||
"pages/super-article-mine/super-article-mine",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail",
|
||||
"pages/mentors/mentors",
|
||||
@@ -62,6 +66,10 @@
|
||||
"pagePath": "pages/match/match",
|
||||
"text": "找伙伴"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/super-moments/super-moments",
|
||||
"text": "动态"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/my",
|
||||
"text": "我的"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 8l4 4-4 4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="q5" x1="2" y1="3" x2="22" y2="21" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EEAD4"/>
|
||||
<stop offset="1" stop-color="#14B8A6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H12v20H6.5A2.5 2.5 0 0 1 4 19.5v-15z" fill="url(#q5)" fill-opacity="0.35" stroke="url(#q5)" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M12 4.5A2.5 2.5 0 0 1 14.5 2H20v14a2 2 0 0 1-2 2h-6V4.5z" fill="url(#q5)" fill-opacity="0.95" stroke="url(#q5)" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M15 9l2.5 2.5L15 14" stroke="#0D1F1C" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 765 B |
@@ -1,8 +1,10 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="8" y1="6" x2="21" y2="6" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="q1" x1="4" y1="3" x2="20" y2="21" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EEAD4"/>
|
||||
<stop offset="1" stop-color="#2DD4BF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M7 3h10a2 2 0 0 1 2 2v15l-2.5-1.5L14 20l-2.5-1.5L9 20l-2.5-1.5L4 20V5a2 2 0 0 1 2-2z" fill="url(#q1)" fill-opacity="0.92"/>
|
||||
<path d="M8 8h8M8 12h6M8 16h5" stroke="#0D1F1C" stroke-width="1.5" stroke-linecap="round" opacity="0.45"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 536 B |
@@ -1,7 +1,12 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="5" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="6" cy="12" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="18" cy="19" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="q2" x1="3" y1="4" x2="21" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EEAD4"/>
|
||||
<stop offset="1" stop-color="#14B8A6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="18" cy="5" r="3" fill="url(#q2)"/>
|
||||
<circle cx="6" cy="12" r="3" fill="url(#q2)" fill-opacity="0.88"/>
|
||||
<circle cx="18" cy="19" r="3" fill="url(#q2)" fill-opacity="0.78"/>
|
||||
<path d="M8.6 10.5L15.4 6.5M15.4 17.5L8.6 13.5" stroke="url(#q2)" stroke-width="2.15" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 690 B After Width: | Height: | Size: 593 B |
@@ -1,6 +1,12 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="7" r="4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="q4" x1="2" y1="3" x2="22" y2="21" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EEAD4"/>
|
||||
<stop offset="1" stop-color="#2DD4BF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="9" cy="7" r="3.8" fill="url(#q4)"/>
|
||||
<path d="M2 21v-1.8a4.5 4.5 0 0 1 4.5-4.5h4A4.5 4.5 0 0 1 15 19.2V21H2z" fill="url(#q4)" fill-opacity="0.9"/>
|
||||
<circle cx="17.5" cy="6.5" r="2.6" fill="url(#q4)" fill-opacity="0.75"/>
|
||||
<path d="M22 21v-1.3a3.6 3.6 0 0 0-2.6-3.45" stroke="url(#q4)" stroke-width="1.85" stroke-linecap="round" fill="none" opacity="0.9"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 665 B |
@@ -1,4 +1,11 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="q3" x1="2" y1="5" x2="22" y2="19" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EEAD4"/>
|
||||
<stop offset="1" stop-color="#0D9488"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M3 10V8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2" stroke="url(#q3)" stroke-width="1.65" stroke-linecap="round" fill="url(#q3)" fill-opacity="0.2"/>
|
||||
<rect x="2.25" y="9.75" width="19.5" height="11.5" rx="2.75" fill="url(#q3)" fill-opacity="0.95" stroke="url(#q3)" stroke-width="1.25"/>
|
||||
<rect x="14.5" y="12.8" width="5.2" height="3.6" rx="0.85" fill="#0D1F1C" opacity="0.32"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 675 B |
@@ -5,17 +5,7 @@
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 卡若创业派对</text>
|
||||
<text class="login-desc">{{desc}}</text>
|
||||
|
||||
<button id="agree-phone-btn" class="btn-login {{agreeProtocol ? '' : 'btn-login-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onPhoneLogin" bindagreeprivacyauthorization="onAgreePrivacy" disabled="{{isLoggingIn || !agreeProtocol}}" hover-class="btn-login-hover">
|
||||
<text class="btn-login-icon">📱</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '手机号快捷登录'}}</text>
|
||||
</button>
|
||||
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacy" hover-class="privacy-agree-btn-hover">同意</button>
|
||||
</view>
|
||||
<view class="login-modal-cancel" wx:if="{{showCancel}}" bindtap="onClose">取消</view>
|
||||
|
||||
<view class="login-agree-row" catchtap="onToggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
@@ -23,5 +13,16 @@
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="onOpenPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
|
||||
<button id="agree-phone-btn" class="btn-login {{agreeProtocol ? '' : 'btn-login-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onPhoneLogin" bindagreeprivacyauthorization="onAgreePrivacy" disabled="{{isLoggingIn || !agreeProtocol}}" hover-class="btn-login-hover">
|
||||
<icon class="btn-login-icon" name="smartphone" size="36" color="#ffffff"></icon>
|
||||
<text>{{isLoggingIn ? '登录中...' : '手机号快捷登录'}}</text>
|
||||
</button>
|
||||
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacy" hover-class="privacy-agree-btn-hover">同意</button>
|
||||
</view>
|
||||
<view class="login-modal-cancel" wx:if="{{showCancel}}" bindtap="onClose">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
padding: 48rpx;
|
||||
}
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
max-width: 560rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
@@ -25,6 +26,10 @@
|
||||
right: 24rpx;
|
||||
z-index: 1;
|
||||
padding: 16rpx;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
line-height: 0;
|
||||
}
|
||||
.login-modal {
|
||||
padding: 48rpx 32rpx;
|
||||
@@ -41,18 +46,30 @@
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
text-shadow: none;
|
||||
-webkit-text-fill-color: #ffffff;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.login-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 32rpx;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #00CED1, #00B4D8);
|
||||
color: #ffffff;
|
||||
@@ -61,19 +78,31 @@
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.btn-login::after { border: none; }
|
||||
.btn-login-icon {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.btn-login-disabled { opacity: 0.55; }
|
||||
.btn-login[disabled] {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.btn-login-disabled { opacity: 0.6; }
|
||||
.login-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
padding: 20rpx 24rpx 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.privacy-wechat-row {
|
||||
margin: 24rpx 0;
|
||||
@@ -100,11 +129,16 @@
|
||||
.login-agree-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-top: 0;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: left;
|
||||
}
|
||||
.agree-checkbox {
|
||||
width: 32rpx;
|
||||
@@ -127,6 +161,11 @@
|
||||
color: #00CED1;
|
||||
text-decoration: underline;
|
||||
padding: 0 4rpx;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* 显式 hover 类名,避免基础库 3.x 报 hoverClass / hoverClassDisable 类型非法 */
|
||||
|
||||
@@ -1,157 +1,183 @@
|
||||
/**
|
||||
* Soul创业实验 - 自定义TabBar组件
|
||||
* 根据后台配置动态显示/隐藏"找伙伴"按钮
|
||||
* Soul创业实验 - 自定义 TabBar
|
||||
* 支持:首页 / 目录 / [找伙伴,审核模式隐藏] / [动态,审核模式隐藏] / 我的
|
||||
*/
|
||||
|
||||
console.log('[TabBar] ===== 组件文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
console.log('[TabBar] App 对象:', app)
|
||||
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
color: '#8e8e93',
|
||||
selectedColor: '#00CED1',
|
||||
matchEnabled: false, // 找伙伴功能开关,默认关闭
|
||||
list: [
|
||||
matchEnabled: false,
|
||||
tabList: [
|
||||
{
|
||||
pagePath: '/pages/index/index',
|
||||
text: '首页',
|
||||
iconType: 'home'
|
||||
iconPath: '/assets/icons/home.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/chapters/chapters',
|
||||
text: '目录',
|
||||
iconType: 'list'
|
||||
iconPath: '/assets/icons/list.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/match/match',
|
||||
text: '找伙伴',
|
||||
iconType: 'match',
|
||||
isSpecial: true
|
||||
pagePath: '/pages/super-moments/super-moments',
|
||||
text: '动态',
|
||||
iconPath: '/assets/icons/sparkles.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/my/my',
|
||||
text: '我的',
|
||||
iconType: 'user'
|
||||
}
|
||||
]
|
||||
iconPath: '/assets/icons/user.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
console.log('[TabBar] Component attached 生命周期触发')
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
ready() {
|
||||
console.log('[TabBar] Component ready 生命周期触发')
|
||||
// 如果 attached 中没有成功加载,在 ready 中再次尝试
|
||||
if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) {
|
||||
console.log('[TabBar] 在 ready 中重新加载配置')
|
||||
this.loadFeatureConfig()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 页面加载时也调用(兼容性更好)
|
||||
attached() {
|
||||
console.log('[TabBar] attached() 方法触发')
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载功能配置
|
||||
buildTabList(matchEnabled, tabUi, auditMode) {
|
||||
const audit = !!auditMode
|
||||
const ui = tabUi || {}
|
||||
const h = ui.home ? String(ui.home) : '首页'
|
||||
const ch = ui.chapters ? String(ui.chapters) : '目录'
|
||||
const ma = ui.match ? String(ui.match) : '找伙伴'
|
||||
const mo = ui.moments ? String(ui.moments) : ui.feed ? String(ui.feed) : '动态'
|
||||
const my = ui.my ? String(ui.my) : '我的'
|
||||
const tabs = [
|
||||
{
|
||||
pagePath: '/pages/index/index',
|
||||
text: h,
|
||||
iconPath: '/assets/icons/home.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/chapters/chapters',
|
||||
text: ch,
|
||||
iconPath: '/assets/icons/list.svg',
|
||||
isSpecial: false,
|
||||
},
|
||||
]
|
||||
if (matchEnabled && !audit) {
|
||||
tabs.push({
|
||||
pagePath: '/pages/match/match',
|
||||
text: ma,
|
||||
iconPath: '/assets/icons/partners.svg',
|
||||
isSpecial: true,
|
||||
})
|
||||
}
|
||||
if (!audit) {
|
||||
tabs.push({
|
||||
pagePath: '/pages/super-moments/super-moments',
|
||||
text: mo,
|
||||
iconPath: '/assets/icons/sparkles.svg',
|
||||
isSpecial: false,
|
||||
})
|
||||
}
|
||||
tabs.push({
|
||||
pagePath: '/pages/my/my',
|
||||
text: my,
|
||||
iconPath: '/assets/icons/user.svg',
|
||||
isSpecial: false,
|
||||
})
|
||||
return tabs
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
console.log('[TabBar] 开始加载功能配置...')
|
||||
console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
|
||||
|
||||
const res = await app.getConfig()
|
||||
|
||||
|
||||
// 兼容两种返回格式
|
||||
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 || {}))
|
||||
}
|
||||
|
||||
const tabUi = app.globalData.configCache?.mpConfig?.mpUi?.tabBar || {}
|
||||
const list = [...this.data.list]
|
||||
if (tabUi.home) list[0] = { ...list[0], text: String(tabUi.home) }
|
||||
if (tabUi.chapters) list[1] = { ...list[1], text: String(tabUi.chapters) }
|
||||
if (tabUi.match) list[2] = { ...list[2], text: String(tabUi.match) }
|
||||
if (tabUi.my) list[3] = { ...list[3], text: String(tabUi.my) }
|
||||
|
||||
this.setData({ matchEnabled, list }, () => {
|
||||
console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled)
|
||||
// 配置加载完成后,根据当前路由设置选中状态
|
||||
let matchEnabled = false
|
||||
if (res && res.success && res.features) {
|
||||
matchEnabled = res.features.matchEnabled === true
|
||||
} else if (res && res.configs && res.configs.feature_config) {
|
||||
matchEnabled = res.configs.feature_config.matchEnabled === true
|
||||
}
|
||||
|
||||
const tabUi = app.globalData.configCache?.mpConfig?.mpUi?.tabBar || {}
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (_) {}
|
||||
const auditMode = !!app.globalData.auditMode
|
||||
const tabList = this.buildTabList(matchEnabled, tabUi, auditMode)
|
||||
|
||||
this.setData({ matchEnabled, tabList }, () => {
|
||||
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 }, () => {
|
||||
|
||||
if (auditMode) {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const route = currentPage && currentPage.route
|
||||
if (route === 'pages/super-moments/super-moments' || route === 'pages/match/match') {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (__) {}
|
||||
const auditMode = !!app.globalData.auditMode
|
||||
const tabList = this.buildTabList(false, {}, auditMode)
|
||||
this.setData({ matchEnabled: false, tabList }, () => {
|
||||
this.updateSelected()
|
||||
})
|
||||
if (auditMode) {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const route = currentPage && currentPage.route
|
||||
if (route === 'pages/super-moments/super-moments' || route === 'pages/match/match') {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 根据当前路由更新选中状态
|
||||
updateSelected() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length === 0) return
|
||||
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const route = currentPage.route
|
||||
|
||||
const route = pages[pages.length - 1].route
|
||||
const tabList = this.data.tabList || []
|
||||
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
|
||||
for (let i = 0; i < tabList.length; i++) {
|
||||
const p = tabList[i].pagePath.replace(/^\//, '')
|
||||
if (route === p) {
|
||||
selected = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({ selected })
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const data = e.currentTarget.dataset
|
||||
const url = data.path
|
||||
const index = data.index
|
||||
|
||||
const url = e.currentTarget.dataset.path
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
if (Number.isNaN(index)) return
|
||||
if (this.data.selected === index) return
|
||||
|
||||
wx.switchTab({ url })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
<!--custom-tab-bar/index.wxml-->
|
||||
<view class="tab-bar {{matchEnabled ? 'tab-bar-four' : 'tab-bar-three'}}">
|
||||
<view class="tab-bar" wx:if="{{tabList.length}}">
|
||||
<view class="tab-bar-border"></view>
|
||||
|
||||
<!-- 首页 -->
|
||||
<view class="tab-bar-item" data-path="{{list[0].pagePath}}" data-index="0" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<image class="tab-icon {{selected === 0 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/home.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 0 ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 0 ? selectedColor : color}}">{{list[0].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录 -->
|
||||
<view class="tab-bar-item" data-path="{{list[1].pagePath}}" data-index="1" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<image class="tab-icon {{selected === 1 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/list.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 1 ? selectedColor : color}}"></image>
|
||||
<block wx:for="{{tabList}}" wx:key="pagePath">
|
||||
<view
|
||||
wx:if="{{item.isSpecial}}"
|
||||
class="tab-bar-item special-item"
|
||||
data-path="{{item.pagePath}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="special-button {{selected === index ? 'special-active' : ''}}">
|
||||
<image class="special-icon" src="{{item.iconPath}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text special-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 找伙伴 - 中间突出按钮(根据配置显示) -->
|
||||
<view class="tab-bar-item special-item" wx:if="{{matchEnabled}}" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
|
||||
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
|
||||
<image class="special-icon"
|
||||
src="/assets/icons/partners.svg"
|
||||
mode="aspectFit"></image>
|
||||
<view
|
||||
wx:else
|
||||
class="tab-bar-item"
|
||||
data-path="{{item.pagePath}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="icon-wrapper">
|
||||
<image
|
||||
class="tab-icon {{selected === index ? 'icon-active' : ''}}"
|
||||
src="{{item.iconPath}}"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
|
||||
</view>
|
||||
<view class="tab-bar-text special-text" style="color: {{selected === 2 ? selectedColor : color}}">{{list[2].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的 -->
|
||||
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="{{matchEnabled ? 3 : 2}}" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<image class="tab-icon {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/user.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}">{{list[3].text}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { resolveChooseAvatarFilePath, pickLocalAvatarImagePath } = require('../../utils/util.js')
|
||||
const { uploadByApi } = require('../../utils/miniprogramUpload.js')
|
||||
|
||||
function isPlaceholderNickname(n) {
|
||||
const s = (n || '').trim()
|
||||
@@ -133,34 +135,38 @@ Page({
|
||||
fail: () => {},
|
||||
})
|
||||
},
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
onTapPickAvatar() {
|
||||
trackClick('avatar_nickname', 'btn_click', '选择头像')
|
||||
await this.uploadAndSaveAvatar(tempAvatarUrl)
|
||||
const run = () => {
|
||||
pickLocalAvatarImagePath()
|
||||
.then((p) => this.uploadAndSaveAvatar(p))
|
||||
.catch((err) => {
|
||||
if (err && err.cancelled) return
|
||||
wx.showToast({ title: (err && err.message) || '选择图片失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
if (typeof wx.requirePrivacyAuthorize === 'function') {
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: run,
|
||||
fail: () => wx.showToast({ title: '需同意隐私指引后再换头像', icon: 'none' }),
|
||||
})
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
},
|
||||
|
||||
async uploadAndSaveAvatar(tempPath) {
|
||||
async uploadAndSaveAvatar(tempUrl) {
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch {
|
||||
reject(new Error('解析失败'))
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
let filePath
|
||||
try {
|
||||
filePath = await resolveChooseAvatarFilePath(tempUrl)
|
||||
} catch (readErr) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: readErr.message || '无法读取所选图片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const uploadRes = await uploadByApi({ filePath, folder: 'avatars' })
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
|
||||
@@ -18,19 +18,18 @@
|
||||
<text class="guide-desc guide-desc-sub" wx:if="{{fromNewUser}}">完成后即可继续使用小程序。</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像:点击直接弹出微信原生选择器;新用户必填 -->
|
||||
<!-- 头像:相册/相机选图后上传(避免开发者工具 chooseAvatar + http://tmp 渲染报错) -->
|
||||
<view class="avatar-section {{uiFocus === 'avatar' ? 'section-focus' : ''}}">
|
||||
<text class="form-label form-label-row" wx:if="{{fromNewUser}}">头像<text class="required-mark">*</text></text>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap-btn" hover-class="none" bindtap="onTapPickAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<!-- 无头像时用不透明层遮住微信在 chooseAvatar 按钮内可能绘制的默认头像(非 data 回显) -->
|
||||
<view wx:if="{{!avatar}}" class="avatar-placeholder avatar-native-mask">?</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称:隐私授权由 app.js onNeedPrivacyAuthorization 走 wx.requirePrivacyAuthorize 原生流程 -->
|
||||
|
||||
@@ -99,14 +99,13 @@
|
||||
border-color: rgba(94, 234, 212, 0.55);
|
||||
box-shadow: 0 0 0 2rpx rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
/* 头像点击区:view + chooseMedia(原 chooseAvatar button) */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
padding: 0; margin: 0; background: transparent;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 192rpx;
|
||||
@@ -138,7 +137,7 @@
|
||||
color: #5EEAD4;
|
||||
background: rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
/* 盖住 chooseAvatar 原生层默认头像,避免误以为是业务「回显」 */
|
||||
/* 无头像时占位层,避免圆内误显为「已有头像」 */
|
||||
.avatar-native-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -452,6 +452,9 @@
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.part-right {
|
||||
|
||||
@@ -41,12 +41,17 @@ Page({
|
||||
wx.showToast({ title: '请输入11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const pwd = String(password || '').trim()
|
||||
if (pwd.length < 6) {
|
||||
wx.showToast({ title: '密码至少6位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/dev/login-by-phone', {
|
||||
method: 'POST',
|
||||
data: { phone, password: password || '' }
|
||||
data: { phone, password: pwd }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
|
||||
@@ -14,7 +14,19 @@
|
||||
|
||||
.form-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.form-item { margin-bottom: 32rpx; }
|
||||
.form-item:last-of-type { margin-bottom: 48rpx; }
|
||||
.form-item:last-of-type { margin-bottom: 24rpx; }
|
||||
.login-forgot-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
font-size: 26rpx;
|
||||
color: #00ced1;
|
||||
text-decoration: underline;
|
||||
margin: 0 0 32rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.form-label { font-size: 28rpx; color: rgba(255,255,255,0.8); display: block; margin-bottom: 16rpx; }
|
||||
.form-input-wrap { padding: 16rpx 24rpx; background: #1F2937; border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; }
|
||||
.form-input-inner { width: 100%; font-size: 28rpx; background: transparent; color: #fff; }
|
||||
|
||||
@@ -23,12 +23,21 @@ Page({
|
||||
// 创建态
|
||||
isCreateMode: false,
|
||||
giftQuantity: 1,
|
||||
unitPrice: 0
|
||||
unitPrice: 0,
|
||||
auditMode: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (e) {}
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
if (options?.ref || options?.referralCode) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
@@ -112,6 +121,10 @@ Page({
|
||||
|
||||
// 发起人支付(改造后:我帮别人付款)
|
||||
async doInitiatorPay() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持代付支付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.data.isSinglePageMode) {
|
||||
wx.showModal({
|
||||
title: '朋友圈单页',
|
||||
|
||||
@@ -114,8 +114,11 @@
|
||||
<view wx:if="{{isSinglePageMode}}" class="footer-tip-single">
|
||||
<text>请点击底部「前往小程序」进入完整版后再操作</text>
|
||||
</view>
|
||||
<view wx:elif="{{auditMode && (isCreateMode || (isInitiator && detail.action === 'pay'))}}" class="footer-tip-single">
|
||||
<text>审核中,暂不支持代付支付</text>
|
||||
</view>
|
||||
<!-- 发起人 创建态 或 action=pay:去支付 -->
|
||||
<button wx:elif="{{(isCreateMode || (isInitiator && detail.action === 'pay'))}}" class="footer-btn pay-btn" bindtap="doInitiatorPay" disabled="{{paying}}">
|
||||
<button wx:elif="{{!auditMode && (isCreateMode || (isInitiator && detail.action === 'pay'))}}" class="footer-btn pay-btn" bindtap="doInitiatorPay" disabled="{{paying}}">
|
||||
<image class="btn-icon" src="/assets/icons/wallet.svg" mode="aspectFit"/>
|
||||
<text>{{paying ? '支付中...' : (isCreateMode ? '去支付' : '立即支付')}}</text>
|
||||
</button>
|
||||
|
||||
@@ -10,11 +10,21 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
async onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options && (options.ref || options.referralCode)) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (e) {}
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
|
||||
@@ -12,8 +12,18 @@ Page({
|
||||
remaining: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
async onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (e) {}
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
const requestSn = (options.requestSn || '').trim()
|
||||
if (!requestSn) {
|
||||
wx.showToast({ title: '链接无效', icon: 'none' })
|
||||
|
||||
@@ -283,6 +283,12 @@ Page({
|
||||
micDefaultGiftId: '',
|
||||
/** 右上角唯一入口:由置顶超级个体决定 */
|
||||
headerCornerAction: { show: false, kind: '', label: '', emoji: '', avatar: '' },
|
||||
|
||||
/** 文旅/落地体验链:未登录时弹一键登录,成功后继续打赏/上麦/留资 */
|
||||
showLoginModal: false,
|
||||
showPrivacyModal: false,
|
||||
loginModalDesc: '登录后可参与打赏、上麦与链接嘉宾',
|
||||
pendingAfterLogin: null,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -742,18 +748,67 @@ Page({
|
||||
this._openHorizontalGiftModal(a.kind === 'mic' ? 'live_mic' : 'home_reward')
|
||||
},
|
||||
|
||||
/**
|
||||
* 体验环节未登录:弹公用 login-modal(单页模式仍引导「前往小程序」)
|
||||
* @param {{ type: 'tip', tipSource: string } | { type: 'link_karuo' }} pending
|
||||
*/
|
||||
_showHomeLoginModal(pending, desc) {
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为单页模式,登录与支付需点击底部「前往小程序」进入完整版。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Index] 检测单页模式失败,回退为登录弹窗:', e)
|
||||
}
|
||||
this.setData({
|
||||
pendingAfterLogin: pending,
|
||||
loginModalDesc: desc || '登录后可参与打赏、上麦与链接嘉宾',
|
||||
showLoginModal: true,
|
||||
})
|
||||
},
|
||||
|
||||
onLoginModalClose() {
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false, pendingAfterLogin: null })
|
||||
},
|
||||
onLoginModalPrivacyAgree() {
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
onLoginModalSuccess() {
|
||||
const pending = this.data.pendingAfterLogin
|
||||
this.setData({ showLoginModal: false, pendingAfterLogin: null })
|
||||
this.updateUserStatus()
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
if (!pending) return
|
||||
const run = (fn) => {
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(fn)
|
||||
else setTimeout(fn, 0)
|
||||
}
|
||||
if (pending.type === 'tip' && pending.tipSource) {
|
||||
run(() => this._openHorizontalGiftModal(pending.tipSource))
|
||||
} else if (pending.type === 'link_karuo') {
|
||||
run(() => this.onLinkKaruo())
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开横排礼物弹窗(上麦/打赏统一入口) */
|
||||
_openHorizontalGiftModal(tipSource) {
|
||||
if (app.globalData.auditMode) return
|
||||
const label = tipSource === 'live_mic' ? '申请上麦' : '打赏支持'
|
||||
trackClick('home', tipSource === 'live_mic' ? 'live_mic_request' : 'btn_click', label)
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: `请先登录后再${label}`,
|
||||
confirmText: '去登录',
|
||||
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) },
|
||||
})
|
||||
const desc =
|
||||
tipSource === 'live_mic'
|
||||
? '登录后可申请上麦并完成礼遇支付'
|
||||
: '登录后可打赏支持'
|
||||
this._showHomeLoginModal({ type: 'tip', tipSource }, desc)
|
||||
return
|
||||
}
|
||||
const sch = this.data.liveMicSchedule
|
||||
@@ -887,6 +942,10 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
trackClick('home', 'btn_click', '加入创业派对')
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
@@ -895,6 +954,10 @@ Page({
|
||||
const pinned = this.data.homePinnedPerson
|
||||
if (!pinned || !pinned.token) return
|
||||
trackClick('home', 'btn_click', '置顶@人物留资')
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
this._showHomeLoginModal({ type: 'link_karuo' }, '登录后可向嘉宾留下联系方式')
|
||||
return
|
||||
}
|
||||
await submitCkbLead(getApp(), {
|
||||
targetUserId: pinned.token,
|
||||
targetNickname: pinned.name,
|
||||
|
||||
@@ -223,6 +223,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 体验环节一键登录(与阅读页/我的共用组件) -->
|
||||
<login-modal
|
||||
show="{{showLoginModal}}"
|
||||
desc="{{loginModalDesc}}"
|
||||
showPrivacyModal="{{showPrivacyModal}}"
|
||||
showCancel="{{true}}"
|
||||
bind:close="onLoginModalClose"
|
||||
bind:success="onLoginModalSuccess"
|
||||
bind:privacyagree="onLoginModalPrivacyAgree"
|
||||
/>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
const app = getApp()
|
||||
|
||||
function safeDecode(value) {
|
||||
try {
|
||||
return decodeURIComponent(value || '')
|
||||
} catch (_) {
|
||||
return String(value || '')
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHttpUrl(raw) {
|
||||
let s = String(raw || '').trim()
|
||||
if (!s) return ''
|
||||
if (/^\/\//.test(s)) return 'https:' + s
|
||||
if (!/^https?:\/\//i.test(s)) s = 'https://' + s.replace(/^\/+/, '')
|
||||
return s
|
||||
}
|
||||
|
||||
// 为外链补充移动端提示参数:目标站支持时按手机网页输出
|
||||
function appendMobileHints(rawUrl) {
|
||||
const url = normalizeHttpUrl(rawUrl)
|
||||
if (!url) return ''
|
||||
const hasQuery = url.indexOf('?') >= 0
|
||||
const hasFrom = /(?:\?|&)from=/i.test(url)
|
||||
const hasView = /(?:\?|&)view=/i.test(url)
|
||||
const extras = []
|
||||
if (!hasFrom) extras.push('from=miniprogram')
|
||||
if (!hasView) extras.push('view=mobile')
|
||||
if (!extras.length) return url
|
||||
return url + (hasQuery ? '&' : '?') + extras.join('&')
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
url: '',
|
||||
@@ -10,8 +40,9 @@ Page({
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const url = decodeURIComponent(options.url || '')
|
||||
const title = options.title ? decodeURIComponent(options.title) : '链接预览'
|
||||
const rawUrl = safeDecode(options.url || '')
|
||||
const url = appendMobileHints(rawUrl)
|
||||
const title = options.title ? safeDecode(options.title) : '链接预览'
|
||||
this.setData({
|
||||
url,
|
||||
title,
|
||||
|
||||
@@ -94,9 +94,16 @@ Page({
|
||||
|
||||
async onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (_) {}
|
||||
if (app.globalData.auditMode) {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: app.globalData.auditMode || false
|
||||
auditMode: false,
|
||||
})
|
||||
await this.loadMatchConfig()
|
||||
this.loadStoredContact()
|
||||
@@ -104,13 +111,28 @@ Page({
|
||||
await this.refreshMatchQuotaFromServer()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
async onShow() {
|
||||
try {
|
||||
await app.getAuditMode()
|
||||
} catch (_) {}
|
||||
if (app.globalData.auditMode) {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
tabBar.setData({ selected: 2 })
|
||||
const tabList = tabBar.data.tabList || []
|
||||
let sel = 0
|
||||
for (let i = 0; i < tabList.length; i++) {
|
||||
if (tabList[i].pagePath.replace(/^\//, '') === 'pages/match/match') {
|
||||
sel = i
|
||||
break
|
||||
}
|
||||
}
|
||||
tabBar.setData({ selected: sel })
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
@@ -692,7 +714,11 @@ Page({
|
||||
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
const resp = e && e.response
|
||||
wx.showToast({
|
||||
title: (resp && (resp.message || resp.error)) || (e && e.message) || '网络异常,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
this.setData({ isJoining: false })
|
||||
}
|
||||
@@ -717,6 +743,11 @@ Page({
|
||||
// 购买匹配次数
|
||||
async buyMatchCount() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持购买', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const openId = await app.ensurePayOpenId()
|
||||
|
||||
@@ -17,8 +17,67 @@ const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||
/** 从「我的」登录成功后 reLaunch 回本页并自动继续链接流程 */
|
||||
const LOGIN_RESUME_MEMBER_DETAIL_KEY = 'login_resume_member_detail_id'
|
||||
|
||||
const THUMB_MAX = 3
|
||||
|
||||
function parseImagesField(images) {
|
||||
if (images == null) return []
|
||||
if (typeof images === 'string') {
|
||||
const s = images.trim()
|
||||
if (!s) return []
|
||||
try {
|
||||
const p = JSON.parse(s)
|
||||
return Array.isArray(p) ? p : []
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return Array.isArray(images) ? images : []
|
||||
}
|
||||
|
||||
function normalizeArticleImageSrc(raw) {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === 'undefined' || u === 'null') return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return `https:${u}`
|
||||
if (u.startsWith('wxfile://') || u.startsWith('cloud://')) return u
|
||||
const base = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
if (u.startsWith('/') && base) return `${base}${u}`
|
||||
return u
|
||||
}
|
||||
|
||||
function buildArticleThumbSlots(imagesField) {
|
||||
const rawList = parseImagesField(imagesField)
|
||||
const n = Math.min(rawList.length, THUMB_MAX)
|
||||
const thumbSlots = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const srcNorm = normalizeArticleImageSrc(String(rawList[i] || '').trim())
|
||||
const ok =
|
||||
!!srcNorm &&
|
||||
(/^https?:\/\//i.test(srcNorm) ||
|
||||
srcNorm.startsWith('wxfile://') ||
|
||||
srcNorm.startsWith('cloud://'))
|
||||
thumbSlots.push({ slotIdx: i, src: ok ? srcNorm : '', loadFailed: false })
|
||||
}
|
||||
return {
|
||||
showThumbRow: rawList.length > 0,
|
||||
thumbSlots,
|
||||
thumbMoreCount: rawList.length > THUMB_MAX ? rawList.length - THUMB_MAX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false, auditMode: false },
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarTotalPx: 88,
|
||||
member: null,
|
||||
loading: true,
|
||||
isOwnProfile: false,
|
||||
auditMode: false,
|
||||
articleList: [],
|
||||
articleLoading: false,
|
||||
memberBizTab: 'profile',
|
||||
showMemberBizTabs: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
@@ -33,6 +92,10 @@ Page({
|
||||
if (options.id) this.loadMember(options.id)
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
},
|
||||
|
||||
/** 朋友圈单页等受限环境 */
|
||||
_isSinglePageMode() {
|
||||
try {
|
||||
@@ -136,6 +199,8 @@ Page({
|
||||
}),
|
||||
loading: false
|
||||
})
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
}
|
||||
@@ -148,6 +213,8 @@ Page({
|
||||
const d = Array.isArray(res.data) ? res.data[0] : res.data
|
||||
if (d) {
|
||||
this.setData({ member: this.enrichAndFormat(d), loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(d.id || id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
}
|
||||
@@ -174,12 +241,15 @@ Page({
|
||||
helpNeed: u.helpNeed || u.help_need,
|
||||
ckbLeadToken: u.ckbLeadToken || u.ckb_lead_token,
|
||||
}), loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(u.id || id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
this.setData({ loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this._resumeLinkAfterLoad = false
|
||||
},
|
||||
|
||||
@@ -285,7 +355,14 @@ Page({
|
||||
content: '您的免费解锁次数已用完,开通VIP会员(¥1980/年)可无限解锁',
|
||||
confirmText: '去开通',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持购买', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
}
|
||||
})
|
||||
return false
|
||||
},
|
||||
@@ -597,7 +674,117 @@ Page({
|
||||
},
|
||||
|
||||
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
|
||||
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
|
||||
goToVip() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
goToSuperArticleDetail(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/read/read?superArticleId=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
/** 有档案块(职业画像/擅长/故事/互助)或动态(含加载中)时展示 Tab 区 */
|
||||
_syncMemberBizTabs() {
|
||||
const m = this.data.member
|
||||
if (!m) {
|
||||
this.setData({ showMemberBizTabs: false })
|
||||
return
|
||||
}
|
||||
const hasCareer = !!(m.industry || m.position || m.businessScale)
|
||||
const hasProfileExtra = !!(
|
||||
m.skills ||
|
||||
m.bestMonth ||
|
||||
m.achievement ||
|
||||
m.turningPoint ||
|
||||
m.canHelp ||
|
||||
m.needHelp
|
||||
)
|
||||
const profileOk = hasCareer || hasProfileExtra
|
||||
const hasArticles = (this.data.articleList || []).length > 0
|
||||
const loading = !!this.data.articleLoading
|
||||
const show = profileOk || hasArticles || loading
|
||||
if (!show) {
|
||||
this.setData({ showMemberBizTabs: false })
|
||||
return
|
||||
}
|
||||
let tab = this.data.memberBizTab || 'profile'
|
||||
if (tab !== 'profile' && tab !== 'dynamics') tab = 'profile'
|
||||
const dynamicsOk = hasArticles || loading
|
||||
if (tab === 'profile' && !profileOk && dynamicsOk) tab = 'dynamics'
|
||||
else if (tab === 'dynamics' && !dynamicsOk && profileOk) tab = 'profile'
|
||||
this.setData({ showMemberBizTabs: true, memberBizTab: tab })
|
||||
},
|
||||
|
||||
switchMemberBizTab(e) {
|
||||
const tab = e.currentTarget?.dataset?.tab
|
||||
if (tab !== 'dynamics' && tab !== 'profile') return
|
||||
this.setData({ memberBizTab: tab })
|
||||
},
|
||||
|
||||
onMemberArticleThumbError(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
const slotIdx = Number(e.currentTarget.dataset.slotidx)
|
||||
if (Number.isNaN(rowIdx) || Number.isNaN(slotIdx)) return
|
||||
this.setData({ [`articleList[${rowIdx}].thumbSlots[${slotIdx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewMemberArticleImages(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
if (Number.isNaN(rowIdx)) return
|
||||
const row = this.data.articleList[rowIdx]
|
||||
if (!row) return
|
||||
const urls = parseImagesField(row.images)
|
||||
.map((u) => normalizeArticleImageSrc(String(u || '').trim()))
|
||||
.filter((u) => /^https?:\/\//i.test(u))
|
||||
if (!urls.length) return
|
||||
const current = String(e.currentTarget.dataset.current || '').trim() || urls[0]
|
||||
wx.previewImage({ urls, current })
|
||||
},
|
||||
|
||||
async loadMemberArticles(memberId) {
|
||||
if (!memberId) return
|
||||
this.setData({ articleLoading: true, articleList: [] })
|
||||
this._syncMemberBizTabs()
|
||||
try {
|
||||
const uid = app.globalData.userInfo?.id
|
||||
const mine = !!(this.data.isOwnProfile && uid && String(memberId) === String(uid))
|
||||
const url = mine
|
||||
? `/api/miniprogram/super/articles/mine?userId=${encodeURIComponent(String(memberId))}&page=1&pageSize=10`
|
||||
: `/api/miniprogram/super/articles?authorUserId=${encodeURIComponent(String(memberId))}&page=1&pageSize=10`
|
||||
const res = await app.request({
|
||||
url,
|
||||
silent: true
|
||||
})
|
||||
const listRaw = Array.isArray(res?.list) ? res.list : []
|
||||
const list = listRaw.map((it) => {
|
||||
const st = String(it.auditStatus || 'approved').toLowerCase()
|
||||
let auditLabel = ''
|
||||
if (mine) {
|
||||
if (st === 'pending') auditLabel = '审核中'
|
||||
else if (st === 'rejected') auditLabel = '已驳回'
|
||||
else auditLabel = '已发布'
|
||||
}
|
||||
const norm = st === 'pending' || st === 'rejected' ? st : 'approved'
|
||||
const thumb = buildArticleThumbSlots(it.images)
|
||||
return Object.assign({}, it, {
|
||||
auditStatus: mine ? norm : '',
|
||||
auditLabel,
|
||||
showThumbRow: thumb.showThumbRow,
|
||||
thumbSlots: thumb.thumbSlots,
|
||||
thumbMoreCount: thumb.thumbMoreCount,
|
||||
})
|
||||
})
|
||||
this.setData({ articleList: list, articleLoading: false })
|
||||
this._syncMemberBizTabs()
|
||||
} catch (_) {
|
||||
this.setData({ articleLoading: false, articleList: [] })
|
||||
this._syncMemberBizTabs()
|
||||
}
|
||||
},
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
/**
|
||||
|
||||
@@ -110,86 +110,158 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一体化信息区(单卡片内分区) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp || member.project}}">
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
<!-- 档案 | 动态(Tab:档案含职业画像/擅长/故事/互助) -->
|
||||
<view class="article-spotlight member-biz-tab-card" wx:if="{{showMemberBizTabs}}">
|
||||
<view class="mdb-tab-bar">
|
||||
<view
|
||||
class="mdb-tab {{memberBizTab === 'profile' ? 'mdb-tab-active' : ''}}"
|
||||
data-tab="profile"
|
||||
bindtap="switchMemberBizTab"
|
||||
>
|
||||
<text>{{isOwnProfile ? '我的档案' : 'TA档案'}}</text>
|
||||
</view>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
</view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="mdb-tab {{memberBizTab === 'dynamics' ? 'mdb-tab-active' : ''}}"
|
||||
data-tab="dynamics"
|
||||
bindtap="switchMemberBizTab"
|
||||
>
|
||||
<text>{{isOwnProfile ? '我的动态' : 'TA动态'}}</text>
|
||||
<text class="mdb-tab-badge" wx:if="{{articleList.length}}">{{articleList.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.industry || member.position || member.businessScale) && member.skills}}"></view>
|
||||
|
||||
<!-- 核心能力 -->
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<!-- 互助 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
<!-- 档案:职业画像 + 我擅长 + 个人故事 + 互助需求 -->
|
||||
<view class="mdb-panel mdb-panel-profile" wx:if="{{memberBizTab === 'profile'}}">
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
</view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mono-divider mdb-inner-divider"
|
||||
wx:if="{{(member.industry || member.position || member.businessScale) && (member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp)}}"
|
||||
></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mono-divider mdb-inner-divider"
|
||||
wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp)}}"
|
||||
></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider mdb-inner-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互动需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:if="{{!(member.industry || member.position || member.businessScale) && !member.skills && !(member.bestMonth || member.achievement || member.turningPoint) && !(member.canHelp || member.needHelp)}}"
|
||||
class="article-spotlight-empty mdb-panel-hint"
|
||||
>
|
||||
<text>暂未填写档案资料</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.canHelp || member.needHelp) && member.project}}"></view>
|
||||
<view class="mdb-panel" wx:if="{{memberBizTab === 'dynamics'}}">
|
||||
<view wx:if="{{articleLoading}}" class="article-spotlight-empty mdb-panel-hint"><text>加载中…</text></view>
|
||||
<view wx:elif="{{articleList.length > 0}}" class="article-spotlight-list">
|
||||
<view
|
||||
class="article-spotlight-item"
|
||||
wx:for="{{articleList}}"
|
||||
wx:for-index="rowIdx"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="goToSuperArticleDetail"
|
||||
>
|
||||
<view wx:if="{{isOwnProfile && item.auditStatus}}" class="article-audit-pill article-audit-{{item.auditStatus}}">{{item.auditLabel}}</view>
|
||||
<text class="article-spotlight-item-title">{{item.title}}</text>
|
||||
<text class="article-spotlight-item-preview">{{item.preview || item.content}}</text>
|
||||
<view class="mdb-thumb-row" wx:if="{{item.showThumbRow}}" catchtap="">
|
||||
<view class="mdb-thumb-cell" wx:for="{{item.thumbSlots}}" wx:for-item="th" wx:key="slotIdx">
|
||||
<block wx:if="{{th.src && !th.loadFailed}}">
|
||||
<image
|
||||
class="mdb-thumb-img"
|
||||
mode="aspectFill"
|
||||
src="{{th.src}}"
|
||||
data-rowidx="{{rowIdx}}"
|
||||
data-slotidx="{{index}}"
|
||||
data-current="{{th.src}}"
|
||||
binderror="onMemberArticleThumbError"
|
||||
bindtap="previewMemberArticleImages"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="mdb-thumb-ph">
|
||||
<text class="mdb-thumb-ph-text">{{th.loadFailed ? '加载失败' : '无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.thumbMoreCount > 0}}" class="mdb-thumb-more">
|
||||
<text>+{{item.thumbMoreCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="article-spotlight-empty mdb-panel-hint"><text>暂无动态</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.project}}">
|
||||
<!-- 项目介绍(单独卡片) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.project}}">
|
||||
<view class="mono-sec mono-sec-tight">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
@@ -200,7 +272,7 @@
|
||||
<!-- 底部:分享 + 双入口(同一视觉块) -->
|
||||
<view class="footer-panel">
|
||||
<view class="footer-pills">
|
||||
<view class="pill pill-gold" bindtap="goToVip">
|
||||
<view class="pill pill-gold" wx:if="{{!auditMode}}" bindtap="goToVip">
|
||||
<icon name="sparkles" size="30" color="#FBBF24" customClass="pill-ic"></icon>
|
||||
<text class="pill-txt">成为超级个体</text>
|
||||
</view>
|
||||
|
||||
@@ -417,6 +417,188 @@
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.mono-sec-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.article-spotlight {
|
||||
margin: 20rpx 24rpx 0;
|
||||
padding: 22rpx 20rpx;
|
||||
border-radius: 22rpx;
|
||||
border: 1rpx solid rgba(34, 211, 238, 0.45);
|
||||
background: linear-gradient(135deg, rgba(8, 47, 73, 0.9), rgba(15, 23, 42, 0.95));
|
||||
box-shadow: 0 10rpx 28rpx rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
.article-spotlight-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.article-spotlight-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #67e8f9;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.article-spotlight-count {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
color: #cffafe;
|
||||
border: 1rpx solid rgba(103, 232, 249, 0.45);
|
||||
background: rgba(14, 116, 144, 0.35);
|
||||
}
|
||||
|
||||
/* Tab:动态 | 职业画像 */
|
||||
.member-biz-tab-card {
|
||||
padding-top: 18rpx;
|
||||
}
|
||||
.mdb-tab-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.mdb-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(203, 213, 225, 0.75);
|
||||
background: rgba(2, 6, 23, 0.35);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
.mdb-tab-active {
|
||||
color: #67e8f9;
|
||||
border-color: rgba(103, 232, 249, 0.45);
|
||||
background: rgba(14, 116, 144, 0.28);
|
||||
}
|
||||
.mdb-tab-badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #cffafe;
|
||||
border: 1rpx solid rgba(103, 232, 249, 0.35);
|
||||
background: rgba(14, 116, 144, 0.35);
|
||||
}
|
||||
.mdb-panel-hint {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
.mdb-inner-divider {
|
||||
margin: 14rpx 4rpx;
|
||||
}
|
||||
.mdb-thumb-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.mdb-thumb-cell {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
.mdb-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
}
|
||||
.mdb-thumb-ph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mdb-thumb-ph-text {
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.mdb-thumb-more {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(34, 211, 238, 0.88);
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.article-spotlight-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.article-spotlight-empty {
|
||||
padding: 16rpx 14rpx 8rpx;
|
||||
color: rgba(203, 213, 225, 0.8);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.article-spotlight-item {
|
||||
padding: 14rpx 14rpx;
|
||||
border-radius: 14rpx;
|
||||
background: rgba(2, 6, 23, 0.45);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.article-audit-pill {
|
||||
display: inline-block;
|
||||
margin-bottom: 8rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 18rpx;
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.25);
|
||||
color: rgba(226, 232, 240, 0.85);
|
||||
}
|
||||
.article-audit-pill.article-audit-approved {
|
||||
color: #5eead4;
|
||||
border-color: rgba(94, 234, 212, 0.35);
|
||||
}
|
||||
.article-audit-pill.article-audit-pending {
|
||||
color: #fcd34d;
|
||||
border-color: rgba(252, 211, 77, 0.35);
|
||||
}
|
||||
.article-audit-pill.article-audit-rejected {
|
||||
color: #fca5a5;
|
||||
border-color: rgba(252, 165, 165, 0.35);
|
||||
}
|
||||
.article-spotlight-item-title {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.article-spotlight-item-preview {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: rgba(203, 213, 225, 0.86);
|
||||
font-size: 21rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.member-article-tag {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.35);
|
||||
background: rgba(45, 212, 191, 0.08);
|
||||
color: #67e8f9;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.mono-divider {
|
||||
height: 1rpx;
|
||||
margin: 4rpx 28rpx;
|
||||
@@ -566,6 +748,34 @@
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.member-article-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.member-article-item {
|
||||
padding: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.12);
|
||||
}
|
||||
.member-article-title {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.member-article-preview {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: rgba(203, 213, 225, 0.85);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.member-article-empty {
|
||||
color: rgba(148, 163, 184, 0.9);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* —— 底栏:分享 + 双入口 —— */
|
||||
.footer-panel {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 联系导师按钮 → 弹出 v2 弹窗(选择咨询项目)
|
||||
*/
|
||||
const app = getApp()
|
||||
const soulBridge = require('../../utils/soulBridge')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -14,13 +15,21 @@ Page({
|
||||
selectedType: '',
|
||||
selectedAmount: 0,
|
||||
creating: false,
|
||||
auditMode: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: !!app.globalData.auditMode,
|
||||
})
|
||||
if (options.id) this.loadDetail(options.id)
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
},
|
||||
|
||||
async loadDetail(id) {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
@@ -59,6 +68,10 @@ Page({
|
||||
},
|
||||
|
||||
onContactTap() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法支付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.data.mentor || this.data.consultOptions.length === 0) {
|
||||
wx.showToast({ title: '暂无咨询项目', icon: 'none' })
|
||||
return
|
||||
@@ -79,7 +92,17 @@ Page({
|
||||
this.setData({ selectedType: item.type, selectedAmount: item.price })
|
||||
},
|
||||
|
||||
_consultationTypeLabel(t) {
|
||||
const m = { single: '单次咨询', half_year: '半年咨询', year: '年度咨询' }
|
||||
return m[t] || '咨询'
|
||||
},
|
||||
|
||||
async onConfirmConsult() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法支付', icon: 'none' })
|
||||
this.setData({ showConsultModal: false, creating: false })
|
||||
return
|
||||
}
|
||||
const { mentor, selectedType } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
@@ -94,22 +117,57 @@ Page({
|
||||
method: 'POST',
|
||||
data: { userId, consultationType: selectedType },
|
||||
})
|
||||
if (res?.success && res.data) {
|
||||
this.setData({ showConsultModal: false, creating: false })
|
||||
wx.showToast({ title: '预约创建成功', icon: 'success' })
|
||||
// TODO: 调起支付 productType: mentor_consultation, productId: res.data.id
|
||||
if (!res?.success || !res.data) {
|
||||
wx.showToast({ title: res?.error || '创建失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ showConsultModal: false })
|
||||
const consult = res.data
|
||||
const openId = await app.ensurePayOpenId()
|
||||
if (!openId) {
|
||||
wx.showModal({
|
||||
title: '预约成功',
|
||||
content: '请联系客服完成后续对接',
|
||||
title: '无法拉起支付',
|
||||
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
|
||||
showCancel: false,
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '创建失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '正在拉起支付...', mask: true })
|
||||
try {
|
||||
const label = this._consultationTypeLabel(selectedType)
|
||||
const payRes = await app.request('/api/miniprogram/pay', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
openId,
|
||||
userId,
|
||||
productType: 'mentor_consultation',
|
||||
productId: String(consult.id),
|
||||
amount: Number(consult.amount),
|
||||
description: `导师咨询 · ${(mentor && mentor.name) || '导师'}(${label})`,
|
||||
referralCode: soulBridge.getReferralCodeForPay(app) || undefined,
|
||||
},
|
||||
})
|
||||
if (payRes?.success && payRes.data?.payParams) {
|
||||
try {
|
||||
await soulBridge.requestWxJsapiPayment(payRes.data.payParams)
|
||||
await soulBridge.syncOrderStatusQuery(app, payRes.data.orderSn)
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
} catch (pe) {
|
||||
const msg = pe && pe.errMsg ? String(pe.errMsg) : ''
|
||||
if (msg.indexOf('cancel') !== -1) wx.showToast({ title: '支付取消', icon: 'none' })
|
||||
else wx.showToast({ title: '支付未完成', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
wx.showToast({ title: payRes?.error || '获取支付参数失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '创建失败', icon: 'none' })
|
||||
wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ creating: false })
|
||||
}
|
||||
this.setData({ creating: false })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<text class="block-text">{{mentor.offering}}</text>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.priceSingle || mentor.priceHalfYear || mentor.priceYear}}">
|
||||
<view class="block" wx:if="{{(mentor.priceSingle || mentor.priceHalfYear || mentor.priceYear) && !auditMode}}">
|
||||
<view class="block-header"><text class="block-num">03</text><text class="block-title">收费标准</text></view>
|
||||
<view class="price-table">
|
||||
<view class="price-thead">
|
||||
@@ -72,7 +72,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-btn-area">
|
||||
<view class="bottom-btn-area" wx:if="{{!auditMode}}">
|
||||
<view class="contact-btn" bindtap="onContactTap">
|
||||
<icon name="message-circle" size="40" color="#00CED1" customClass="contact-icon"></icon>
|
||||
<text>联系导师</text>
|
||||
|
||||
@@ -10,6 +10,11 @@ const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
const { navigateMpPath } = require('../../utils/mpNavigate.js')
|
||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
const { appendQueryToPath, ensureLoggedInUserPhone } = require('../../utils/linkedMiniprogramNavigate.js')
|
||||
|
||||
/** MBTI 测试小程序(写死;须与 app.json navigateToMiniProgramAppIdList 一致) */
|
||||
const MBTI_MP_APP_ID = 'wx3d15ed02e98b04e3'
|
||||
const MBTI_MP_PATH = 'pages/index/index'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -86,8 +91,13 @@ Page({
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 超级个体:快捷入口「客资 / 链接与轨迹」
|
||||
// 超级个体:快捷入口「链接轨迹」
|
||||
showSuperCrmEntry: false,
|
||||
// 超级个体:发动态入口
|
||||
showSuperArticleEntry: false,
|
||||
|
||||
/** 快捷入口网格列数:与 WXML 四个 wx:if 一致,≤4 时等于入口数(三项铺满)、否则为 4(自动换行) */
|
||||
quickEntryGridCols: 3,
|
||||
|
||||
// mp_config.mpUi.myPage(后台可改文案/跳转)
|
||||
mpUiCardLabel: '名片',
|
||||
@@ -95,6 +105,8 @@ Page({
|
||||
mpUiVipLabelGuest: '成为会员',
|
||||
mpUiReadStatLabel: '已读章节',
|
||||
mpUiRecentTitle: '最近阅读',
|
||||
/** 资料区 MBTI 标签文案:已测为四字母大写,未测为「MBTI」 */
|
||||
mbtiTagText: '',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -112,15 +124,22 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
this.setData({ auditMode: app.globalData.auditMode || false }, () => this._syncQuickEntryGridCols())
|
||||
// 设置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 })
|
||||
const tabList = tabBar.data.tabList || []
|
||||
let sel = 0
|
||||
for (let i = 0; i < tabList.length; i++) {
|
||||
if (tabList[i].pagePath.replace(/^\//, '') === 'pages/my/my') {
|
||||
sel = i
|
||||
break
|
||||
}
|
||||
}
|
||||
tabBar.setData({ selected: sel })
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
@@ -147,6 +166,27 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
/** 快捷入口网格:与 WXML 中各 stat-box 的 wx:if 一致;≤4 列时等于入口数(三项铺满一行),否则 4 列自动换行 */
|
||||
_syncQuickEntryGridCols() {
|
||||
const auditMode = !!this.data.auditMode
|
||||
const showSuperCrmEntry = !!this.data.showSuperCrmEntry
|
||||
const showSuperArticleEntry = !!this.data.showSuperArticleEntry
|
||||
let n = 1
|
||||
if (!auditMode) n += 1
|
||||
if (showSuperCrmEntry) n += 1
|
||||
if (showSuperArticleEntry && !auditMode) n += 1
|
||||
const quickEntryGridCols = n <= 4 ? n : 4
|
||||
if (this.data.quickEntryGridCols !== quickEntryGridCols) {
|
||||
this.setData({ quickEntryGridCols })
|
||||
}
|
||||
},
|
||||
|
||||
_formatMbtiTagText(user) {
|
||||
const raw = String(user && user.mbti != null ? user.mbti : '').trim()
|
||||
if (!raw) return 'MBTI'
|
||||
return raw.toUpperCase()
|
||||
},
|
||||
|
||||
async _refreshMyAvatarDisplay(safeUser) {
|
||||
if (!safeUser || !app.globalData.isLoggedIn) return
|
||||
try {
|
||||
@@ -157,6 +197,33 @@ Page({
|
||||
this.setData({ profileAvatarDisplay: url || '' })
|
||||
},
|
||||
|
||||
async refreshUserProfileForMbti() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId || !app.globalData.isLoggedIn) return
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/user/profile?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
if (!res?.success || !res.data) return
|
||||
const profile = res.data
|
||||
const current = this.data.userInfo || app.globalData.userInfo || {}
|
||||
const merged = {
|
||||
...current,
|
||||
...(profile.mbti != null ? { mbti: profile.mbti } : {})
|
||||
}
|
||||
app.globalData.userInfo = merged
|
||||
try {
|
||||
wx.setStorageSync('userInfo', merged)
|
||||
} catch (_) {}
|
||||
this.setData({
|
||||
userInfo: merged,
|
||||
mbtiTagText: this._formatMbtiTagText(merged)
|
||||
})
|
||||
this._refreshMyAvatarDisplay(merged)
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const res = await app.getConfig()
|
||||
@@ -169,11 +236,11 @@ Page({
|
||||
await app.getAuditMode()
|
||||
const auditMode = app.globalData.auditMode || false
|
||||
app.globalData.features = { matchEnabled, referralEnabled, searchEnabled }
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled, auditMode })
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled, auditMode }, () => this._syncQuickEntryGridCols())
|
||||
this._applyMyMpUiLabels()
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true })
|
||||
this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true }, () => this._syncQuickEntryGridCols())
|
||||
this._applyMyMpUiLabels()
|
||||
}
|
||||
},
|
||||
@@ -198,6 +265,7 @@ Page({
|
||||
isLoggedIn: true,
|
||||
userInfo: safeUser,
|
||||
profileAvatarDisplay: '',
|
||||
mbtiTagText: this._formatMbtiTagText(safeUser),
|
||||
userIdShort,
|
||||
userWechat,
|
||||
readCount: 0,
|
||||
@@ -211,22 +279,25 @@ Page({
|
||||
readCountText: '0',
|
||||
totalReadTimeText: '0',
|
||||
matchHistoryText: '0'
|
||||
})
|
||||
}, () => this._syncQuickEntryGridCols())
|
||||
this.loadDashboardStats()
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadSuperCrmEntry()
|
||||
this.loadWalletBalance()
|
||||
this.refreshUserProfileForMbti()
|
||||
this._refreshMyAvatarDisplay(safeUser)
|
||||
} else {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
const guestReadCount = app.getReadCount()
|
||||
const guestRecent = this._mergeRecentChaptersFromLocal([])
|
||||
this.setData({
|
||||
showSuperCrmEntry: false,
|
||||
showSuperArticleEntry: false,
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
profileAvatarDisplay: '',
|
||||
mbtiTagText: '',
|
||||
userIdShort: '',
|
||||
readCount: guestReadCount,
|
||||
readCountText: formatStatNum(guestReadCount),
|
||||
@@ -239,7 +310,7 @@ Page({
|
||||
matchHistory: 0,
|
||||
totalReadTimeText: '0',
|
||||
matchHistoryText: '0'
|
||||
})
|
||||
}, () => this._syncQuickEntryGridCols())
|
||||
}
|
||||
},
|
||||
|
||||
@@ -628,75 +699,6 @@ Page({
|
||||
trackClick('my', 'btn_click', '资料编辑')
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit?full=1&wizard=0' })
|
||||
},
|
||||
|
||||
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(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
this._refreshMyAvatarDisplay(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) {
|
||||
@@ -898,14 +900,21 @@ Page({
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
|
||||
const auditBlockedIds = { giftPay: true, wallet: true, withdrawRecords: true, superArticleMine: true }
|
||||
if (app.globalData.auditMode && auditBlockedIds[id]) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
giftPay: '/pages/gift-pay/list',
|
||||
giftPay: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
wallet: '/pages/wallet/wallet',
|
||||
superCrm: '/pages/super-crm/super-crm',
|
||||
superArticleMine: '/pages/super-article-mine/super-article-mine',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
@@ -997,7 +1006,7 @@ Page({
|
||||
async loadSuperCrmEntry() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
this.setData({ showSuperCrmEntry: false, showSuperArticleEntry: false }, () => this._syncQuickEntryGridCols())
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -1006,9 +1015,9 @@ Page({
|
||||
silent: true
|
||||
})
|
||||
const show = !!(res && res.success && res.data && res.data.isSuperIndividual === true)
|
||||
this.setData({ showSuperCrmEntry: show })
|
||||
this.setData({ showSuperCrmEntry: show, showSuperArticleEntry: show }, () => this._syncQuickEntryGridCols())
|
||||
} catch (_) {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
this.setData({ showSuperCrmEntry: false, showSuperArticleEntry: false }, () => this._syncQuickEntryGridCols())
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1047,6 +1056,10 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
trackClick('my', 'btn_click', '会员中心')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const p = String(this._getMyPageUi().vipPath || '').trim()
|
||||
@@ -1065,6 +1078,26 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${encodeURIComponent(uid)}` })
|
||||
},
|
||||
|
||||
/** MBTI 测试小程序:写死 appId + path,path 上带 phone(有则传,与阅读页一致) */
|
||||
async onMbtiTagTap() {
|
||||
trackClick('my', 'btn_click', 'MBTI标签')
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
const phone = await ensureLoggedInUserPhone()
|
||||
let path = MBTI_MP_PATH.trim().replace(/^\/+/, '')
|
||||
if (phone) path = appendQueryToPath(path, 'phone', phone)
|
||||
wx.navigateToMiniProgram({
|
||||
appId: MBTI_MP_APP_ID,
|
||||
path,
|
||||
envVersion: 'release',
|
||||
fail: (err) => {
|
||||
wx.showToast({ title: err.errMsg || '跳转失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
goToProfileEdit() {
|
||||
trackClick('my', 'nav_click', '资料编辑')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
@@ -1079,6 +1112,10 @@ Page({
|
||||
},
|
||||
|
||||
async handleWithdraw() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持提现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const amount = parseFloat(this.data.pendingEarnings)
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<view class="profile-actions-row profile-actions-under-name" wx:if="{{!auditMode}}">
|
||||
<view class="profile-action-btn" catchtap="goToMySuperCard">{{mpUiCardLabel}}</view>
|
||||
<view class="profile-action-btn" catchtap="goToVip">{{isVip ? mpUiVipLabelVip : mpUiVipLabelGuest}}</view>
|
||||
<view class="profile-mbti-tag" catchtap="onMbtiTagTap">{{mbtiTagText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-settings-hit" catchtap="goToProfileEdit" hover-class="profile-settings-hit-active" aria-label="编辑资料">
|
||||
@@ -88,37 +89,41 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口:我的订单 + 我的代付 -->
|
||||
<!-- 快捷入口:订单与代付(合并列表) -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">快捷入口</text>
|
||||
</view>
|
||||
<view class="stats-grid {{showSuperCrmEntry ? 'stats-grid-4' : ''}}">
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="orders">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">订单</text>
|
||||
<text class="stat-label">我的订单</text>
|
||||
<view class="stats-grid" style="grid-template-columns: repeat({{quickEntryGridCols}}, minmax(0, 1fr));">
|
||||
<view class="stat-box" hover-class="stat-box-hover" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">订单与代付</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">代付</text>
|
||||
<text class="stat-label">我的代付</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="wallet">
|
||||
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{walletBalanceText}}</text>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">我的余额</text>
|
||||
</view>
|
||||
<view class="stat-box" wx:if="{{showSuperCrmEntry}}" bindtap="handleMenuTap" data-id="superCrm">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">客资</text>
|
||||
<text class="stat-label">链接与轨迹</text>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperCrmEntry}}" bindtap="handleMenuTap" data-id="superCrm">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">链接轨迹</text>
|
||||
</view>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperArticleEntry && !auditMode}}" bindtap="handleMenuTap" data-id="superArticleMine">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">我的动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁/充值/代付等流水已迁至「我的订单」页 -->
|
||||
<!-- 已解锁/充值/代付等流水在「订单与代付」统一列表 -->
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
|
||||
@@ -105,6 +105,26 @@
|
||||
font-size: 24rpx; font-weight: 500; border-radius: 40rpx; white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.profile-action-btn:active { opacity: 0.75; }
|
||||
/* MBTI:高亮胶囊(实心青渐变 + 白字 + 外发光),与描边按钮区分 */
|
||||
.profile-mbti-tag {
|
||||
padding: 12rpx 26rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #5eead4 0%, #4FD1C5 45%, #2dd4bf 100%);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
0 4rpx 16rpx rgba(79, 209, 197, 0.45),
|
||||
0 0 0 1rpx rgba(79, 209, 197, 0.25) inset;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.profile-mbti-tag:active {
|
||||
opacity: 0.88;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.user-wechat { font-size: 26rpx; color: #6B7280; }
|
||||
.super-card-entry {
|
||||
position: relative;
|
||||
@@ -167,6 +187,11 @@
|
||||
.card-icon-img { width: 40rpx; height: 40rpx; flex-shrink: 0; }
|
||||
.card-title { font-size: 32rpx; font-weight: bold; color: #fff; }
|
||||
|
||||
/* 快捷入口:略减卡片底边距,避免第二行下方大块留白 */
|
||||
.stats-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* ===== 一键收款卡片 ===== */
|
||||
.receive-card { padding: 28rpx 32rpx; }
|
||||
.receive-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
@@ -196,23 +221,47 @@
|
||||
.earnings-val.primary { color: #4FD1C5; }
|
||||
.earnings-label { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
|
||||
/* 阅读统计 - 统一高度避免真机错位 */
|
||||
/* 快捷入口:列数由页面 quickEntryGridCols 绑定(≤4 铺满一行;>4 时为 4 列换行) */
|
||||
.stats-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24rpx;
|
||||
display: grid;
|
||||
gap: 20rpx 16rpx;
|
||||
align-items: stretch;
|
||||
}
|
||||
.stats-grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.stat-box {
|
||||
background: #252525; border-radius: 20rpx; padding: 24rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 140rpx;
|
||||
background: linear-gradient(155deg, rgba(42, 42, 42, 0.98) 0%, rgba(28, 35, 34, 0.99) 100%);
|
||||
border-radius: 22rpx;
|
||||
padding: 18rpx 12rpx 16rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
|
||||
min-height: 128rpx;
|
||||
border: 1rpx solid rgba(79, 209, 197, 0.22);
|
||||
box-shadow:
|
||||
0 6rpx 22rpx rgba(0, 0, 0, 0.35),
|
||||
0 0 0 1rpx rgba(79, 209, 197, 0.06) inset,
|
||||
0 0 32rpx rgba(79, 209, 197, 0.05);
|
||||
}
|
||||
.stat-box-hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.stat-icon { font-size: 40rpx; margin-bottom: 8rpx; color: #4FD1C5; flex-shrink: 0; }
|
||||
.stat-icon-img { width: 44rpx; height: 44rpx; margin-bottom: 8rpx; flex-shrink: 0; display: block; }
|
||||
.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; line-height: 1.2; }
|
||||
.stat-label { font-size: 20rpx; color: #6B7280; margin-top: 4rpx; line-height: 1.2; }
|
||||
.stat-icon-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8rpx;
|
||||
background: linear-gradient(145deg, rgba(79, 209, 197, 0.28) 0%, rgba(79, 209, 197, 0.06) 55%, rgba(20, 60, 55, 0.35) 100%);
|
||||
border: 1rpx solid rgba(79, 209, 197, 0.38);
|
||||
box-shadow:
|
||||
0 0 24rpx rgba(79, 209, 197, 0.28),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.25),
|
||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.stat-icon-img { width: 48rpx; height: 48rpx; flex-shrink: 0; display: block; }
|
||||
.stat-label { font-size: 22rpx; color: #8B939E; margin-top: 4rpx; line-height: 1.25; text-align: center; }
|
||||
|
||||
/* 最近阅读 */
|
||||
.recent-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
|
||||
@@ -9,10 +9,20 @@
|
||||
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式;VIP 展示全部
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
const { toAvatarPath, pickLocalAvatarImagePath } = require('../../utils/util.js')
|
||||
const { uploadByApi } = require('../../utils/miniprogramUpload.js')
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
/** 补齐小程序可用的绝对 URL(本地/OSS 相对路径拼 baseUrl) */
|
||||
function ensureAbsMediaUrl(u, base) {
|
||||
const s = String(u || '').trim()
|
||||
if (!s) return ''
|
||||
if (/^https?:\/\//i.test(s)) return s
|
||||
const b = String(base || '').replace(/\/$/, '')
|
||||
return b ? b + (s.startsWith('/') ? s : '/' + s) : s
|
||||
}
|
||||
|
||||
/** 首次分步完善完成后写入;与手机号+昵称齐全时自动写入,老用户免向导 */
|
||||
const PROFILE_WIZARD_DONE_KEY = 'profile_wizard_v1_done'
|
||||
|
||||
@@ -408,47 +418,91 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 微信原生 chooseAvatar 回调(点击头像直接弹出原生选择器:用微信头像/从相册选择/拍照)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
/** 将接口返回的 profile data 写入 globalData.userInfo、storage;可选同步本页展示字段(头像上传时用) */
|
||||
_syncUserCacheFromProfileData(d, patchPage) {
|
||||
if (!d || typeof d !== 'object') return
|
||||
const base = app.globalData.baseUrl || ''
|
||||
const ensureAbs = (u) => ensureAbsMediaUrl(u, base)
|
||||
|
||||
if (patchPage) {
|
||||
const patch = {}
|
||||
if (d.avatar != null && String(d.avatar).trim() !== '') patch.avatar = ensureAbs(d.avatar)
|
||||
if (d.nickname != null) patch.nickname = String(d.nickname)
|
||||
if (d.phone != null) patch.phone = String(d.phone)
|
||||
if (d.wechatId != null) patch.wechatId = String(d.wechatId)
|
||||
if (d.mbti != null) {
|
||||
patch.mbti = String(d.mbti)
|
||||
const idx = MBTI_OPTIONS.indexOf(patch.mbti)
|
||||
if (idx >= 0) patch.mbtiIndex = idx
|
||||
}
|
||||
if (Object.keys(patch).length) {
|
||||
this.setData(patch, () => this._syncAvatarPreview())
|
||||
} else {
|
||||
this._syncAvatarPreview()
|
||||
}
|
||||
}
|
||||
|
||||
const u = app.globalData.userInfo
|
||||
if (!u) return
|
||||
if (d.nickname != null && String(d.nickname).trim() !== '') u.nickname = String(d.nickname).trim()
|
||||
if (d.avatar != null && String(d.avatar).trim() !== '') u.avatar = ensureAbs(d.avatar)
|
||||
if (d.phone != null && String(d.phone).trim() !== '') {
|
||||
u.phone = String(d.phone).trim()
|
||||
wx.setStorageSync('user_phone', u.phone)
|
||||
}
|
||||
if (d.wechatId != null) {
|
||||
const w = String(d.wechatId).trim()
|
||||
u.wechatId = w
|
||||
u.wechat = w
|
||||
u.wechat_id = w
|
||||
if (w) wx.setStorageSync('user_wechat', w)
|
||||
}
|
||||
if (d.mbti != null && String(d.mbti).trim() !== '') u.mbti = String(d.mbti).trim()
|
||||
wx.setStorageSync('userInfo', u)
|
||||
},
|
||||
|
||||
/** 点击头像:相册/相机选图(不使用 open-type=chooseAvatar,避免 Windows 开发者工具 http://tmp 渲染报错) */
|
||||
onTapPickAvatar() {
|
||||
const run = () => {
|
||||
pickLocalAvatarImagePath()
|
||||
.then((filePath) => this._uploadChosenAvatar(filePath))
|
||||
.catch((err) => {
|
||||
if (err && err.cancelled) return
|
||||
wx.showToast({ title: (err && err.message) || '选择图片失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
if (typeof wx.requirePrivacyAuthorize === 'function') {
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: run,
|
||||
fail: () => wx.showToast({ title: '需同意隐私指引后再换头像', icon: 'none' }),
|
||||
})
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
},
|
||||
|
||||
async _uploadChosenAvatar(filePath) {
|
||||
if (!filePath) 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: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch {
|
||||
reject(new Error('解析失败'))
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const uploadRes = await uploadByApi({ filePath, folder: 'avatars' })
|
||||
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
if (avatarUrl && !/^https?:\/\//i.test(avatarUrl)) {
|
||||
avatarUrl = ensureAbsMediaUrl(avatarUrl, app.globalData.baseUrl)
|
||||
}
|
||||
this.setData({ avatar: avatarUrl }, () => this._syncAvatarPreview())
|
||||
const avatarToSave = toAvatarPath(avatarUrl)
|
||||
await app.request({
|
||||
const profileRes = await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarToSave },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
if (!profileRes?.success) {
|
||||
throw new Error(profileRes?.error || profileRes?.message || '保存头像失败')
|
||||
}
|
||||
this._syncUserCacheFromProfileData(profileRes.data, true)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
@@ -521,12 +575,7 @@ Page({
|
||||
data: payload,
|
||||
})
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (app.globalData.userInfo) {
|
||||
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
|
||||
if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
|
||||
if (payload.phone) app.globalData.userInfo.phone = payload.phone
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
if (res?.data) this._syncUserCacheFromProfileData(res.data, false)
|
||||
if (wizardComplete) {
|
||||
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
|
||||
this.setData({ wizardMode: false, saving: false })
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<text class="tip-text">第 1 步:先设置对外展示的头像与昵称</text>
|
||||
</view>
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap-btn" hover-class="none" bindtap="onTapPickAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatarPreviewUrl}}" class="avatar-img" src="{{avatarPreviewUrl}}" mode="aspectFill"/>
|
||||
@@ -39,7 +39,7 @@
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section section-wizard">
|
||||
<view class="form-row">
|
||||
@@ -181,7 +181,7 @@
|
||||
</view>
|
||||
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap-btn" hover-class="none" bindtap="onTapPickAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatarPreviewUrl}}" class="avatar-img" src="{{avatarPreviewUrl}}" mode="aspectFill"/>
|
||||
@@ -189,7 +189,7 @@
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
|
||||
@@ -41,13 +41,12 @@
|
||||
.tip-text { font-size: 26rpx; color: rgba(94,234,212,0.95); line-height: 1.6; }
|
||||
|
||||
.avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 48rpx; }
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
/* 头像点击区(原 chooseAvatar button,现为 view + chooseMedia) */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
padding: 0; margin: 0; background: transparent;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%;
|
||||
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
|
||||
|
||||
@@ -20,6 +20,7 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
if (this.data.profile) this.loadProfile()
|
||||
},
|
||||
|
||||
@@ -100,6 +101,10 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
<view class="scroll-pad"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-bar" wx:if="{{!loading}}">
|
||||
<!-- 底部按钮(审核模式隐藏会员入口,与读页/钱包一致) -->
|
||||
<view class="bottom-bar" wx:if="{{!loading && !auditMode}}">
|
||||
<view class="vip-btn" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="32" color="#0f172a"></icon>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业实验 - 订单页(已支付消费流水:章节/VIP/余额/代付等)
|
||||
* Soul创业派对 - 订单与代付(消费流水 + 我发起的代付,按时间合并)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
@@ -25,6 +25,46 @@ function midForSection(sectionId, bookFlat) {
|
||||
return row?.mid ?? row?.MID ?? 0
|
||||
}
|
||||
|
||||
function giftPayStatusLabel(status) {
|
||||
const s = String(status || '').toLowerCase()
|
||||
if (s === 'pending' || s === 'pending_pay') return '待支付'
|
||||
if (s === 'paid') return '已支付'
|
||||
if (s === 'refunded') return '已退款'
|
||||
if (s === 'cancelled') return '已取消'
|
||||
if (s === 'expired') return '已过期'
|
||||
return String(status || '').trim() || '--'
|
||||
}
|
||||
|
||||
function parseIsoMs(iso) {
|
||||
const t = new Date(iso || 0).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
function mapGiftPayToRow(item) {
|
||||
const requestSn = String(item.requestSn || '').trim()
|
||||
if (!requestSn) return null
|
||||
const t = parseIsoMs(item.createdAt)
|
||||
const amt = Number(item.amount)
|
||||
const amountStr = Number.isFinite(amt) ? amt.toFixed(2) : '--'
|
||||
const titleRaw = cleanSingleLineField(String(item.description || '').trim())
|
||||
const title = titleRaw || '代付'
|
||||
const st = String(item.status || '').toLowerCase()
|
||||
const stLabel = giftPayStatusLabel(item.status)
|
||||
const gpPending = st === 'pending' || st === 'pending_pay'
|
||||
const gpPaid = st === 'paid'
|
||||
return {
|
||||
rowKey: `gp_${requestSn}`,
|
||||
kind: 'giftPay',
|
||||
requestSn,
|
||||
title,
|
||||
subLine: `¥${amountStr} · ${stLabel} · ${formatShortDate(t)}`,
|
||||
actionLabel: '查看',
|
||||
gpPending,
|
||||
gpPaid,
|
||||
_sortMs: t,
|
||||
}
|
||||
}
|
||||
|
||||
function classifyNav(productType, productId, mid) {
|
||||
const pt = String(productType || '').toLowerCase()
|
||||
if (pt === 'section' && productId) {
|
||||
@@ -43,7 +83,7 @@ function classifyNav(productType, productId, mid) {
|
||||
return { kind: 'page', path: '/pages/wallet/wallet', label: '余额' }
|
||||
}
|
||||
if (pt === 'gift_pay' || pt === 'gift_pay_batch') {
|
||||
return { kind: 'page', path: '/pages/gift-pay/list', label: '代付记录' }
|
||||
return { kind: 'page', path: '/pages/purchases/purchases', label: '查看' }
|
||||
}
|
||||
if (productId && (/^\d+\.\d+/.test(productId) || productId.length > 0)) {
|
||||
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
|
||||
@@ -72,6 +112,7 @@ function mapApiOrderToRow(item, bookFlat) {
|
||||
|
||||
return {
|
||||
rowKey: String(item.order_sn || item.id || `o_${t}`),
|
||||
kind: 'order',
|
||||
title,
|
||||
subLine: `¥${amountStr} · ${formatShortDate(t)}`,
|
||||
actionLabel: nav.label,
|
||||
@@ -120,40 +161,55 @@ Page({
|
||||
const userId = app.globalData.userInfo?.id
|
||||
|
||||
try {
|
||||
let orderRows = []
|
||||
|
||||
if (userId) {
|
||||
const res = await app.request({
|
||||
const orderRes = await app.request({
|
||||
url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
const rows = res.data
|
||||
.map((item) => mapApiOrderToRow(item, bookFlat))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b._sortMs - a._sortMs)
|
||||
.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
this.applyDisplay(false)
|
||||
return
|
||||
silent: true,
|
||||
}).catch(() => null)
|
||||
if (orderRes && orderRes.success && Array.isArray(orderRes.data)) {
|
||||
orderRows = orderRes.data.map((item) => mapApiOrderToRow(item, bookFlat)).filter(Boolean)
|
||||
}
|
||||
}
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const rows = ids.map((id, index) => {
|
||||
const mid = midForSection(id, bookFlat)
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
const title =
|
||||
cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
const t = Date.now() - index * 86400000
|
||||
return {
|
||||
rowKey: `p_${id}_${index}`,
|
||||
title,
|
||||
subLine: `已解锁 · ${formatShortDate(t)}`,
|
||||
actionLabel: '阅读',
|
||||
nav: { kind: 'read', id, mid, label: '阅读' }
|
||||
|
||||
if (!orderRows.length) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
orderRows = ids.map((id, index) => {
|
||||
const mid = midForSection(id, bookFlat)
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
const title =
|
||||
cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
const t = Date.now() - index * 86400000
|
||||
return {
|
||||
rowKey: `p_${id}_${index}`,
|
||||
kind: 'order',
|
||||
title,
|
||||
subLine: `已解锁 · ${formatShortDate(t)}`,
|
||||
actionLabel: '阅读',
|
||||
nav: { kind: 'read', id, mid, label: '阅读' },
|
||||
_sortMs: t,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let giftRows = []
|
||||
if (userId && !app.globalData.auditMode) {
|
||||
const giftRes = await app
|
||||
.request(`/api/miniprogram/gift-pay/my-requests?userId=${encodeURIComponent(userId)}`, {
|
||||
silent: true,
|
||||
})
|
||||
.catch(() => null)
|
||||
if (giftRes && giftRes.success && Array.isArray(giftRes.list)) {
|
||||
giftRows = giftRes.list.map(mapGiftPayToRow).filter(Boolean)
|
||||
}
|
||||
})
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
}
|
||||
|
||||
const merged = [...orderRows, ...giftRows].sort((a, b) => b._sortMs - a._sortMs)
|
||||
const stripped = merged.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ allRows: stripped, loading: false })
|
||||
this.applyDisplay(false)
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
@@ -162,10 +218,17 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onOrderRowTap(e) {
|
||||
onUnifiedRowTap(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const row = (this.data.displayRows || [])[index]
|
||||
if (!row || !row.nav) return
|
||||
if (!row) return
|
||||
if (row.kind === 'giftPay' && row.requestSn) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/gift-pay/redemption-detail?requestSn=${encodeURIComponent(String(row.requestSn))}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!row.nav) return
|
||||
const { nav } = row
|
||||
if (nav.kind === 'read' && nav.id) {
|
||||
const q = nav.mid ? `mid=${nav.mid}` : `id=${nav.id}`
|
||||
@@ -173,6 +236,13 @@ Page({
|
||||
return
|
||||
}
|
||||
if (nav.kind === 'page' && nav.path) {
|
||||
if (app.globalData.auditMode) {
|
||||
const block = { '/pages/vip/vip': true, '/pages/wallet/wallet': true }
|
||||
if (block[nav.path]) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
wx.navigateTo({ url: nav.path })
|
||||
return
|
||||
}
|
||||
@@ -181,6 +251,40 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async onGiftPayCancel(e) {
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation()
|
||||
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.loadOrders()
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '取消失败', icon: 'none' })
|
||||
}
|
||||
} catch (_) {
|
||||
wx.showToast({ title: '取消失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
onGiftPayShare(e) {
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation()
|
||||
const requestSn = e?.currentTarget?.dataset?.sn
|
||||
if (!requestSn) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/gift-pay/detail?requestSn=${encodeURIComponent(String(requestSn))}`,
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
@@ -188,13 +292,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: '卡若创业派对 - 购买记录',
|
||||
title: '卡若创业派对 - 订单与代付',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: '卡若创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 订单与代付', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><icon name="chevron-left" size="44" color="rgba(255,255,255,0.8)" customClass="back-icon"></icon></view>
|
||||
<text class="nav-title">我的订单</text>
|
||||
<text class="nav-title">订单与代付</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
@@ -19,21 +19,23 @@
|
||||
<image class="order-history-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="oh-list">
|
||||
<view
|
||||
class="oh-row"
|
||||
wx:for="{{displayRows}}"
|
||||
wx:key="rowKey"
|
||||
bindtap="onOrderRowTap"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<view class="oh-left">
|
||||
<text class="oh-index">{{index + 1}}</text>
|
||||
<view class="oh-text-wrap">
|
||||
<text class="oh-title">{{item.title}}</text>
|
||||
<text class="oh-sub" wx:if="{{item.subLine}}">{{item.subLine}}</text>
|
||||
<view class="oh-block" wx:for="{{displayRows}}" wx:key="rowKey">
|
||||
<view class="oh-row" bindtap="onUnifiedRowTap" data-index="{{index}}">
|
||||
<view class="oh-left">
|
||||
<text class="oh-kind {{item.kind === 'giftPay' ? 'oh-kind-gift' : 'oh-kind-order'}}">{{item.kind === 'giftPay' ? '代付' : '订单'}}</text>
|
||||
<view class="oh-text-wrap">
|
||||
<text class="oh-title">{{item.title}}</text>
|
||||
<text class="oh-sub" wx:if="{{item.subLine}}">{{item.subLine}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="oh-link">{{item.actionLabel}}</text>
|
||||
</view>
|
||||
<view class="oh-gift-tools" wx:if="{{item.kind === 'giftPay' && item.gpPending}}" catchtap="">
|
||||
<text class="oh-tool oh-tool-cancel" catchtap="onGiftPayCancel" data-sn="{{item.requestSn}}">取消</text>
|
||||
</view>
|
||||
<view class="oh-gift-tools" wx:if="{{item.kind === 'giftPay' && item.gpPaid}}" catchtap="">
|
||||
<text class="oh-tool" catchtap="onGiftPayShare" data-sn="{{item.requestSn}}">分享</text>
|
||||
</view>
|
||||
<text class="oh-link">{{item.actionLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
@@ -49,7 +51,7 @@
|
||||
|
||||
<view class="empty" wx:else>
|
||||
<icon name="package" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
<text class="empty-text">暂无订单与代付记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -10,15 +10,54 @@
|
||||
.order-history-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 24rpx 16rpx; }
|
||||
.order-history-head { padding: 0 8rpx 16rpx 8rpx; }
|
||||
.order-history-icon { width: 40rpx; height: 40rpx; opacity: 0.92; }
|
||||
.oh-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.oh-list { display: flex; flex-direction: column; gap: 0; }
|
||||
.oh-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.oh-block:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.oh-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.oh-row:active { opacity: 0.92; }
|
||||
.oh-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; flex: 1; }
|
||||
.oh-text-wrap { display: flex; flex-direction: column; gap: 6rpx; min-width: 0; flex: 1; }
|
||||
.oh-index { font-size: 28rpx; color: #6B7280; font-family: monospace; flex-shrink: 0; }
|
||||
.oh-left { display: flex; align-items: flex-start; gap: 16rpx; overflow: hidden; min-width: 0; flex: 1; }
|
||||
.oh-text-wrap { display: flex; flex-direction: column; gap: 6rpx; min-width: 0; flex: 1; padding-top: 4rpx; }
|
||||
.oh-kind {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 10rpx;
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oh-kind-order {
|
||||
color: rgba(0, 206, 209, 0.92);
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.35);
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
}
|
||||
.oh-kind-gift {
|
||||
color: #fcd34d;
|
||||
border: 1rpx solid rgba(252, 211, 77, 0.35);
|
||||
background: rgba(252, 211, 77, 0.08);
|
||||
}
|
||||
.oh-gift-tools {
|
||||
padding: 0 8rpx 4rpx 8rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.oh-tool {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.oh-tool-cancel {
|
||||
color: rgba(248, 113, 113, 0.95);
|
||||
}
|
||||
.oh-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.oh-sub { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.oh-link { font-size: 24rpx; color: #00CED1; font-weight: 500; flex-shrink: 0; margin-left: 16rpx; }
|
||||
|
||||
@@ -17,9 +17,10 @@ const accessManager = require('../../utils/chapterAccessManager')
|
||||
const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { trackClick, trackViewChapter } = require('../../utils/trackClick')
|
||||
const { checkAndExecute, executeRule, getChapterReadMarketingRule } = require('../../utils/ruleEngine')
|
||||
const soulBridge = require('../../utils/soulBridge.js')
|
||||
const superArticleDetailUtil = require('../../utils/superArticleDetail.js')
|
||||
|
||||
const app = getApp()
|
||||
const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||
@@ -218,7 +219,17 @@ Page({
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
|
||||
/** chapter:书籍章节;super_article:超级个体动态(与独立动态详情合并为本页) */
|
||||
pageMode: 'chapter',
|
||||
superArticle: null,
|
||||
superArticleId: '',
|
||||
superGalleryItems: [],
|
||||
superLoading: true,
|
||||
superAuditTip: '',
|
||||
superAuditRejectReason: '',
|
||||
superShowAuditEdit: false,
|
||||
|
||||
// 章节信息
|
||||
sectionId: '',
|
||||
section: null,
|
||||
@@ -460,15 +471,109 @@ Page({
|
||||
|
||||
onShow() {
|
||||
const sp = this._detectReadSinglePage()
|
||||
const audit = app.globalData.auditMode || false
|
||||
this.setData({
|
||||
auditMode: app.globalData.auditMode || false,
|
||||
auditMode: audit,
|
||||
readSinglePageMode: sp,
|
||||
...(sp ? {} : { momentsPaywallExpanded: false, showMomentsLaunchGuideModal: false }),
|
||||
...(audit && this.data.showGiftModal ? { showGiftModal: false } : {}),
|
||||
})
|
||||
this._refreshAccessOnShow()
|
||||
if (!sp) {
|
||||
this.refreshPay365MarketingFromRules()
|
||||
}
|
||||
if (this.data.pageMode === 'super_article' && this.data.superArticleId) {
|
||||
this._superArticleShowCount = (this._superArticleShowCount || 0) + 1
|
||||
if (this._superArticleShowCount > 1) {
|
||||
this.loadSuperArticleDetail(this.data.superArticleId)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 超级个体动态:与章节阅读共用本页(query superArticleId / saId)
|
||||
*/
|
||||
async _bootstrapSuperArticlePage(idStr) {
|
||||
this._superArticleShowCount = 0
|
||||
this.setData({
|
||||
pageMode: 'super_article',
|
||||
superArticleId: String(idStr),
|
||||
superLoading: true,
|
||||
loading: false,
|
||||
accessState: 'unknown',
|
||||
sectionId: '',
|
||||
sectionMid: null,
|
||||
superArticle: null,
|
||||
superGalleryItems: [],
|
||||
superAuditTip: '',
|
||||
superAuditRejectReason: '',
|
||||
superShowAuditEdit: false,
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
navBarHeight: app.globalData.navBarHeight || 88,
|
||||
})
|
||||
await this.loadSuperArticleDetail(idStr)
|
||||
},
|
||||
|
||||
async loadSuperArticleDetail(id) {
|
||||
this.setData({ superLoading: true })
|
||||
const baseUrl = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
try {
|
||||
const viewerId = app.globalData.userInfo?.id
|
||||
const q =
|
||||
viewerId != null && String(viewerId).trim() !== ''
|
||||
? `?viewerUserId=${encodeURIComponent(String(viewerId).trim())}`
|
||||
: ''
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}${q}`,
|
||||
silent: true,
|
||||
})
|
||||
const article = res?.data
|
||||
if (!article) {
|
||||
this.setData({ superLoading: false, superArticle: null, superGalleryItems: [] })
|
||||
return
|
||||
}
|
||||
const audit = superArticleDetailUtil.buildSuperArticleAuditMeta(article, app.globalData.userInfo?.id)
|
||||
const galleryItems = superArticleDetailUtil.buildGalleryItems(article.images, baseUrl)
|
||||
this.setData({
|
||||
superArticle: Object.assign({}, article, {
|
||||
createdAtText: superArticleDetailUtil.formatSuperArticleTime(article.createdAt),
|
||||
contentHtml: superArticleDetailUtil.renderSuperArticleHtml(article.content),
|
||||
}),
|
||||
superGalleryItems: galleryItems,
|
||||
superAuditTip: audit.auditTip,
|
||||
superAuditRejectReason: audit.auditRejectReason,
|
||||
superShowAuditEdit: audit.showAuditEdit,
|
||||
superLoading: false,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ superLoading: false, superArticle: null, superGalleryItems: [] })
|
||||
}
|
||||
},
|
||||
|
||||
goSuperArticleEdit() {
|
||||
const id = this.data.superArticleId
|
||||
if (!id) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/super-article-editor/super-article-editor?id=${encodeURIComponent(String(id))}`,
|
||||
})
|
||||
},
|
||||
|
||||
onSuperGalleryImgError(e) {
|
||||
const idx = Number(e.currentTarget.dataset.idx)
|
||||
if (Number.isNaN(idx)) return
|
||||
this.setData({ [`superGalleryItems[${idx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewSuperGallery(e) {
|
||||
const current = e.currentTarget.dataset.src || ''
|
||||
const urls = (this.data.superGalleryItems || [])
|
||||
.filter((x) => x.src && !x.loadFailed)
|
||||
.map((x) => x.src)
|
||||
if (!urls.length) return
|
||||
wx.previewImage({
|
||||
urls,
|
||||
current: current || urls[0],
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -476,6 +581,7 @@ Page({
|
||||
* 修复:管理端取消超级个体后,若仍用订单兜底 VIP,需配合后端 isVipEffective;同时刷新端上 showPaywall/全文裁切。
|
||||
*/
|
||||
async _refreshAccessOnShow() {
|
||||
if (this.data.pageMode === 'super_article') return
|
||||
const id = this.data.sectionId
|
||||
if (!id || this.data.loading || this.data.readSinglePageMode) return
|
||||
const uid = app.globalData.userInfo?.id
|
||||
@@ -519,6 +625,13 @@ Page({
|
||||
async onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
|
||||
const superRaw = (options && (options.superArticleId || options.saId)) || ''
|
||||
const superArticleCandidate = String(superRaw).trim()
|
||||
if (superArticleCandidate && /^\d+$/.test(superArticleCandidate)) {
|
||||
await this._bootstrapSuperArticlePage(superArticleCandidate)
|
||||
return
|
||||
}
|
||||
|
||||
// 预加载:core+auditMode(getConfig)+ read-extras 懒加载(linkTags、linkedMiniprograms)
|
||||
Promise.all([
|
||||
app.getConfig(),
|
||||
@@ -581,6 +694,7 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const readSinglePageMode = this._detectReadSinglePage()
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight,
|
||||
@@ -589,7 +703,7 @@ Page({
|
||||
loading: true,
|
||||
accessState: 'unknown',
|
||||
pendingGiftRequestSn: giftRequestSn || '',
|
||||
readSinglePageMode: this._detectReadSinglePage(),
|
||||
readSinglePageMode,
|
||||
momentsPaywallExpanded: false,
|
||||
showMomentsLaunchGuideModal: false,
|
||||
pendingAutoSectionPay: parseDatasetBool(options && options.openPay),
|
||||
@@ -601,6 +715,7 @@ Page({
|
||||
app.handleReferralCode({ query: { ref } })
|
||||
}
|
||||
|
||||
let viewChapterPvAccessState = 'unknown'
|
||||
try {
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
@@ -618,7 +733,8 @@ Page({
|
||||
const chapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
|
||||
let accessState = await accessManager.determineAccessState(id, chapterRes)
|
||||
let canAccess = accessManager.canAccessFullContent(accessState)
|
||||
|
||||
viewChapterPvAccessState = accessState
|
||||
|
||||
this.setData({
|
||||
accessState,
|
||||
canAccess,
|
||||
@@ -638,6 +754,7 @@ Page({
|
||||
const freshChapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
|
||||
accessState = await accessManager.determineAccessState(id, freshChapterRes)
|
||||
canAccess = accessManager.canAccessFullContent(accessState)
|
||||
viewChapterPvAccessState = accessState
|
||||
this.setData({ accessState, canAccess, showPaywall: !canAccess, pendingGiftRequestSn: '' })
|
||||
if (canAccess) {
|
||||
await this.loadContent(id, accessState, freshChapterRes)
|
||||
@@ -662,8 +779,17 @@ Page({
|
||||
} catch (e) {
|
||||
console.error('[Read] 初始化失败:', e)
|
||||
wx.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||||
viewChapterPvAccessState = 'error'
|
||||
this.setData({ accessState: 'error', loading: false })
|
||||
} finally {
|
||||
// 章节浏览 PV:放在 finally,避免 loadContent/初始化抛错导致「有付款却无点击」的统计缺口(与 reading_progress 无关)
|
||||
if (id) {
|
||||
trackViewChapter(id, {
|
||||
singlePage: !!this.data.readSinglePageMode,
|
||||
accessState: viewChapterPvAccessState,
|
||||
sectionMid: this.data.sectionMid != null ? this.data.sectionMid : (mid || null),
|
||||
})
|
||||
}
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
@@ -1257,6 +1383,22 @@ Page({
|
||||
wx.showToast({ title: '暂无跳转地址', icon: 'none' })
|
||||
},
|
||||
|
||||
// 点击正文普通超链接:用小程序 web-view 打开(微信内置浏览器)
|
||||
onArticleLinkTap(e) {
|
||||
const url = String((e.currentTarget.dataset.url || '')).trim()
|
||||
if (!url) {
|
||||
wx.showToast({ title: '链接地址为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const text = String((e.currentTarget.dataset.text || '链接预览')).trim() || '链接预览'
|
||||
wx.navigateTo({
|
||||
url: `/pages/link-preview/link-preview?url=${encodeURIComponent(url)}&title=${encodeURIComponent(text)}`,
|
||||
fail: (err) => {
|
||||
wx.showToast({ title: err.errMsg || '打开失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// 点击正文图片 → 全屏预览
|
||||
onImageTap(e) {
|
||||
const src = e.currentTarget.dataset.src
|
||||
@@ -1302,6 +1444,10 @@ Page({
|
||||
|
||||
// 代付分享:直接跳转代付页,在代付页输入数量并支付(简化流程)
|
||||
showGiftShareModal() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持代付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!app.globalData.userInfo?.id) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
@@ -1334,6 +1480,10 @@ Page({
|
||||
|
||||
async confirmGiftPay() {
|
||||
if (this.data.giftPaying) return
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持代付', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 朋友圈单页模式禁止支付
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
@@ -1471,6 +1621,16 @@ Page({
|
||||
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage(e) {
|
||||
if (this.data.pageMode === 'super_article') {
|
||||
const a = this.data.superArticle
|
||||
const sid = this.data.superArticleId
|
||||
const q = sid ? `superArticleId=${encodeURIComponent(String(sid))}` : ''
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
path: sid ? `/pages/read/read?${q}` : '/pages/read/read',
|
||||
}
|
||||
}
|
||||
|
||||
trackClick('read', 'btn_click', '分享_' + this.data.sectionId)
|
||||
const { section, sectionId, sectionMid } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
@@ -1533,6 +1693,16 @@ Page({
|
||||
|
||||
// 分享到朋友圈:带文章标题,过长时截断
|
||||
onShareTimeline() {
|
||||
if (this.data.pageMode === 'super_article') {
|
||||
const a = this.data.superArticle
|
||||
const sid = this.data.superArticleId
|
||||
const query = sid ? `superArticleId=${encodeURIComponent(String(sid))}` : ''
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
const { section, sectionId, sectionMid, chapterTitle, readUi } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
@@ -1705,6 +1875,14 @@ Page({
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
if (app.globalData.auditMode) {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
} catch (e) {}
|
||||
wx.showToast({ title: '审核中,暂不支持购买', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!--pages/read/read.wxml-->
|
||||
<!--卡若创业派对 - 阅读页-->
|
||||
<!--卡若创业派对 - 阅读页(章节 / 超级个体动态共用;动态:query superArticleId)-->
|
||||
<view class="page">
|
||||
<block wx:if="{{pageMode === 'chapter'}}">
|
||||
<!-- 阅读进度条 -->
|
||||
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
|
||||
<view class="progress-fill" style="width: {{readingProgress}}%;"></view>
|
||||
@@ -78,14 +79,14 @@
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph">
|
||||
<image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image>
|
||||
</view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap">
|
||||
<video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video' && !auditMode}}" class="paragraph content-video-wrap">
|
||||
<video class="content-video" src="{{item[0].src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video>
|
||||
</view>
|
||||
<view wx:else class="paragraph">
|
||||
<text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
|
||||
<text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text><text wx:elif="{{seg.type === 'link'}}" class="article-link" bindtap="onArticleLinkTap" data-url="{{seg.url}}" data-text="{{seg.text}}">{{seg.text}}</text></block></text>
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
<video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video>
|
||||
<video wx:elif="{{seg.type === 'video' && !auditMode}}" class="content-video content-video--inline" src="{{seg.src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
@@ -157,8 +158,8 @@
|
||||
<scroll-view wx:elif="{{item.length === 1 && item[0].type === 'table'}}" scroll-x class="table-scroll"><view class="content-table"><view class="table-row table-header-row" wx:if="{{item[0].headers.length}}"><text class="table-cell table-header-cell" wx:for="{{item[0].headers}}" wx:key="index" wx:for-item="hdr">{{hdr}}</text></view><view class="table-row" wx:for="{{item[0].rows}}" wx:key="index" wx:for-item="row"><text class="table-cell" wx:for="{{row}}" wx:key="index" wx:for-item="cell">{{cell}}</text></view></view></scroll-view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'listItem'}}" class="seg-list-item"><text class="seg-list-marker" wx:if="{{item[0].ordered}}">{{item[0].number}}.</text><text class="seg-list-marker" wx:else>•</text><text class="seg-list-text" user-select>{{item[0].text}}</text></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph"><image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video></view>
|
||||
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video></block></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video' && !auditMode}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video></view>
|
||||
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text><text wx:elif="{{seg.type === 'link'}}" class="article-link" bindtap="onArticleLinkTap" data-url="{{seg.url}}" data-text="{{seg.text}}">{{seg.text}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video' && !auditMode}}" class="content-video content-video--inline" src="{{seg.src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video></block></view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="fade-mask"></view>
|
||||
@@ -250,8 +251,8 @@
|
||||
<scroll-view wx:elif="{{item.length === 1 && item[0].type === 'table'}}" scroll-x class="table-scroll"><view class="content-table"><view class="table-row table-header-row" wx:if="{{item[0].headers.length}}"><text class="table-cell table-header-cell" wx:for="{{item[0].headers}}" wx:key="index" wx:for-item="hdr">{{hdr}}</text></view><view class="table-row" wx:for="{{item[0].rows}}" wx:key="index" wx:for-item="row"><text class="table-cell" wx:for="{{row}}" wx:key="index" wx:for-item="cell">{{cell}}</text></view></view></scroll-view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'listItem'}}" class="seg-list-item"><text class="seg-list-marker" wx:if="{{item[0].ordered}}">{{item[0].number}}.</text><text class="seg-list-marker" wx:else>•</text><text class="seg-list-text" user-select>{{item[0].text}}</text></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph"><image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video></view>
|
||||
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video></block></view>
|
||||
<view wx:elif="{{item.length === 1 && item[0].type === 'video' && !auditMode}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video></view>
|
||||
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text><text wx:elif="{{seg.type === 'link'}}" class="article-link" bindtap="onArticleLinkTap" data-url="{{seg.url}}" data-text="{{seg.text}}">{{seg.text}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video' && !auditMode}}" class="content-video content-video--inline" src="{{seg.src}}" controls show-center-play-btn enable-play-gesture object-fit="contain"></video></block></view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="fade-mask"></view>
|
||||
@@ -512,4 +513,56 @@
|
||||
<text class="pay365-anchor-float-text">{{pay365AnchorLabel}}</text>
|
||||
<text class="pay365-anchor-float-chevron">›</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{pageMode === 'super_article'}}">
|
||||
<view class="read-super-nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="read-super-nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.85)"></icon>
|
||||
</view>
|
||||
<text class="read-super-nav-title">文章详情</text>
|
||||
<view class="read-super-nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="read-super-nav-offset" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="read-super-loading" wx:if="{{superLoading}}">
|
||||
<text class="read-super-loading-text">加载中…</text>
|
||||
</view>
|
||||
|
||||
<view class="read-super-body" wx:elif="{{superArticle}}">
|
||||
<view class="read-super-audit-banner" wx:if="{{superAuditTip}}">
|
||||
<text class="read-super-audit-main">{{superAuditTip}}</text>
|
||||
<text class="read-super-audit-sub" wx:if="{{superAuditRejectReason}}">{{superAuditRejectReason}}</text>
|
||||
<view wx:if="{{superShowAuditEdit}}" class="read-super-audit-edit-btn" bindtap="goSuperArticleEdit">重新编辑并提交</view>
|
||||
</view>
|
||||
<text class="read-super-title" wx:if="{{superArticle.title}}">{{superArticle.title}}</text>
|
||||
<text class="read-super-title read-super-title-muted" wx:else>动态</text>
|
||||
<text class="read-super-meta">{{superArticle.authorNickname || '超级个体'}} · {{superArticle.createdAtText}}</text>
|
||||
|
||||
<view class="read-super-gallery" wx:if="{{superGalleryItems.length}}">
|
||||
<view class="read-super-gallery-cell" wx:for="{{superGalleryItems}}" wx:key="idx">
|
||||
<block wx:if="{{item.src && !item.loadFailed}}">
|
||||
<image
|
||||
class="read-super-gallery-img"
|
||||
mode="widthFix"
|
||||
src="{{item.src}}"
|
||||
bindtap="previewSuperGallery"
|
||||
data-src="{{item.src}}"
|
||||
binderror="onSuperGalleryImgError"
|
||||
data-idx="{{index}}"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="read-super-gallery-ph">
|
||||
<text class="read-super-gallery-ph-text">{{item.src ? '图片加载失败' : '配图缺失或地址无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<rich-text class="read-super-content" nodes="{{superArticle.contentHtml}}"></rich-text>
|
||||
</view>
|
||||
|
||||
<view class="read-super-empty" wx:elif="{{!superLoading}}">
|
||||
<text>动态不存在或已下线</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -282,6 +282,14 @@
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
/* 正文内普通超链接(管理端插入 a 标签) */
|
||||
.paragraph .article-link {
|
||||
color: #67e8f9;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 正文内图片 */
|
||||
.content-image {
|
||||
width: 100%;
|
||||
@@ -290,6 +298,22 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-video-wrap {
|
||||
width: 100%;
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
|
||||
.content-video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 12rpx;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.content-video--inline {
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
|
||||
/* 正文内表格 */
|
||||
.table-scroll {
|
||||
margin: 24rpx 0 36rpx;
|
||||
@@ -1940,3 +1964,140 @@ button.action-share-native {
|
||||
color: #38bdac;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
/* ---------- 超级个体动态(与章节共用阅读页 pageMode=super_article) ---------- */
|
||||
.read-super-nav-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16rpx;
|
||||
background: rgba(5, 11, 20, 0.92);
|
||||
border-bottom: 1rpx solid rgba(34, 211, 238, 0.12);
|
||||
}
|
||||
.read-super-nav-back,
|
||||
.read-super-nav-placeholder {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.read-super-nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.read-super-nav-offset {
|
||||
width: 100%;
|
||||
}
|
||||
.read-super-loading {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.read-super-loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.read-super-body {
|
||||
padding: 8rpx 24rpx 48rpx;
|
||||
}
|
||||
.read-super-audit-banner {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.28);
|
||||
}
|
||||
.read-super-audit-main {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #fcd34d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.read-super-audit-sub {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(226, 232, 240, 0.88);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.read-super-audit-edit-btn {
|
||||
margin-top: 18rpx;
|
||||
display: inline-block;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #0b1220;
|
||||
background: linear-gradient(135deg, #00ced1, #20b2aa);
|
||||
}
|
||||
.read-super-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
.read-super-title-muted {
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
.read-super-meta {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
}
|
||||
.read-super-gallery {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.read-super-gallery-cell {
|
||||
width: 100%;
|
||||
}
|
||||
.read-super-gallery-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
.read-super-gallery-ph {
|
||||
min-height: 220rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.read-super-gallery-ph-text {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
.read-super-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.75;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.read-super-empty {
|
||||
margin-top: 200rpx;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
@@ -648,6 +648,10 @@ Page({
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
trackClick('referral', 'btn_click', '申请提现')
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持提现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const availableEarnings = this.data.availableEarningsNum || 0
|
||||
const minWithdrawAmount = this.data.minWithdrawAmount || 10
|
||||
const hasWechatId = this.data.hasWechatId
|
||||
@@ -695,11 +699,19 @@ Page({
|
||||
// 跳转提现记录页
|
||||
goToWithdrawRecords() {
|
||||
trackClick('referral', 'nav_click', '提现记录')
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
},
|
||||
|
||||
// 执行提现(success:false 时 app.request 会 reject 并挂 e.response,需 catch 里处理 needBindWechat)
|
||||
async doWithdraw(amount) {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核中,暂不支持提现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提现中...' })
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Soul创业派对 - 兼容入口:动态详情已并入阅读页 pages/read/read(superArticleId)
|
||||
* 旧链接 / 分享路径仍可能指向本页,此处统一 redirect。
|
||||
*/
|
||||
Page({
|
||||
onLoad(options) {
|
||||
const id = options?.id != null ? String(options.id).trim() : ''
|
||||
if (!id || !/^\d+$/.test(id)) {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
|
||||
return
|
||||
}
|
||||
wx.redirectTo({
|
||||
url: `/pages/read/read?superArticleId=${encodeURIComponent(id)}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="super-article-detail-redirect"></view>
|
||||
@@ -0,0 +1,4 @@
|
||||
.super-article-detail-redirect {
|
||||
min-height: 100vh;
|
||||
background: #0b1220;
|
||||
}
|
||||
433
miniprogram/pages/super-article-editor/super-article-editor.js
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体发动态(AI 辅助 + 朋友圈式编辑)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { uploadByApi } = require('../../utils/miniprogramUpload')
|
||||
|
||||
function newAttachId() {
|
||||
return `f_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 「AI 帮你写」入口(弹窗内填写提示词并生成) */
|
||||
showAiWriteSection: true,
|
||||
aiAssistVisible: false,
|
||||
statusBarHeight: 44,
|
||||
auditMode: false,
|
||||
title: '',
|
||||
content: '',
|
||||
bodyImages: [],
|
||||
saving: false,
|
||||
meNickname: '',
|
||||
aiPrompt: '',
|
||||
generating: false,
|
||||
editArticleId: '',
|
||||
/** 正文光标,用于 @自己 插入 */
|
||||
contentCursor: 0,
|
||||
},
|
||||
|
||||
/** 遮挡层拦截滚动穿透 */
|
||||
preventMove() {},
|
||||
|
||||
syncAuditMode() {
|
||||
const audit = !!app.globalData.auditMode
|
||||
const patch = { auditMode: audit }
|
||||
if (audit) patch.aiAssistVisible = false
|
||||
this.setData(patch)
|
||||
return audit
|
||||
},
|
||||
|
||||
openAiAssist() {
|
||||
this.syncAuditMode()
|
||||
if (this.data.auditMode || app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核模式下不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.data.showAiWriteSection) return
|
||||
this.setData({ aiAssistVisible: true })
|
||||
},
|
||||
|
||||
closeAiAssist() {
|
||||
if (this.data.generating) {
|
||||
wx.showToast({ title: '生成中,请稍候…', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ aiAssistVisible: false })
|
||||
},
|
||||
|
||||
/** 阻止弹层内容区点击冒泡到遮罩(部分机型兼容) */
|
||||
noopModalTap() {},
|
||||
|
||||
onLoad(options) {
|
||||
const editId = options && options.id != null ? String(options.id).trim() : ''
|
||||
if (editId) {
|
||||
this._pendingEditId = editId
|
||||
this.setData({ editArticleId: editId })
|
||||
}
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
meNickname: String(app.globalData.userInfo?.nickname || '我').trim() || '我',
|
||||
})
|
||||
this.syncAuditMode()
|
||||
try {
|
||||
app.getAuditMode && app.getAuditMode().catch(() => {})
|
||||
} catch (_) {}
|
||||
this.ensureSuperIdentity()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.syncAuditMode()
|
||||
try {
|
||||
app.getAuditMode && app.getAuditMode().catch(() => {})
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
async ensureSuperIdentity() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!app.globalData.isLoggedIn || !userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/my/super-stats?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true,
|
||||
})
|
||||
if (!(res?.success && res.data?.isSuperIndividual === true)) {
|
||||
wx.showToast({ title: '仅超级个体可发动态', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
const pid = this._pendingEditId ? String(this._pendingEditId).trim() : ''
|
||||
if (pid) {
|
||||
this._pendingEditId = ''
|
||||
await this.loadArticleForEdit(pid)
|
||||
}
|
||||
} catch (_) {
|
||||
wx.showToast({ title: '身份校验失败', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
}
|
||||
},
|
||||
|
||||
async loadArticleForEdit(id) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
wx.showLoading({ title: '加载中…', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}?viewerUserId=${encodeURIComponent(String(userId))}`,
|
||||
silent: true,
|
||||
})
|
||||
const article = res?.data
|
||||
if (!article) throw new Error('加载失败')
|
||||
if (String(article.userId) !== String(userId)) throw new Error('无权编辑')
|
||||
const st = String(article.auditStatus || '').toLowerCase()
|
||||
if (st !== 'pending' && st !== 'rejected') throw new Error('仅待审核或已驳回的动态可编辑')
|
||||
const imgs = Array.isArray(article.images)
|
||||
? article.images.map((u) => String(u || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const bodyImages = imgs.map((url) => ({ id: newAttachId(), url, uploading: false }))
|
||||
this.setData({
|
||||
editArticleId: String(id),
|
||||
title: '',
|
||||
content: String(article.content || ''),
|
||||
bodyImages,
|
||||
contentCursor: String(article.content || '').length,
|
||||
})
|
||||
} catch (e) {
|
||||
wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 500)
|
||||
} finally {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
|
||||
onAiPromptInput(e) {
|
||||
this.setData({ aiPrompt: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onContentInput(e) {
|
||||
const v = e.detail.value != null ? String(e.detail.value) : ''
|
||||
let cursor = e.detail.cursor
|
||||
if (typeof cursor !== 'number' || Number.isNaN(cursor)) cursor = v.length
|
||||
cursor = Math.max(0, Math.min(cursor, v.length))
|
||||
this.setData({ contentCursor: cursor })
|
||||
},
|
||||
|
||||
onContentBlur(e) {
|
||||
const v = e.detail.value != null ? String(e.detail.value) : ''
|
||||
let cursor = e.detail.cursor
|
||||
if (typeof cursor !== 'number' || Number.isNaN(cursor)) cursor = v.length
|
||||
cursor = Math.max(0, Math.min(cursor, v.length))
|
||||
this.setData({ contentCursor: cursor })
|
||||
},
|
||||
|
||||
insertAtContentCursor(insert) {
|
||||
if (this.data.auditMode) return false
|
||||
const raw = this.data.content != null ? String(this.data.content) : ''
|
||||
const seg = String(insert || '')
|
||||
if (!seg) return false
|
||||
let pos = this.data.contentCursor
|
||||
if (typeof pos !== 'number' || Number.isNaN(pos)) pos = raw.length
|
||||
pos = Math.max(0, Math.min(pos, raw.length))
|
||||
const next = raw.slice(0, pos) + seg + raw.slice(pos)
|
||||
const newCursor = pos + seg.length
|
||||
this.setData({
|
||||
content: next,
|
||||
contentCursor: newCursor,
|
||||
})
|
||||
return true
|
||||
},
|
||||
|
||||
insertMentionSelf() {
|
||||
if (this.data.auditMode) return
|
||||
const nick = this.data.meNickname || '我'
|
||||
this.insertAtContentCursor(`@${nick} `)
|
||||
},
|
||||
|
||||
/** 动态配图(发动态展示 + AI 生成多模态共用),最多 9 张 */
|
||||
chooseArticleImages() {
|
||||
this.syncAuditMode()
|
||||
if (this.data.auditMode || app.globalData.auditMode) {
|
||||
wx.showToast({ title: '审核模式下不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const max = 9
|
||||
const cur = this.data.bodyImages.length
|
||||
if (cur >= max) {
|
||||
wx.showToast({ title: '最多 9 张配图', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const pickCount = max - cur
|
||||
const mapFromChooseMedia = (res) => {
|
||||
const files = res.tempFiles || []
|
||||
return files.map((f, i) => ({
|
||||
path: f.tempFilePath,
|
||||
name: `配图_${Date.now()}_${i}.jpg`,
|
||||
size: f.size || 0,
|
||||
}))
|
||||
}
|
||||
const mapFromChooseImage = (res) => {
|
||||
const paths = res.tempFilePaths || []
|
||||
const t = Date.now()
|
||||
return paths.map((path, i) => ({
|
||||
path,
|
||||
name: `配图_${t}_${i}.jpg`,
|
||||
size: 0,
|
||||
}))
|
||||
}
|
||||
const runUpload = async (tempFiles) => {
|
||||
for (const f of tempFiles) {
|
||||
if (this.data.bodyImages.length >= max) break
|
||||
await this._uploadOneArticleBodyImage(f)
|
||||
}
|
||||
}
|
||||
if (typeof wx.chooseMedia === 'function') {
|
||||
wx.chooseMedia({
|
||||
count: pickCount,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
sizeType: ['compressed'],
|
||||
success: (res) => {
|
||||
void runUpload(mapFromChooseMedia(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
const em = String((err && err.errMsg) || '')
|
||||
if (/cancel|取消/i.test(em)) return
|
||||
wx.showToast({ title: '选择图片失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof wx.chooseImage === 'function') {
|
||||
wx.chooseImage({
|
||||
count: pickCount,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
void runUpload(mapFromChooseImage(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
const em = String((err && err.errMsg) || '')
|
||||
if (/cancel|取消/i.test(em)) return
|
||||
wx.showToast({ title: '选择图片失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '当前环境不支持选图片', icon: 'none' })
|
||||
},
|
||||
|
||||
async _uploadOneArticleBodyImage(f) {
|
||||
const filePath = f.path
|
||||
const name = String(f.name || '配图').trim() || '配图'
|
||||
const size = f.size || 0
|
||||
if (!filePath) return
|
||||
if (size > 29 * 1024 * 1024) {
|
||||
wx.showToast({ title: '单张请勿超过 30MB', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const id = newAttachId()
|
||||
this.setData({
|
||||
bodyImages: this.data.bodyImages.concat([{ id, url: '', uploading: true }]),
|
||||
})
|
||||
try {
|
||||
const up = await uploadByApi({ filePath, folder: 'super-article-images' })
|
||||
const url = this._pickUploadUrl(up)
|
||||
if (!url) throw new Error('no url')
|
||||
const next = this.data.bodyImages.map((x) =>
|
||||
x.id === id ? Object.assign({}, x, { url, uploading: false }) : x,
|
||||
)
|
||||
this.setData({ bodyImages: next })
|
||||
} catch (_) {
|
||||
this.setData({
|
||||
bodyImages: this.data.bodyImages.filter((x) => x.id !== id),
|
||||
})
|
||||
wx.showToast({ title: `${name} 上传失败`, icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
removeArticleImage(e) {
|
||||
if (this.data.auditMode) return
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (!id) return
|
||||
this.setData({
|
||||
bodyImages: this.data.bodyImages.filter((x) => x.id !== id),
|
||||
})
|
||||
},
|
||||
|
||||
_pickUploadUrl(up) {
|
||||
if (!up || typeof up !== 'object') return ''
|
||||
return String(up.url || up.fullUrl || up.data?.url || up.data?.fullUrl || '').trim()
|
||||
},
|
||||
|
||||
async generateArticle() {
|
||||
if (this.data.auditMode || app.globalData.auditMode) return
|
||||
if (this.data.generating) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.data.bodyImages.some((x) => x.uploading)) {
|
||||
wx.showToast({ title: '请等待配图上传完成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const prompt = String(this.data.aiPrompt || '').trim()
|
||||
const referenceText = ''
|
||||
const materialUrls = []
|
||||
const imageUrls = this.data.bodyImages
|
||||
.map((x) => String(x.url || '').trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (!prompt && imageUrls.length === 0) {
|
||||
wx.showToast({ title: '请填写提示词或添加配图', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ generating: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/super/articles/generate',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
data: {
|
||||
userId,
|
||||
description: prompt,
|
||||
referenceText,
|
||||
materialUrls,
|
||||
imageUrls,
|
||||
},
|
||||
})
|
||||
if (!res?.success) throw new Error(res?.error || '生成失败')
|
||||
const title = String(res.data?.title || '').trim()
|
||||
const content = String(res.data?.content || '').trim()
|
||||
let merged = content
|
||||
if (title && merged) merged = `${title}\n\n${merged}`
|
||||
else if (title) merged = title
|
||||
if (!merged.trim()) throw new Error('生成结果不完整')
|
||||
const mergedStr = merged.trim()
|
||||
this.setData({
|
||||
title: '',
|
||||
content: mergedStr,
|
||||
contentCursor: mergedStr.length,
|
||||
aiAssistVisible: false,
|
||||
})
|
||||
wx.showToast({ title: '已填入正文', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.showToast({
|
||||
title: (e && e.message) || '生成失败',
|
||||
icon: 'none',
|
||||
duration: 2800,
|
||||
})
|
||||
} finally {
|
||||
this.setData({ generating: false })
|
||||
}
|
||||
},
|
||||
|
||||
async submitArticle() {
|
||||
if (this.data.auditMode || app.globalData.auditMode) return
|
||||
if (this.data.saving) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const content = String(this.data.content || '').trim()
|
||||
if (this.data.bodyImages.some((x) => x.uploading)) {
|
||||
wx.showToast({ title: '请等待配图上传完成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const images = this.data.bodyImages
|
||||
.map((x) => String(x.url || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!content && images.length === 0) {
|
||||
wx.showToast({ title: '请填写文字或添加配图', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const editId = String(this.data.editArticleId || '').trim()
|
||||
this.setData({ saving: true })
|
||||
wx.showLoading({ title: editId ? '提交中…' : '发布中…', mask: true })
|
||||
try {
|
||||
let res
|
||||
if (editId) {
|
||||
res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(editId)}`,
|
||||
method: 'PUT',
|
||||
data: { userId, title: '', content, images },
|
||||
})
|
||||
} else {
|
||||
res = await app.request({
|
||||
url: '/api/miniprogram/super/articles',
|
||||
method: 'POST',
|
||||
data: { userId, title: '', content, images },
|
||||
})
|
||||
}
|
||||
if (!res?.success) throw new Error(res?.error || '发布失败')
|
||||
if (!editId) {
|
||||
this.setData({ title: '', content: '', bodyImages: [] })
|
||||
}
|
||||
wx.showToast({ title: '已提交审核', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
wx.redirectTo({ url: '/pages/super-article-mine/super-article-mine' })
|
||||
}, 450)
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '发布失败', icon: 'none' })
|
||||
} finally {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
} catch (_) {}
|
||||
this.setData({ saving: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.85)"></icon>
|
||||
</view>
|
||||
<text class="nav-title">{{editArticleId ? '编辑动态' : '发动态'}}</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 主编曲面:配图 → 正文(快捷插入含 AI,点开底部弹层) -->
|
||||
<view class="composer-sheet">
|
||||
<view class="sheet-section">
|
||||
<view class="section-head">
|
||||
<text class="section-title-text">配图</text>
|
||||
<text class="section-badge">{{bodyImages.length}}/9</text>
|
||||
</view>
|
||||
<text class="section-desc">可选 · 弹窗内「AI 帮你写」会与下方配图共用 · 发布后展示在正文下方</text>
|
||||
<view class="moment-photo-grid">
|
||||
<view wx:for="{{bodyImages}}" wx:key="id" class="moment-photo-cell">
|
||||
<image wx:if="{{item.url}}" class="moment-photo-img" mode="aspectFill" src="{{item.url}}" />
|
||||
<view wx:if="{{item.uploading}}" class="moment-photo-mask">上传中…</view>
|
||||
<text class="moment-photo-remove" data-id="{{item.id}}" catchtap="removeArticleImage">×</text>
|
||||
</view>
|
||||
<view wx:if="{{bodyImages.length < 9}}" class="moment-photo-cell moment-photo-add" bindtap="chooseArticleImages">
|
||||
<text class="moment-photo-add-plus">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hint-micro">单张建议不超 30MB</text>
|
||||
</view>
|
||||
|
||||
<view class="section-divider"></view>
|
||||
|
||||
<view class="sheet-section sheet-section--body">
|
||||
<view class="section-head">
|
||||
<text class="section-title-text">正文</text>
|
||||
<text class="section-meta">最多 5000 字</text>
|
||||
</view>
|
||||
<view class="inline-toolbar">
|
||||
<text class="toolbar-label">快捷插入</text>
|
||||
<view class="tool-btn" hover-class="tool-btn-hover" catchtap="insertMentionSelf">@自己</view>
|
||||
<view wx:if="{{showAiWriteSection && !auditMode}}" class="tool-btn" hover-class="tool-btn-hover" catchtap="openAiAssist">✨ AI 帮你写</view>
|
||||
</view>
|
||||
<view class="textarea-wrap moment-textarea-wrap">
|
||||
<textarea
|
||||
class="textarea moment-textarea"
|
||||
maxlength="5000"
|
||||
placeholder="写点什么…支持 Markdown、多段落"
|
||||
model:value="{{content}}"
|
||||
bindinput="onContentInput"
|
||||
bindblur="onContentBlur"
|
||||
cursor="{{contentCursor}}"
|
||||
disabled="{{auditMode}}"
|
||||
auto-height
|
||||
show-confirm-bar="{{false}}"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!auditMode}}" class="bottom-publish-bar">
|
||||
<view class="bottom-publish-btn {{saving ? 'bottom-publish-btn-disabled' : ''}}" bindtap="submitArticle">
|
||||
<text>{{saving ? (editArticleId ? '提交中…' : '发布中…') : (editArticleId ? '提交审核' : '发布')}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 底部弹层 -->
|
||||
<view wx:if="{{aiAssistVisible}}" class="ai-modal-root" catchtouchmove="preventMove">
|
||||
<view class="ai-modal-mask" catchtap="closeAiAssist"></view>
|
||||
<view class="ai-modal-panel" catchtap="noopModalTap">
|
||||
<view class="ai-modal-head">
|
||||
<text class="ai-modal-title">AI 帮你写</text>
|
||||
<text class="ai-modal-close" catchtap="closeAiAssist">关闭</text>
|
||||
</view>
|
||||
<text class="ai-modal-desc">提示词 + 当前配图一并交给模型;生成后写入正文,可关闭弹窗继续编辑</text>
|
||||
<text class="label-ai">提示词</text>
|
||||
<view class="textarea-wrap textarea-wrap-ai">
|
||||
<textarea
|
||||
class="textarea prompt-textarea-inner"
|
||||
maxlength="2000"
|
||||
placeholder="例如:根据配图写一篇派对复盘…"
|
||||
value="{{aiPrompt}}"
|
||||
bindinput="onAiPromptInput"
|
||||
disabled="{{auditMode}}"
|
||||
show-confirm-bar="{{false}}"
|
||||
></textarea>
|
||||
</view>
|
||||
<view class="generate-btn {{generating ? 'generate-btn-loading' : ''}}" bindtap="generateArticle">
|
||||
{{generating ? '生成中…' : '生成并填入正文'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{auditMode}}" class="audit-mode-mask" catchtouchmove="preventMove">
|
||||
<text class="audit-mode-text">开发中...</text>
|
||||
</view>
|
||||
</view>
|
||||
790
miniprogram/pages/super-article-editor/super-article-editor.wxss
Normal file
@@ -0,0 +1,790 @@
|
||||
.page {
|
||||
|
||||
min-height: 100vh;
|
||||
|
||||
background: linear-gradient(180deg, #080f18 0%, #0b1220 32%, #0b1220 100%);
|
||||
|
||||
color: #fff;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-bar {
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
height: 44px;
|
||||
|
||||
padding: 0 24rpx;
|
||||
|
||||
background: rgba(5, 11, 20, 0.92);
|
||||
|
||||
border-bottom: 1rpx solid rgba(94, 234, 212, 0.08);
|
||||
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
|
||||
width: 64rpx;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
|
||||
width: 64rpx;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: center;
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ========== 主编曲面 ========== */
|
||||
|
||||
.composer-sheet {
|
||||
|
||||
margin: 16rpx 24rpx 24rpx;
|
||||
|
||||
border-radius: 28rpx;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.22);
|
||||
|
||||
box-shadow: 0 24rpx 48rpx rgba(0, 0, 0, 0.35);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sheet-section {
|
||||
|
||||
padding: 28rpx 28rpx 26rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sheet-section--body {
|
||||
|
||||
padding-top: 24rpx;
|
||||
|
||||
padding-bottom: 28rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sheet-section--ai {
|
||||
|
||||
padding-top: 24rpx;
|
||||
|
||||
padding-bottom: 30rpx;
|
||||
|
||||
background: linear-gradient(
|
||||
|
||||
180deg,
|
||||
|
||||
rgba(8, 47, 73, 0.22) 0%,
|
||||
|
||||
rgba(15, 23, 42, 0.05) 100%
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-divider {
|
||||
|
||||
height: 1rpx;
|
||||
|
||||
margin: 0 28rpx;
|
||||
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-head {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 16rpx;
|
||||
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-title-text {
|
||||
|
||||
font-size: 30rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
color: #f1f5f9;
|
||||
|
||||
letter-spacing: 1rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-badge {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: #22d3ee;
|
||||
|
||||
padding: 6rpx 16rpx;
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
background: rgba(34, 211, 238, 0.12);
|
||||
|
||||
border: 1rpx solid rgba(34, 211, 238, 0.28);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-meta {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #94a3b8;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-tag-ai {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #a5f3fc;
|
||||
|
||||
padding: 4rpx 14rpx;
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.35);
|
||||
|
||||
background: rgba(34, 211, 238, 0.08);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-desc {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #94a3b8;
|
||||
|
||||
line-height: 1.55;
|
||||
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hint-micro {
|
||||
|
||||
display: block;
|
||||
|
||||
margin-top: 14rpx;
|
||||
|
||||
font-size: 20rpx;
|
||||
|
||||
color: #64748b;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inline-toolbar {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 12rpx;
|
||||
|
||||
margin-bottom: 14rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toolbar-label {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #64748b;
|
||||
|
||||
margin-right: 4rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tool-btn {
|
||||
|
||||
padding: 12rpx 22rpx;
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
font-size: 24rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: #67e8f9;
|
||||
|
||||
border: 1rpx solid rgba(34, 211, 238, 0.4);
|
||||
|
||||
background: rgba(8, 47, 73, 0.5);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tool-btn-hover {
|
||||
|
||||
opacity: 0.82;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.label-ai {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 24rpx;
|
||||
|
||||
color: #cbd5e1;
|
||||
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
margin-top: 4rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.textarea-wrap,
|
||||
|
||||
.textarea-wrap-ai {
|
||||
|
||||
background: rgba(2, 6, 23, 0.78);
|
||||
|
||||
border-radius: 18rpx;
|
||||
|
||||
padding: 18rpx 20rpx;
|
||||
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.12);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.textarea-wrap-ai {
|
||||
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.moment-textarea-wrap {
|
||||
|
||||
padding: 20rpx 22rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.textarea {
|
||||
|
||||
width: 100%;
|
||||
|
||||
min-height: 320rpx;
|
||||
|
||||
color: #fff;
|
||||
|
||||
font-size: 30rpx;
|
||||
|
||||
line-height: 1.65;
|
||||
|
||||
background: transparent;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.moment-textarea {
|
||||
|
||||
min-height: 280rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.prompt-textarea-inner {
|
||||
|
||||
min-height: 160rpx;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 28rpx;
|
||||
|
||||
line-height: 1.55;
|
||||
|
||||
color: #f1f5f9;
|
||||
|
||||
background: transparent;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.generate-btn {
|
||||
|
||||
margin-top: 8rpx;
|
||||
|
||||
height: 88rpx;
|
||||
|
||||
border-radius: 18rpx;
|
||||
|
||||
border: 1rpx solid rgba(34, 211, 238, 0.5);
|
||||
|
||||
color: #ecfeff;
|
||||
|
||||
font-size: 28rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
background: linear-gradient(135deg, rgba(8, 47, 73, 0.85), rgba(15, 118, 110, 0.35));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.generate-btn-loading {
|
||||
|
||||
opacity: 0.65;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bottom-publish-bar {
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
z-index: 50;
|
||||
|
||||
padding: 20rpx 28rpx;
|
||||
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
|
||||
background: rgba(11, 18, 32, 0.96);
|
||||
|
||||
border-top: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
|
||||
}
|
||||
|
||||
.bottom-publish-btn {
|
||||
|
||||
height: 92rpx;
|
||||
|
||||
border-radius: 18rpx;
|
||||
|
||||
background: linear-gradient(135deg, #22d3ee, #14b8a6);
|
||||
|
||||
color: #032b35;
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.bottom-publish-btn-disabled {
|
||||
|
||||
opacity: 0.65;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 配图宫格 */
|
||||
|
||||
.moment-photo-grid {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-cell {
|
||||
|
||||
position: relative;
|
||||
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
|
||||
padding-bottom: calc((100% - 32rpx) / 3);
|
||||
|
||||
height: 0;
|
||||
|
||||
border-radius: 14rpx;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
background: rgba(2, 6, 23, 0.65);
|
||||
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.15);
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-img {
|
||||
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 100%;
|
||||
|
||||
display: block;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-mask {
|
||||
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #94a3b8;
|
||||
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-remove {
|
||||
|
||||
position: absolute;
|
||||
|
||||
right: 6rpx;
|
||||
|
||||
top: 4rpx;
|
||||
|
||||
width: 44rpx;
|
||||
|
||||
height: 44rpx;
|
||||
|
||||
line-height: 40rpx;
|
||||
|
||||
text-align: center;
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
color: #fff;
|
||||
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-add {
|
||||
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.moment-photo-add-plus {
|
||||
|
||||
position: absolute;
|
||||
|
||||
left: 50%;
|
||||
|
||||
top: 50%;
|
||||
|
||||
transform: translate(-50%, -52%);
|
||||
|
||||
font-size: 64rpx;
|
||||
|
||||
font-weight: 300;
|
||||
|
||||
color: rgba(148, 163, 184, 0.85);
|
||||
|
||||
line-height: 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* AI 底部弹层 */
|
||||
|
||||
.ai-modal-root {
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
z-index: 600;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-mask {
|
||||
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
background: rgba(0, 0, 0, 0.58);
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-panel {
|
||||
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
|
||||
padding: 28rpx 28rpx 32rpx;
|
||||
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
|
||||
max-height: 78vh;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.22);
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
box-shadow: 0 -12rpx 48rpx rgba(0, 0, 0, 0.45);
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-head {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-title {
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
color: #f1f5f9;
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-close {
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
color: #67e8f9;
|
||||
|
||||
padding: 12rpx 16rpx;
|
||||
|
||||
}
|
||||
|
||||
.ai-modal-desc {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #94a3b8;
|
||||
|
||||
line-height: 1.55;
|
||||
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.audit-mode-mask {
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: 0;
|
||||
|
||||
right: 0;
|
||||
|
||||
top: 0;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
z-index: 10000;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
background: rgba(11, 18, 32, 0.94);
|
||||
|
||||
}
|
||||
|
||||
.audit-mode-text {
|
||||
|
||||
font-size: 36rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
|
||||
letter-spacing: 4rpx;
|
||||
|
||||
}
|
||||
|
||||
270
miniprogram/pages/super-article-mine/super-article-mine.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Soul创业派对 - 我的动态(含待审/通过/驳回,驳回可跳转编辑)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
const THUMB_MAX = 3
|
||||
|
||||
function relativeTime(iso) {
|
||||
if (!iso) return ''
|
||||
const d = new Date(iso)
|
||||
const t = d.getTime()
|
||||
if (Number.isNaN(t)) return ''
|
||||
const sec = Math.max(0, Math.floor((Date.now() - t) / 1000))
|
||||
if (sec < 60) return '刚刚'
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)}分钟前`
|
||||
if (sec < 86400) return `${Math.floor(sec / 3600)}小时前`
|
||||
if (sec < 86400 * 7) return `${Math.floor(sec / 86400)}天前`
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${m}-${day}`
|
||||
}
|
||||
|
||||
function auditLabel(st) {
|
||||
const s = String(st || '').toLowerCase()
|
||||
if (s === 'pending') return '审核中'
|
||||
if (s === 'rejected') return '已驳回'
|
||||
return '已发布'
|
||||
}
|
||||
|
||||
function parseImagesField(images) {
|
||||
if (images == null) return []
|
||||
if (typeof images === 'string') {
|
||||
const s = images.trim()
|
||||
if (!s) return []
|
||||
try {
|
||||
const p = JSON.parse(s)
|
||||
return Array.isArray(p) ? p : []
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return Array.isArray(images) ? images : []
|
||||
}
|
||||
|
||||
function normalizeArticleImageSrc(raw) {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === 'undefined' || u === 'null') return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return `https:${u}`
|
||||
if (u.startsWith('wxfile://') || u.startsWith('cloud://')) return u
|
||||
const base = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
if (u.startsWith('/') && base) return `${base}${u}`
|
||||
return u
|
||||
}
|
||||
|
||||
function buildThumbSlots(imagesField) {
|
||||
const rawList = parseImagesField(imagesField)
|
||||
const n = Math.min(rawList.length, THUMB_MAX)
|
||||
const thumbSlots = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const srcNorm = normalizeArticleImageSrc(String(rawList[i] || '').trim())
|
||||
const ok =
|
||||
!!srcNorm &&
|
||||
(/^https?:\/\//i.test(srcNorm) ||
|
||||
srcNorm.startsWith('wxfile://') ||
|
||||
srcNorm.startsWith('cloud://'))
|
||||
thumbSlots.push({ slotIdx: i, src: ok ? srcNorm : '', loadFailed: false })
|
||||
}
|
||||
return {
|
||||
showThumbRow: rawList.length > 0,
|
||||
thumbSlots,
|
||||
thumbMoreCount: rawList.length > THUMB_MAX ? rawList.length - THUMB_MAX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mapRows(rows) {
|
||||
return (rows || []).map((r) => {
|
||||
const st = String(r.auditStatus || 'approved').toLowerCase()
|
||||
const thumb = buildThumbSlots(r.images)
|
||||
const contentTrim = String(r.content || '').trim()
|
||||
const titleTrim = String(r.title || '').trim()
|
||||
return Object.assign({}, r, {
|
||||
auditStatus: st === 'pending' || st === 'rejected' ? st : 'approved',
|
||||
auditLabel: auditLabel(st),
|
||||
timeText: relativeTime(r.createdAt),
|
||||
preview: String(r.preview || '').trim(),
|
||||
contentTrim,
|
||||
titleTrim,
|
||||
rejectReason: String(r.rejectReason || '').trim(),
|
||||
showThumbRow: thumb.showThumbRow,
|
||||
thumbSlots: thumb.thumbSlots,
|
||||
thumbMoreCount: thumb.thumbMoreCount,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
auditMode: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: !!app.globalData.auditMode,
|
||||
})
|
||||
try {
|
||||
app.getAuditMode && app.getAuditMode().catch(() => {})
|
||||
} catch (_) {}
|
||||
this.reload()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
},
|
||||
|
||||
goCompose() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/super-article-editor/super-article-editor' })
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore()
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
|
||||
noop() {},
|
||||
|
||||
openDetail(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/read/read?superArticleId=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
openEdit(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/super-article-editor/super-article-editor?id=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
tapDeleteArticle(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id || this._articleDeleting) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '删除动态',
|
||||
content: '删除后不可恢复,确定删除?',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#dc2626',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
this._articleDeleting = true
|
||||
wx.showLoading({ title: '删除中…', mask: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}?userId=${encodeURIComponent(String(userId))}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
wx.hideLoading()
|
||||
this._articleDeleting = false
|
||||
const list = this.data.list.filter((x) => String(x.id) !== String(id))
|
||||
const total = Math.max(0, (Number(this.data.total) || 0) - 1)
|
||||
this.setData({
|
||||
list,
|
||||
total,
|
||||
hasMore: list.length < total,
|
||||
})
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
} catch (_) {
|
||||
wx.hideLoading()
|
||||
this._articleDeleting = false
|
||||
/* app.request 已 toast */
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onThumbError(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
const slotIdx = Number(e.currentTarget.dataset.slotidx)
|
||||
if (Number.isNaN(rowIdx) || Number.isNaN(slotIdx)) return
|
||||
this.setData({ [`list[${rowIdx}].thumbSlots[${slotIdx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewRowImages(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
if (Number.isNaN(rowIdx)) return
|
||||
const row = this.data.list[rowIdx]
|
||||
if (!row) return
|
||||
const urls = parseImagesField(row.images)
|
||||
.map((u) => normalizeArticleImageSrc(String(u || '').trim()))
|
||||
.filter((u) => /^https?:\/\//i.test(u))
|
||||
if (!urls.length) return
|
||||
const current = String(e.currentTarget.dataset.current || '').trim() || urls[0]
|
||||
wx.previewImage({ urls, current })
|
||||
},
|
||||
|
||||
async reload() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId || !app.globalData.isLoggedIn) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true, page: 1, list: [], hasMore: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/mine?userId=${encodeURIComponent(String(userId))}&page=1&pageSize=${PAGE_SIZE}`,
|
||||
silent: true,
|
||||
})
|
||||
const raw = Array.isArray(res?.list) ? res.list : []
|
||||
const list = mapRows(raw)
|
||||
const total = Number(res?.total) || 0
|
||||
this.setData({
|
||||
list,
|
||||
total,
|
||||
hasMore: list.length < total,
|
||||
loading: false,
|
||||
page: 1,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ loading: false, list: [] })
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (this.data.loading || this.data.loadingMore || !this.data.hasMore) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
const nextPage = this.data.page + 1
|
||||
this.setData({ loadingMore: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/mine?userId=${encodeURIComponent(String(userId))}&page=${nextPage}&pageSize=${PAGE_SIZE}`,
|
||||
silent: true,
|
||||
})
|
||||
const raw = Array.isArray(res?.list) ? res.list : []
|
||||
const chunk = mapRows(raw)
|
||||
const merged = this.data.list.concat(chunk)
|
||||
const total = Number(res?.total) || this.data.total
|
||||
this.setData({
|
||||
list: merged,
|
||||
page: nextPage,
|
||||
total,
|
||||
hasMore: merged.length < total,
|
||||
loadingMore: false,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ loadingMore: false })
|
||||
}
|
||||
},
|
||||
})
|
||||