更新个人资料页实现评估会议记录,明确展示与编辑页字段一致性要求,补充技能字段的展示与编辑需求。优化小程序页面,增加联系方式完善弹窗,确保用户在使用找伙伴功能前填写手机号或微信号。调整相关文档以反映最新进展,提升用户体验与功能一致性。
This commit is contained in:
@@ -5,3 +5,8 @@
|
||||
- **stitch_soul 串联「内容→会员→导师」变现路径**:临时需求池 10 个稿子覆盖目录、导师、会员、首页、资料编辑,需在正式需求文档中明确 73 章、导师、案例库、会员的业务定义与验收标准。
|
||||
- **待澄清项**:73 章与现有内容库是否同一套;导师与内容作者是否同一人;「案例库」是独立内容池还是章节分类;会员权益与价格策略。
|
||||
- **优先级建议**:首页/目录/会员 > 导师列表/详情 > 资料编辑。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **展示/编辑页一致性**:展示页(enhanced_professional_profile)与编辑页(comprehensive_profile_editor_v1_1)需字段一一对应、配色统一。
|
||||
- **skills「我擅长」**:展示页已有,编辑页必须补充;验收时确认两页数据一致。
|
||||
|
||||
@@ -5,3 +5,13 @@
|
||||
- **现有基础**:soul-api 已有 chapter、book、vip 模型;导师能力需新建或扩展现有 match 体系(现有 mentor 为 match 类型,非独立导师实体)。
|
||||
- **待设计**:导师列表/详情/搜索筛选、预约单、会员权益与预约支付打通;接口挂 `/api/miniprogram/*`。
|
||||
- **协同**:与产品核对 chapter/book/vip 现状后,给出导师/预约/会员权益的模型与接口方案。
|
||||
|
||||
## chapter/book/vip 模型补充(问题 5 作答)
|
||||
|
||||
- **chapters 表**:每行一节,id 如 1.1/preface;part/chapter/section 三层;73 章=行数统计。
|
||||
- **book**:无独立表,= chapters 聚合;接口 `/api/book/all-chapters`、`/api/book/chapter/:id`。
|
||||
- **vip**:vip_roles 配置角色;users 存 is_vip/vip_expire_date 等;权益优先 users,无则 orders 兜底;¥1980。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **profile API**:`GET/POST /api/miniprogram/user/profile` 已覆盖 skills 等全部扩展字段;无需新增接口。
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
## 会议规则升级
|
||||
|
||||
- **问题与作答区**:开完会后必须将待确认/待澄清问题列出,在会议纪要中增加「问题与作答区」节,问题表含:序号、问题、责任角色、作答(留空供后续填写);便于追溯闭环。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **展示/编辑页协同**:profile-show 与 profile-edit 共用同一 API,skills 等扩展字段需双向同步;配色统一为 enhanced(#5EEAD4)强化品牌一致。
|
||||
|
||||
@@ -5,3 +5,14 @@
|
||||
- **页面范围**:首页、目录、导师列表/详情、会员落地页、个人资料/编辑;全部接口走 `/api/miniprogram/*`。
|
||||
- **支付**:会员购买、导师预约支付需按微信支付规范实现。
|
||||
- **时机**:待需求与接口确定后按优先级分阶段排期(建议:内容→会员→导师→资料编辑)。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **profile-show**:已按 enhanced_professional_profile 完成,accent #5EEAD4;profile-edit 需与其视觉统一。
|
||||
- **profile-edit 待做**:① 增加「我擅长」输入框(skills 后端已支持);② 配色统一为 enhanced(#5EEAD4 / #050B14 / #0F1720)。
|
||||
- **流程**:我的 → profile-show → ⋯ 编辑 → profile-edit → 保存 → 返回;已打通,无需改。
|
||||
|
||||
## input/textarea padding 规范
|
||||
|
||||
- **原则**:给 input 或 textarea 设置 padding 时,必须用 view 包裹,padding 写在 view 上;不在 input/textarea 自身上设 padding,避免原生组件光标截断、布局异常。
|
||||
- **已升级**:miniprogram-dev SKILL §6。
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
- **待支撑能力**:章节管理(增删改、排序、免费/付费/NEW)、导师管理(审核、标签、价格、展示)、会员配置(权益、价格、有效期)、预约管理(列表、状态)。
|
||||
- **接口依赖**:`/api/admin/*` 与 `/api/db/*`;字段需与 miniprogram 端统一。
|
||||
- **时机**:待后端方案确定后规划管理端页面与接口对接。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **无新增任务**:个人资料展示/编辑为 C 端能力,管理端沿用现有能力即可。
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
- **关键联调场景**:阅读进度、免费/付费解锁、会员权益、导师预约与支付、资料完善与提现限制。
|
||||
- **三端**:miniprogram ↔ soul-api、soul-admin ↔ soul-api;变更后需回归支付、登录、提现等现有流程。
|
||||
- **待办**:需求确定后补充三端联调用例与回归清单。
|
||||
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **验证点**:profile-show 与 profile-edit 字段一一对应;保存后两页及「我的」数据一致;手机/微信号脱敏与复制;头像上传、昵称、MBTI 选择。
|
||||
|
||||
80
.cursor/meeting/2026-02-28_P0测试清单.md
Normal file
80
.cursor/meeting/2026-02-28_P0测试清单.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# stitch_soul P0 测试清单
|
||||
|
||||
> 测试人员按此清单验证 P0 功能。
|
||||
|
||||
---
|
||||
|
||||
## 一、开发完成情况
|
||||
|
||||
| 阶段 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| **P0** | ✅ 完成 | 首页/目录 + NEW + 精选推荐算法 |
|
||||
| P1 | 未开始 | 会员落地页 |
|
||||
| P2 | 未开始 | 导师 + 预约 |
|
||||
| P3 | 未开始 | 资料编辑扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 二、P0 接口测试
|
||||
|
||||
**前提**:soul-api 已启动,数据库已执行 `add-chapters-is-new.sql`。
|
||||
|
||||
### 2.1 后端接口(可用 PowerShell 脚本或 curl 验证)
|
||||
|
||||
```powershell
|
||||
# 在 soul-api 目录下执行
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
.\scripts\test-p0-endpoints.ps1
|
||||
```
|
||||
|
||||
或手动验证:
|
||||
|
||||
| 接口 | 期望 |
|
||||
|------|------|
|
||||
| `GET /api/miniprogram/book/all-chapters` | success: true,data 为数组,每项含 `isNew` 字段 |
|
||||
| `GET /api/miniprogram/book/recommended` | success: true,data 为 1~3 条,每项含 `tag`(热门/推荐/精选) |
|
||||
| `GET /api/miniprogram/book/latest-chapters` | success: true,data 为数组(按 updated_at 降序) |
|
||||
| `GET /api/miniprogram/book/hot` | success: true,data 为数组(按阅读量或兜底排序) |
|
||||
|
||||
### 2.2 管理端测试
|
||||
|
||||
| 步骤 | 操作 | 期望 |
|
||||
|------|------|------|
|
||||
| 1 | 登录 soul-admin | 成功 |
|
||||
| 2 | 进入「内容管理」 | 章节列表正常 |
|
||||
| 3 | 点击某一节「编辑」 | 弹出编辑框 |
|
||||
| 4 | 勾选「标记 NEW」并保存 | 保存成功,无报错 |
|
||||
| 5 | 刷新列表,再次编辑同一节 | 「标记 NEW」保持勾选 |
|
||||
|
||||
### 2.3 小程序测试
|
||||
|
||||
| 步骤 | 操作 | 期望 |
|
||||
|------|------|------|
|
||||
| 1 | 打开小程序首页 | 加载正常 |
|
||||
| 2 | 查看「最新更新」Banner | 显示一条章节,点击可进入阅读 |
|
||||
| 3 | 查看「精选推荐」 | 显示 3 条,带 热门/推荐/精选 标签 |
|
||||
| 4 | 查看「最新新增」 | 有 isNew 的章节在此展示 |
|
||||
| 5 | 进入「目录」页 | 从服务端加载,按篇章聚合 |
|
||||
| 6 | 在目录中查看标记 NEW 的章节 | 显示 NEW 标签 |
|
||||
| 7 | 查看免费/¥1 显示 | 免费节显示「免费」,付费节显示「¥1」 |
|
||||
|
||||
---
|
||||
|
||||
## 三、联调验证
|
||||
|
||||
| 验证点 | 说明 |
|
||||
|--------|------|
|
||||
| 管理端标记 NEW → 小程序展示 | 在管理端勾选某节 NEW,小程序目录/首页「最新新增」应出现 |
|
||||
| 精选推荐排除序言/尾声/附录 | 若 part_title 含「序言」「尾声」「附录」,不应出现在 recommended/hot |
|
||||
| 阅读量兜底 | 无 reading_progress 数据时,hot/recommended 应返回 updated_at 排序的兜底结果 |
|
||||
|
||||
---
|
||||
|
||||
## 四、已知限制
|
||||
|
||||
- **阅读量**:当前依赖 `reading_progress` 表,新环境无数据时会走兜底(按 updated_at)。
|
||||
- **固定 3 章兜底**:若连章节列表都拿不到,会返回空;未实现「预设固定 3 章」配置。
|
||||
|
||||
---
|
||||
|
||||
*测试完成后可更新本文件,标注通过/失败及问题。*
|
||||
91
.cursor/meeting/2026-02-28_个人资料页实现评估.md
Normal file
91
.cursor/meeting/2026-02-28_个人资料页实现评估.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 会议纪要 - 2026-02-28 | 个人资料页实现评估
|
||||
|
||||
> 本文件由**助理橙子**在会议结束后自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **时间**:2026-02-28
|
||||
- **议题**:个人资料展示页(profile-show)与编辑页(profile-edit)实现评估
|
||||
- **触发方式**:开个会议评估怎么实现
|
||||
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
|
||||
|
||||
---
|
||||
|
||||
## 各角色发言
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
- **展示页**(enhanced_professional_profile):面向他人查看,含头像、昵称、MBTI/地区、基本信息、个人故事、互助需求、项目介绍。
|
||||
- **编辑页**(comprehensive_profile_editor_v1_1):完整表单,需与展示页字段一一对应。
|
||||
- **待澄清**:展示页有「我擅长」展示,编辑页需补充;两页配色建议统一。
|
||||
|
||||
**验收标准**:我的 → 展示页 → 编辑页 → 保存 → 返回,流程闭环;字段完整对应。
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
- 现有 `GET/POST /api/miniprogram/user/profile` 已覆盖 nickname, avatar, mbti, region, industry, position, businessScale, **skills**, phone, wechatId, 个人故事三字段、互助需求两字段、projectIntro。
|
||||
- 无需新接口;skills 已支持读写,编辑页只需前端对接。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
- 个人资料为 C 端能力,管理端无新增任务。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
- **profile-show**:已按 enhanced_professional_profile 完成,配色 #5EEAD4 / #050B14 / #0F1720。
|
||||
- **profile-edit**:已按 comprehensive_profile_editor_v1_1 实现功能,配色 #4FD1C5 / #000。
|
||||
- **待做**:① 编辑页增加「我擅长」输入框;② 配色统一为 enhanced 风格。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
- 验证展示页与编辑页字段一致、保存后数据正确回显。
|
||||
- 手机号/微信号脱敏与复制;头像上传、昵称、MBTI 选择;空/超长输入边界。
|
||||
|
||||
---
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 产品确认:skills 必须在编辑页体现。
|
||||
- 产品确认:两页配色统一为 enhanced 风格(#5EEAD4)以强化品牌一致。
|
||||
- 小程序确认:skills 后端已有,配色替换工作量小。
|
||||
|
||||
---
|
||||
|
||||
## 会议决议
|
||||
|
||||
1. **skills 字段**:在 profile-edit「基本信息」区块增加「我擅长」输入框,与 profile-show 对应。
|
||||
2. **视觉统一**:profile-edit 配色统一为 enhanced(accent #5EEAD4, background #050B14, card #0F1720)。
|
||||
3. **实现顺序**:先补 skills,再做配色统一。
|
||||
4. **流程**:我的 → profile-show → ⋯ → profile-edit → 保存 → 返回,已打通,无需修改。
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
| 责任角色 | 任务 | 优先级 | 截止建议 |
|
||||
|---------|------|--------|---------|
|
||||
| 小程序开发工程师 | profile-edit 增加「我擅长」输入框及 JS 读写 | 高 | 本次迭代 |
|
||||
| 小程序开发工程师 | profile-edit 配色统一为 enhanced 风格 | 中 | 本次迭代 |
|
||||
|
||||
---
|
||||
|
||||
## 问题与作答区
|
||||
|
||||
| # | 问题 | 责任角色 | 作答 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 编辑页导航栏是否需增加右侧 more_horiz + 头像(设计稿有此元素)? | 产品经理 | (待补充) |
|
||||
| 2 | 展示页「成为超级个体」按钮点击后的具体跳转路径? | 产品经理 | (待补充) |
|
||||
|
||||
---
|
||||
|
||||
## 各角色经验与业务理解更新
|
||||
|
||||
- 个人资料展示页与编辑页需字段、配色、流程一致,便于用户理解。
|
||||
- profile-edit 与 profile-show 共用同一套 API,skills 等扩展字段需双向同步。
|
||||
- 本次会议决议已写入本纪要;各角色经验已同步至 `agent/{角色}/evolution/2026-02-28.md`。
|
||||
|
||||
---
|
||||
|
||||
*会议纪要由助理橙子生成 | 2026-02-28*
|
||||
@@ -7,8 +7,8 @@
|
||||
## 基本信息
|
||||
|
||||
- **时间**:2026-02-28
|
||||
- **议题**:临时需求池 `soul20260228/stitch_soul` 中的 10 个 UI 稿需求讨论,形成开发共识
|
||||
- **触发方式**:开个会议、所有人讨论这个需求
|
||||
- **议题**:分析临时需求池 soul20260228/stitch_soul 全部需求
|
||||
- **触发方式**:开个会议,所有人都来,分析这个需求
|
||||
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
|
||||
|
||||
---
|
||||
@@ -17,53 +17,58 @@
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
- 这些稿子串联了「内容阅读 + 导师咨询」两条线:目录→会员→导师咨询。
|
||||
- 用户价值:目录+阅读进度提升内容留存;导师列表/详情支持 1v1 咨询变现;会员落地页支撑付费转化。
|
||||
- 待澄清:73 章与现有内容库是否同一套?导师与内容作者是否同一人?「案例库」是独立内容池还是章节分类?
|
||||
- 优先级建议:首页/目录/会员 > 导师列表/详情 > 资料编辑。
|
||||
从 10 个稿子可归纳出 **stitch_soul 串联「内容→会员→导师」变现路径**:
|
||||
|
||||
- **首页**(optimized_home_content_feed_v1):品牌、搜索、最新更新、阅读进度、超级个体、精选推荐、最新新增(NEW)
|
||||
- **目录**(catalog_with_new_additions_v1):73 章、篇章结构、NEW 标签、免费/¥1 付费
|
||||
- **会员落地**(premium_membership_landing_v1):¥1980/年,内容权益(章节、案例库、智能纪要、会议纪要)+ 社交权益(匹配、排行、资源、VIP 标识)
|
||||
- **导师**:列表(搜索/分类)+ 详情(介绍/服务/价格/预约),单次咨询 ¥600~2500
|
||||
- **个人资料**:展示(基本信息/个人故事/互助需求/项目介绍)、编辑(完整表单)、手机号/微信号弹窗
|
||||
- **我的**:VIP 标识、分享收益、阅读统计、最近阅读、订单
|
||||
|
||||
**待澄清**:73 章与现有内容库是否同一套;导师与内容作者是否同一人;「案例库」是独立内容池还是章节分类;会员权益与价格策略。
|
||||
|
||||
**建议优先级**:首页/目录/会员 > 导师 > 资料。
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
- 需要接口:目录/章节(列表、分组、阅读进度、免费/付费/NEW)、导师(列表、详情、搜索/筛选、预约)、会员权益校验、资料(手机号、微信号、头像)。
|
||||
- 数据模型:章节、导师、会员权益、预约单、阅读进度等,需核对 soul-api 现有模型。
|
||||
- 支付/会员:会员付费、导师预约支付需与现有支付流程打通。
|
||||
- 待确认:现有 soul-api 中已有 chapter、book、vip;导师需新建或扩展现有 match 体系。
|
||||
**现有基础**:soul-api 已有 chapter、book、vip 模型;导师能力需新建或扩展现有 match 体系(现有 mentor 为 match 类型,非独立导师实体)。
|
||||
|
||||
**待设计**:导师列表/详情/搜索筛选、预约单、会员权益与预约支付打通;接口挂 `/api/miniprogram/*`。与产品核对 chapter/book/vip 现状后,给出导师/预约/会员权益的模型与接口方案。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
- 需要能力:章节管理(增删改、排序、免费/付费/NEW 配置)、导师管理(审核、标签、价格、展示)、会员配置(权益、价格、有效期)、预约管理(列表、状态)。
|
||||
- 接口依赖 `/api/admin/*` 与 `/api/db/*`。
|
||||
- 字段需与小程序端保持一致。
|
||||
管理端需配套:章节/导师 CRUD、NEW 标记、会员权益配置、预约单管理、收益/提现审核。接口走 `/api/admin/*`、`/api/db/*`,字段与小程序/后端一致。待后端方案确定后规划具体页面。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
- 页面:首页、目录、导师列表/详情、会员落地页、个人资料/编辑。
|
||||
- 全部接口走 `/api/miniprogram/*`。
|
||||
- 支付:会员购买、导师预约支付需按微信支付规范实现。
|
||||
10 个稿子覆盖首页/目录/会员/导师列表/导师详情/资料展示/资料编辑/我的五类页面,交互清晰,需 `/api/miniprogram/*`。待需求与接口确定后分阶段实现。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
- 三端联调:miniprogram ↔ soul-api、soul-admin ↔ soul-api。
|
||||
- 关键场景:阅读进度、免费/付费解锁、会员权益、导师预约与支付、资料完善与提现限制。
|
||||
- 回归:变更后需回归支付、登录、提现等现有流程。
|
||||
关键场景为阅读/付费、会员、导师预约、资料完善;三端联调(小程序↔API、管理端↔API)验证点。待需求确定后补充联调用例和回归清单。
|
||||
|
||||
---
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 产品强调 73 章与现有内容需先澄清,建议产品补充正式需求文档。
|
||||
- 后端需梳理 soul-api 现有 chapter/book/vip 模型,导师能力需新建或扩展 match。
|
||||
- 前端(管理端、小程序)需统一字段约定,避免后期频繁联调。
|
||||
- 测试建议在需求确定后补充三端联调用例清单。
|
||||
- 产品询问后端:73 章、book、vip 现状是否明确。
|
||||
- 后端回复:需与产品共同核对 chapter/book/vip 后再定导师/预约/会员权益模型。
|
||||
- 管理端确认需管理章节、导师、会员、预约、收益。
|
||||
- 小程序确认页面稿清晰,等接口与需求后分阶段开发。
|
||||
- 测试:待业务规则确定后补充用例。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 会议决议
|
||||
|
||||
1. **需求池定性**:`soul20260228/stitch_soul` 作为原型/参考,需产品补充正式需求文档,明确业务规则与验收标准。
|
||||
2. **数据与接口**:后端与产品核对现有 chapter/book/vip 模型,并给出导师、预约、会员权益的模型与接口建议。
|
||||
3. **优先级**:以产品最终确认为准,建议顺序为「内容→会员→导师→资料编辑」。
|
||||
4. **待确认**:73 章与现有内容关系、导师与作者关系、「案例库」定义、会员权益与价格策略。
|
||||
1. **stitch_soul 定位**: stitch 产品线在 Soul 创业派对上的扩展,串联「内容阅读 + 会员 + 导师咨询」变现路径。
|
||||
2. **产品**:需在正式需求文档中明确 73 章、导师、案例库、会员的业务定义与验收标准。
|
||||
3. **后端**:梳理 chapter/book/vip,设计导师/预约/会员权益模型与接口方案。
|
||||
4. **开发优先级**:首页/目录/会员 > 导师列表与详情 > 资料编辑 / 我的。
|
||||
5. **待确认项**:73 章与内容库关系、导师与作者关系、案例库定义、会员权益与价格。
|
||||
6. **管理端跟进原则(已采纳)**:以后小程序有功能变更时,管理端须根据 C 端能力主动补充管理功能;后端需支持对应配置能力。**本需求示例**:导师价格 → 后端支持每个导师独立价格配置(单次/半年/年度),管理端导师编辑页提供价格配置。已写入 role-flow-control、admin-dev Skill。
|
||||
|
||||
---
|
||||
|
||||
@@ -71,65 +76,496 @@
|
||||
|
||||
| 责任角色 | 任务 | 优先级 | 截止建议 |
|
||||
|---------|------|--------|---------|
|
||||
| 产品经理 | 产出 stitch_soul 正式需求文档,澄清 73 章/导师/案例库/会员规则 | 高 | 需求排期前 |
|
||||
| 后端开发 | 梳理 chapter/book/vip 现状,给出导师/预约/会员权益模型与接口方案 | 高 | 需求评审后 |
|
||||
| 管理端开发工程师 | 待后端方案确定后,规划章节/导师/会员/预约管理页面 | 中 | 后端接口就绪后 |
|
||||
| 小程序开发工程师 | 待需求与接口确定后,按优先级实现首页/目录/导师/会员/资料页面 | 中 | 分阶段排期 |
|
||||
| 测试人员 | 需求确定后,补充三端联调用例与回归清单 | 中 | 开发启动前 |
|
||||
| 产品经理 | 撰写 stitch_soul 正式需求文档,明确业务边界 | 高 | 3 天内 |
|
||||
| 后端工程师 | 梳理 chapter/book/vip,输出导师/预约/会员权益模型与接口方案 | 高 | 产品文档确认后 |
|
||||
| 管理端开发工程师 | 待后端方案确定后,规划章节/导师/会员/预约管理页面 | 中 | 后端方案确定后 |
|
||||
| 小程序开发工程师 | 待需求与接口确定后,分阶段实现首页/目录/会员/导师/资料 | 中 | 接口确定后 |
|
||||
| 测试人员 | 待需求确定后,补充阅读/付费/会员/导师/资料联调用例 | 中 | 需求确定后 |
|
||||
|
||||
---
|
||||
|
||||
## 问题与作答区
|
||||
|
||||
> 会议中提出的待确认问题在此列出;作答区域供后续补充答案,便于追溯闭环。
|
||||
|
||||
| # | 问题 | 责任角色 | 作答 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 73 章与现有内容库是否同一套? | 产品经理 | (待补充) |
|
||||
| 1 | 73 章与现有内容库是否同一套? | 产品经理 | 73 章为内容章数的统计 |
|
||||
| 2 | 导师与内容作者是否同一人? | 产品经理 | 不是。导师是导师,属于咨询服务对接的人 |
|
||||
| 3 | 「案例库」是独立内容池还是章节分类? | 产品经理 | 章节分类 |
|
||||
| 4 | 会员权益与价格策略(¥1980/年、权益边界)? | 产品经理 | 会员权益:所有章节全部免费,并自动进入超级个体名单 |
|
||||
| 5 | chapter/book/vip 现有模型与业务定义? | 后端工程师 | 见下方「后端补充说明」 |
|
||||
|
||||
| 2 | 导师与内容作者是否同一人? | 产品经理 | (待补充) |
|
||||
### 后端补充说明(问题 5)
|
||||
|
||||
| 3 | 「案例库」是独立内容池还是章节分类? | 产品经理 | (待补充) |
|
||||
**Chapter(chapters 表)**
|
||||
- 每行 = 一节(section),`id` 为业务标识如 `1.1`、`preface`
|
||||
- `part_id` / `part_title`:篇章;`chapter_id` / `chapter_title`:章;`section_title`:节标题
|
||||
- `content`、`is_free`、`price`、`sort_order`:正文、免费/付费、价格、排序
|
||||
- 73 章 = chapters 表行数统计,与产品「73 章为内容章数」一致
|
||||
|
||||
| 4 | 会员权益与价格策略如何设计? | 产品经理 | (待补充) |
|
||||
**Book**
|
||||
- 无独立表;「书」= chapters 的聚合视图
|
||||
- 接口:`/api/book/all-chapters` 返回全部 chapters,`/api/book/chapter/:id` 按 id 查单节
|
||||
|
||||
| 5 | 导师能力是新建实体还是扩展现有 match 体系? | 后端开发 | (待补充) |
|
||||
**VIP**
|
||||
- `vip_roles` 表:超级个体角色配置(name、sort),供管理端下拉选择
|
||||
- `users` 表:`is_vip`、`vip_expire_date`、`vip_activated_at`、`vip_sort`、`vip_role`、`vip_name`、`vip_avatar`、`vip_project`、`vip_contact`、`vip_bio`
|
||||
- 权益判断:`is_vip=1` 且 `vip_expire_date>NOW()`;无则从 orders 兜底(product_type=`fullbook`/`vip`,pay_time+365 天)
|
||||
- 默认价格:¥1980;权益已定义在 vip.go(智能纪要、会议纪要库、案例库、链接资源、解锁全章、匹配伙伴、排行、VIP 标识)
|
||||
|
||||
**精选推荐与热门章节(业务规则补充)**
|
||||
- **精选推荐**(首页「为你推荐」前 3 章):按正文章节阅读量从高到低排序,同量按更新时间;取前 3 章,依次标「热门」「推荐」「精选」。兜底:无阅读数据时按最近更新取 3 章;再兜底为预设固定 3 章。
|
||||
- **热门章节**(搜索页等):同上阅读量排序,取更多条(如 10 条)。兜底:无阅读数据时按购买次数;再兜底为默认列表。
|
||||
- **排除**:序言、尾声、附录不参与排序与推荐。
|
||||
- **管理端**:算法驱动,无需运营勾选「推荐」;固定兜底章节可产品预设或后台配置。
|
||||
|
||||
以下是回答:
|
||||
1、73章为内容章数的统计
|
||||
2、不是。导师是导师,属于咨询服务对接的人
|
||||
3、章节分类
|
||||
4、会员1980元。
|
||||
5、后端创建导师
|
||||
---
|
||||
|
||||
## 实现方案讨论(基于澄清后的需求)
|
||||
|
||||
> 各角色分析理解 1~5 题作答及后端补充说明后,发表实现看法。
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
需求已厘清,可按以下 MVP 范围推进:
|
||||
|
||||
- **73 章**:沿用 chapters 表,73 = 行数统计;「案例库」按篇章/章节分类展示即可。
|
||||
- **会员**:全章免费 + 自动进入超级个体名单;¥1980 沿用现 vip 逻辑。
|
||||
- **导师**:独立于内容作者,需新建导师实体与预约流程。
|
||||
|
||||
**验收标准建议**:① 首页展示最新更新、阅读进度、超级个体、精选推荐、最新新增;② 目录按篇章聚合、支持 NEW 标识、免费/¥1;③ 会员落地页支付后 is_vip=1、vip_expire_date 正确;④ 导师列表可搜索筛选、详情可预约;⑤ 资料编辑保存后手机/微信号必填方可使用提现与找伙伴。
|
||||
|
||||
### 【后端工程师】
|
||||
|
||||
**可直接复用的**:chapters、users(含 vip 字段)、orders、vip 开通逻辑。小程序已有 `/api/miniprogram/book/*`、`/api/miniprogram/vip/*`、`/api/miniprogram/user/*`。
|
||||
|
||||
**需新增/扩展**:
|
||||
|
||||
1. **chapters 表**:新增 `is_new`(或类似字段)支持 NEW 标签;若无则用 `created_at` 近 N 天判断。
|
||||
2. **首页聚合**:`book/latest-chapters` 已有;可新增 `book/recommended`(精选)、首页「最新新增」复用 latest 按时间筛。
|
||||
3. **导师模块**:新建 `mentors` 表(头像、姓名、简介、技能标签、价格、服务内容、判断风格等);新建 `mentor_consultations`(预约单:user_id、mentor_id、时间、状态、支付);接口:`GET/POST /api/miniprogram/mentors`(列表/详情/预约)。
|
||||
4. **个人资料扩展**:users 表可扩展 `story_*`、`help_offer`、`help_need`、`project_intro` 等;或新建 `user_profiles` 关联 users。编辑接口扩展现有 `user/profile`。
|
||||
|
||||
**实施顺序**:① 章节 NEW 标记 + 首页/目录所需接口补齐 → ② 会员落地(现 vip 已够)→ ③ 导师表 + 预约接口 → ④ 资料扩展。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
**可复用**:章节管理(admin/chapters、db/book)、用户/VIP 管理(db/users、db/vip-roles)。
|
||||
|
||||
**需新增**:
|
||||
|
||||
1. **导师管理**:`/api/admin/mentors` 或 `/api/db/mentors`,CRUD + 上下架;依赖后端 mentors 表。
|
||||
2. **预约管理**:`/api/admin/mentor-consultations`,列表、状态筛选、导出。
|
||||
3. **章节 NEW**:若 chapters 新增 is_new,管理端章节编辑页增加「标记 NEW」勾选。
|
||||
4. **会员**:现 db/users 已支持 Set VIP;权益文案可配置化(若后续需要)。
|
||||
|
||||
**实施顺序**:待后端 mentors、consultations 表与接口就绪后,再开发导师管理、预约管理页面;章节 NEW 可与后端同步上线。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
**可复用**:首页(index)、目录(catalog)、VIP 页(vip)、个人中心(profile)、支付流程。现有 `book/all-chapters`、`vip/status`、`user/profile`、`pay` 等已覆盖基础能力。
|
||||
|
||||
**需新增/改造**:
|
||||
|
||||
1. **首页**:按稿子接入「最新更新」「精选推荐」「最新新增」;`book/latest-chapters`、`book/hot` 已有,需确认 recommended 接口;超级个体复用 `vip/members`。
|
||||
2. **目录**:按 part 聚合、展示 NEW、免费/¥1;数据源 `book/all-chapters`,NEW 依赖后端字段或策略。
|
||||
3. **会员落地**:新页或改造 vip 页,权益展示 + 购买按钮,支付走现 `pay`。
|
||||
4. **导师**:新页「选择导师」「导师详情」,接入 `mentors` 列表/详情/预约接口。
|
||||
5. **资料编辑**:扩展表单字段(个人故事、互助需求、项目介绍)、手机/微信号弹窗(稿子 comprehensive_profile_editor_v1_2)。
|
||||
|
||||
**实施顺序**:① 首页/目录 UI 与数据对接 → ② 会员落地 → ③ 导师列表+详情 → ④ 资料编辑扩展。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
**核心场景**:
|
||||
|
||||
1. **阅读/付费**:免费节直接读;付费节未购/VIP 不可读;VIP 全章可读;单节购买与 VIP 购买互不冲突。
|
||||
2. **会员**:开通后 is_vip、vip_expire_date 正确;超级个体名单可见;权益生效。
|
||||
3. **导师**:列表搜索/筛选、详情展示、预约创建、支付(若预约收费)。
|
||||
4. **资料**:编辑保存成功;手机/微信号未填时提现、找伙伴应拦截并引导弹窗。
|
||||
|
||||
**联调**:小程序↔API(book、vip、user、mentors);管理端↔API(chapters、mentors、consultations)。
|
||||
|
||||
**实施顺序**:接口就绪后补充用例;优先阅读/会员,再导师、资料。
|
||||
|
||||
### 实现路线图(共识)
|
||||
|
||||
| 阶段 | 后端 | 管理端 | 小程序 | 测试 |
|
||||
|-----|------|-------|--------|------|
|
||||
| **P0** | chapters 支持 NEW;book/latest、book/recommended 确认或补齐 | 章节编辑支持 NEW | 首页/目录 UI 与数据对接 | 阅读/会员用例 |
|
||||
| **P1** | 会员沿用现 vip;无新增接口 | — | 会员落地页 | 会员开通验收 |
|
||||
| **P2** | mentors 表 + consultations 表;列表/详情/预约接口 | 导师 CRUD、预约列表 | 导师列表+详情+预约 | 导师预约流程 |
|
||||
| **P3** | users 扩展或 user_profiles;profile 接口扩展 | — | 资料编辑扩展、手机/微弹窗 | 资料+拦截校验 |
|
||||
|
||||
**启动条件**:产品确认 MVP 范围与验收标准后,后端先输出 P0 接口方案,管理端/小程序按路线图跟进。
|
||||
|
||||
---
|
||||
|
||||
## 各开发对新需求的看法
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
需求清晰,与现有 chapter/book/vip 模型兼容度高,可复用为主、增量开发。导师和资料扩展是主要新增点,技术风险可控。建议产品尽早确认 P2 导师价格配置方式(固定/可配置)、预约状态流转,便于接口设计。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
见下方「管理端建设性与补充说明」。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
稿子完整、交互明确,实现难度主要在数据对接和组件复用。首页/目录 P0 已落地;会员、导师、资料按阶段推进即可。建议后端接口响应格式稳定后再做样式微调,减少返工。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
场景边界清楚,可分批补充用例。需关注:导师预约与支付的联调、资料未填时的拦截逻辑、会员与单节购买的权益优先级。
|
||||
|
||||
---
|
||||
|
||||
## 管理端建设性与补充说明(基于小程序需求)
|
||||
|
||||
> 管理端对应小程序各模块,除基础 CRUD 外,建议补充以下能力以更好支持运营与数据闭环。
|
||||
|
||||
| 小程序模块 | 管理端基础能力 | 建设性补充 | 说明 |
|
||||
|-----------|----------------|------------|------|
|
||||
| **首页/目录** | 章节 NEW 标记 | ① 精选推荐固定兜底章节配置<br>② 章节阅读量/点击数据看板 | 算法兜底可运营配置;运营需看到哪些章节受欢迎 |
|
||||
| **会员落地** | 用户 VIP 开通 | ① 会员权益文案配置化<br>② 会员开通/续费统计 | 权益文案可随活动调整;统计支撑运营决策 |
|
||||
| **导师/预约** | 导师 CRUD、预约列表 | ① 导师排序/推荐位<br>② 咨询项目与价格配置<br>③ 预约数据统计(按导师/按时间) | 小程序列表顺序可运营控制;价格可调;数据支撑导师运营 |
|
||||
| **我的/分享收益** | (若已有收益逻辑) | ① 收益明细与分润规则配置<br>② 提现审核流程 | 小程序有分享收益、可提现金额,管理端需审核与配置 |
|
||||
| **个人资料** | — | ① 用户资料完善率统计<br>② (若涉及敏感)资料审核 | 支撑找伙伴匹配质量;可选能力 |
|
||||
|
||||
### 管理端补充优先级建议
|
||||
|
||||
| 优先级 | 补充项 | 与小程序关联 |
|
||||
|--------|--------|--------------|
|
||||
| 高 | 导师排序/推荐位、咨询项目价格配置 | 小程序导师列表展示顺序、v2 弹窗价格 |
|
||||
| 高 | 精选推荐兜底章节配置 | 小程序首页精选推荐无数据时的展示 |
|
||||
| 中 | 会员权益文案配置、开通统计 | 小程序会员落地页权益展示 |
|
||||
| 中 | 预约数据统计 | 导师运营效果评估 |
|
||||
| 低 | 资料完善率、提现审核 | 找伙伴质量、收益闭环 |
|
||||
|
||||
---
|
||||
|
||||
## 开发协作方案
|
||||
|
||||
> 各开发角色如何协作、谁先谁后、交接点、并行与串行。
|
||||
|
||||
### 协作总原则
|
||||
|
||||
- **产品先行**:MVP 范围与验收标准确定后,开发方可启动。
|
||||
- **后端先行**:接口契约先出,小程序/管理端再对接。
|
||||
- **分阶段接力**:按 P0→P1→P2→P3 推进,每阶段有明确交付与验收。
|
||||
- **接口契约**:后端每阶段输出接口文档(路径、请求/响应、字段),前端按契约开发。
|
||||
|
||||
### 阶段内协作时序
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ P0:首页/目录 + NEW 标记 │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
产品确认 MVP 与验收
|
||||
│
|
||||
▼
|
||||
【后端】输出 P0 接口契约
|
||||
· chapters 是否新增 is_new?若否,说明「最新新增」判定规则(如 created_at 近 7 天)
|
||||
· book/latest-chapters、book/recommended 响应格式
|
||||
· book/all-chapters 是否返回 is_new 或等价信息
|
||||
│
|
||||
├──────────────────────────┬──────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
【后端】实现 P0 接口 【管理端】章节编辑支持 NEW 【小程序】首页/目录 UI
|
||||
(迁移脚本 + handler) (依赖 chapters 结构) (按契约 Mock 或直连)
|
||||
│ │ │
|
||||
└──────────────────────────┴──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
【测试】阅读/会员用例补充 ──► 联调验证 ──► P0 验收
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ P1:会员落地 │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
无新接口,沿用现 vip 与 pay
|
||||
│
|
||||
▼
|
||||
【小程序】会员落地页(权益展示 + 购买按钮)
|
||||
│
|
||||
▼
|
||||
【测试】会员开通验收 ──► P1 验收
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ P2:导师 + 预约 │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
【后端】输出 P2 接口契约
|
||||
· mentors 表结构、字段
|
||||
· mentor_consultations 表结构、状态流转
|
||||
· GET/POST /api/miniprogram/mentors(列表/详情/预约)
|
||||
· GET/POST /api/admin/mentors、/api/admin/mentor-consultations
|
||||
│
|
||||
├──────────────────────────┬──────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
【后端】实现 mentors + 预约接口 【管理端】导师 CRUD、预约列表 【小程序】导师列表+详情+预约
|
||||
(迁移 + 小程序接口 + admin 接口) (依赖 admin 接口) (按契约对接)
|
||||
│ │ │
|
||||
└──────────────────────────┴──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
【测试】导师预约流程 ──► 三端联调 ──► P2 验收
|
||||
```
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ P3:资料编辑扩展 │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
【后端】输出 P3 接口契约
|
||||
· users 扩展字段 或 user_profiles 表
|
||||
· user/profile 接口扩展(个人故事、互助需求、项目介绍、手机、微信号)
|
||||
· 提现/找伙伴入口的「手机/微未填」校验规则
|
||||
│
|
||||
├──────────────────────────┐
|
||||
▼ ▼
|
||||
【后端】实现 profile 扩展 【小程序】资料编辑扩展、弹窗
|
||||
│ │
|
||||
└──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
【测试】资料 + 拦截校验 ──► P3 验收
|
||||
```
|
||||
|
||||
### 角色职责与交接
|
||||
|
||||
| 角色 | 职责 | 交接给谁 | 交接物 |
|
||||
|------|------|----------|--------|
|
||||
| **产品** | 确认 MVP、验收标准;P0 前完成 | 全体 | 需求文档(或会议纪要中验收部分) |
|
||||
| **后端** | 每阶段输出接口契约并实现 | 管理端、小程序、测试 | 接口文档(路径、字段、示例) |
|
||||
| **管理端** | P0 章节 NEW;P2 导师/预约管理 | 测试 | 可用的管理端页面 |
|
||||
| **小程序** | P0~P3 按阶段实现 C 端页面 | 测试 | 可联调的小程序 |
|
||||
| **测试** | 每阶段补充用例、联调验收 | 产品 | 验收报告 |
|
||||
|
||||
### 并行与串行
|
||||
|
||||
| 关系 | 说明 |
|
||||
|------|------|
|
||||
| **产品 → 后端** | 串行:产品确认后,后端才能定方案 |
|
||||
| **后端 → 管理端/小程序** | 串行开头:接口契约出后才能开发;契约出后可并行 |
|
||||
| **管理端 ↔ 小程序** | 并行:同阶段内各自对接各自接口,无互相依赖 |
|
||||
| **P0 ↔ P1** | P1 可早于 P0 完成(会员无新接口);但建议 P0 先验收再开 P2 |
|
||||
| **P2 管理端** | 依赖后端 mentors 接口;可与小程序并行,但都等后端 |
|
||||
|
||||
### 沟通节点
|
||||
|
||||
| 节点 | 参与 | 目的 |
|
||||
|------|------|------|
|
||||
| 需求确认会 | 产品 + 全体 | 定 MVP、验收标准 |
|
||||
| P0 接口评审 | 后端 + 管理端 + 小程序 | 确认 chapters is_new、book 接口格式 |
|
||||
| P2 接口评审 | 后端 + 管理端 + 小程序 | 确认 mentors、consultations 模型 |
|
||||
| 每阶段联调 | 后端 + 管理端 + 小程序 + 测试 | 验证功能、过 checklist |
|
||||
| 阻塞时 | 阻塞方 + 被依赖方 | 快速澄清、调整契约 |
|
||||
|
||||
### 协作 checklist(每阶段结束前)
|
||||
|
||||
- [ ] 后端:接口已挂到正确路由组,文档已更新
|
||||
- [ ] 管理端:仅用 `/api/admin/*`、`/api/db/*`,字段与接口一致
|
||||
- [ ] 小程序:仅用 `/api/miniprogram/*`,错误处理完整
|
||||
- [ ] 测试:用例已补充,联调通过
|
||||
- [ ] 全体:过 soul-change-checklist
|
||||
|
||||
---
|
||||
|
||||
## 各角色经验与业务理解更新
|
||||
|
||||
本次会议结论已同步至各角色 `agent/{角色}/evolution/2026-02-28.md`。
|
||||
|
||||
### 产品经理
|
||||
|
||||
- stitch_soul 串联「内容→会员→导师」变现路径;73 章、导师、案例库、会员需在需求文档中明确定义。
|
||||
- stitch_soul 串联「内容→会员→导师」变现路径;临时需求池 10 个稿子覆盖完整流程;需在正式需求文档中明确业务定义与验收标准。
|
||||
|
||||
### 后端开发
|
||||
|
||||
- soul-api 已有 chapter、book、vip;导师能力需新建或扩展现有 match 体系;预约、会员权益需与支付打通。
|
||||
- 需新建或扩展导师实体;现有 chapter/book/vip 可与产品核对后复用;接口挂 `/api/miniprogram/*`。
|
||||
|
||||
### 管理端开发工程师
|
||||
|
||||
- 需支撑章节、导师、会员、预约的 CRUD 与配置,字段与 miniprogram 端统一。
|
||||
- 需管理章节、导师、会员、预约、收益;待后端方案确定后规划管理页面。
|
||||
|
||||
### 小程序开发工程师
|
||||
|
||||
- 首页/目录/导师/会员/资料五类页面,全部走 `/api/miniprogram/*`,支付走微信支付。
|
||||
- 首页/目录/会员/导师/资料五类页面;待需求与接口确定后分阶段实现。
|
||||
|
||||
### 测试人员
|
||||
|
||||
- 阅读进度、免费/付费、会员权益、导师预约与支付、资料完善与提现限制为关键联调场景。
|
||||
- 关键场景:阅读/付费/会员/导师预约/资料;待需求确定后补充联调用例。
|
||||
|
||||
### 团队共享
|
||||
|
||||
- stitch_soul 为 stitch 产品线在 Soul 创业派对上的扩展,需与现有 soul-api/soul-admin/miniprogram 架构协同,避免混用 admin/miniprogram 路由。
|
||||
- stitch_soul 与现有三端架构协同;路由约定保持不变:小程序 `/api/miniprogram/*`,管理端 `/api/admin/*`、`/api/db/*`。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 开发团队重新分析:怎么实现(可执行方案)
|
||||
|
||||
> 在问题 1~5 作答、精选推荐算法、实现路线图基础上,结合现有代码梳理出的可执行实现方案。
|
||||
|
||||
### 现状与差异
|
||||
|
||||
| 能力 | 现状 | 与需求差异 |
|
||||
|------|------|------------|
|
||||
| 精选推荐 / 热门 | `book/hot` 按 sort_order 取 10 条 | 需按**阅读量**排序;排除序言/尾声/附录;精选取 3 条并打「热门/推荐/精选」 |
|
||||
| 最新更新 / 最新新增 | `book/latest-chapters` 按 updated_at 取 20 条;**未挂 miniprogram** | 需挂到 miniprogram;「最新新增」可复用或加 is_new 筛选 |
|
||||
| 目录 NEW | chapters 无 is_new | 需新增 is_new 或按 created_at 近 N 天 |
|
||||
| 会员 | vip + pay 已有 | 无差异 |
|
||||
| 导师 | 无 | 需新建 mentors、consultations |
|
||||
| 资料扩展 | user/profile 有基础字段 | 需扩展 story/help_offer/help_need/project_intro |
|
||||
|
||||
### 分阶段实现清单(可执行)
|
||||
|
||||
#### P0:首页/目录 + NEW + 精选推荐算法
|
||||
|
||||
| 序号 | 角色 | 动作 | 产出 |
|
||||
|-----|------|------|------|
|
||||
| P0-1 | 后端 | chapters 表新增 `is_new`(bool);迁移脚本 + AutoMigrate | 字段可用 |
|
||||
| P0-2 | 后端 | `book/hot` 改为按阅读量排序:从 reading_progress 按 section_id 分组 count;兜底按 updated_at;排除 part 含「序言/尾声/附录」 | 符合算法 |
|
||||
| P0-3 | 后端 | 新增 `book/recommended`:同 hot 逻辑,取前 3 条,返回时带 tag(热门/推荐/精选) | 首页精选用 |
|
||||
| P0-4 | 后端 | `book/latest-chapters` 挂到 miniprogram 组 | 小程序可调 |
|
||||
| P0-5 | 后端 | `book/all-chapters` 响应中每章带 `isNew` | 目录 NEW 展示 |
|
||||
| P0-6 | 管理端 | 章节编辑页增加「标记 NEW」勾选 | 运营可配置 |
|
||||
| P0-7 | 小程序 | 首页:最新更新(latest)、精选推荐(recommended)、最新新增(all-chapters 筛 isNew)、超级个体(vip/members)、阅读进度(已有) | 首页按稿子完成 |
|
||||
| P0-8 | 小程序 | 目录:按 part 聚合、展示 NEW、免费/¥1 | 目录按稿子完成 |
|
||||
| P0-9 | 测试 | 阅读/会员用例;精选推荐取数、NEW 展示 | 联调通过 |
|
||||
|
||||
**阅读量数据来源**:`reading_progress` 表按 `section_id` 分组 count;若无数据则用兜底(updated_at 或固定 3 章)。
|
||||
|
||||
#### P1:会员落地
|
||||
|
||||
| 序号 | 角色 | 动作 | 产出 |
|
||||
|-----|------|------|------|
|
||||
| P1-1 | 小程序 | 会员落地页(权益展示 + ¥1980 购买),支付走现 pay | 可开通会员 |
|
||||
| P1-2 | 测试 | 会员开通验收 | 通过 |
|
||||
|
||||
#### P2:导师 + 预约
|
||||
|
||||
| 序号 | 角色 | 动作 | 产出 |
|
||||
|-----|------|------|------|
|
||||
| P2-1 | 后端 | 新建 mentors 表(**支持每个导师独立价格配置**:单次/半年/年度)、mentor_consultations 表;迁移脚本 | 表就绪 |
|
||||
| P2-2 | 后端 | `GET/POST /api/miniprogram/mentors`(列表/详情/预约,价格从导师配置读取);`GET/POST /api/admin/mentors`、`/api/admin/mentor-consultations` | 接口可用 |
|
||||
| P2-3 | 管理端 | 导师管理 CRUD、**导师价格配置(每个导师独立)**、预约列表(状态筛选、导出) | 可管理导师 |
|
||||
| P2-4 | 小程序 | 导师列表、导师详情、**联系导师按钮点击 → 弹出 v2 弹窗(选择咨询项目)**、预约入口 | 可预约 |
|
||||
| P2-5 | 测试 | 导师预约流程 | 联调通过 |
|
||||
|
||||
#### P3:资料编辑扩展
|
||||
|
||||
| 序号 | 角色 | 动作 | 产出 |
|
||||
|-----|------|------|------|
|
||||
| P3-1 | 后端 | users 扩展 story_best_month、story_achievement、story_turning、help_offer、help_need、project_intro;或新建 user_profiles | 字段可用 |
|
||||
| P3-2 | 后端 | `user/profile` 接口读写扩展字段;提现/找伙伴入口校验手机/微 | 接口可用 |
|
||||
| P3-3 | 小程序 | 资料编辑扩展表单;手机/微未填时弹窗并拦截提现/找伙伴;**我的页头像资料卡片加「编辑」图标 → 跳转个人资料展示页** | 符合稿子 |
|
||||
| P3-4 | 测试 | 资料保存、拦截校验 | 通过 |
|
||||
|
||||
### 接口契约速查(后端输出后可据此开发)
|
||||
|
||||
**P0**
|
||||
|
||||
- `GET /api/miniprogram/book/recommended` → `{ data: [{ id, mid, sectionTitle, partTitle, tag: "热门"|"推荐"|"精选", ... }] }`
|
||||
- `GET /api/miniprogram/book/hot` → 同算法,limit 10,无 tag
|
||||
- `GET /api/miniprogram/book/latest-chapters` → 新增挂载
|
||||
- `GET /api/miniprogram/book/all-chapters` → 每项增加 `isNew`
|
||||
|
||||
**P2**
|
||||
|
||||
- `GET /api/miniprogram/mentors?q=&skill=` → 列表(含每导师价格,从配置读取)
|
||||
- `GET /api/miniprogram/mentors/:id` → 详情(含单次/半年/年度价格,从配置读取)
|
||||
- `POST /api/miniprogram/mentors/:id/book` → 预约
|
||||
- 后端 mentors 表/模型:支持 `price_single`、`price_half_year`、`price_year` 等按导师配置;管理端 PUT `/api/admin/mentors` 或 db 接口支持写入
|
||||
|
||||
**P3**
|
||||
|
||||
- `user/profile` 请求/响应增加 story_*、help_offer、help_need、project_intro
|
||||
|
||||
### 实施顺序(单人在多端开发时)
|
||||
|
||||
1. 产品确认 MVP 与验收标准(可复用会议纪要)。
|
||||
2. 后端完成 P0-1~P0-5,输出接口契约 → 管理端 P0-6、小程序 P0-7~P0-8 并行。
|
||||
3. P0 联调验收后,P1 小程序独立完成。
|
||||
4. 后端 P2-1~P2-2 → 管理端 P2-3、小程序 P2-4 并行。
|
||||
5. 后端 P3-1~P3-2 → 小程序 P3-3。
|
||||
|
||||
---
|
||||
|
||||
## 附录:页面重构专项会议(设计稿全覆盖 10 张图)
|
||||
|
||||
> **触发**:用户要求读取 stitch_soul 全部图片并开会,明确涉及「页面重构」的需求;此前会议未逐张覆盖设计稿。
|
||||
|
||||
---
|
||||
|
||||
### 各角色发言(页面重构专项)
|
||||
|
||||
**【产品经理】**
|
||||
10 张稿子覆盖 8 类页面:首页、目录、会员落地、我的(VIP+收益)、个人资料展示、资料编辑(完整+弹窗)、导师列表、导师详情(含咨询选择弹窗)。页面重构优先级:首页/目录(P0 已有)→ 会员落地(P1)→ 导师(P2)→ 资料展示与编辑(P3)。需澄清:找伙伴高亮逻辑、导师详情 v1/v2 与弹窗关系、我的页与个人资料的跳转关系。
|
||||
|
||||
**【后端开发】**
|
||||
页面重构主要影响前端;后端需配合:P2 导师列表/详情/预约接口返回的字段需支撑卡片展示(头像、简介、标签数组、价格);P3 资料扩展字段需覆盖个人故事、互助需求、项目介绍。接口契约与现有实现方案一致。
|
||||
|
||||
**【管理端开发工程师】**
|
||||
管理端无对应设计稿,但导师管理、预约列表、章节 NEW 勾选等页面需与小程序风格一致(深色主题、标签样式)。可参考 stitch_soul 的组件规范做管理端组件库扩展。
|
||||
|
||||
**【小程序开发工程师】**
|
||||
10 张稿子结构清晰,适合组件化:权益卡片、标签、弹窗、底部按钮、数据统计卡片可抽成通用组件。深色主题需统一定义变量。首页、目录 P0 已完成;会员落地、导师、资料按阶段推进时需严格按稿子还原布局与交互。
|
||||
|
||||
**【测试人员】**
|
||||
页面重构验收重点:① 各页面与设计稿一致性(布局、颜色、标签);② 弹窗触发时机(手机/微未填、咨询选择);③ 深色主题在小程序中的表现;④ 组件复用时样式无错乱。
|
||||
|
||||
---
|
||||
|
||||
### 设计稿清单与重构识别
|
||||
|
||||
> 基于对 stitch_soul 下 **全部 10 张 design 图片** 的逐张阅读,补充「页面重构」识别与组件化建议。此前会议未逐张覆盖,本节补齐。
|
||||
|
||||
### 设计稿清单与重构识别
|
||||
|
||||
| # | 目录 | 页面类型 | 页面重构要点 | 映射阶段 |
|
||||
|---|------|----------|--------------|----------|
|
||||
| 1 | `optimized_home_content_feed_v1` | 首页内容流 | **布局**:顶部品牌+搜索→最新更新大卡片→阅读进度→超级个体→精选推荐→最新新增;**组件**:搜索框、大卡片、进度条、头像列表、内容卡片、NEW 标签;**交互**:展开/折叠、价格 ¥1 | P0 |
|
||||
| 2 | `catalog_with_new_additions_v1` | 目录 | **布局**:书籍概览卡片→按 part 可展开/折叠列表;**组件**:免费/NEW/¥1 标签、章节列表项、折叠箭头;**主题**:深色模式;**交互**:找伙伴图标高亮(动态状态) | P0 |
|
||||
| 3 | `premium_membership_landing_v1` | 会员落地页 | **布局**:导航→VIP 宣传区→内容权益 4 卡 + 社交权益 4 卡(双列)→底部固定按钮;**组件**:权益卡片(图标+文字)、¥1980 按钮;**色彩**:绿/黄/橙强调色;**需提取**:权益卡片通用组件 | P1 |
|
||||
| 4 | `professional_profile_with_earnings_vip` | 我的(VIP+收益) | **布局**:用户区(VIP 徽章、会员/匹配/排行标签、到期时间)→分享收益→阅读统计→最近阅读→我的订单/关于作者;**组件**:数据卡片、统计三列、最近阅读项;**状态**:VIP、收益、可提现 | P1/P3 |
|
||||
| 5 | `enhanced_professional_profile` | 个人资料展示 | **布局**:头像+昵称+MBTI/地区→基本信息→个人故事→互助需求→项目介绍→「成为超级个体」;**组件**:卡片分组、复制按钮、奖杯/星星/循环图标;**动态内容**:故事/需求长度不固定 | P3 |
|
||||
| 6 | `comprehensive_profile_editor_v1_1` | 资料编辑(完整版) | **布局**:温馨提示→头像→基本信息→核心联系方式→个人故事→互助需求→项目介绍→保存;**组件**:表单输入、下拉、地区图钉、多行文本;**样式**:深色主题,浅绿强调 | P3 |
|
||||
| 7 | `comprehensive_profile_editor_v1_2` | 资料编辑(弹窗) | **组件**:居中弹窗,手机号/微信号输入、保存/取消;**触发**:提现/找伙伴入口时手机或微信号未填;**需设计**:弹窗触发时机、必填校验 | P3 |
|
||||
| 8 | `mentor_listing_screen` | 导师列表 | **布局**:搜索→筛选标签→推荐导师列表;**组件**:导师卡片(头像、姓名、简介、标签、价格、预约);**数据**:需头像、姓名、简介、标签数组、价格、ID | P2 |
|
||||
| 9 | `mentor_detail_profile_1` | 导师详情 v1 | **布局**:头像+姓名+理念+引言→01 为什么找→02 提供什么→03 收费标准→04 判断风格→联系导师;**组件**:编号区块、标签组、收费表格、划线价;**强调色**:青色 | P2 |
|
||||
| 10 | `mentor_detail_profile_2` | 导师详情 v2(咨询选择弹窗) | **组件**:居中弹窗,单选(单次/半年/年度)、原价划掉、推荐标签、确认选择;**背景**:模糊的「我的」页;**复用**:可与会员/购买类弹窗共用模式 | P2 |
|
||||
|
||||
### 跨稿子组件抽取建议
|
||||
|
||||
| 组件 | 复用页面 | 说明 |
|
||||
|------|----------|------|
|
||||
| 权益卡片 | 会员落地、导师详情 | 图标+标题+描述,圆角深灰背景 |
|
||||
| 标签(Tag) | 目录、导师列表、导师详情、个人资料 | 免费/NEW/¥1、技能标签、MBTI/地区 |
|
||||
| 弹窗(手机/微、咨询选择) | 资料编辑、导师详情 | 居中圆角、输入/单选、保存/确认+取消 |
|
||||
| 底部固定按钮 | 会员落地、导师详情、资料编辑 | 宽按钮、主色填充 |
|
||||
| 数据统计卡片 | 我的、阅读进度 | 多列数字+图标+说明 |
|
||||
| 头像+昵称+标签区 | 个人资料、超级个体、导师 | 圆形头像、下方标签 |
|
||||
|
||||
### 深色主题与色彩体系(统一约束)
|
||||
|
||||
- **主色**:深黑/深灰背景,白/浅灰文字
|
||||
- **强调色**:绿色(主 CTA、VIP)、黄色(会员、推荐)、橙色(社交权益、部分标签)、青色(导师详情)
|
||||
- **一致性**:所有 10 张稿均为深色模式,重构时需统一定义 CSS 变量或主题配置
|
||||
|
||||
### 待确认(页面重构相关)— 已澄清
|
||||
|
||||
| # | 问题 | 作答 |
|
||||
|---|------|------|
|
||||
| 1 | 底部导航「找伙伴」高亮逻辑? | **不用改**,保持现状 |
|
||||
| 2 | 导师详情 v1 与 v2 弹窗关系? | **导师详情 v1** 点击下方「联系导师」按钮 → 弹出 **v2 弹窗**(选择咨询项目) |
|
||||
| 3 | 「我的」页与「个人资料展示」的跳转? | **我的**页头像资料卡片增加「编辑」图标,点击进入**个人资料展示页**(enhanced_professional_profile) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -59,4 +59,5 @@ YYYY-MM-DD_会议主题.md
|
||||
| 日期 | 主题 | 参与角色 | 文件 |
|
||||
|------|------|---------|------|
|
||||
| 2026-02-27 | 开发进度同步会议 | 产品、后端、管理端、小程序 | [2026-02-27_开发进度同步会议.md](2026-02-27_开发进度同步会议.md) |
|
||||
| 2026-02-28 | 临时需求池 stitch_soul 需求评审 | 产品、后端、管理端、小程序、测试 | [2026-02-28_临时需求池stitch_soul需求评审.md](2026-02-28_临时需求池stitch_soul需求评审.md) |
|
||||
| 2026-02-28 | 临时需求池 stitch_soul 需求评审(含页面重构专项·10 张图全覆盖) | 产品、后端、管理端、小程序、测试 | [2026-02-28_临时需求池stitch_soul需求评审.md](2026-02-28_临时需求池stitch_soul需求评审.md) |
|
||||
| 2026-02-28 | 个人资料页实现评估(profile-show / profile-edit) | 产品、后端、管理端、小程序、测试 | [2026-02-28_个人资料页实现评估.md](2026-02-28_个人资料页实现评估.md) |
|
||||
|
||||
@@ -93,7 +93,18 @@ description: Soul 创业派对管理端开发规范。在 soul-admin/ 下编辑
|
||||
|
||||
---
|
||||
|
||||
## 7. 何时使用本 Skill
|
||||
## 7. 小程序变更驱动的管理端补充(必守)
|
||||
|
||||
> **小程序有功能变更时,管理端须根据 C 端能力主动补充管理功能。**
|
||||
|
||||
- **触发**:小程序新增/优化任何可配置、可运营的功能(如价格、推荐位、文案、审核流等)
|
||||
- **动作**:管理端开发工程师分析该功能在后台需要哪些配置、审核、统计,并输出管理端需求
|
||||
- **后端配合**:需为每个可配置项设计对应的 admin/db 接口(如每个导师独立价格配置)
|
||||
- **示例**:导师价格 → 后端 mentors 表支持按导师配置价格;管理端导师编辑页增加价格字段
|
||||
|
||||
---
|
||||
|
||||
## 8. 何时使用本 Skill
|
||||
|
||||
- 在 **soul-admin/** 下新增或修改页面、组件、API 调用时。
|
||||
- 在管理端新增任何网络请求时(必须仅使用 admin/db 等管理端路径)。
|
||||
|
||||
@@ -60,8 +60,10 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
|
||||
## 6. 表单与输入框
|
||||
|
||||
- **输入框 padding**:设置 padding、背景、边框时,用 `<view>` 包裹 `<input>`;padding/背景/边框写在 view 上,input 设置 `width: 100%`、`font-size`、`color`、`background: transparent`。可避免光标截断、布局异常。
|
||||
- **示例**:`<view class="form-input"><input ... /></view>`,`.form-input { padding: 16rpx 24rpx; ... }`,`.form-input input { width: 100%; font-size: 28rpx; ... }`
|
||||
- **input / textarea padding**:给 `<input>` 或 `<textarea>` 设置 padding 时,**必须**用 `<view>` 包裹,padding 写在 view 上;不在 input/textarea 自身上设 padding。可避免光标截断、布局异常等原生组件表现问题。
|
||||
- **正确写法**:
|
||||
- input:`<view class="form-input"><input class="form-input-inner" ... /></view>`,`.form-input { padding: 16rpx 24rpx; background: #1F2937; }`,`.form-input-inner { width: 100%; font-size: 28rpx; background: transparent; }`
|
||||
- textarea:`<view class="form-textarea-wrap"><textarea class="form-textarea-inner" ... /></view>`,`.form-textarea-wrap { padding: 16rpx 24rpx; background: #1F2937; }`,`.form-textarea-inner { width: 100%; font-size: 28rpx; background: transparent; min-height: 160rpx; }`
|
||||
|
||||
---
|
||||
|
||||
@@ -70,6 +72,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
- 在 **miniprogram/** 下新增或修改页面、组件、utils 时。
|
||||
- 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。
|
||||
- 做阅读、支付、推荐、提现等与 soul-api 对接的功能时。
|
||||
- 做表单、输入框样式时(遵循 §6 包裹 input 的写法)。
|
||||
- 做表单、input/textarea 样式时(遵循 §6,用 view 包裹,padding 写在 view 上)。
|
||||
|
||||
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。
|
||||
|
||||
@@ -218,6 +218,19 @@ sequenceDiagram
|
||||
|
||||
**原则**:不确定时,管理端开发工程师主动与产品/小程序开发工程师确认。
|
||||
|
||||
### 4.1 管理端跟进原则(必守)
|
||||
|
||||
> **小程序变更 → 管理端须跟进做管理功能补充。**
|
||||
|
||||
当小程序有功能新增或变更时,管理端开发工程师**必须**主动分析:该功能在后台需要哪些**配置、审核、统计**能力,并同步提出管理端需求。后端需配合提供相应的 admin/db 接口。
|
||||
|
||||
**示例**(stitch_soul 导师模块):
|
||||
- 小程序:导师列表展示价格、v2 弹窗选咨询项目(单次/半年/年度)
|
||||
- 管理端补充:导师 CRUD + **每个导师独立价格配置**(单次、半年、年度)
|
||||
- 后端:mentors 表支持 price_single、price_half_year、price_year 等字段,或 mentor_prices 关联表
|
||||
|
||||
**流程**:小程序需求评审时,管理端即参与并输出「管理端补充项」;后端设计接口时一并考虑 admin 配置能力。
|
||||
|
||||
---
|
||||
|
||||
## 5. 与现有 Skills/Rules 的配合
|
||||
|
||||
@@ -9,8 +9,8 @@ App({
|
||||
globalData: {
|
||||
// API基础地址 - 连接真实后端
|
||||
// baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
baseUrl: 'https://souldev.quwanzhi.com',
|
||||
// baseUrl: 'http://localhost:8080',
|
||||
// baseUrl: 'https://souldev.quwanzhi.com',
|
||||
baseUrl: 'http://localhost:8080',
|
||||
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"pages/addresses/edit",
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail"
|
||||
"pages/member-detail/member-detail","pages/mentors/mentors","pages/mentor-detail/mentor-detail","pages/profile-show/profile-show","pages/profile-edit/profile-edit"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
|
||||
@@ -212,6 +212,7 @@ Page({
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadBookDataFromServer()
|
||||
this.loadDailyChapters()
|
||||
this.loadTotalFromServer()
|
||||
},
|
||||
@@ -219,12 +220,64 @@ Page({
|
||||
async loadTotalFromServer() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
if (res && res.total) {
|
||||
this.setData({ totalSections: res.total })
|
||||
if (res && (res.total || (res.data && res.data.length))) {
|
||||
this.setData({ totalSections: res.total || (res.data || []).length })
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
// stitch_soul P0-8:从服务端加载目录,按 part 聚合,带 isNew、免费/¥1
|
||||
async loadBookDataFromServer() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const rows = (res && res.data) || (res && res.chapters) || []
|
||||
if (rows.length === 0) return
|
||||
const partMap = new Map()
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
||||
rows.forEach((r, idx) => {
|
||||
const pid = r.partId || r.part_id || 'part-1'
|
||||
const cid = r.chapterId || r.chapter_id || 'chapter-1'
|
||||
if (!partMap.has(pid)) {
|
||||
const partIdx = partMap.size
|
||||
partMap.set(pid, {
|
||||
id: pid,
|
||||
number: numbers[partIdx] || String(partIdx + 1),
|
||||
title: r.partTitle || r.part_title || '未分类',
|
||||
subtitle: r.chapterTitle || r.chapter_title || '',
|
||||
chapters: new Map()
|
||||
})
|
||||
}
|
||||
const part = partMap.get(pid)
|
||||
if (!part.chapters.has(cid)) {
|
||||
part.chapters.set(cid, {
|
||||
id: cid,
|
||||
title: r.chapterTitle || r.chapter_title || '未分类',
|
||||
sections: []
|
||||
})
|
||||
}
|
||||
const ch = part.chapters.get(cid)
|
||||
ch.sections.push({
|
||||
id: r.id,
|
||||
mid: r.mid ?? r.MID ?? 0,
|
||||
title: r.sectionTitle || r.section_title || r.title || '',
|
||||
isFree: r.isFree === true || (r.price !== undefined && r.price === 0),
|
||||
price: r.price ?? 1,
|
||||
isNew: r.isNew === true || r.is_new === true
|
||||
})
|
||||
})
|
||||
const bookData = Array.from(partMap.values()).map(p => ({
|
||||
...p,
|
||||
chapters: Array.from(p.chapters.values())
|
||||
}))
|
||||
const firstPart = bookData[0] && bookData[0].id
|
||||
this.setData({
|
||||
bookData,
|
||||
totalSections: rows.length,
|
||||
expandedPart: firstPart || this.data.expandedPart
|
||||
})
|
||||
} catch (e) { console.log('[Chapters] 加载目录失败:', e) }
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
|
||||
@@ -71,10 +71,11 @@
|
||||
<view class="chapter-header">{{chapter.title}}</view>
|
||||
<view class="section-list">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section">
|
||||
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}">
|
||||
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<text class="section-lock {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? 'lock-open' : 'lock-closed'}}">{{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '○' : '●'}}</text>
|
||||
<text class="section-title {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
|
||||
<text wx:if="{{section.isNew}}" class="tag tag-new">NEW</text>
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
|
||||
@@ -224,6 +224,14 @@
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.tag-new {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.text-brand {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
@@ -162,60 +162,89 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 从服务端获取精选推荐(加权算法:阅读量50% + 时效30% + 付款率20%)和最新更新
|
||||
// 从服务端获取精选推荐、最新更新(stitch_soul:book/recommended、book/latest-chapters)
|
||||
async loadFeaturedFromServer() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) ? res.data : (res && res.chapters) ? res.chapters : []
|
||||
let featured = (res && res.featuredSections) ? res.featuredSections : []
|
||||
// 服务端未返回精选时,从前端按更新时间取前3条有效章节作为回退
|
||||
if (featured.length === 0 && chapters.length > 0) {
|
||||
const valid = chapters.filter(c => {
|
||||
const id = (c.id || '').toLowerCase()
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
|
||||
&& !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
featured = valid
|
||||
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
.slice(0, 5)
|
||||
}
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
const tagClassMap = ['tag-hot', 'tag-rec', 'tag-rec']
|
||||
if (featured.length > 0) {
|
||||
this.setData({
|
||||
featuredSections: featured.slice(0, 3).map((s, i) => ({
|
||||
// 1. 精选推荐:优先用 book/recommended(按阅读量+算法,带 热门/推荐/精选 标签)
|
||||
let featured = []
|
||||
try {
|
||||
const recRes = await app.request({ url: '/api/miniprogram/book/recommended', silent: true })
|
||||
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
|
||||
featured = recRes.data.map((s, i) => ({
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.cleanPartTitle || s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i] || '精选',
|
||||
tagClass: tagClassMap[i] || 'tag-rec'
|
||||
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
|
||||
part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: s.tag || ['热门', '推荐', '精选'][i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
}))
|
||||
this.setData({ featuredSections: featured })
|
||||
}
|
||||
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
|
||||
|
||||
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
|
||||
if (featured.length === 0) {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const valid = chapters.filter(c => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (valid.length > 0) {
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
featured = valid
|
||||
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
.slice(0, 3)
|
||||
.map((s, i) => ({
|
||||
id: s.id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
}))
|
||||
this.setData({ featuredSections: featured })
|
||||
}
|
||||
}
|
||||
|
||||
// 最新更新 = 按 updated_at 排序第1篇(排除序言/尾声/附录)
|
||||
const validChapters = chapters.filter(c => {
|
||||
const id = (c.id || '').toLowerCase()
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !id.includes('preface') && !id.includes('epilogue') && !id.includes('appendix')
|
||||
&& !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (validChapters.length > 0) {
|
||||
validChapters.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
const latest = validChapters[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: latest.id || latest.section_id,
|
||||
mid: latest.mid ?? latest.MID ?? 0,
|
||||
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
|
||||
part: latest.cleanPartTitle || latest.part_title || latest.partTitle || ''
|
||||
}
|
||||
// 2. 最新更新:用 book/latest-chapters 取第1条
|
||||
try {
|
||||
const latestRes = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
|
||||
const latestList = (latestRes && latestRes.data) ? latestRes.data : []
|
||||
if (latestList.length > 0) {
|
||||
const l = latestList[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: l.id,
|
||||
mid: l.mid ?? l.MID ?? 0,
|
||||
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
|
||||
part: l.part_title || l.partTitle || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// 兜底:从 all-chapters 取
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const valid = chapters.filter(c => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (valid.length > 0) {
|
||||
valid.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
const latest = valid[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: latest.id,
|
||||
mid: latest.mid ?? latest.MID ?? 0,
|
||||
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
|
||||
part: latest.part_title || latest.partTitle || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Index] 从服务端加载推荐失败,使用默认:', e)
|
||||
console.log('[Index] 从服务端加载推荐失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -286,16 +315,12 @@ Page({
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const sortOrder = c => (c.sortOrder ?? c.sectionOrder ?? c.sort_order ?? 0)
|
||||
// 优先取 sort_order > 62 的「新增」章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
|
||||
let candidates = chapters.filter(c => sortOrder(c) > 62)
|
||||
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
||||
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
||||
// stitch_soul:优先取 isNew 标记的章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
|
||||
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
|
||||
if (candidates.length === 0) {
|
||||
const id = (c) => (c.id || '').toLowerCase()
|
||||
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
||||
candidates = chapters.filter(c =>
|
||||
!id(c).includes('preface') && !id(c).includes('epilogue') && !id(c).includes('appendix')
|
||||
&& !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
||||
)
|
||||
candidates = chapters.filter(exclude)
|
||||
}
|
||||
const latest = candidates
|
||||
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
|
||||
|
||||
@@ -65,6 +65,12 @@ Page({
|
||||
|
||||
// 解锁弹窗
|
||||
showUnlockModal: false,
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
contactWechat: '',
|
||||
contactSaving: false,
|
||||
|
||||
// 匹配价格(可配置)
|
||||
matchPrice: 1,
|
||||
@@ -192,8 +198,86 @@ Page({
|
||||
},
|
||||
|
||||
// 点击匹配按钮
|
||||
handleMatchClick() {
|
||||
async handleMatchClick() {
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// stitch_soul:导师顾问 → 跳转选择导师页
|
||||
if (currentType && currentType.id === 'mentor') {
|
||||
wx.navigateTo({ url: '/pages/mentors/mentors' })
|
||||
return
|
||||
}
|
||||
|
||||
// 找伙伴/资源对接:需先完善联系方式
|
||||
if (this.data.isLoggedIn && currentType?.matchFromDB) {
|
||||
await this.ensureContactInfo(() => this._handleMatchClickInner(currentType))
|
||||
} else {
|
||||
this._handleMatchClickInner(currentType)
|
||||
}
|
||||
},
|
||||
|
||||
async ensureContactInfo(callback) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
contactWechat: wechat || '',
|
||||
})
|
||||
this._contactCallback = callback
|
||||
} catch (e) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
closeContactModal() {
|
||||
this.setData({ showContactModal: false })
|
||||
this._contactCallback = null
|
||||
},
|
||||
|
||||
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: app.globalData.userInfo?.id,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechat || undefined,
|
||||
},
|
||||
})
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
if (wechat) wx.setStorageSync('user_wechat', wechat)
|
||||
this.loadStoredContact()
|
||||
this.closeContactModal()
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
const cb = this._contactCallback
|
||||
this._contactCallback = null
|
||||
if (cb) cb()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ contactSaving: false })
|
||||
},
|
||||
|
||||
_handleMatchClickInner(currentType) {
|
||||
|
||||
// 资源对接类型需要登录+购买章节才能使用
|
||||
if (currentType && currentType.id === 'investor') {
|
||||
|
||||
@@ -265,6 +265,32 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2) -->
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
<view class="contact-modal" catchtap="preventBubble">
|
||||
<text class="contact-modal-title">请完善联系方式</text>
|
||||
<view class="contact-modal-hint">需完善手机号或微信号才能使用找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">📱</text>
|
||||
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">💬</text>
|
||||
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-modal-btn" bindtap="saveContactInfo" disabled="{{contactSaving}}">
|
||||
{{contactSaving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<text class="contact-modal-cancel" bindtap="closeContactModal">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解锁弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showUnlockModal}}" bindtap="closeUnlockModal">
|
||||
<view class="modal-content unlock-modal" catchtap="preventBubble">
|
||||
|
||||
@@ -1200,3 +1200,26 @@
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 手机/微信号弹窗 - comprehensive_profile_editor_v1_2 */
|
||||
.contact-modal-overlay { background: rgba(0,0,0,0.85); backdrop-filter: blur(8rpx); }
|
||||
.contact-modal {
|
||||
width: 90%; max-width: 600rpx; background: #1A1A1A; border-radius: 48rpx;
|
||||
padding: 48rpx 40rpx; border: 1rpx solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.contact-modal-title { display: block; text-align: center; font-size: 40rpx; font-weight: bold; color: #fff; margin-bottom: 16rpx; }
|
||||
.contact-modal-hint { display: block; font-size: 24rpx; color: #9CA3AF; text-align: center; margin-bottom: 40rpx; }
|
||||
.contact-modal .form-input-wrap { margin-bottom: 32rpx; }
|
||||
.contact-modal .form-label { display: block; font-size: 24rpx; color: #9CA3AF; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
.contact-modal .form-input-inner {
|
||||
display: flex; align-items: center; padding: 24rpx 32rpx; background: #262626; border-radius: 24rpx;
|
||||
}
|
||||
.contact-modal .form-input-inner .form-icon { font-size: 36rpx; margin-right: 16rpx; opacity: 0.7; }
|
||||
.contact-modal .form-input-inner .form-input { flex: 1; font-size: 28rpx; color: #fff; background: transparent; }
|
||||
.contact-modal-btn {
|
||||
width: 100%; height: 96rpx; line-height: 96rpx; text-align: center;
|
||||
background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.contact-modal-btn[disabled] { opacity: 0.6; }
|
||||
.contact-modal-cancel { display: block; text-align: center; font-size: 28rpx; color: #9CA3AF; margin-top: 24rpx; padding: 16rpx; }
|
||||
|
||||
113
miniprogram/pages/mentor-detail/mentor-detail.js
Normal file
113
miniprogram/pages/mentor-detail/mentor-detail.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Soul创业派对 - 导师详情(stitch_soul)
|
||||
* 联系导师按钮 → 弹出 v2 弹窗(选择咨询项目)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
mentor: null,
|
||||
loading: true,
|
||||
showConsultModal: false,
|
||||
consultOptions: [],
|
||||
selectedType: '',
|
||||
selectedAmount: 0,
|
||||
creating: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options.id) this.loadDetail(options.id)
|
||||
},
|
||||
|
||||
async loadDetail(id) {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/mentors/${id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
if (d.judgmentStyle && typeof d.judgmentStyle === 'string') {
|
||||
d.judgmentStyleArr = d.judgmentStyle.split(/[,,]/).map(s => s.trim()).filter(Boolean)
|
||||
} else {
|
||||
d.judgmentStyleArr = []
|
||||
}
|
||||
const fmt = v => v != null ? String(Math.floor(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',') : ''
|
||||
d.priceSingleFmt = fmt(d.priceSingle)
|
||||
d.priceHalfYearFmt = fmt(d.priceHalfYear)
|
||||
d.priceYearFmt = fmt(d.priceYear)
|
||||
const options = []
|
||||
if (d.priceSingle != null) options.push({ type: 'single', label: '单次咨询', desc: '1小时深度沟通', price: d.priceSingle, priceFmt: fmt(d.priceSingle), original: null })
|
||||
if (d.priceHalfYear != null) options.push({ type: 'half_year', label: '半年咨询', desc: '不限次数 · 关键节点陪伴', price: d.priceHalfYear, priceFmt: fmt(d.priceHalfYear), original: '98,000' })
|
||||
if (d.priceYear != null) options.push({ type: 'year', label: '年度咨询', desc: '全年度战略顾问', price: d.priceYear, priceFmt: fmt(d.priceYear), original: '196,000', recommend: true })
|
||||
this.setData({
|
||||
mentor: d,
|
||||
consultOptions: options,
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
onContactTap() {
|
||||
if (!this.data.mentor || this.data.consultOptions.length === 0) {
|
||||
wx.showToast({ title: '暂无咨询项目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showConsultModal: true,
|
||||
selectedType: this.data.consultOptions[0].type,
|
||||
selectedAmount: this.data.consultOptions[0].price,
|
||||
})
|
||||
},
|
||||
|
||||
closeConsultModal() {
|
||||
this.setData({ showConsultModal: false })
|
||||
},
|
||||
|
||||
onSelectOption(e) {
|
||||
const item = e.currentTarget.dataset.item
|
||||
this.setData({ selectedType: item.type, selectedAmount: item.price })
|
||||
},
|
||||
|
||||
async onConfirmConsult() {
|
||||
const { mentor, selectedType } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
wx.navigateTo({ url: '/pages/my/my' })
|
||||
return
|
||||
}
|
||||
this.setData({ creating: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/mentors/${mentor.id}/book`,
|
||||
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
|
||||
wx.showModal({
|
||||
title: '预约成功',
|
||||
content: '请联系客服完成后续对接',
|
||||
showCancel: false,
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '创建失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '创建失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ creating: false })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
1
miniprogram/pages/mentor-detail/mentor-detail.json
Normal file
1
miniprogram/pages/mentor-detail/mentor-detail.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom"}
|
||||
118
miniprogram/pages/mentor-detail/mentor-detail.wxml
Normal file
118
miniprogram/pages/mentor-detail/mentor-detail.wxml
Normal file
@@ -0,0 +1,118 @@
|
||||
<!-- 导师详情 -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-icon">‹</text></view>
|
||||
<text class="nav-title">导师详情</text>
|
||||
<view class="nav-placeholder-r"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<block wx:else>
|
||||
<view class="mentor-header" wx:if="{{mentor}}">
|
||||
<view class="mentor-avatar-wrap">
|
||||
<image wx:if="{{mentor.avatar}}" class="mentor-avatar" src="{{mentor.avatar}}" mode="aspectFill"></image>
|
||||
<view wx:else class="mentor-avatar-placeholder">{{mentor.name ? mentor.name[0] : '?'}}</view>
|
||||
</view>
|
||||
<text class="mentor-name">{{mentor.name}}</text>
|
||||
<text class="mentor-intro">{{mentor.intro}}</text>
|
||||
<view class="mentor-quote" wx:if="{{mentor.quote}}">{{mentor.quote}}</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.whyFind}}">
|
||||
<view class="block-header"><text class="block-num">01</text><text class="block-title">为什么找{{mentor.name}}?</text></view>
|
||||
<text class="block-text">{{mentor.whyFind}}</text>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.offering}}">
|
||||
<view class="block-header"><text class="block-num">02</text><text class="block-title">提供什么?</text></view>
|
||||
<text class="block-text">{{mentor.offering}}</text>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.priceSingle || mentor.priceHalfYear || mentor.priceYear}}">
|
||||
<view class="block-header"><text class="block-num">03</text><text class="block-title">收费标准</text></view>
|
||||
<view class="price-table">
|
||||
<view class="price-thead">
|
||||
<text class="p-col-2">咨询项目</text>
|
||||
<text class="p-col-center">时长</text>
|
||||
<text class="p-col-right">价格</text>
|
||||
</view>
|
||||
<view class="price-row" wx:if="{{mentor.priceSingle}}">
|
||||
<text class="p-col-2">单次咨询</text>
|
||||
<text class="p-col-center">1小时</text>
|
||||
<text class="p-col-right price-num">¥{{mentor.priceSingleFmt || mentor.priceSingle}}</text>
|
||||
</view>
|
||||
<view class="price-row price-row-alt" wx:if="{{mentor.priceHalfYear}}">
|
||||
<text class="p-col-2">半年咨询</text>
|
||||
<text class="p-col-center">-</text>
|
||||
<view class="p-col-right">
|
||||
<text class="price-original">98,000</text>
|
||||
<text class="price-num">¥{{mentor.priceHalfYearFmt || mentor.priceHalfYear}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-row" wx:if="{{mentor.priceYear}}">
|
||||
<text class="p-col-2">年度咨询</text>
|
||||
<text class="p-col-center">-</text>
|
||||
<view class="p-col-right">
|
||||
<text class="price-original">196,000</text>
|
||||
<text class="price-num">¥{{mentor.priceYearFmt || mentor.priceYear}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.judgmentStyle}}">
|
||||
<view class="block-header"><text class="block-num">04</text><text class="block-title">判断风格</text></view>
|
||||
<view class="style-tags">
|
||||
<text class="style-tag" wx:for="{{mentor.judgmentStyleArr}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-btn-area">
|
||||
<view class="contact-btn" bindtap="onContactTap">
|
||||
<text class="contact-icon">💬</text>
|
||||
<text>联系导师</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- v2 弹窗:选择咨询项目 -->
|
||||
<view class="modal-overlay" wx:if="{{showConsultModal}}" bindtap="closeConsultModal">
|
||||
<view class="modal-content" catchtap="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择咨询项目</text>
|
||||
<text class="modal-close" bindtap="closeConsultModal">✕</text>
|
||||
</view>
|
||||
<view class="consult-options">
|
||||
<view
|
||||
wx:for="{{consultOptions}}"
|
||||
wx:key="type"
|
||||
class="consult-option {{selectedType === item.type ? 'option-selected' : ''}}"
|
||||
data-item="{{item}}"
|
||||
bindtap="onSelectOption"
|
||||
>
|
||||
<view class="option-row1">
|
||||
<text class="option-label">{{item.label}}</text>
|
||||
<text class="option-rec" wx:if="{{item.recommend}}">推荐</text>
|
||||
<view class="option-radio {{selectedType === item.type ? 'radio-selected' : ''}}"></view>
|
||||
</view>
|
||||
<view class="option-row2">
|
||||
<text class="option-desc">{{item.desc}}</text>
|
||||
<view class="option-price-wrap">
|
||||
<text class="option-price-old" wx:if="{{item.original}}">¥{{item.original}}</text>
|
||||
<text class="option-price">¥{{item.priceFmt || item.price}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="confirm-btn" bindtap="onConfirmConsult" disabled="{{creating}}">
|
||||
{{creating ? '处理中...' : '确认选择 →'}}
|
||||
</view>
|
||||
<text class="footer-hint">点击确认即代表同意 <text class="footer-link">服务协议</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
70
miniprogram/pages/mentor-detail/mentor-detail.wxss
Normal file
70
miniprogram/pages/mentor-detail/mentor-detail.wxss
Normal file
@@ -0,0 +1,70 @@
|
||||
/* 按 mentor_detail_profile_1 + mentor_detail_profile_2 设计稿 */
|
||||
.page { background: #000; min-height: 100vh; color: #fff; }
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 24rpx; background: rgba(0,0,0,0.9); border-bottom: 1rpx solid #27272a; }
|
||||
.nav-back { width: 60rpx; }
|
||||
.back-icon { font-size: 44rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 500; }
|
||||
.nav-placeholder-r { width: 60rpx; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #71717a; }
|
||||
.mentor-header { display: flex; flex-direction: column; align-items: center; padding: 48rpx 24rpx; text-align: center; }
|
||||
.mentor-avatar-wrap { margin-bottom: 24rpx; }
|
||||
.mentor-avatar { width: 192rpx; height: 192rpx; border-radius: 50%; border: 4rpx solid #4FD1C5; display: block; background: rgba(255,255,255,0.1); }
|
||||
.mentor-avatar-placeholder { width: 192rpx; height: 192rpx; border-radius: 50%; border: 4rpx solid #4FD1C5; background: rgba(79,209,197,0.2); display: flex; align-items: center; justify-content: center; font-size: 72rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-name { font-size: 48rpx; font-weight: bold; margin-bottom: 8rpx; }
|
||||
.mentor-intro { font-size: 28rpx; color: #4FD1C5; font-weight: 500; margin-bottom: 24rpx; }
|
||||
.mentor-quote { padding: 32rpx; background: #1E1E1E; border-left: 8rpx solid #4FD1C5; border-radius: 24rpx; font-size: 28rpx; color: #d4d4d8; text-align: left; width: 100%; max-width: 600rpx; box-sizing: border-box; }
|
||||
|
||||
.block { padding: 0 24rpx 64rpx; }
|
||||
.block-header { display: flex; align-items: center; margin-bottom: 24rpx; }
|
||||
.block-num { background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: bold; padding: 8rpx 20rpx; border-radius: 999rpx; margin-right: 16rpx; }
|
||||
.block-title { font-size: 36rpx; font-weight: bold; }
|
||||
.block-text { font-size: 26rpx; color: #A0AEC0; line-height: 1.7; display: block; }
|
||||
|
||||
/* 03 收费标准 - 表头青色 */
|
||||
.price-table { background: #1E1E1E; border-radius: 24rpx; overflow: hidden; }
|
||||
.price-thead { display: flex; align-items: center; padding: 24rpx 32rpx; background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: bold; }
|
||||
.p-col-2 { flex: 2; }
|
||||
.p-col-center { flex: 1; text-align: center; }
|
||||
.p-col-right { flex: 1; text-align: right; min-width: 160rpx; }
|
||||
.price-row { display: flex; align-items: center; padding: 32rpx; border-bottom: 1rpx solid #27272a; }
|
||||
.price-row:last-child { border-bottom: none; }
|
||||
.price-row-alt { background: rgba(255,255,255,0.02); }
|
||||
.price-row .p-col-2 { font-size: 28rpx; font-weight: 500; }
|
||||
.price-row .p-col-center { font-size: 24rpx; color: #71717a; }
|
||||
.price-num { font-size: 28rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.price-original { font-size: 20rpx; color: #71717a; text-decoration: line-through; display: block; margin-bottom: 4rpx; }
|
||||
.price-row .p-col-right { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
|
||||
.style-tags { display: flex; flex-wrap: wrap; gap: 24rpx; }
|
||||
.style-tag { padding: 16rpx 32rpx; background: #1E1E1E; border: 1rpx solid #3f3f46; border-radius: 999rpx; font-size: 28rpx; font-weight: 500; }
|
||||
|
||||
.bottom-btn-area { position: fixed; bottom: 0; left: 0; right: 0; padding: 24rpx 32rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.95); border-top: 1rpx solid #27272a; z-index: 50; }
|
||||
.contact-btn { display: flex; align-items: center; justify-content: center; gap: 16rpx; width: 100%; height: 96rpx; background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx; box-shadow: 0 8rpx 32rpx rgba(79,209,197,0.25); }
|
||||
.contact-icon { font-size: 36rpx; }
|
||||
|
||||
/* v2 弹窗 - mentor_detail_profile_2 */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 200; display: flex; align-items: flex-end; justify-content: center; }
|
||||
.modal-content { width: 100%; max-height: 85vh; background: #121212; border-radius: 32rpx 32rpx 0 0; padding: 32rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); border: 1rpx solid rgba(255,255,255,0.08); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32rpx; padding-bottom: 24rpx; border-bottom: 1rpx solid #27272a; }
|
||||
.modal-title { font-size: 40rpx; font-weight: bold; }
|
||||
.modal-close { font-size: 40rpx; color: #71717a; padding: 16rpx; }
|
||||
.consult-options { margin-bottom: 32rpx; }
|
||||
.consult-option { padding: 32rpx; margin-bottom: 24rpx; background: #1E1E1E; border-radius: 24rpx; border: 2rpx solid #3f3f46; }
|
||||
.option-selected { border-color: #4FD1C5; background: rgba(79,209,197,0.08); }
|
||||
.option-row1 { display: flex; align-items: center; margin-bottom: 16rpx; }
|
||||
.option-label { font-size: 32rpx; font-weight: bold; flex: 1; }
|
||||
.option-rec { font-size: 20rpx; padding: 6rpx 16rpx; background: #ED8936; color: #fff; border-radius: 8rpx; margin-right: 16rpx; }
|
||||
.option-radio { width: 40rpx; height: 40rpx; border-radius: 50%; border: 2rpx solid #71717a; flex-shrink: 0; }
|
||||
.radio-selected { border-color: #4FD1C5; background: #4FD1C5; }
|
||||
.option-row2 { display: flex; justify-content: space-between; align-items: flex-end; }
|
||||
.option-desc { font-size: 24rpx; color: #71717a; }
|
||||
.option-price-wrap { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.option-price-old { font-size: 22rpx; color: #71717a; text-decoration: line-through; margin-bottom: 4rpx; }
|
||||
.option-price { font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.confirm-btn { width: 100%; height: 100rpx; line-height: 100rpx; text-align: center; background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx; box-shadow: 0 8rpx 32rpx rgba(79,209,197,0.25); }
|
||||
.confirm-btn[disabled] { opacity: 0.6; }
|
||||
.footer-hint { display: block; font-size: 22rpx; color: #71717a; text-align: center; margin-top: 24rpx; }
|
||||
.footer-link { color: #4FD1C5; }
|
||||
|
||||
.bottom-space { height: 180rpx; }
|
||||
65
miniprogram/pages/mentors/mentors.js
Normal file
65
miniprogram/pages/mentors/mentors.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Soul创业派对 - 选择导师(stitch_soul)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
mentors: [],
|
||||
loading: true,
|
||||
searchKw: '',
|
||||
filterSkill: '',
|
||||
skills: ['全部', '项目结构判断', '产品架构', 'BP梳理', '职业转型'],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchKw: e.detail.value })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
onFilterTap(e) {
|
||||
const skill = e.currentTarget.dataset.skill
|
||||
this.setData({ filterSkill: skill === '全部' ? '' : skill })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
async loadMentors() {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
let url = '/api/miniprogram/mentors'
|
||||
const params = []
|
||||
if (this.data.searchKw) params.push(`q=${encodeURIComponent(this.data.searchKw)}`)
|
||||
if (this.data.filterSkill) params.push(`skill=${encodeURIComponent(this.data.filterSkill)}`)
|
||||
if (params.length) url += '?' + params.join('&')
|
||||
|
||||
const res = await app.request({ url, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
this.setData({ mentors: res.data })
|
||||
} else {
|
||||
this.setData({ mentors: [] })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ mentors: [] })
|
||||
}
|
||||
this.setData({ loading: false })
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
goDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: `/pages/mentor-detail/mentor-detail?id=${id}` })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
1
miniprogram/pages/mentors/mentors.json
Normal file
1
miniprogram/pages/mentors/mentors.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom"}
|
||||
70
miniprogram/pages/mentors/mentors.wxml
Normal file
70
miniprogram/pages/mentors/mentors.wxml
Normal file
@@ -0,0 +1,70 @@
|
||||
<!-- 选择导师 -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-icon">‹</text></view>
|
||||
<text class="nav-title">选择导师</text>
|
||||
<view class="nav-placeholder-r"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrap">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索导师、技能或行业..."
|
||||
value="{{searchKw}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-row">
|
||||
<view
|
||||
class="filter-tag {{!filterSkill ? 'filter-active' : ''}}"
|
||||
data-skill="全部"
|
||||
bindtap="onFilterTap"
|
||||
>全部</view>
|
||||
<view
|
||||
wx:for="{{skills}}"
|
||||
wx:key="*this"
|
||||
wx:if="{{item !== '全部'}}"
|
||||
class="filter-tag {{filterSkill === item ? 'filter-active' : ''}}"
|
||||
data-skill="{{item}}"
|
||||
bindtap="onFilterTap"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section-header">
|
||||
<text class="section-title">推荐导师</text>
|
||||
<text class="section-more" bindtap="loadMentors">查看全部 ›</text>
|
||||
</view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<view class="mentor-list" wx:else>
|
||||
<view class="mentor-card" wx:for="{{mentors}}" wx:key="id">
|
||||
<view class="mentor-card-inner">
|
||||
<view class="mentor-avatar-wrap">
|
||||
<image wx:if="{{item.avatar}}" class="mentor-avatar" src="{{item.avatar}}" mode="aspectFill"></image>
|
||||
<view wx:else class="mentor-avatar-placeholder">{{item.name ? item.name[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="mentor-info">
|
||||
<text class="mentor-name">{{item.name}}</text>
|
||||
<text class="mentor-intro">{{item.intro || ''}}</text>
|
||||
<view class="mentor-tags" wx:if="{{item.tagsArr && item.tagsArr.length}}">
|
||||
<text class="mentor-tag" wx:for="{{item.tagsArr}}" wx:key="*this" wx:for-item="tag">{{tag}}</text>
|
||||
</view>
|
||||
<view class="mentor-price-row">
|
||||
<view class="mentor-price-wrap">
|
||||
<text class="mentor-price-num">¥{{item.priceSingle || 0}}</text>
|
||||
<text class="mentor-price-unit">起 / 单次咨询</text>
|
||||
</view>
|
||||
<view class="mentor-btn" data-id="{{item.id}}" bindtap="goDetail">预约</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" wx:if="{{!loading && mentors.length === 0}}">暂无导师</view>
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
40
miniprogram/pages/mentors/mentors.wxss
Normal file
40
miniprogram/pages/mentors/mentors.wxss
Normal file
@@ -0,0 +1,40 @@
|
||||
/* 按 mentor_listing_screen 设计稿 */
|
||||
.page { background: #000; min-height: 100vh; color: #fff; }
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 24rpx; background: rgba(0,0,0,0.95); border-bottom: 1rpx solid #27272a; }
|
||||
.nav-back { width: 60rpx; }
|
||||
.back-icon { font-size: 44rpx; color: #fff; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 500; }
|
||||
.nav-placeholder-r { width: 60rpx; }
|
||||
|
||||
.search-bar { padding: 24rpx 24rpx 16rpx; }
|
||||
.search-input-wrap { display: flex; align-items: center; background: #1C1C1E; border-radius: 24rpx; padding: 24rpx 32rpx; border: 1rpx solid #27272a; }
|
||||
.search-icon { font-size: 36rpx; color: #71717a; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.search-input { flex: 1; font-size: 28rpx; color: #fff; background: transparent; }
|
||||
.filter-row { display: flex; gap: 24rpx; padding: 24rpx; overflow-x: auto; }
|
||||
.filter-tag { flex-shrink: 0; padding: 12rpx 32rpx; border-radius: 999rpx; font-size: 24rpx; background: #1C1C1E; border: 1rpx solid #27272a; color: #d4d4d8; }
|
||||
.filter-active { background: #4FD1C5; border-color: #4FD1C5; color: #000; font-weight: 600; }
|
||||
|
||||
.section-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 24rpx 24rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: bold; color: #e4e4e7; }
|
||||
.section-more { font-size: 24rpx; color: #4FD1C5; font-weight: 500; }
|
||||
|
||||
.loading { padding: 48rpx; text-align: center; color: #71717a; }
|
||||
.mentor-list { padding: 0 24rpx 48rpx; }
|
||||
.mentor-card { margin-bottom: 24rpx; background: #1C1C1E; border-radius: 32rpx; padding: 32rpx; border: 1rpx solid #27272a; }
|
||||
.mentor-card-inner { display: flex; gap: 32rpx; }
|
||||
.mentor-avatar-wrap { width: 112rpx; height: 112rpx; flex-shrink: 0; }
|
||||
.mentor-avatar { width: 112rpx; height: 112rpx; border-radius: 50%; display: block; border: 1rpx solid #3f3f46; }
|
||||
.mentor-avatar-placeholder { width: 112rpx; height: 112rpx; border-radius: 50%; background: rgba(79,209,197,0.2); border: 1rpx solid #3f3f46; display: flex; align-items: center; justify-content: center; font-size: 40rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-info { flex: 1; min-width: 0; }
|
||||
.mentor-name { display: block; font-size: 32rpx; font-weight: bold; color: #fff; margin-bottom: 8rpx; }
|
||||
.mentor-intro { display: block; font-size: 24rpx; color: #A1A1AA; margin-bottom: 16rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mentor-tags { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 24rpx; }
|
||||
.mentor-tag { font-size: 20rpx; padding: 8rpx 16rpx; background: #2C2C2E; color: #E5E7EB; border-radius: 12rpx; border: 1rpx solid rgba(63,63,70,0.5); }
|
||||
.mentor-price-row { display: flex; justify-content: space-between; align-items: center; padding-top: 24rpx; border-top: 1rpx solid #27272a; }
|
||||
.mentor-price-wrap { display: flex; align-items: baseline; gap: 8rpx; }
|
||||
.mentor-price-num { font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-price-unit { font-size: 24rpx; color: #71717a; }
|
||||
.mentor-btn { padding: 12rpx 32rpx; background: #fff; color: #000; font-size: 24rpx; font-weight: bold; border-radius: 999rpx; }
|
||||
|
||||
.empty { padding: 96rpx; text-align: center; color: rgba(255,255,255,0.4); }
|
||||
.bottom-space { height: 80rpx; }
|
||||
@@ -56,7 +56,14 @@ Page({
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: ''
|
||||
editingNickname: '',
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
contactWechat: '',
|
||||
contactSaving: false,
|
||||
pendingWithdraw: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -687,6 +694,12 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
// 进入个人资料编辑页(stitch_soul)
|
||||
goToProfileEdit() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
async handleWithdraw() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const amount = parseFloat(this.data.pendingEarnings)
|
||||
@@ -694,6 +707,10 @@ Page({
|
||||
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this.ensureContactInfo(() => this.doWithdraw(amount))
|
||||
},
|
||||
|
||||
async doWithdraw(amount) {
|
||||
wx.showModal({
|
||||
title: '申请提现',
|
||||
content: `确认提现 ¥${amount.toFixed(2)} ?`,
|
||||
@@ -714,6 +731,69 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 提现/找伙伴前检查手机或微信号,未填则弹窗(stitch_soul)
|
||||
async ensureContactInfo(callback) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
contactWechat: wechat || '',
|
||||
pendingWithdraw: true,
|
||||
})
|
||||
this._contactCallback = callback
|
||||
} catch (e) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
closeContactModal() {
|
||||
this.setData({ showContactModal: false, pendingWithdraw: false })
|
||||
this._contactCallback = null
|
||||
},
|
||||
|
||||
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: app.globalData.userInfo?.id,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechat || undefined,
|
||||
},
|
||||
})
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
if (wechat) wx.setStorageSync('user_wechat', wechat)
|
||||
this.closeContactModal()
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
const cb = this._contactCallback
|
||||
this._contactCallback = null
|
||||
if (cb) cb()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ contactSaving: false })
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
|
||||
@@ -1,156 +1,108 @@
|
||||
<!--pages/my/my.wxml-->
|
||||
<!--Soul创业实验 - 我的页面 1:1还原Web版本-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏 -->
|
||||
<!-- 我的页 - professional_profile_with_earnings_vip 1:1 还原 -->
|
||||
<view class="page">
|
||||
<!-- 顶部导航:仅标题(设置已移至用户区右侧,避让胶囊) -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<text class="nav-title-left brand-color">我的</text>
|
||||
</view>
|
||||
<text class="nav-title">我的</text>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 用户卡片 - 未登录:假资料界面,名字旁点击登录打开弹窗 -->
|
||||
<view class="user-card card-gradient" wx:if="{{!isLoggedIn}}">
|
||||
<view class="user-header-row">
|
||||
<view class="avatar avatar-placeholder">
|
||||
<image class="avatar-img" wx:if="{{guestAvatar}}" src="{{guestAvatar}}" mode="aspectFill"/>
|
||||
<text class="avatar-text" wx:else>{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<view class="user-info-block">
|
||||
<view class="user-name-row">
|
||||
<text class="user-name">{{guestNickname}}</text>
|
||||
<view class="btn-login-inline" bindtap="showLogin">点击登录</view>
|
||||
</view>
|
||||
<view class="user-id-row">
|
||||
<text class="user-id user-id-guest">登录后查看完整信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">--</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">--</text>
|
||||
<text class="stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value gold-color">--</text>
|
||||
<text class="stat-label">待领收益</text>
|
||||
</view>
|
||||
<!-- 未登录:引导登录 -->
|
||||
<view class="guest-block" wx:if="{{!isLoggedIn}}">
|
||||
<view class="guest-avatar">
|
||||
<image wx:if="{{guestAvatar}}" class="guest-avatar-img" src="{{guestAvatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="guest-avatar-text">{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<text class="guest-name">{{guestNickname}}</text>
|
||||
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户卡片 - 已登录状态 -->
|
||||
<view class="user-card card-gradient" wx:else>
|
||||
<view class="user-header-row">
|
||||
<!-- 头像 - 点击进VIP/设置头像 -->
|
||||
<!-- 已登录:用户区 -->
|
||||
<view class="header-block" wx:else>
|
||||
<view class="user-row">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar {{isVip ? 'avatar-vip' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<text class="avatar-text" wx:else>{{userInfo.nickname[0] || '用'}}</text>
|
||||
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
|
||||
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
|
||||
<view class="vip-badge vip-badge-gray" wx:else bindtap="goToVip">VIP</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info-block">
|
||||
<view class="user-name-row">
|
||||
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
|
||||
<view class="vip-tags-row">
|
||||
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</view>
|
||||
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">匹配</view>
|
||||
<view class="vip-tag-mini {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</view>
|
||||
</view>
|
||||
<view class="become-vip-chip" wx:if="{{!isVip}}" bindtap="goToVip">
|
||||
<text class="chip-star">⭐</text><text class="chip-text">成为会员</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-id-row" bindtap="copyUserId">
|
||||
<text class="user-id">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
|
||||
<view class="user-meta">
|
||||
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
|
||||
<view class="vip-tags">
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">匹配</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</text>
|
||||
</view>
|
||||
<text class="user-id" bindtap="copyUserId">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
|
||||
<text class="vip-expire" wx:if="{{isVip && vipExpireDate}}">会员到期时间:{{vipExpireDate}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">{{readCount}}</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">{{referralCount}}</text>
|
||||
<text class="stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="stat-item" bindtap="goToReferral">
|
||||
<text class="stat-value gold-color">{{pendingEarnings > 0 ? '¥' + pendingEarnings : '--'}}</text>
|
||||
<text class="stat-label">我的收益</text>
|
||||
<view class="user-actions">
|
||||
<view class="action-btn" bindtap="handleMenuTap" data-id="settings"><text class="action-icon">⚙️</text></view>
|
||||
<view class="action-btn" catchtap="goToProfileEdit"><text class="action-icon">✎</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统一内容区 - 仅登录用户显示 -->
|
||||
<view class="tab-content" wx:if="{{isLoggedIn}}">
|
||||
<!-- 菜单:我的订单 + 设置 -->
|
||||
<view class="menu-card card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-brand">📦</view>
|
||||
<text class="menu-title">我的订单</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<text class="menu-arrow">→</text>
|
||||
</view>
|
||||
<!-- 已登录:内容区 -->
|
||||
<view class="main-content" wx:if="{{isLoggedIn}}">
|
||||
<!-- 分享收益 -->
|
||||
<view class="card earnings-card">
|
||||
<view class="card-header">
|
||||
<text class="card-icon">💰</text>
|
||||
<text class="card-title">分享收益</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-gray">⚙️</view>
|
||||
<text class="menu-title">设置</text>
|
||||
<view class="earnings-grid">
|
||||
<view class="earnings-col">
|
||||
<text class="earnings-val primary">{{referralCount}}</text>
|
||||
<text class="earnings-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<text class="menu-arrow">→</text>
|
||||
<view class="earnings-col">
|
||||
<text class="earnings-val primary">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="earnings-label">我的收益</text>
|
||||
</view>
|
||||
<view class="earnings-col" bindtap="handleWithdraw">
|
||||
<text class="earnings-val primary">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="earnings-label">可提现金额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<view class="stats-card card">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">👁️</text>
|
||||
<text>阅读统计</text>
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<text class="card-icon">👁️</text>
|
||||
<text class="card-title">阅读统计</text>
|
||||
</view>
|
||||
<view class="stats-row">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-box">
|
||||
<text class="stat-icon brand-color">📖</text>
|
||||
<text class="stat-icon">📖</text>
|
||||
<text class="stat-num">{{readCount}}</text>
|
||||
<text class="stat-text">已读章节</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="stat-box">
|
||||
<text class="stat-icon gold-color">⏱️</text>
|
||||
<text class="stat-icon">⏱</text>
|
||||
<text class="stat-num">{{totalReadTime}}</text>
|
||||
<text class="stat-text">阅读分钟</text>
|
||||
<text class="stat-label">阅读分钟</text>
|
||||
</view>
|
||||
<view class="stat-box" wx:if="{{matchEnabled}}">
|
||||
<text class="stat-icon pink-color">👥</text>
|
||||
<view class="stat-box">
|
||||
<text class="stat-icon">👥</text>
|
||||
<text class="stat-num">{{matchHistory}}</text>
|
||||
<text class="stat-text">匹配伙伴</text>
|
||||
<text class="stat-label">匹配伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="recent-card card">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">📖</text>
|
||||
<text>最近阅读</text>
|
||||
<view class="card recent-card">
|
||||
<view class="card-header">
|
||||
<text class="card-icon">📖</text>
|
||||
<text class="card-title">最近阅读</text>
|
||||
</view>
|
||||
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{recentChapters}}"
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{recentChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
@@ -160,48 +112,46 @@
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="recent-btn">继续阅读</text>
|
||||
<text class="recent-link">继续阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" wx:else>
|
||||
<text class="empty-icon">📖</text>
|
||||
<text class="empty-text">暂无阅读记录</text>
|
||||
<view class="empty-btn" bindtap="goToChapters">去阅读 →</view>
|
||||
<view class="recent-empty" wx:else>
|
||||
<text class="recent-empty-text">暂无阅读记录</text>
|
||||
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关于作者(最底部) -->
|
||||
<view class="menu-card card" style="margin-top: 16rpx;">
|
||||
<!-- 我的订单 + 关于作者 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><text class="menu-icon">📦</text></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-brand">ℹ️</view>
|
||||
<text class="menu-title">关于作者</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<text class="menu-arrow">→</text>
|
||||
<view class="menu-icon-wrap icon-blue"><text class="menu-icon">ℹ</text></view>
|
||||
<text class="menu-text">关于作者</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗:可取消,用户主动选择是否登录 -->
|
||||
<!-- 登录弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal">✕</view>
|
||||
<view class="login-icon">🔐</view>
|
||||
<text class="login-title">登录 Soul创业实验</text>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
|
||||
<button
|
||||
class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}"
|
||||
bindtap="handleWechatLogin"
|
||||
disabled="{{isLoggingIn || !agreeProtocol}}"
|
||||
>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
</button>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
@@ -212,6 +162,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机/微信号弹窗 -->
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
<view class="contact-modal" catchtap="stopPropagation">
|
||||
<text class="contact-modal-title">请完善联系方式</text>
|
||||
<view class="contact-modal-hint">需完善手机号或微信号才能使用提现和找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">📱</text>
|
||||
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">💬</text>
|
||||
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-modal-btn" bindtap="saveContactInfo" disabled="{{contactSaving}}">{{contactSaving ? '保存中...' : '保存'}}</view>
|
||||
<text class="contact-modal-cancel" bindtap="closeContactModal">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改昵称弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
@@ -220,21 +194,10 @@
|
||||
<text class="modal-icon">✏️</text>
|
||||
<text class="modal-title">修改昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="nickname-input-wrap">
|
||||
<input
|
||||
class="nickname-input"
|
||||
type="nickname"
|
||||
value="{{editingNickname}}"
|
||||
placeholder="点击输入昵称"
|
||||
placeholder-class="nickname-placeholder"
|
||||
bindchange="onNicknameChange"
|
||||
bindinput="onNicknameInput"
|
||||
maxlength="20"
|
||||
/>
|
||||
<input class="nickname-input" type="nickname" value="{{editingNickname}}" placeholder="点击输入昵称" placeholder-class="nickname-placeholder" bindchange="onNicknameChange" bindinput="onNicknameInput" maxlength="20"/>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="modal-btn modal-btn-cancel" bindtap="closeNicknameModal">取消</view>
|
||||
<view class="modal-btn modal-btn-confirm" bindtap="confirmNickname">确定</view>
|
||||
@@ -242,6 +205,5 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
199
miniprogram/pages/profile-edit/profile-edit.js
Normal file
199
miniprogram/pages/profile-edit/profile-edit.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Soul创业派对 - 资料编辑完整版(comprehensive_profile_editor_v1_1)
|
||||
* 温馨提示、头像、基本信息、核心联系方式、个人故事、互助需求、项目介绍
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
mbti: '',
|
||||
mbtiIndex: 0,
|
||||
region: '',
|
||||
industry: '',
|
||||
businessScale: '',
|
||||
position: '',
|
||||
skills: '',
|
||||
phone: '',
|
||||
wechatId: '',
|
||||
storyBestMonth: '',
|
||||
storyAchievement: '',
|
||||
storyTurning: '',
|
||||
helpOffer: '',
|
||||
helpNeed: '',
|
||||
projectIntro: '',
|
||||
mbtiOptions: MBTI_OPTIONS,
|
||||
showMbtiPicker: false,
|
||||
saving: false,
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
async loadProfile() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo?.id) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userInfo.id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
const mbtiIndex = MBTI_OPTIONS.indexOf(d.mbti || '') >= 0 ? MBTI_OPTIONS.indexOf(d.mbti) : 0
|
||||
this.setData({
|
||||
avatar: d.avatar || '',
|
||||
nickname: d.nickname || '',
|
||||
mbti: d.mbti || '',
|
||||
mbtiIndex,
|
||||
region: d.region || '',
|
||||
industry: d.industry || '',
|
||||
businessScale: d.businessScale || '',
|
||||
position: d.position || '',
|
||||
skills: d.skills || '',
|
||||
phone: d.phone || '',
|
||||
wechatId: d.wechatId || wx.getStorageSync('user_wechat') || '',
|
||||
storyBestMonth: d.storyBestMonth || '',
|
||||
storyAchievement: d.storyAchievement || '',
|
||||
storyTurning: d.storyTurning || '',
|
||||
helpOffer: d.helpOffer || '',
|
||||
helpNeed: d.helpNeed || '',
|
||||
projectIntro: d.projectIntro || '',
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() { wx.navigateBack() },
|
||||
|
||||
onNicknameInput(e) { this.setData({ nickname: e.detail.value }) },
|
||||
onRegionInput(e) { this.setData({ region: e.detail.value }) },
|
||||
onIndustryInput(e) { this.setData({ industry: e.detail.value }) },
|
||||
onBusinessScaleInput(e) { this.setData({ businessScale: e.detail.value }) },
|
||||
onPositionInput(e) { this.setData({ position: e.detail.value }) },
|
||||
onSkillsInput(e) { this.setData({ skills: e.detail.value }) },
|
||||
onPhoneInput(e) { this.setData({ phone: e.detail.value }) },
|
||||
onWechatInput(e) { this.setData({ wechatId: e.detail.value }) },
|
||||
onStoryBestMonthInput(e) { this.setData({ storyBestMonth: e.detail.value }) },
|
||||
onStoryAchievementInput(e) { this.setData({ storyAchievement: e.detail.value }) },
|
||||
onStoryTurningInput(e) { this.setData({ storyTurning: e.detail.value }) },
|
||||
onHelpOfferInput(e) { this.setData({ helpOffer: e.detail.value }) },
|
||||
onHelpNeedInput(e) { this.setData({ helpNeed: e.detail.value }) },
|
||||
onProjectIntroInput(e) { this.setData({ projectIntro: e.detail.value }) },
|
||||
|
||||
onMbtiPickerChange(e) {
|
||||
const i = parseInt(e.detail.value, 10)
|
||||
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
|
||||
},
|
||||
|
||||
chooseAvatar() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
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,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
try {
|
||||
const payload = {
|
||||
userId,
|
||||
nickname: this.data.nickname.trim() || undefined,
|
||||
mbti: this.data.mbti || undefined,
|
||||
region: this.data.region.trim() || undefined,
|
||||
industry: this.data.industry.trim() || undefined,
|
||||
businessScale: this.data.businessScale.trim() || undefined,
|
||||
position: this.data.position.trim() || undefined,
|
||||
skills: this.data.skills.trim() || undefined,
|
||||
phone: this.data.phone.trim() || undefined,
|
||||
wechatId: this.data.wechatId.trim() || undefined,
|
||||
storyBestMonth: this.data.storyBestMonth.trim() || undefined,
|
||||
storyAchievement: this.data.storyAchievement.trim() || undefined,
|
||||
storyTurning: this.data.storyTurning.trim() || undefined,
|
||||
helpOffer: this.data.helpOffer.trim() || undefined,
|
||||
helpNeed: this.data.helpNeed.trim() || undefined,
|
||||
projectIntro: this.data.projectIntro.trim() || undefined,
|
||||
}
|
||||
if (payload.wechatId) wx.setStorageSync('user_wechat', payload.wechatId)
|
||||
if (payload.phone) wx.setStorageSync('user_phone', payload.phone)
|
||||
const hasUpdate = Object.keys(payload).some(k => k !== 'userId' && payload[k] != null)
|
||||
if (!hasUpdate) {
|
||||
wx.showToast({ title: '无变更', icon: 'none' })
|
||||
this.setData({ saving: false })
|
||||
return
|
||||
}
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (app.globalData.userInfo && payload.nickname) {
|
||||
app.globalData.userInfo.nickname = payload.nickname
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
setTimeout(() => wx.navigateBack(), 800)
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ saving: false })
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/profile-edit/profile-edit.json
Normal file
4
miniprogram/pages/profile-edit/profile-edit.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "编辑资料",
|
||||
"usingComponents": {}
|
||||
}
|
||||
135
miniprogram/pages/profile-edit/profile-edit.wxml
Normal file
135
miniprogram/pages/profile-edit/profile-edit.wxml
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- 资料编辑 - comprehensive_profile_editor_v1_1 | input/textarea 用 view 包裹,配色 enhanced -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-icon">‹</text></view>
|
||||
<text class="nav-title">编辑资料</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tip-card">
|
||||
<text class="tip-icon">ℹ</text>
|
||||
<text class="tip-text">温馨提示:需完善手机号和微信号才能使用提现和找伙伴功能</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="chooseAvatar">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
<view class="avatar-camera">📷</view>
|
||||
</view>
|
||||
<text class="avatar-change">更换头像</text>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入昵称" value="{{nickname}}" bindinput="onNicknameInput"/></view>
|
||||
</view>
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
|
||||
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap form-input-suffix">
|
||||
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
|
||||
<text class="form-suffix">📍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">行业</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/电商" value="{{industry}}" bindinput="onIndustryInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">业务体量</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:年GMV 5000万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">职位</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人/联合创始人" value="{{position}}" bindinput="onPositionInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我擅长</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频制作、IP打造、私域运营" value="{{skills}}" bindinput="onSkillsInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心联系方式 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">📞</text>
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">💡</text>
|
||||
<text>个人故事</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">你最赚钱的一个月做的是什么</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:2021年主导电商大促,单月GMV突破500W..." value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">最有成就感的一件事</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:帮助3个素人打造个人IP,每月稳定变现5万+" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">人生的转折点</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:辞去大厂工作开始做自媒体..." value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">🤝</text>
|
||||
<text>互助需求</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我能帮助大家什么</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频脚本、账号冷启动、私域转化" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我需要什么帮助</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:寻找供应链资源、线下活动合作" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">🚀</text>
|
||||
<text>项目介绍</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="详细介绍您的项目,让潜在伙伴更好地了解您..." value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
99
miniprogram/pages/profile-edit/profile-edit.wxss
Normal file
99
miniprogram/pages/profile-edit/profile-edit.wxss
Normal file
@@ -0,0 +1,99 @@
|
||||
/* 资料编辑 - comprehensive_profile_editor_v1_1 | 配色 enhanced,input/textarea 用 view 包裹 */
|
||||
.page {
|
||||
background: #050B14; min-height: 100vh; color: #fff;
|
||||
width: 100%; box-sizing: border-box; overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 44px; padding: 0 24rpx;
|
||||
background: rgba(5,11,20,0.9); backdrop-filter: blur(8rpx);
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.nav-back { width: 60rpx; padding: 16rpx 0; }
|
||||
.back-icon { font-size: 44rpx; color: #5EEAD4; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; }
|
||||
.nav-placeholder { width: 60rpx; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
|
||||
.scroll-main { width: 100%; height: calc(100vh - 88rpx); padding: 24rpx; box-sizing: border-box; overflow-x: hidden; }
|
||||
|
||||
.tip-card {
|
||||
display: flex; align-items: flex-start; gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: rgba(94,234,212,0.08); border: 1rpx solid rgba(94,234,212,0.25);
|
||||
border-radius: 24rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.tip-icon { font-size: 40rpx; color: #5EEAD4; flex-shrink: 0; }
|
||||
.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; }
|
||||
.avatar-wrap {
|
||||
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%; overflow: hidden;
|
||||
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; display: block; }
|
||||
.avatar-placeholder {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 72rpx; font-weight: bold; color: #5EEAD4; background: rgba(94,234,212,0.2);
|
||||
}
|
||||
.avatar-camera {
|
||||
position: absolute; bottom: 0; right: 0;
|
||||
width: 56rpx; height: 56rpx; background: #5EEAD4; color: #000;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx;
|
||||
}
|
||||
.avatar-change { font-size: 28rpx; color: #5EEAD4; font-weight: 500; margin-top: 16rpx; }
|
||||
|
||||
.section {
|
||||
margin-bottom: 48rpx; padding-top: 32rpx;
|
||||
border-top: 1rpx solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.section-title { display: flex; align-items: center; gap: 16rpx; font-size: 32rpx; font-weight: 600; margin-bottom: 32rpx; }
|
||||
.section-icon { font-size: 40rpx; }
|
||||
|
||||
.form-row { margin-bottom: 32rpx; }
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
.form-row-2 { display: flex; gap: 24rpx; }
|
||||
.form-row-2 .form-item { flex: 1; min-width: 0; }
|
||||
.form-label { display: block; font-size: 24rpx; color: #94A3B8; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
|
||||
/* input/textarea 用 view 包裹,padding 写在 view 上 */
|
||||
.form-input-wrap {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 24rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-input-suffix { position: relative; padding-right: 64rpx; }
|
||||
.form-input-suffix .form-suffix {
|
||||
position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
|
||||
font-size: 32rpx; color: #94A3B8;
|
||||
}
|
||||
.form-input-inner {
|
||||
width: 100%; max-width: 100%; font-size: 28rpx; color: #fff; background: transparent;
|
||||
box-sizing: border-box; display: block;
|
||||
}
|
||||
.form-picker { color: #fff; }
|
||||
|
||||
.form-textarea-wrap {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 24rpx; min-height: 160rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-textarea-wrap.form-textarea-lg { min-height: 240rpx; }
|
||||
.form-textarea-inner {
|
||||
width: 100%; max-width: 100%; min-height: 112rpx; font-size: 28rpx; color: #fff;
|
||||
background: transparent; line-height: 1.5; box-sizing: border-box; display: block;
|
||||
}
|
||||
.form-textarea-lg .form-textarea-inner { min-height: 192rpx; }
|
||||
|
||||
.save-btn {
|
||||
width: 100%; height: 96rpx; line-height: 96rpx; text-align: center;
|
||||
background: #5EEAD4; color: #050B14; font-size: 36rpx; font-weight: bold;
|
||||
border-radius: 24rpx; margin-top: 48rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.save-btn[disabled] { opacity: 0.6; }
|
||||
.bottom-space { height: 120rpx; }
|
||||
79
miniprogram/pages/profile-show/profile-show.js
Normal file
79
miniprogram/pages/profile-show/profile-show.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Soul创业派对 - 个人资料展示页(stitch_soul enhanced_professional_profile)
|
||||
* 从「我的」页编辑图标进入;展示基本信息、个人故事、互助需求、项目介绍
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
profile: null,
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.profile) this.loadProfile()
|
||||
},
|
||||
|
||||
async loadProfile() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo?.id) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userInfo.id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
const phone = d.phone || ''
|
||||
const wechat = d.wechatId || wx.getStorageSync('user_wechat') || ''
|
||||
this.setData({
|
||||
profile: {
|
||||
...d,
|
||||
phoneMask: phone ? phone.slice(0, 3) + '****' + phone.slice(-2) : '',
|
||||
wechatMask: wechat ? (wechat.length > 8 ? wechat.slice(0, 4) + '****' + wechat.slice(-3) : wechat) : '',
|
||||
phone,
|
||||
wechat,
|
||||
},
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ profile: null, loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ profile: null, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
goToEdit() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
copyPhone() {
|
||||
const p = this.data.profile?.phone
|
||||
if (!p) return
|
||||
wx.setClipboardData({ data: p, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
copyWechat() {
|
||||
const w = this.data.profile?.wechat
|
||||
if (!w) return
|
||||
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/profile-show/profile-show.json
Normal file
4
miniprogram/pages/profile-show/profile-show.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人资料",
|
||||
"usingComponents": {}
|
||||
}
|
||||
135
miniprogram/pages/profile-show/profile-show.wxml
Normal file
135
miniprogram/pages/profile-show/profile-show.wxml
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- 个人资料展示页 - enhanced_professional_profile 1:1 重构 -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-icon">‹</text></view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-right" bindtap="goToEdit"><text class="nav-more">⋯</text></view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 头像区卡片 -->
|
||||
<view class="hero-card" wx:if="{{profile}}">
|
||||
<view class="hero-gradient"></view>
|
||||
<view class="hero-content">
|
||||
<view class="hero-avatar">
|
||||
<image wx:if="{{profile.avatar}}" class="avatar-img" src="{{profile.avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{profile.nickname ? profile.nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<text class="hero-name">{{profile.nickname || '未设置昵称'}}</text>
|
||||
<view class="hero-tags">
|
||||
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
|
||||
<text class="tag tag-region" wx:if="{{profile.region}}">📍 {{profile.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">👤</text>
|
||||
<text class="section-title">基本信息</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="field" wx:if="{{profile.industry}}">
|
||||
<text class="field-label">行业</text>
|
||||
<text class="field-value">{{profile.industry}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.position}}">
|
||||
<text class="field-label">职位</text>
|
||||
<text class="field-value">{{profile.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.businessScale}}">
|
||||
<text class="field-label">业务体量</text>
|
||||
<text class="field-value">{{profile.businessScale}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.industry || profile.position || profile.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{profile.skills}}">
|
||||
<text class="field-label">我擅长</text>
|
||||
<text class="field-value">{{profile.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.phoneMask || profile.phone}}">
|
||||
<text class="field-label">联系方式</text>
|
||||
<view class="field-value-row" bindtap="copyPhone">
|
||||
<text class="field-value mono">{{profile.phoneMask || profile.phone || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.phone}}">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.wechatMask || profile.wechat}}">
|
||||
<text class="field-label">微信号</text>
|
||||
<view class="field-value-row" bindtap="copyWechat">
|
||||
<text class="field-value mono">{{profile.wechatMask || profile.wechat || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.wechat}}">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.phone && !profile.wechat && !profile.skills}}">
|
||||
点击右上角 ⋯ 编辑完善资料
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="section" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon section-icon-yellow">💡</text>
|
||||
<text class="section-title">个人故事</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="story-block" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><text class="story-emoji">🏆</text><text class="story-label">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-text">{{profile.storyBestMonth}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyAchievement}}">
|
||||
<view class="story-head"><text class="story-emoji">⭐</text><text class="story-label">最有成就感的一件事</text></view>
|
||||
<text class="story-text">{{profile.storyAchievement}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyTurning}}">
|
||||
<view class="story-head"><text class="story-emoji">🔄</text><text class="story-label">人生的转折点</text></view>
|
||||
<text class="story-text">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="section" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">🤝</text>
|
||||
<text class="section-title">互助需求</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="help-block" wx:if="{{profile.helpOffer}}">
|
||||
<text class="help-tag help-tag-accent">我能帮你</text>
|
||||
<text class="help-text">{{profile.helpOffer}}</text>
|
||||
</view>
|
||||
<view class="help-block" wx:if="{{profile.helpNeed}}">
|
||||
<text class="help-tag help-tag-orange">我需要帮助</text>
|
||||
<text class="help-text">{{profile.helpNeed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="section" wx:if="{{profile.projectIntro}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">🚀</text>
|
||||
<text class="section-title">项目介绍</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<text class="project-text">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 - 设计稿为描边橙色 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="vip-btn-outline" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<text class="vip-btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
114
miniprogram/pages/profile-show/profile-show.wxss
Normal file
114
miniprogram/pages/profile-show/profile-show.wxss
Normal file
@@ -0,0 +1,114 @@
|
||||
/* 个人资料展示页 - enhanced_professional_profile 1:1 重构 */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 44px; padding: 0 32rpx;
|
||||
background: rgba(5,11,20,0.9); backdrop-filter: blur(8rpx);
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.nav-back { padding: 16rpx; margin-left: -8rpx; }
|
||||
.back-icon { font-size: 40rpx; color: #5EEAD4; }
|
||||
.nav-title { font-size: 34rpx; font-weight: bold; }
|
||||
.nav-right { padding: 16rpx; }
|
||||
.nav-more { font-size: 48rpx; color: #fff; line-height: 1; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
|
||||
.scroll-main { height: calc(100vh - 120rpx); padding: 0 32rpx 32rpx; }
|
||||
|
||||
/* 头像区卡片 */
|
||||
.hero-card {
|
||||
position: relative; overflow: hidden;
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 64rpx 32rpx; display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.hero-gradient {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(to bottom, rgba(30,58,69,0.3) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.hero-avatar {
|
||||
width: 176rpx; height: 176rpx; border-radius: 50%;
|
||||
overflow: hidden; border: 2rpx solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; display: block; }
|
||||
.avatar-placeholder {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 72rpx; font-weight: bold; color: #5EEAD4;
|
||||
background: rgba(94,234,212,0.2);
|
||||
}
|
||||
.hero-name { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
|
||||
.hero-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; }
|
||||
.tag { padding: 8rpx 24rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 500; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94,234,212,0.2); }
|
||||
.tag-region { background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
|
||||
|
||||
/* 通用区块 */
|
||||
.section {
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 40rpx; box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
.section-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.section-icon { font-size: 40rpx; }
|
||||
.section-icon-yellow { filter: brightness(1.2); }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; }
|
||||
|
||||
.section-body { }
|
||||
.field { margin-bottom: 48rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field-label { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 16rpx; }
|
||||
.field-value { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.5; }
|
||||
.field-value.mono { font-family: monospace; letter-spacing: 0.02em; }
|
||||
.field-value-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.field-hint { font-size: 24rpx; color: #5EEAD4; }
|
||||
.field-divider { height: 1rpx; background: rgba(255,255,255,0.05); margin: 32rpx 0; }
|
||||
.field-empty { font-size: 26rpx; color: #64748b; }
|
||||
|
||||
/* 个人故事 */
|
||||
.story-block { margin-bottom: 48rpx; }
|
||||
.story-block:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
|
||||
.story-emoji { font-size: 32rpx; }
|
||||
.story-label { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; display: block; }
|
||||
|
||||
/* 互助需求 */
|
||||
.help-block {
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.05);
|
||||
border-radius: 20rpx; padding: 32rpx; margin-bottom: 24rpx;
|
||||
}
|
||||
.help-block:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block; font-size: 22rpx; font-weight: 500;
|
||||
padding: 8rpx 16rpx; border-radius: 8rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.help-tag-accent { background: #112D2A; color: #5EEAD4; }
|
||||
.help-tag-orange { background: #2D1F0D; color: #F59E0B; }
|
||||
.help-text { font-size: 26rpx; color: #fff; line-height: 1.6; display: block; }
|
||||
|
||||
.project-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; }
|
||||
|
||||
.bottom-spacer { height: 180rpx; }
|
||||
|
||||
/* 底部按钮 - 设计稿:透明背景 + 橙色描边 */
|
||||
.bottom-bar {
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
|
||||
padding: 32rpx; padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(5,11,20,0.95); backdrop-filter: blur(8rpx);
|
||||
border-top: 1rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.vip-btn-outline {
|
||||
display: flex; align-items: center; justify-content: center; gap: 16rpx;
|
||||
width: 100%; height: 96rpx;
|
||||
background: transparent; color: #F59E0B;
|
||||
border: 2rpx solid rgba(245,158,11,0.3);
|
||||
border-radius: 999rpx; font-size: 30rpx; font-weight: 500;
|
||||
}
|
||||
.vip-btn-arrow { font-size: 36rpx; }
|
||||
@@ -8,17 +8,18 @@ Page({
|
||||
expireDateStr: '',
|
||||
price: 1980,
|
||||
originalPrice: 6980,
|
||||
/* 按 premium_membership_landing_v1 设计稿 */
|
||||
contentRights: [
|
||||
{ title: '解锁全部章节', desc: '365天全部章节内容' },
|
||||
{ title: '案例库', desc: '30-100个创业项目案例' },
|
||||
{ title: '智能纪要', desc: '每天推送派对精华' },
|
||||
{ title: '会议纪要库', desc: '之前所有场次的会议纪要' }
|
||||
{ title: '解锁全部章节', desc: '365天全案精读', icon: '📖' },
|
||||
{ title: '案例库', desc: '100+创业实战案例', icon: '📚' },
|
||||
{ title: '智能纪要', desc: 'AI每日精华推送', icon: '💡' },
|
||||
{ title: '会议纪要库', desc: '往期完整沉淀', icon: '📁' }
|
||||
],
|
||||
socialRights: [
|
||||
{ title: '匹配创业伙伴', desc: '匹配所有创业伙伴' },
|
||||
{ title: '创业老板排行', desc: '排行榜展示您的项目' },
|
||||
{ title: '链接资源', desc: '进群聊天、链接资源的权利' },
|
||||
{ title: '专属VIP标识', desc: '头像金色VIP光圈' }
|
||||
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: '👥' },
|
||||
{ title: '创业老板排行', desc: '项目曝光展示', icon: '📊' },
|
||||
{ title: '链接资源', desc: '深度私域资源池', icon: '🔗' },
|
||||
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: '✓' }
|
||||
],
|
||||
profile: { vipName: '', vipProject: '', vipContact: '', vipAvatar: '', vipBio: '' },
|
||||
purchasing: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- VIP会员页 -->
|
||||
<!-- VIP会员页 - 按 premium_membership_landing_v1 设计稿 -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
@@ -8,55 +8,53 @@
|
||||
<view class="nav-placeholder-r"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
<!-- 会员状态 -->
|
||||
<!-- 会员宣传区 - 设计稿 premium 卡片 -->
|
||||
<view class="vip-hero {{isVip ? 'vip-hero-active' : ''}}">
|
||||
<text class="vip-hero-tag">卡若创业派对</text>
|
||||
<text class="vip-hero-tag">VIP PREMIUM</text>
|
||||
<text class="vip-hero-title">
|
||||
加入卡若的
|
||||
<text class="gold">创业派对</text>
|
||||
会员
|
||||
</text>
|
||||
<text class="vip-hero-sub" wx:if="{{isVip}}">有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天)</text>
|
||||
<text class="vip-hero-sub" wx:else>专属会员尊享权益</text>
|
||||
<text class="vip-hero-sub" wx:else>一次加入 尊享终身陪伴与成长</text>
|
||||
</view>
|
||||
<!-- 内容权益 -->
|
||||
<view class="rights-card">
|
||||
<text class="rights-section-title">内容权益</text>
|
||||
<view class="rights-item" wx:for="{{contentRights}}" wx:key="title">
|
||||
<view class="rights-check-wrap">
|
||||
<text class="rights-check">✓</text>
|
||||
<!-- 双列权益:内容权益 + 社交权益 -->
|
||||
<view class="rights-grid">
|
||||
<view class="rights-col">
|
||||
<view class="rights-col-header">
|
||||
<text class="rights-dot rights-dot-teal"></text>
|
||||
<text class="rights-col-title">内容权益</text>
|
||||
</view>
|
||||
<view class="rights-info">
|
||||
<text class="rights-title">{{item.title}}</text>
|
||||
<text class="rights-desc">{{item.desc}}</text>
|
||||
<view class="benefit-card" wx:for="{{contentRights}}" wx:key="title">
|
||||
<text class="benefit-icon">{{item.icon || '✓'}}</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-title">{{item.title}}</text>
|
||||
<text class="benefit-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rights-col">
|
||||
<view class="rights-col-header">
|
||||
<text class="rights-dot rights-dot-gold"></text>
|
||||
<text class="rights-col-title rights-col-title-gold">社交权益</text>
|
||||
</view>
|
||||
<view class="benefit-card" wx:for="{{socialRights}}" wx:key="title">
|
||||
<text class="benefit-icon benefit-icon-gold">{{item.icon || '✓'}}</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-title">{{item.title}}</text>
|
||||
<text class="benefit-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 社交权益 -->
|
||||
<view class="rights-card">
|
||||
<text class="rights-section-title">社交权益</text>
|
||||
<view class="rights-item" wx:for="{{socialRights}}" wx:key="title">
|
||||
<view class="rights-check-wrap">
|
||||
<text class="rights-check">✓</text>
|
||||
</view>
|
||||
<view class="rights-info">
|
||||
<text class="rights-title">{{item.title}}</text>
|
||||
<text class="rights-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 价格区 + 购买按钮 -->
|
||||
<view class="buy-area" wx:if="{{!isVip}}">
|
||||
<view class="price-row">
|
||||
<text class="price-original">¥{{originalPrice}}</text>
|
||||
<text class="price-current">¥{{price}}</text>
|
||||
<text class="price-unit">/年</text>
|
||||
</view>
|
||||
<button class="buy-btn" bindtap="handlePurchase" disabled="{{purchasing}}">
|
||||
{{purchasing ? '处理中...' : '¥' + price + ' 加入创业派对'}}
|
||||
<!-- 底部固定购买按钮(非 VIP 时显示) -->
|
||||
<view class="buy-footer" wx:if="{{!isVip}}">
|
||||
<button class="buy-btn-fixed" bindtap="handlePurchase" disabled="{{purchasing}}">
|
||||
{{purchasing ? "处理中..." : "¥" + price + "/年 加入创业派对"}}
|
||||
</button>
|
||||
<text class="buy-sub">加入卡若创业派对,获取创业资讯与优质人脉资源</text>
|
||||
</view>
|
||||
<view class="bottom-spacer" wx:if="{{!isVip}}"></view>
|
||||
<!-- VIP资料填写(仅VIP可见) -->
|
||||
<view class="profile-card" wx:if="{{isVip}}">
|
||||
<text class="profile-title">会员资料(展示在创业老板排行)</text>
|
||||
|
||||
@@ -12,26 +12,28 @@
|
||||
.gold { color: #FFD700; }
|
||||
.vip-hero-sub { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; }
|
||||
|
||||
.rights-card { margin: 24rpx; padding: 0 8rpx; }
|
||||
.rights-item { display: flex; align-items: flex-start; padding: 24rpx; margin-bottom: 16rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.06); border-radius: 16rpx; }
|
||||
.rights-item .rights-check-wrap { margin-right: 20rpx; }
|
||||
.rights-check-wrap { width: 44rpx; height: 44rpx; border-radius: 50%; background: rgba(0,206,209,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 4rpx; }
|
||||
.rights-check { color: #00CED1; font-size: 24rpx; font-weight: bold; }
|
||||
.rights-info { display: flex; flex-direction: column; }
|
||||
.rights-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.95); }
|
||||
.rights-desc { font-size: 24rpx; color: rgba(255,255,255,0.45); margin-top: 6rpx; }
|
||||
/* 双列权益 - 按 premium_membership_landing_v1 */
|
||||
.rights-grid { display: flex; gap: 24rpx; margin: 24rpx; }
|
||||
.rights-col { flex: 1; min-width: 0; }
|
||||
.rights-col-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; padding-left: 8rpx; }
|
||||
.rights-dot { width: 8rpx; height: 24rpx; border-radius: 4rpx; }
|
||||
.rights-dot-teal { background: #4FD1C5; }
|
||||
.rights-dot-gold { background: #FFBD2E; }
|
||||
.rights-col-title { font-size: 24rpx; font-weight: bold; color: #4FD1C5; letter-spacing: 2rpx; }
|
||||
.rights-col-title-gold { color: #FFBD2E; }
|
||||
.benefit-card { display: flex; flex-direction: column; gap: 16rpx; padding: 24rpx; margin-bottom: 16rpx; background: #141414; border: 1rpx solid rgba(255,255,255,0.05); border-radius: 24rpx; }
|
||||
.benefit-icon { font-size: 36rpx; color: #4FD1C5; }
|
||||
.benefit-icon-gold { color: #FFBD2E; }
|
||||
.benefit-info { display: flex; flex-direction: column; }
|
||||
.benefit-title { font-size: 26rpx; font-weight: bold; color: #fff; }
|
||||
.benefit-desc { font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; line-height: 1.4; }
|
||||
|
||||
.rights-section-title { display: block; font-size: 26rpx; color: #00CED1; font-weight: 600; margin-bottom: 16rpx; margin-left: 16rpx; padding-bottom: 12rpx; border-bottom: 1rpx solid rgba(0,206,209,0.15); }
|
||||
|
||||
.buy-area { margin: 24rpx; padding: 32rpx; text-align: center; background: rgba(255,255,255,0.03); border-radius: 20rpx; }
|
||||
.price-row { display: flex; align-items: baseline; justify-content: center; gap: 12rpx; margin-bottom: 24rpx; }
|
||||
.price-original { font-size: 28rpx; color: rgba(255,255,255,0.35); text-decoration: line-through; }
|
||||
.price-current { font-size: 64rpx; font-weight: bold; color: #FF4444; }
|
||||
.price-unit { font-size: 26rpx; color: rgba(255,255,255,0.5); }
|
||||
.buy-btn { width: 90%; height: 88rpx; padding: 0; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; border: none; margin: 0 auto; }
|
||||
.buy-btn::after { border: none; }
|
||||
.buy-btn[disabled] { opacity: 0.5; }
|
||||
.buy-sub { display: block; font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 16rpx; }
|
||||
/* 底部固定购买按钮 - 设计稿 */
|
||||
.buy-footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 24rpx 32rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.95); border-top: 1rpx solid rgba(255,255,255,0.05); z-index: 50; }
|
||||
.buy-btn-fixed { width: 100%; height: 96rpx; padding: 0; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #FFD700, #FFB000); color: #000; font-size: 32rpx; font-weight: bold; border-radius: 48rpx; border: none; box-shadow: 0 8rpx 32rpx rgba(255,188,46,0.2); }
|
||||
.buy-btn-fixed::after { border: none; }
|
||||
.buy-btn-fixed[disabled] { opacity: 0.6; }
|
||||
.bottom-spacer { height: 180rpx; }
|
||||
|
||||
.profile-card { margin: 24rpx; padding: 32rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.08); border-radius: 20rpx; }
|
||||
.profile-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.9); display: block; margin-bottom: 24rpx; }
|
||||
|
||||
@@ -24,50 +24,15 @@
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "vip资料填写",
|
||||
"pathName": "pages/vip/vip",
|
||||
"name": "个人资料",
|
||||
"pathName": "pages/profile-show/profile-show",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "pages/read/read",
|
||||
"pathName": "pages/read/read",
|
||||
"query": "id=1.1",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/match/match",
|
||||
"pathName": "pages/match/match",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "看书",
|
||||
"pathName": "pages/read/read",
|
||||
"query": "id=1.4",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "分销中心",
|
||||
"pathName": "pages/referral/referral",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "阅读",
|
||||
"pathName": "pages/read/read",
|
||||
"query": "id=1.1",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "分销中心",
|
||||
"pathName": "pages/referral/referral",
|
||||
"name": "pages/mentors/mentors",
|
||||
"pathName": "pages/mentors/mentors",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
@@ -78,13 +43,6 @@
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "新增地址",
|
||||
"pathName": "pages/addresses/edit",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { QRCodesPage } from './pages/qrcodes/QRCodesPage'
|
||||
import { MatchPage } from './pages/match/MatchPage'
|
||||
import { MatchRecordsPage } from './pages/match-records/MatchRecordsPage'
|
||||
import { VipRolesPage } from './pages/vip-roles/VipRolesPage'
|
||||
import { MentorsPage } from './pages/mentors/MentorsPage'
|
||||
import { MentorConsultationsPage } from './pages/mentor-consultations/MentorConsultationsPage'
|
||||
import { ApiDocPage } from './pages/api-doc/ApiDocPage'
|
||||
import { NotFoundPage } from './pages/not-found/NotFoundPage'
|
||||
|
||||
@@ -34,6 +36,8 @@ function App() {
|
||||
<Route path="chapters" element={<ChaptersPage />} />
|
||||
<Route path="referral-settings" element={<ReferralSettingsPage />} />
|
||||
<Route path="vip-roles" element={<VipRolesPage />} />
|
||||
<Route path="mentors" element={<MentorsPage />} />
|
||||
<Route path="mentor-consultations" element={<MentorConsultationsPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="payment" element={<PaymentPage />} />
|
||||
<Route path="site" element={<SitePage />} />
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
BookOpen,
|
||||
GitMerge,
|
||||
Crown,
|
||||
GraduationCap,
|
||||
Calendar,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
import { clearAdminToken } from '@/api/auth'
|
||||
@@ -19,6 +21,8 @@ const menuItems = [
|
||||
{ icon: BookOpen, label: '内容管理', href: '/content' },
|
||||
{ icon: Users, label: '用户管理', href: '/users' },
|
||||
{ icon: Crown, label: 'VIP 角色', href: '/vip-roles' },
|
||||
{ icon: GraduationCap, label: '导师管理', href: '/mentors' },
|
||||
{ icon: Calendar, label: '导师预约', href: '/mentor-consultations' },
|
||||
{ icon: Wallet, label: '交易中心', href: '/distribution' },
|
||||
{ icon: GitMerge, label: '匹配记录', href: '/match-records' },
|
||||
{ icon: CreditCard, label: '推广设置', href: '/referral-settings' },
|
||||
|
||||
@@ -49,6 +49,7 @@ interface SectionListItem {
|
||||
title: string
|
||||
price: number
|
||||
isFree?: boolean
|
||||
isNew?: boolean
|
||||
partId?: string
|
||||
partTitle?: string
|
||||
chapterId?: string
|
||||
@@ -62,6 +63,7 @@ interface Section {
|
||||
price: number
|
||||
filePath?: string
|
||||
isFree?: boolean
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
@@ -83,6 +85,7 @@ interface EditingSection {
|
||||
content?: string
|
||||
filePath?: string
|
||||
isFree?: boolean
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
function buildTree(sections: SectionListItem[]): Part[] {
|
||||
@@ -108,6 +111,7 @@ function buildTree(sections: SectionListItem[]): Part[] {
|
||||
price: s.price ?? 1,
|
||||
filePath: s.filePath,
|
||||
isFree: s.isFree,
|
||||
isNew: s.isNew,
|
||||
})
|
||||
}
|
||||
return Array.from(partMap.values()).map((p) => ({
|
||||
@@ -201,6 +205,7 @@ export function ContentPage() {
|
||||
`/api/db/book?action=read&id=${encodeURIComponent(section.id)}`,
|
||||
)
|
||||
if (data?.success && data.section) {
|
||||
const sec = data.section as { isNew?: boolean }
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: data.section.title ?? section.title,
|
||||
@@ -208,6 +213,7 @@ export function ContentPage() {
|
||||
content: data.section.content ?? '',
|
||||
filePath: section.filePath,
|
||||
isFree: section.isFree || section.price === 0,
|
||||
isNew: sec.isNew ?? section.isNew,
|
||||
})
|
||||
} else {
|
||||
setEditingSection({
|
||||
@@ -217,6 +223,7 @@ export function ContentPage() {
|
||||
content: '',
|
||||
filePath: section.filePath,
|
||||
isFree: section.isFree,
|
||||
isNew: section.isNew,
|
||||
})
|
||||
if (data && !(data as { success?: boolean }).success) {
|
||||
alert('无法读取文件内容: ' + ((data as { error?: string }).error || '未知错误'))
|
||||
@@ -256,6 +263,7 @@ export function ContentPage() {
|
||||
price: editingSection.isFree ? 0 : editingSection.price,
|
||||
content,
|
||||
isFree: editingSection.isFree || editingSection.price === 0,
|
||||
isNew: editingSection.isNew,
|
||||
saveToFile: true,
|
||||
})
|
||||
if (res && (res as { success?: boolean }).success !== false) {
|
||||
@@ -557,6 +565,25 @@ export function ContentPage() {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">最新新增</Label>
|
||||
<div className="flex items-center h-10">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingSection.isNew ?? false}
|
||||
onChange={(e) =>
|
||||
setEditingSection({
|
||||
...editingSection,
|
||||
isNew: e.target.checked,
|
||||
})
|
||||
}
|
||||
className="w-5 h-5 rounded border-gray-600 bg-[#0a1628] text-[#38bdac] focus:ring-[#38bdac]"
|
||||
/>
|
||||
<span className="ml-2 text-gray-400 text-sm">标记 NEW</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">章节标题</Label>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Calendar, RefreshCw } from 'lucide-react'
|
||||
import { get } from '@/api/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface Consultation {
|
||||
id: number
|
||||
userId: number
|
||||
mentorId: number
|
||||
consultationType: string
|
||||
amount: number
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function MentorConsultationsPage() {
|
||||
const [list, setList] = useState<Consultation[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [statusFilter, setStatusFilter] = useState('')
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const url = statusFilter ? `/api/db/mentor-consultations?status=${statusFilter}` : '/api/db/mentor-consultations'
|
||||
const data = await get<{ success?: boolean; data?: Consultation[] }>(url)
|
||||
if (data?.success && data.data) setList(data.data)
|
||||
} catch (e) {
|
||||
console.error('Load consultations error:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [statusFilter])
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
created: '已创建',
|
||||
pending_pay: '待支付',
|
||||
paid: '已支付',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
const typeMap: Record<string, string> = {
|
||||
single: '单次',
|
||||
half_year: '半年',
|
||||
year: '年度',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-[#38bdac]" />
|
||||
导师预约列表
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
stitch_soul 导师咨询预约记录
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="bg-[#0f2137] border border-gray-700 rounded-lg px-3 py-2 text-gray-300 text-sm"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
{Object.entries(statusMap).map(([k, v]) => (
|
||||
<option key={k} value={k}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button onClick={load} disabled={loading} variant="outline" className="border-gray-600 text-gray-300">
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-gray-400">加载中...</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">ID</TableHead>
|
||||
<TableHead className="text-gray-400">用户ID</TableHead>
|
||||
<TableHead className="text-gray-400">导师ID</TableHead>
|
||||
<TableHead className="text-gray-400">类型</TableHead>
|
||||
<TableHead className="text-gray-400">金额</TableHead>
|
||||
<TableHead className="text-gray-400">状态</TableHead>
|
||||
<TableHead className="text-gray-400">创建时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((r) => (
|
||||
<TableRow key={r.id} className="border-gray-700/50">
|
||||
<TableCell className="text-gray-300">{r.id}</TableCell>
|
||||
<TableCell className="text-gray-400">{r.userId}</TableCell>
|
||||
<TableCell className="text-gray-400">{r.mentorId}</TableCell>
|
||||
<TableCell className="text-gray-400">{typeMap[r.consultationType] || r.consultationType}</TableCell>
|
||||
<TableCell className="text-white">¥{r.amount}</TableCell>
|
||||
<TableCell className="text-gray-400">{statusMap[r.status] || r.status}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{r.createdAt}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{list.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
||||
暂无预约记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
429
soul-admin/src/pages/mentors/MentorsPage.tsx
Normal file
429
soul-admin/src/pages/mentors/MentorsPage.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Users, Plus, Edit3, Trash2, X, Save } from 'lucide-react'
|
||||
import { get, post, put, del } from '@/api/client'
|
||||
|
||||
interface Mentor {
|
||||
id: number
|
||||
name: string
|
||||
avatar?: string
|
||||
intro?: string
|
||||
tags?: string
|
||||
priceSingle?: number
|
||||
priceHalfYear?: number
|
||||
priceYear?: number
|
||||
quote?: string
|
||||
whyFind?: string
|
||||
offering?: string
|
||||
judgmentStyle?: string
|
||||
sort: number
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function MentorsPage() {
|
||||
const [mentors, setMentors] = useState<Mentor[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editing, setEditing] = useState<Mentor | null>(null)
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
tags: '',
|
||||
priceSingle: '',
|
||||
priceHalfYear: '',
|
||||
priceYear: '',
|
||||
quote: '',
|
||||
whyFind: '',
|
||||
offering: '',
|
||||
judgmentStyle: '',
|
||||
sort: 0,
|
||||
enabled: true,
|
||||
})
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await get<{ success?: boolean; data?: Mentor[] }>('/api/db/mentors')
|
||||
if (data?.success && data.data) setMentors(data.data)
|
||||
} catch (e) {
|
||||
console.error('Load mentors error:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
const resetForm = () => {
|
||||
setForm({
|
||||
name: '',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
tags: '',
|
||||
priceSingle: '',
|
||||
priceHalfYear: '',
|
||||
priceYear: '',
|
||||
quote: '',
|
||||
whyFind: '',
|
||||
offering: '',
|
||||
judgmentStyle: '',
|
||||
sort: mentors.length > 0 ? Math.max(...mentors.map((m) => m.sort)) + 1 : 0,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditing(null)
|
||||
resetForm()
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleEdit = (m: Mentor) => {
|
||||
setEditing(m)
|
||||
setForm({
|
||||
name: m.name,
|
||||
avatar: m.avatar || '',
|
||||
intro: m.intro || '',
|
||||
tags: m.tags || '',
|
||||
priceSingle: m.priceSingle != null ? String(m.priceSingle) : '',
|
||||
priceHalfYear: m.priceHalfYear != null ? String(m.priceHalfYear) : '',
|
||||
priceYear: m.priceYear != null ? String(m.priceYear) : '',
|
||||
quote: m.quote || '',
|
||||
whyFind: m.whyFind || '',
|
||||
offering: m.offering || '',
|
||||
judgmentStyle: m.judgmentStyle || '',
|
||||
sort: m.sort,
|
||||
enabled: m.enabled ?? true,
|
||||
})
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name.trim()) {
|
||||
alert('导师姓名不能为空')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const num = (s: string) => (s === '' ? undefined : parseFloat(s))
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
avatar: form.avatar.trim() || undefined,
|
||||
intro: form.intro.trim() || undefined,
|
||||
tags: form.tags.trim() || undefined,
|
||||
priceSingle: num(form.priceSingle),
|
||||
priceHalfYear: num(form.priceHalfYear),
|
||||
priceYear: num(form.priceYear),
|
||||
quote: form.quote.trim() || undefined,
|
||||
whyFind: form.whyFind.trim() || undefined,
|
||||
offering: form.offering.trim() || undefined,
|
||||
judgmentStyle: form.judgmentStyle.trim() || undefined,
|
||||
sort: form.sort,
|
||||
enabled: form.enabled,
|
||||
}
|
||||
if (editing) {
|
||||
const data = await put<{ success?: boolean; error?: string }>('/api/db/mentors', {
|
||||
id: editing.id,
|
||||
...payload,
|
||||
})
|
||||
if (data?.success) {
|
||||
setShowModal(false)
|
||||
load()
|
||||
} else {
|
||||
alert('更新失败: ' + (data as { error?: string })?.error)
|
||||
}
|
||||
} else {
|
||||
const data = await post<{ success?: boolean; error?: string }>('/api/db/mentors', payload)
|
||||
if (data?.success) {
|
||||
setShowModal(false)
|
||||
load()
|
||||
} else {
|
||||
alert('新增失败: ' + (data as { error?: string })?.error)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Save error:', e)
|
||||
alert('保存失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('确定删除该导师?')) return
|
||||
try {
|
||||
const data = await del<{ success?: boolean; error?: string }>(`/api/db/mentors?id=${id}`)
|
||||
if (data?.success) load()
|
||||
else alert('删除失败: ' + (data as { error?: string })?.error)
|
||||
} catch (e) {
|
||||
console.error('Delete error:', e)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const fmt = (v?: number) => (v != null ? `¥${v}` : '-')
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-[#38bdac]" />
|
||||
导师管理
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
stitch_soul 导师列表,支持每个导师独立配置单次/半年/年度价格
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleAdd} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新增导师
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-gray-400">加载中...</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">ID</TableHead>
|
||||
<TableHead className="text-gray-400">姓名</TableHead>
|
||||
<TableHead className="text-gray-400">简介</TableHead>
|
||||
<TableHead className="text-gray-400">单次</TableHead>
|
||||
<TableHead className="text-gray-400">半年</TableHead>
|
||||
<TableHead className="text-gray-400">年度</TableHead>
|
||||
<TableHead className="text-gray-400">排序</TableHead>
|
||||
<TableHead className="text-right text-gray-400">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mentors.map((m) => (
|
||||
<TableRow key={m.id} className="border-gray-700/50">
|
||||
<TableCell className="text-gray-300">{m.id}</TableCell>
|
||||
<TableCell className="text-white">{m.name}</TableCell>
|
||||
<TableCell className="text-gray-400 max-w-[200px] truncate">{m.intro || '-'}</TableCell>
|
||||
<TableCell className="text-gray-400">{fmt(m.priceSingle)}</TableCell>
|
||||
<TableCell className="text-gray-400">{fmt(m.priceHalfYear)}</TableCell>
|
||||
<TableCell className="text-gray-400">{fmt(m.priceYear)}</TableCell>
|
||||
<TableCell className="text-gray-400">{m.sort}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(m)}
|
||||
className="text-gray-400 hover:text-[#38bdac]"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(m.id)}
|
||||
className="text-gray-400 hover:text-red-400"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{mentors.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-gray-500">
|
||||
暂无导师,点击「新增导师」添加
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{editing ? '编辑导师' : '新增导师'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">姓名 *</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="如:卡若"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">排序</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={form.sort}
|
||||
onChange={(e) => setForm((f) => ({ ...f, sort: parseInt(e.target.value, 10) || 0 }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">头像 URL</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="https://..."
|
||||
value={form.avatar}
|
||||
onChange={(e) => setForm((f) => ({ ...f, avatar: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">简介</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="如:结构判断型咨询 · Decision > Execution"
|
||||
value={form.intro}
|
||||
onChange={(e) => setForm((f) => ({ ...f, intro: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">技能标签(逗号分隔)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="如:项目结构判断、风险止损、人×项目匹配"
|
||||
value={form.tags}
|
||||
onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-700 pt-4">
|
||||
<Label className="text-gray-300 block mb-2">价格配置(每个导师独立)</Label>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-500 text-xs">单次咨询 ¥</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="980"
|
||||
value={form.priceSingle}
|
||||
onChange={(e) => setForm((f) => ({ ...f, priceSingle: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-500 text-xs">半年咨询 ¥</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="19800"
|
||||
value={form.priceHalfYear}
|
||||
onChange={(e) => setForm((f) => ({ ...f, priceHalfYear: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-500 text-xs">年度咨询 ¥</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="29800"
|
||||
value={form.priceYear}
|
||||
onChange={(e) => setForm((f) => ({ ...f, priceYear: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">引言</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="如:大多数人失败,不是因为不努力..."
|
||||
value={form.quote}
|
||||
onChange={(e) => setForm((f) => ({ ...f, quote: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">为什么找(文本)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder=""
|
||||
value={form.whyFind}
|
||||
onChange={(e) => setForm((f) => ({ ...f, whyFind: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">提供什么(文本)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder=""
|
||||
value={form.offering}
|
||||
onChange={(e) => setForm((f) => ({ ...f, offering: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">判断风格(逗号分隔)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="如:冷静、克制、偏风险视角"
|
||||
value={form.judgmentStyle}
|
||||
onChange={(e) => setForm((f) => ({ ...f, judgmentStyle: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
checked={form.enabled}
|
||||
onChange={(e) => setForm((f) => ({ ...f, enabled: e.target.checked }))}
|
||||
className="rounded border-gray-600 bg-[#0a1628]"
|
||||
/>
|
||||
<Label htmlFor="enabled" className="text-gray-300 cursor-pointer">上架(小程序可见)</Label>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowModal(false)}
|
||||
className="border-gray-600 text-gray-300"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,12 @@ func Init(dsn string) error {
|
||||
if err := db.AutoMigrate(&model.Order{}); err != nil {
|
||||
log.Printf("database: orders migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Mentor{}); err != nil {
|
||||
log.Printf("database: mentors migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.MentorConsultation{}); err != nil {
|
||||
log.Printf("database: mentor_consultations migrate warning: %v", err)
|
||||
}
|
||||
log.Println("database: connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// excludeParts 排除序言、尾声、附录(不参与精选推荐/热门排序)
|
||||
var excludeParts = []string{"序言", "尾声", "附录"}
|
||||
|
||||
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
|
||||
func BookAllChapters(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
@@ -174,13 +177,96 @@ func BookChapters(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
|
||||
// BookHot GET /api/book/hot
|
||||
// bookHotChaptersSorted 按精选推荐算法排序:阅读量优先,同量按更新时间;排除序言/尾声/附录
|
||||
func bookHotChaptersSorted(db *gorm.DB, limit int) []model.Chapter {
|
||||
q := db.Model(&model.Chapter{})
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
}
|
||||
var all []model.Chapter
|
||||
if err := q.Order("sort_order ASC, id ASC").Find(&all).Error; err != nil || len(all) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 从 reading_progress 统计阅读量
|
||||
ids := make([]string, 0, len(all))
|
||||
for _, c := range all {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
var counts []struct {
|
||||
SectionID string `gorm:"column:section_id"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
}
|
||||
db.Table("reading_progress").Select("section_id, COUNT(*) as cnt").
|
||||
Where("section_id IN ?", ids).Group("section_id").Scan(&counts)
|
||||
countMap := make(map[string]int64)
|
||||
for _, r := range counts {
|
||||
countMap[r.SectionID] = r.Cnt
|
||||
}
|
||||
// 按阅读量降序、同量按 updated_at 降序
|
||||
type withSort struct {
|
||||
ch model.Chapter
|
||||
cnt int64
|
||||
}
|
||||
withCnt := make([]withSort, 0, len(all))
|
||||
for _, c := range all {
|
||||
withCnt = append(withCnt, withSort{ch: c, cnt: countMap[c.ID]})
|
||||
}
|
||||
for i := 0; i < len(withCnt)-1; i++ {
|
||||
for j := i + 1; j < len(withCnt); j++ {
|
||||
if withCnt[j].cnt > withCnt[i].cnt ||
|
||||
(withCnt[j].cnt == withCnt[i].cnt && withCnt[j].ch.UpdatedAt.After(withCnt[i].ch.UpdatedAt)) {
|
||||
withCnt[i], withCnt[j] = withCnt[j], withCnt[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]model.Chapter, 0, limit)
|
||||
for i := 0; i < limit && i < len(withCnt); i++ {
|
||||
out = append(out, withCnt[i].ch)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BookHot GET /api/book/hot 热门章节(按阅读量排序,排除序言/尾声/附录)
|
||||
func BookHot(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
database.DB().Order("sort_order ASC, id ASC").Limit(10).Find(&list)
|
||||
list := bookHotChaptersSorted(database.DB(), 10)
|
||||
if len(list) == 0 {
|
||||
// 兜底:按 sort_order 取前 10,同样排除序言/尾声/附录
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
}
|
||||
q.Order("sort_order ASC, id ASC").Limit(10).Find(&list)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// BookRecommended GET /api/book/recommended 精选推荐(首页「为你推荐」前 3 章,带 热门/推荐/精选 标签)
|
||||
func BookRecommended(c *gin.Context) {
|
||||
list := bookHotChaptersSorted(database.DB(), 3)
|
||||
if len(list) == 0 {
|
||||
// 兜底:按 updated_at 取前 3,同样排除序言/尾声/附录
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
}
|
||||
q.Order("updated_at DESC, id ASC").Limit(3).Find(&list)
|
||||
}
|
||||
tags := []string{"热门", "推荐", "精选"}
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for i, ch := range list {
|
||||
tag := "精选"
|
||||
if i < len(tags) {
|
||||
tag = tags[i]
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": ch.ID, "mid": ch.MID, "sectionTitle": ch.SectionTitle, "partTitle": ch.PartTitle,
|
||||
"chapterTitle": ch.ChapterTitle, "tag": tag,
|
||||
"isFree": ch.IsFree, "price": ch.Price, "isNew": ch.IsNew,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
|
||||
}
|
||||
|
||||
// BookLatestChapters GET /api/book/latest-chapters
|
||||
func BookLatestChapters(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
|
||||
@@ -16,6 +16,7 @@ type sectionListItem struct {
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
IsNew *bool `json:"isNew,omitempty"` // stitch_soul:标记最新新增
|
||||
PartID string `json:"partId"`
|
||||
PartTitle string `json:"partTitle"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
@@ -48,6 +49,7 @@ func DBBookAction(c *gin.Context) {
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
IsNew: r.IsNew,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
@@ -81,6 +83,7 @@ func DBBookAction(c *gin.Context) {
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"isNew": ch.IsNew,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
@@ -101,7 +104,7 @@ func DBBookAction(c *gin.Context) {
|
||||
price = *r.Price
|
||||
}
|
||||
sections = append(sections, sectionListItem{
|
||||
ID: r.ID, Title: r.SectionTitle, Price: price, IsFree: r.IsFree,
|
||||
ID: r.ID, Title: r.SectionTitle, Price: price, IsFree: r.IsFree, IsNew: r.IsNew,
|
||||
PartID: r.PartID, PartTitle: r.PartTitle, ChapterID: r.ChapterID, ChapterTitle: r.ChapterTitle,
|
||||
})
|
||||
}
|
||||
@@ -181,6 +184,7 @@ func DBBookAction(c *gin.Context) {
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
IsNew *bool `json:"isNew"` // stitch_soul:标记最新新增
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||
@@ -202,6 +206,9 @@ func DBBookAction(c *gin.Context) {
|
||||
"price": price,
|
||||
"is_free": isFree,
|
||||
}
|
||||
if body.IsNew != nil {
|
||||
updates["is_new"] = *body.IsNew
|
||||
}
|
||||
err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
|
||||
@@ -207,24 +207,50 @@ func UserProfileGet(c *gin.Context) {
|
||||
}
|
||||
profileComplete := (user.Phone != nil && *user.Phone != "") || (user.WechatID != nil && *user.WechatID != "")
|
||||
hasAvatar := user.Avatar != nil && *user.Avatar != "" && len(*user.Avatar) > 0
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||||
resp := gin.H{
|
||||
"id": user.ID, "openId": user.OpenID, "nickname": user.Nickname, "avatar": user.Avatar,
|
||||
"phone": user.Phone, "wechatId": user.WechatID, "referralCode": user.ReferralCode,
|
||||
"hasFullBook": user.HasFullBook, "earnings": user.Earnings, "pendingEarnings": user.PendingEarnings,
|
||||
"referralCount": user.ReferralCount, "profileComplete": profileComplete, "hasAvatar": hasAvatar,
|
||||
"createdAt": user.CreatedAt,
|
||||
}})
|
||||
}
|
||||
// P3 资料扩展
|
||||
if user.Mbti != nil { resp["mbti"] = user.Mbti }
|
||||
if user.Region != nil { resp["region"] = user.Region }
|
||||
if user.Industry != nil { resp["industry"] = user.Industry }
|
||||
if user.Position != nil { resp["position"] = user.Position }
|
||||
if user.BusinessScale != nil { resp["businessScale"] = user.BusinessScale }
|
||||
if user.Skills != nil { resp["skills"] = user.Skills }
|
||||
if user.StoryBestMonth != nil { resp["storyBestMonth"] = user.StoryBestMonth }
|
||||
if user.StoryAchievement != nil { resp["storyAchievement"] = user.StoryAchievement }
|
||||
if user.StoryTurning != nil { resp["storyTurning"] = user.StoryTurning }
|
||||
if user.HelpOffer != nil { resp["helpOffer"] = user.HelpOffer }
|
||||
if user.HelpNeed != nil { resp["helpNeed"] = user.HelpNeed }
|
||||
if user.ProjectIntro != nil { resp["projectIntro"] = user.ProjectIntro }
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
|
||||
}
|
||||
|
||||
// UserProfilePost POST /api/user/profile 更新用户资料
|
||||
func UserProfilePost(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
OpenID string `json:"openId"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Phone *string `json:"phone"`
|
||||
WechatID *string `json:"wechatId"`
|
||||
UserID string `json:"userId"`
|
||||
OpenID string `json:"openId"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Phone *string `json:"phone"`
|
||||
WechatID *string `json:"wechatId"`
|
||||
Mbti *string `json:"mbti"`
|
||||
Region *string `json:"region"`
|
||||
Industry *string `json:"industry"`
|
||||
Position *string `json:"position"`
|
||||
BusinessScale *string `json:"businessScale"`
|
||||
Skills *string `json:"skills"`
|
||||
StoryBestMonth *string `json:"storyBestMonth"`
|
||||
StoryAchievement *string `json:"storyAchievement"`
|
||||
StoryTurning *string `json:"storyTurning"`
|
||||
HelpOffer *string `json:"helpOffer"`
|
||||
HelpNeed *string `json:"helpNeed"`
|
||||
ProjectIntro *string `json:"projectIntro"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请提供userId或openId"})
|
||||
@@ -264,6 +290,18 @@ func UserProfilePost(c *gin.Context) {
|
||||
if body.WechatID != nil {
|
||||
updates["wechat_id"] = *body.WechatID
|
||||
}
|
||||
if body.Mbti != nil { updates["mbti"] = *body.Mbti }
|
||||
if body.Region != nil { updates["region"] = *body.Region }
|
||||
if body.Industry != nil { updates["industry"] = *body.Industry }
|
||||
if body.Position != nil { updates["position"] = *body.Position }
|
||||
if body.BusinessScale != nil { updates["business_scale"] = *body.BusinessScale }
|
||||
if body.Skills != nil { updates["skills"] = *body.Skills }
|
||||
if body.StoryBestMonth != nil { updates["story_best_month"] = *body.StoryBestMonth }
|
||||
if body.StoryAchievement != nil { updates["story_achievement"] = *body.StoryAchievement }
|
||||
if body.StoryTurning != nil { updates["story_turning"] = *body.StoryTurning }
|
||||
if body.HelpOffer != nil { updates["help_offer"] = *body.HelpOffer }
|
||||
if body.HelpNeed != nil { updates["help_need"] = *body.HelpNeed }
|
||||
if body.ProjectIntro != nil { updates["project_intro"] = *body.ProjectIntro }
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "没有需要更新的字段"})
|
||||
return
|
||||
|
||||
@@ -17,6 +17,7 @@ type Chapter struct {
|
||||
Price *float64 `gorm:"column:price;type:decimal(10,2)" json:"price,omitempty"`
|
||||
SortOrder *int `gorm:"column:sort_order" json:"sortOrder,omitempty"`
|
||||
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||
IsNew *bool `gorm:"column:is_new" json:"isNew,omitempty"` // stitch_soul:目录/首页「最新新增」标记
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,19 @@ type User struct {
|
||||
Avatar *string `gorm:"column:avatar;size:500" json:"avatar,omitempty"`
|
||||
Phone *string `gorm:"column:phone;size:20" json:"phone,omitempty"`
|
||||
WechatID *string `gorm:"column:wechat_id;size:100" json:"wechatId,omitempty"`
|
||||
// P3 资料扩展(stitch_soul)
|
||||
Mbti *string `gorm:"column:mbti;size:16" json:"mbti,omitempty"`
|
||||
Region *string `gorm:"column:region;size:100" json:"region,omitempty"`
|
||||
Industry *string `gorm:"column:industry;size:100" json:"industry,omitempty"`
|
||||
Position *string `gorm:"column:position;size:100" json:"position,omitempty"`
|
||||
BusinessScale *string `gorm:"column:business_scale;size:100" json:"businessScale,omitempty"`
|
||||
Skills *string `gorm:"column:skills;size:500" json:"skills,omitempty"`
|
||||
StoryBestMonth *string `gorm:"column:story_best_month;type:text" json:"storyBestMonth,omitempty"`
|
||||
StoryAchievement *string `gorm:"column:story_achievement;type:text" json:"storyAchievement,omitempty"`
|
||||
StoryTurning *string `gorm:"column:story_turning;type:text" json:"storyTurning,omitempty"`
|
||||
HelpOffer *string `gorm:"column:help_offer;size:500" json:"helpOffer,omitempty"`
|
||||
HelpNeed *string `gorm:"column:help_need;size:500" json:"helpNeed,omitempty"`
|
||||
ProjectIntro *string `gorm:"column:project_intro;type:text" json:"projectIntro,omitempty"`
|
||||
ReferralCode *string `gorm:"column:referral_code;size:20" json:"referralCode,omitempty"`
|
||||
HasFullBook *bool `gorm:"column:has_full_book" json:"hasFullBook,omitempty"`
|
||||
PurchasedSections *string `gorm:"column:purchased_sections;type:json" json:"-"` // 内部字段,实际数据从 orders 表查
|
||||
|
||||
@@ -82,6 +82,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
api.PUT("/book/chapters", handler.BookChapters)
|
||||
api.DELETE("/book/chapters", handler.BookChapters)
|
||||
api.GET("/book/hot", handler.BookHot)
|
||||
api.GET("/book/recommended", handler.BookRecommended)
|
||||
api.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||||
api.GET("/book/search", handler.BookSearch)
|
||||
api.GET("/book/stats", handler.BookStats)
|
||||
@@ -136,6 +137,11 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||||
db.DELETE("/vip-roles", handler.DBVipRolesAction)
|
||||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||||
db.GET("/mentors", handler.DBMentorsList)
|
||||
db.POST("/mentors", handler.DBMentorsAction)
|
||||
db.PUT("/mentors", handler.DBMentorsAction)
|
||||
db.DELETE("/mentors", handler.DBMentorsAction)
|
||||
db.GET("/mentor-consultations", handler.DBMentorConsultationsList)
|
||||
}
|
||||
|
||||
// ----- 分销 -----
|
||||
@@ -227,6 +233,8 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
miniprogram.GET("/book/chapter/:id", handler.BookChapterByID)
|
||||
miniprogram.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID)
|
||||
miniprogram.GET("/book/hot", handler.BookHot)
|
||||
miniprogram.GET("/book/recommended", handler.BookRecommended)
|
||||
miniprogram.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||||
miniprogram.GET("/book/search", handler.BookSearch)
|
||||
miniprogram.GET("/book/stats", handler.BookStats)
|
||||
miniprogram.POST("/referral/visit", handler.ReferralVisit)
|
||||
@@ -264,6 +272,10 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
// 用户列表/单个(首页超级个体、会员详情回退)
|
||||
miniprogram.GET("/users", handler.MiniprogramUsers)
|
||||
miniprogram.GET("/orders", handler.MiniprogramOrders)
|
||||
// 导师(stitch_soul)
|
||||
miniprogram.GET("/mentors", handler.MiniprogramMentorsList)
|
||||
miniprogram.GET("/mentors/:id", handler.MiniprogramMentorsDetail)
|
||||
miniprogram.POST("/mentors/:id/book", handler.MiniprogramMentorsBook)
|
||||
}
|
||||
|
||||
// ----- 提现 -----
|
||||
|
||||
Reference in New Issue
Block a user