Merge branch 'devlop' into yongxu-dev
# Conflicts: # .cursor/skills/miniprogram-dev/SKILL.md resolved by devlop version # miniprogram/pages/index/index.js resolved by devlop version # miniprogram/pages/index/index.wxml resolved by devlop version # miniprogram/pages/my/my.wxml resolved by devlop version # miniprogram/pages/read/read.wxml resolved by devlop version # miniprogram/pages/read/read.wxss resolved by devlop version # soul-admin/dist/index.html resolved by devlop version # soul-admin/src/pages/content/ChapterTree.tsx resolved by devlop version # soul-admin/src/pages/content/ContentPage.tsx resolved by devlop version # soul-admin/src/pages/distribution/DistributionPage.tsx resolved by devlop version # soul-api/internal/handler/book.go resolved by devlop version # soul-api/internal/handler/ckb.go resolved by devlop version # soul-api/internal/handler/db_book.go resolved by devlop version # soul-api/internal/handler/db_ckb_leads.go resolved by devlop version # soul-api/internal/handler/db_person.go resolved by devlop version # soul-api/internal/model/chapter.go resolved by devlop version
This commit is contained in:
@@ -1,127 +1,21 @@
|
||||
# Soul 创业派对 - .cursor 配置说明
|
||||
# Soul 创业派对 · `.cursor` 速览
|
||||
|
||||
本目录按 **cursor标准模板** 重构,rules、skills、agent 为**开发团队**服务,用于约束开发、防止互窜、经验升级。
|
||||
## 路径约定
|
||||
|
||||
---
|
||||
- 所有 Skill、会议纪要、agent 经验路径均以 **本 Git 仓库根** 为基准(与 `miniprogram/`、`soul-api/` 同级)。
|
||||
- Rules 中「必须 Read」的路径形如 `.cursor/skills/{name}/SKILL.md`。
|
||||
- Python 脚本统一可用 `config/paths.py` 的 `ROOT`、`SKILLS`、`AGENT`、`MEETING`。
|
||||
|
||||
## 目录结构
|
||||
## 入口优先级
|
||||
|
||||
```
|
||||
.cursor/
|
||||
├── README.md # 本说明(入口)
|
||||
├── config/ # 配置(paths.py、workspace.txt、model_switch.json)
|
||||
├── rules/ # 规则(boundary、checklist、助理、会议、老板分身-索引)
|
||||
├── skills/ # Skills(按角色分配)
|
||||
├── agent/ # 智能体(标准开发团队结构)
|
||||
│ ├── 老板分身/ # 最高权限,协调所有角色
|
||||
│ ├── 开发助理/ # 规则进化、通用经验、项目索引、bat 入口
|
||||
│ │ ├── evolution/ # 通用经验池
|
||||
│ │ ├── script/ # 一键-列出经验池.bat、一键-添加经验.bat 等
|
||||
│ │ └── 项目索引/ # 各角色开发进度(小程序.md、管理端.md 等)
|
||||
│ ├── 小程序开发工程师/
|
||||
│ ├── 管理端开发工程师/
|
||||
│ ├── 后端工程师/
|
||||
│ ├── 产品经理/
|
||||
│ ├── 软件测试/
|
||||
│ └── 团队/ # 跨角色共享经验
|
||||
├── scripts/ # 共享脚本(evolution.py、经验模板.md、db-exec)
|
||||
├── docs/ # 文档(职责定义、边界、分析)
|
||||
├── process/ # 工作流
|
||||
├── meeting/ # 会议纪要(橙子生成)
|
||||
└── archive/ # 历史归档
|
||||
```
|
||||
1. **三端开发**:`rules/soul-project-boundary.mdc` + `skills/*-dev` / `change-checklist`。
|
||||
2. **派对 AI**:若存在仓库根目录 `派对AI/`,可补充读其 `BOOTSTRAP.md`;与 `.cursor` 冲突时 **以 `.cursor` 三端约定为准**(见 `rules/party-ai-dev.mdc`)。
|
||||
|
||||
---
|
||||
## 噪声与体积
|
||||
|
||||
## 开发团队
|
||||
- `meeting/`、`agent/`:历史纪要/evolution 会增多,属正常;需要时可按月归档到子目录或压缩备份。
|
||||
- `scripts/db-exec/node_modules/`:已在 `.cursorignore` 与 `.gitignore` 中排除,首次使用在 `db-exec` 下执行 `npm install`。
|
||||
|
||||
| 角色 | 负责 | 主 Skill | Agent 目录 |
|
||||
|------|------|----------|------------|
|
||||
| 小程序开发工程师 | miniprogram/ | SKILL-小程序开发.md | agent/小程序开发工程师/ |
|
||||
| 管理端开发工程师 | soul-admin/ | SKILL-管理端开发.md | agent/管理端开发工程师/ |
|
||||
| 后端开发 | soul-api/ | SKILL-API开发.md | agent/后端工程师/ |
|
||||
| 产品经理 | 开发文档/1、需求/、临时需求池/ | SKILL-产品经理.md | agent/产品经理/ |
|
||||
| 测试人员 | miniprogram、soul-admin、soul-api | SKILL-测试.md | agent/软件测试/ |
|
||||
| 助理橙子 | 讨论后记录、经验升级 | SKILL-助理橙子-文档同步.md | agent/开发助理/ |
|
||||
## 文档
|
||||
|
||||
**经验**:每角色 `agent/{角色}/evolution/`,团队共享 `agent/团队/evolution/`。用户说「吸收经验」「升级 skills」→ 入库 + 升级 Skill;说「保存开发进度」「任务完成」→ 更新 `agent/开发助理/项目索引/{角色}.md`。
|
||||
|
||||
---
|
||||
|
||||
## 快速决策(必须 Read = 使用 Read 工具读取完整内容)
|
||||
|
||||
| 编辑/场景 | 必须 Read 的 Skill | 自动加载的 Rule |
|
||||
|-----------|-------------------|----------------|
|
||||
| miniprogram/ | `SKILL-小程序开发.md` | soul-miniprogram-boundary |
|
||||
| soul-admin/ | `SKILL-管理端开发.md` | soul-admin-boundary |
|
||||
| soul-api/ | `SKILL-API开发.md` | soul-api |
|
||||
| 开发文档/1、需求/、临时需求池/ | `SKILL-产品经理.md` | product-manager |
|
||||
| 测试、测试用例、回归测试、功能测试、QA | `SKILL-测试.md` | - |
|
||||
| 小橙、橙子、讨论完毕、记录、同步文档 | `SKILL-助理橙子-文档同步.md` | assistant-xiaofeng |
|
||||
| 吸收经验、升级 skills、保存开发进度、任务完成、搞定了 | `SKILL-助理橙子-文档同步.md` | assistant-xiaofeng |
|
||||
| 跨端功能开发 | `SKILL-角色流程控制.md` | - |
|
||||
| 变更完成 | `SKILL-变更关联检查.md` | soul-change-checklist |
|
||||
| 开个会、团队会议、需求评审、方案讨论 | `SKILL-团队会议.md` | soul-meeting |
|
||||
| 会议结束、散会 | `SKILL-助理橙子-文档同步.md`(会议收尾) | soul-meeting |
|
||||
|
||||
---
|
||||
|
||||
## Rules 一览
|
||||
|
||||
| 规则 | 生效范围 | 用途 |
|
||||
|------|----------|------|
|
||||
| soul-project-boundary | `**`(alwaysApply) | 项目组成、核心原则、会话自检 |
|
||||
| 老板分身-索引 | `**`(alwaysApply) | 经验自动收集、Soul 角色推断、编码习惯 |
|
||||
| soul-change-checklist | miniprogram、soul-admin、soul-api | 变更后必过 |
|
||||
| assistant-xiaofeng | 触发词 | 小橙触发器 → SKILL-助理橙子-文档同步 |
|
||||
| soul-miniprogram-boundary | miniprogram/** | 只调 /api/miniprogram/* |
|
||||
| soul-admin-boundary | soul-admin/** | 只调 /api/admin/*、/api/db/* |
|
||||
| soul-api | soul-api/** | 路由边界 + 编码规范(合并版) |
|
||||
| product-manager | 开发文档/1、需求/、临时需求池/ | 产品经理 glob 触发 |
|
||||
| soul-meeting | 触发词 | 开个会、团队会议、需求评审 → SKILL-团队会议 |
|
||||
|
||||
---
|
||||
|
||||
## Skills 一览
|
||||
|
||||
### 角色主 Skill
|
||||
|
||||
| 角色 | 主 Skill | 辅助 Skill |
|
||||
|------|----------|------------|
|
||||
| 小程序开发工程师 | SKILL-小程序开发 | 三端架构 → API开发 → 变更关联检查 |
|
||||
| 管理端开发工程师 | SKILL-管理端开发 | 三端架构 → API开发 → 变更关联检查 |
|
||||
| 后端开发 | SKILL-API开发 | soul-api 规范 → 三端架构 → 变更关联检查 → MySQL直接操作 |
|
||||
| 产品经理 | SKILL-产品经理 | 需求汇总、运营与变更 |
|
||||
| 测试人员 | SKILL-测试 | 变更关联检查、小程序/管理端/API 规范 |
|
||||
| 助理橙子 | SKILL-助理橙子-文档同步 | - |
|
||||
|
||||
### 场景 Skill
|
||||
|
||||
| 场景 | Skill |
|
||||
|------|-------|
|
||||
| 跨端协同 | SKILL-角色流程控制 |
|
||||
| 变更检查 | SKILL-变更关联检查、soul-change-checklist |
|
||||
| 文档同步、经验升级 | SKILL-助理橙子-文档同步 |
|
||||
| **多角色会议** | **SKILL-团队会议** |
|
||||
| next-project | SKILL-next-project仅预览 |
|
||||
| 项目拆解 | SKILL-Next全栈拆解为前后端分离与小程序 |
|
||||
|
||||
---
|
||||
|
||||
## 文档与脚本
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [开发团队职责定义](./docs/开发团队职责定义.md) | 六角色职责、Skills 分配 |
|
||||
| [三角色边界定义](./docs/三角色边界定义.md) | 开发三角色源码与业务边界 |
|
||||
| [config/目录地图](./config/目录地图.md) | paths.py 路径别名 |
|
||||
| [meeting/](./meeting/) | 会议纪要(橙子生成) |
|
||||
| [经验清单](./agent/开发助理/经验清单.md) | 跨角色经验索引 |
|
||||
| evolution 脚本 | `python .cursor/scripts/evolution.py list` 列出经验池;`add --stdin` 添加经验 |
|
||||
| 一键 bat | `agent/开发助理/script/一键-列出经验池.bat` 等 |
|
||||
|
||||
---
|
||||
|
||||
## 会话启动自检
|
||||
|
||||
新 Cursor 打开本项目时,优先执行 soul-project-boundary 中的「会话启动自检」:仅沿用本项目的 rules、skills、开发风格与配置,排除无关规则。
|
||||
- 架构与迭代说明:`docs/cursor规则与架构分析及优化建议.md`
|
||||
|
||||
@@ -13,5 +13,3 @@
|
||||
| 2026-03-14 | 我的页设置入口隐藏;资料修改引导场景梳理(登录后、@某人、找伙伴、链接卡若) | [2026-03-14.md](./2026-03-14.md) |
|
||||
| 2026-03-16 | 编辑资料页分享名片:转发/朋友圈特殊处理,Canvas 绘制封面,标题「昵称+为您分享名片」 | [2026-03-16.md](./2026-03-16.md) |
|
||||
| 2026-03-17 | 代付美团式:读页→代付页→分享;详情页双态(发起人/好友);目录 loading、最新新增 5 条折叠 | [2026-03-17.md](./2026-03-17.md) |
|
||||
| 2026-03-19 | 原生按钮覆盖定位:chooseAvatar 等用绝对定位 overlay 覆盖,禁止 button 包裹,避免原生样式影响 | [2026-03-19-原生按钮覆盖定位.md](./2026-03-19-原生按钮覆盖定位.md) |
|
||||
| 2026-03-20 | 手机号一键登录与公用组件:getPhoneNumber 耦合 agreePrivacyAuthorization;login-modal 组件;登录后手机号同步 | [2026-03-20-手机号登录与公用组件.md](./2026-03-20-手机号登录与公用组件.md) |
|
||||
|
||||
@@ -53,10 +53,6 @@
|
||||
| 2026-03-17 | 后端、团队 | 架构/最佳实践 | api-dev SKILL | Redis 缓存:parts/hot/recommended/stats/config/章节 content;容灾回退 DB;OSS 上传;/health 返回 database/redis 状态 |
|
||||
| 2026-03-18 | 小程序、团队 | 业务规则/最佳实践 | - | 分享链路兼容好友/朋友圈 singlePage:单页模式能力降级(不支付/不自动领取),引导点击底部“前往小程序”进入完整版 |
|
||||
| 2026-03-18 | 产品、后端、管理端、测试 | 文档归档/需求口径 | - | 文档归档整理:以《以界面定需求》为基准,各角色重整“功能需求+验收口径+风险点”并写入各自经验库;补齐《项目落地推进表》 |
|
||||
| 2026-03-19 | 小程序 | 最佳实践 | miniprogram-dev SKILL §11 | 原生按钮覆盖定位:chooseAvatar 等用绝对定位 overlay 覆盖,禁止 button 包裹,避免原生样式影响(灰色矩形等) |
|
||||
| 2026-03-20 | 安全工程师 | 触发词约定 | security-server-ops、soul-project-boundary | 「帮我部署api到线上」→ 直接执行 soul-api/master.py |
|
||||
| 2026-03-20 | 安全工程师 | 触发词约定 | security-server-ops、soul-project-boundary | 「管理端帮我部署到xx环境」→ 语义化解析:正式→master.py,测试→deploy.py |
|
||||
| 2026-03-20 | 小程序 | 最佳实践 | miniprogram-dev SKILL §8 | 手机号登录:getPhoneNumber 需耦合 agreePrivacyAuthorization;onNeedPrivacyAuthorization 支持页面;登录弹窗公用组件 login-modal |
|
||||
|
||||
---
|
||||
|
||||
@@ -67,4 +63,4 @@
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-20
|
||||
**最后更新**:2026-03-18
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 项目总结
|
||||
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/`(按日期命名,以最新为主;见 `1、需求/索引.md`),项目推进表在 `开发文档/10、项目管理/项目落地推进表.md`,临时需求/分析在 `临时需求池/`。
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/需求汇总.md`,项目推进表在 `开发文档/10、项目管理/项目落地推进表.md`,临时需求/分析在 `临时需求池/`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,11 +31,9 @@ Soul 创业派对全项目架构与约定:路由隔离(miniprogram/admin/db
|
||||
| 2026-03-17 | 性能优化与 Redis 缓存方案落地:Redis 容灾回退 DB、OSS 上传容灾;/health 返回 database/redis 状态 | 已完成 |
|
||||
| 2026-03-18 | 吸收经验:分享进入链路需兼容朋友圈 singlePage;单页模式不执行支付/自动领取等强动作并引导“前往小程序” | 已完成 |
|
||||
| 2026-03-18 | 会议:超级个体开通后自动创建@人统一走 Person;幂等键绑定 userId;默认资料 flags 后端输出 | 已完成 |
|
||||
| 2026-03-20 | 「帮我部署api到线上」→ 安全工程师执行 soul-api/master.py;security-server-ops、soul-project-boundary 触发词升级 | 已完成 |
|
||||
| 2026-03-20 | 「管理端帮我部署到xx环境」→ 语义化解析:正式/线上/生产→master.py,测试/dev→deploy.py;soul-admin 部署脚本索引 | 已完成 |
|
||||
|
||||
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-20
|
||||
**最后更新**:2026-03-18
|
||||
|
||||
@@ -41,11 +41,9 @@
|
||||
| 2026-03-17 | 会议收尾:源码优化 5 项全部完成;开发环境测试通过 | 已完成 |
|
||||
| 2026-03-18 | 吸收经验:分享链路需兼容好友/朋友圈 singlePage;单页模式能力降级并引导“前往小程序”进入完整版 | 已完成 |
|
||||
| 2026-03-18 | 会议:支付超级个体前/开通后资料默认校验,跳转 avatar-nickname 引导页(仅头像+昵称) | 已完成 |
|
||||
| 2026-03-19 | 吸收经验:原生按钮覆盖定位,chooseAvatar 用绝对定位 overlay 覆盖头像,禁止 button 包裹,已升级 SKILL §11 | 已完成 |
|
||||
| 2026-03-20 | 手机号一键登录:getPhoneNumber 耦合 agreePrivacyAuthorization;onNeedPrivacyAuthorization 支持 read/my/gift-pay/index/settings;登录弹窗公用组件 components/login-modal;登录后手机号同步 _syncPhoneFromProfileAfterLogin | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-20
|
||||
**最后更新**:2026-03-18
|
||||
|
||||
44
.cursor/agent/软件测试/evolution/2026-03-15-全站深度测试42问题.md
Normal file
44
.cursor/agent/软件测试/evolution/2026-03-15-全站深度测试42问题.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 2026-03-15 Soul 创业派对全站深度测试
|
||||
|
||||
## 问题背景
|
||||
|
||||
对 Soul 创业派对项目进行首次全站深度测试(只检测不修改),覆盖管理端 20+ 页面、35 个 API 端点、25 个小程序页面、25 张数据库表。
|
||||
|
||||
## 解决过程
|
||||
|
||||
### 测试方法
|
||||
1. 环境检查:确认后端/前端/数据库运行状态
|
||||
2. 管理端浏览器测试:逐页逐按钮截图检查
|
||||
3. API 端点测试:curl 逐个测试 35 个端点,含安全边界测试
|
||||
4. 小程序代码审查:25 个页面 + 8 个工具文件全量代码阅读
|
||||
5. 数据库一致性检查:交叉验证各 API 数据
|
||||
|
||||
### 关键发现(42 个问题)
|
||||
- 严重 11 个:OSS 密钥泄露、登录守卫缺失、小程序模块混用、废弃 API
|
||||
- 高 13 个:硬编码、API 失败伪装成功、分页缺失
|
||||
- 中 12 个:调试日志残留、模拟数据未清理
|
||||
- 低 6 个:版本号未设置等
|
||||
|
||||
## 可提炼规则
|
||||
|
||||
### 安全测试
|
||||
1. **密钥脱敏是硬性规则**:任何返回配置的 API,密钥类字段必须脱敏
|
||||
2. **SPA 路由守卫必查**:直接访问后台路径测试
|
||||
3. **上传接口安全测试**:非图片文件、超大文件、空文件
|
||||
|
||||
### API 测试
|
||||
4. **响应字段名不能假设**:先 print keys() 再解析,不同端点可能用 data/results/orders/records
|
||||
5. **分页必须翻页验证**:测第一页也测 page=2
|
||||
6. **交叉验证**:stats 的总数 vs list API 的实际条数
|
||||
|
||||
### 小程序测试
|
||||
7. **废弃 API 年检制度**:每年核对微信基础库废弃列表
|
||||
8. **模块语法统一检查**:grep -rn "export default" 快速排查
|
||||
9. **死代码扫描**:utils/ 每个文件是否被 pages/ 引用
|
||||
|
||||
### 数据库测试
|
||||
10. **空表不代表无问题**:空表可能是同步逻辑失效
|
||||
|
||||
## 适用角色
|
||||
|
||||
- target_roles: ["软件测试", "团队"]
|
||||
@@ -53,7 +53,6 @@ AGENT_BACKEND = AGENT / "后端工程师"
|
||||
AGENT_PRODUCT = AGENT / "产品经理"
|
||||
AGENT_TEST = AGENT / "软件测试"
|
||||
AGENT_TEAM = AGENT / "团队"
|
||||
AGENT_SECURITY = AGENT / "安全工程师"
|
||||
|
||||
# ========== 常用文件 ==========
|
||||
RULE_MAIN = RULES / "老板分身-索引.mdc"
|
||||
@@ -81,9 +80,6 @@ ROLE_TO_AGENT = {
|
||||
"软件测试": "软件测试",
|
||||
"测试": "软件测试",
|
||||
"测试人员": "软件测试",
|
||||
# 安全
|
||||
"安全工程师": "安全工程师",
|
||||
"安全": "安全工程师",
|
||||
# 通用
|
||||
"团队": "团队",
|
||||
}
|
||||
|
||||
156
.cursor/docs/cursor规则与架构分析及优化建议.md
Normal file
156
.cursor/docs/cursor规则与架构分析及优化建议.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Soul 创业派对 · .cursor 规则与架构分析及优化建议
|
||||
|
||||
> 分析日期:2026-03-19
|
||||
> 范围:`.cursor/` 下 rules、skills、agent、config、meeting、scripts
|
||||
> **2026-03-20**:已批量将 `e:\Gongsi\...` 改为仓库相对路径 `.cursor/...`;`party-ai-dev.mdc` 已补充与 `.cursor` 的优先级;根目录 `.gitignore` 已忽略 `karuo-party/credentials/`;新增 `.cursor/README.md`、`.cursorignore`(db-exec node_modules)。
|
||||
|
||||
---
|
||||
|
||||
## 整体架构图
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 一、整体架构总览
|
||||
|
||||
### 1.1 项目与 .cursor 的关系
|
||||
|
||||
```
|
||||
项目根(一场soul的创业实验-永平)
|
||||
├── miniprogram/ # 微信原生小程序 C 端 → /api/miniprogram/*
|
||||
├── soul-admin/ # React 管理后台(主用)→ /api/admin/*、/api/db/*
|
||||
├── soul-api/ # Go + Gin + GORM 接口服务
|
||||
├── next-project/ # 仅预览,非线上
|
||||
├── new-soul/soul-admin/ # 新版参考,迁移时对照
|
||||
└── .cursor/ # Cursor AI 规则与智能体配置
|
||||
├── rules/ # 全局/场景规则(alwaysApply 或 globs)
|
||||
├── skills/ # 按角色/场景的 Skill(写作、上传、开发、会议等)
|
||||
├── agent/ # 角色经验与项目索引(evolution、项目索引)
|
||||
├── config/ # paths.py、workspace 等
|
||||
├── meeting/ # 会议纪要
|
||||
├── scripts/ # 进化脚本、Gitea 同步、db-exec 等
|
||||
└── docs/ # 本分析等文档
|
||||
```
|
||||
|
||||
### 1.2 规则层(Rules)与技能层(Skills)关系
|
||||
|
||||
| 类型 | 作用 | 典型文件 |
|
||||
|------|------|----------|
|
||||
| **Rules** | 会话自检、项目边界、谁调哪组 API、何时加载哪个 Skill | soul-project-boundary.mdc、老板分身-索引.mdc、soul-meeting.mdc、soul-change-checklist.mdc、party-ai-dev.mdc |
|
||||
| **Skills** | 具体执行规范:怎么写代码、怎么开会、怎么检查变更 | miniprogram-dev、admin-dev、api-dev、team-meeting、change-checklist、assistant-doc-sync |
|
||||
|
||||
- **角色推断**:按「当前编辑目录」或「用户触发词」→ 确定角色 → **必须 Read 对应 Skill 文件**后执行。
|
||||
- **老板分身**:权限最高,可调度所有角色;开会时由乘风按 team-meeting 主持;经验自动收集写各角色 evolution。
|
||||
|
||||
### 1.3 三端与 API 路由(核心原则)
|
||||
|
||||
| 端 | 目录 | 允许调用的 API | 禁止 |
|
||||
|----|------|----------------|------|
|
||||
| 小程序 | miniprogram/ | `/api/miniprogram/*` | admin、db |
|
||||
| 管理端 | soul-admin/ | `/api/admin/*`、`/api/db/*` | miniprogram 混用 |
|
||||
| 后端 | soul-api/ | 按使用方挂 miniprogram / admin / db 分组 | 通用路径混用 |
|
||||
|
||||
---
|
||||
|
||||
## 二、优化与迭代建议
|
||||
|
||||
### 2.1 路径可移植性(高优先级)✅ 已落地
|
||||
|
||||
**原问题**:rules 与部分 skills 中曾写死 **Windows 绝对路径** `e:\Gongsi\Mycontent\.cursor\skills\...`,在 macOS/Linux 或不同机器上会失效。**当前**:已统一为仓库根相对路径 `.cursor/skills/...` 等,详见 `rules/soul-project-boundary.mdc` 的「路径约定」。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- `rules/老板分身-索引.mdc`:team-meeting SKILL 路径
|
||||
- `rules/soul-project-boundary.mdc`:所有「必须 Read 的主 Skill 文件」表格(按编辑目录、按语义触发词、按场景触发词)
|
||||
- `rules/soul-meeting.mdc`:team-meeting、assistant-doc-sync 路径
|
||||
- `rules/soul-change-checklist.mdc`:change-checklist SKILL 路径
|
||||
- `skills/assistant-doc-sync/SKILL.md`:项目索引路径
|
||||
- `skills/mysql-direct/SKILL.md`:`cd e:\Gongsi\Mycontent`
|
||||
|
||||
**建议**:
|
||||
|
||||
1. **统一改为相对项目根的路径**
|
||||
例如:`项目根/.cursor/skills/team-meeting/SKILL.md`,或在规则中明确写:
|
||||
「以当前项目根为基准,Read `.cursor/skills/{skill-name}/SKILL.md`」。
|
||||
2. 若 Cursor 支持「工作区根」变量,可写成占位符(如 `{workspace}/.cursor/skills/...`),在文档中说明各系统下的解析方式。
|
||||
3. **config/paths.py** 已定义 `SKILLS = CURSOR / "skills"`,可在 `.cursor/README.md` 或 rules 中说明:**所有 Skill 路径以 `paths.py` 的 SKILLS 为准,规则中仅写相对 SKILLS 的路径**(如 `skills/team-meeting/SKILL.md`),由 AI 结合当前项目根解析。
|
||||
|
||||
### 2.2 跨平台脚本与入口
|
||||
|
||||
**问题**:老板分身规则里「若无法写文件则输出 JSON,并提示用户双击 `agent/开发助理/script/一键-添加经验.bat`」。`.bat` 仅适用于 Windows,Mac/Linux 用户无法使用。
|
||||
|
||||
**建议**:
|
||||
|
||||
1. 增加 **Shell 版**:`一键-添加经验.sh`,实现相同逻辑(或调用同一份 Python/Node 脚本)。
|
||||
2. 在规则中改为:「提示用户执行 `agent/开发助理/script/一键-添加经验.bat`(Windows)或 `一键-添加经验.sh`(Mac/Linux),或根据环境说明」。
|
||||
|
||||
### 2.3 party-ai-dev.mdc 与 老板分身 的优先级
|
||||
|
||||
**问题**:`party-ai-dev.mdc` 要求「优先使用派对 AI(派对AI/BOOTSTRAP.md、SKILL_REGISTRY.md)」,而老板分身等规则在 `.cursor/rules` 下,若同时生效可能产生「先读卡若还是先读派对」的冲突。
|
||||
|
||||
**建议**:
|
||||
|
||||
1. 在 **party-ai-dev.mdc** 或 **老板分身-索引.mdc** 中明确写清:
|
||||
「当本仓库为 Soul 派对项目且存在 派对AI/ 目录时,优先按 party-ai-dev 启动顺序;否则按 .cursor/rules 与 skills 执行。」
|
||||
2. 或约定:**派对AI 仅用于「在派对AI 目录下开发」的会话**;**在 miniprogram/soul-admin/soul-api 等目录下开发时,仅用 .cursor 的 rules+skills**,避免双重入口。
|
||||
|
||||
### 2.4 soul-change-checklist 与 change-checklist Skill 的引用方式
|
||||
|
||||
**问题**:soul-change-checklist.mdc 第三十条要求 Read 的路径仍是 Windows 绝对路径。
|
||||
|
||||
**建议**:与 2.1 一致,改为「项目根/.cursor/skills/change-checklist/SKILL.md」或相对路径说明,并在 checklist 规则末尾加一句:「Skill 详细流程见 `.cursor/skills/change-checklist/SKILL.md`」。
|
||||
|
||||
### 2.5 会议纪要与收尾路径
|
||||
|
||||
**问题**:soul-meeting.mdc 中会议纪要、项目索引、会议记录索引等路径未写死 Windows,但 assistant-doc-sync SKILL 里项目索引写的是 `e:\Gongsi\Mycontent\.cursor\agent\...`。
|
||||
|
||||
**建议**:assistant-doc-sync 内所有路径改为「项目根/.cursor/agent/...」或相对路径,与 config/paths.py 中的 AGENT、PROJECT_INDEX 等保持一致表述。
|
||||
|
||||
### 2.6 角色与 Skill 的集中索引
|
||||
|
||||
**现状**:角色→Skill 的映射分散在 soul-project-boundary(按目录、按触发词、按场景)和 paths.py(ROLE_TO_AGENT)。
|
||||
|
||||
**建议**:在 `.cursor/README.md` 或 `docs/` 下维护一份「角色 ↔ Skill 一览表」,便于新人/新 Agent 快速查阅;rules 中可写「详见 .cursor/README.md#角色与Skill映射」。
|
||||
|
||||
### 2.7 经验自动收集的脚本与 Mac 兼容
|
||||
|
||||
**现状**:evolution 写入由 scripts/evolution.py 等完成,paths 来自 config/paths.py,已跨平台;仅「一键-添加经验」的入口是 .bat。
|
||||
|
||||
**建议**:同 2.2,补充 .sh 或统一用 Python 脚本入口,在规则中同时给出 Windows 与 Mac/Linux 的说明。
|
||||
|
||||
---
|
||||
|
||||
## 三、规则与 Skill 清单速查
|
||||
|
||||
| 名称 | 类型 | 作用 |
|
||||
|------|------|------|
|
||||
| soul-project-boundary.mdc | Rule | 项目边界、三端 API 约定、角色推断与 Skill 加载 |
|
||||
| 老板分身-索引.mdc | Rule | 老板分身权限、经验自动收集、编码习惯、三端分工 |
|
||||
| soul-meeting.mdc | Rule | 开会/散会触发、会议纪要路径、收尾流程 |
|
||||
| soul-change-checklist.mdc | Rule | 变更后关联检查清单(防漏改) |
|
||||
| party-ai-dev.mdc | Rule | 优先派对 AI、飞书复盘、小程序上传约定 |
|
||||
| miniprogram-dev | Skill | 小程序开发规范 |
|
||||
| admin-dev | Skill | 管理端开发规范 |
|
||||
| api-dev | Skill | 后端 API 规范 |
|
||||
| product-manager | Skill | 产品需求与验收 |
|
||||
| testing | Skill | 测试与回归 |
|
||||
| team-meeting | Skill | 多角色会议流程 |
|
||||
| assistant-doc-sync | Skill | 小橙/文档同步/经验入库/会议收尾 |
|
||||
| change-checklist | Skill | 变更关联检查详细流程 |
|
||||
| role-flow-control | Skill | 跨端协同与角色流程 |
|
||||
| three-tier-arch | Skill | 三端架构与框架分析 |
|
||||
| new-version-analyze | Skill | 新版分析、迁移对比 |
|
||||
| next-preview / next-split | Skill | next-project 仅预览、拆解指引 |
|
||||
| mysql-direct | Skill | MySQL 直接操作、db-exec |
|
||||
|
||||
---
|
||||
|
||||
## 四、总结
|
||||
|
||||
- **架构**:项目为三端(小程序 + 管理端 + soul-api),.cursor 通过 rules 定边界与触发、skills 定执行细节、agent 存经验与项目索引,**config/paths.py** 为路径与角色映射中心。
|
||||
- **优先迭代**:
|
||||
1)所有 **Skill/agent 路径** 改为可移植(相对项目根或相对 .cursor);
|
||||
2)**一键-添加经验** 增加 Mac/Linux 入口;
|
||||
3)**party-ai 与 .cursor** 的适用场景或优先级写清楚。
|
||||
- 按上述调整后,在不同系统和不同克隆路径下都能一致生效,且便于后续扩展角色或 Skill。
|
||||
BIN
.cursor/docs/soul-project-cursor-architecture.png
Normal file
BIN
.cursor/docs/soul-project-cursor-architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -23,7 +23,8 @@
|
||||
|
||||
```powershell
|
||||
# 在 soul-api 目录下执行
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
# 在仓库根目录下执行(与 miniprogram、soul-api 同级)
|
||||
cd soul-api
|
||||
.\scripts\test-p0-endpoints.ps1
|
||||
```
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ alwaysApply: false
|
||||
|
||||
当用户提及**小橙、橙子、橙橙、🍊**,或说**「讨论完毕」「记录一下」「同步到开发文档」「更新文档」「吸收经验」「升级 skills」「记录经验」「保存开发进度」「更新项目索引」「记录开发进度」「任务完成」「搞定了」「完成了」「会议结束」「散会」「会开完了」**时:
|
||||
|
||||
**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` 的完整内容**,然后严格按其流程执行。
|
||||
**必须使用 Read 工具读取 `.cursor/skills/assistant-doc-sync/SKILL.md` 的完整内容**,然后严格按其流程执行。
|
||||
|
||||
### 行为摘要(供模型快速理解,完整流程以 SKILL 文件为准)
|
||||
|
||||
1. **文档同步**:从对话中提炼结论/待办/变更 → 写入 `开发文档/1、需求/YYYY-MM-DD-需求.md`(当日文件,以日期最新为主)、`开发文档/10、项目管理/运营与变更.md`、`临时需求池/` 等对应文档
|
||||
1. **文档同步**:从对话中提炼结论/待办/变更 → 写入 `开发文档/1、需求/需求汇总.md`、`开发文档/10、项目管理/运营与变更.md`、`临时需求池/` 等对应文档
|
||||
2. **经验入库**:提炼经验 → 写入 `agent/{角色}/evolution/YYYY-MM-DD.md` → 更新 `agent/开发助理/项目索引/{索引名}.md`(写日期)→ 更新 `agent/开发助理/经验清单.md` → 升级对应 SKILL
|
||||
|
||||
63
.cursor/rules/party-ai-dev.mdc
Normal file
63
.cursor/rules/party-ai-dev.mdc
Normal file
@@ -0,0 +1,63 @@
|
||||
# 派对 AI 开发规则
|
||||
|
||||
## 与本仓库 `.cursor` 的优先级(避免入口打架)
|
||||
|
||||
- **三端编码与接口**:以本仓库 `.cursor/rules`(如 `soul-project-boundary.mdc`)与 `.cursor/skills` 为准——小程序/管理端/soul-api 路由隔离、变更检查、角色 Skill 加载等**不因派对 AI 而绕过**。
|
||||
- **派对 AI 目录**:若仓库根下存在 `派对AI/`,开发前可补充读取其 `BOOTSTRAP.md`、`SKILL_REGISTRY.md`,用于派对域身份、运营与流程;与 `.cursor` 冲突时,**代码与 API 约定以 `.cursor` 为准**。
|
||||
- **卡若 AI 全局规则**:仅在本会话需要时参考;本仓库会话自检仍以 **本仓库 `.cursor/`** 为主。
|
||||
|
||||
## 强制:使用派对 AI 进行开发(当 `派对AI/` 存在时)
|
||||
|
||||
所有开发操作**优先结合派对 AI**(路径:**仓库根下的** `派对AI/`,与 `miniprogram/` 同级)。若本仓库无 `派对AI/` 目录,则跳过本节启动顺序,仅按 `.cursor` 执行。
|
||||
|
||||
### 启动顺序
|
||||
|
||||
1. 读取 `派对AI/BOOTSTRAP.md` — 了解项目 AI 身份与团队
|
||||
2. 读取 `派对AI/SKILL_REGISTRY.md` — 查找对应技能
|
||||
3. 按匹配的 SKILL.md 执行
|
||||
|
||||
### 开发复盘推送飞书群
|
||||
|
||||
每次开发完成后,**必须将复盘内容推送到 Soul 创业派对开发资料群**:
|
||||
|
||||
```
|
||||
飞书 Webhook: https://open.feishu.cn/open-apis/bot/v2/hook/c558df98-e13a-419f-a3c0-7e428d15f494
|
||||
```
|
||||
|
||||
推送格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": "[开发复盘] 日期时间\n\n🎯 目标:...\n📌 结果:...\n💡 关键判断:...\n📝 遗留:...\n▶ 下一步:..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 需求必须 100% 完成(铁律)
|
||||
|
||||
**禁止**:说「留到下一轮迭代」「后续再处理」。
|
||||
收到需求文档后,**全量开发**到每一项功能都落地为止。
|
||||
若单个功能涉及跨端改动(后端 + 管理端 + 小程序),必须三端同步改完。
|
||||
仅当功能在代码层面**无法实现**(如依赖第三方审核、硬件条件不具备)时,才可标注「依赖外部」并说明原因。
|
||||
|
||||
### 复盘格式
|
||||
|
||||
使用卡若 AI 标准复盘格式(🎯📌💡📝▶ 五块齐全),带日期+时间。
|
||||
开发完成后**必须**立即发送飞书复盘(不需要用户提醒)。
|
||||
|
||||
### 小程序上传约定(强制)
|
||||
|
||||
- **版本号固定 `1.2.6`**:每次上传小程序(soul-party / 派对AI)统一使用版本 `1.2.6`,不递增。
|
||||
- **上传后设为体验版**:上传完成后自动设为体验版,方便扫码测试。
|
||||
- **不自动提交审核**:上传后默认不提交审核。只有用户明确说「上传审核」「提交审核」时,才执行审核提交。
|
||||
- **上传命令**:
|
||||
```bash
|
||||
# 上传 + 体验版(默认,不提审)
|
||||
python 开发文档/小程序管理/scripts/mp_deploy.py deploy soul-party --skip-cert
|
||||
# 上传 + 体验版 + 提交审核(用户明确要求时)
|
||||
python 开发文档/小程序管理/scripts/mp_deploy.py deploy soul-party --skip-cert --submit
|
||||
# 单独提交审核(已上传后)
|
||||
python 开发文档/小程序管理/scripts/mp_deploy.py submit soul-party
|
||||
```
|
||||
@@ -8,4 +8,4 @@ alwaysApply: false
|
||||
|
||||
当编辑 **开发文档/1、需求/**、**临时需求池/**、**开发文档/10、项目管理/** 时,推断当前角色为**产品经理**。
|
||||
|
||||
**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md` 的完整内容**,然后按其规范执行需求分析、文档编写、验收标准制定。
|
||||
**必须使用 Read 工具读取 `.cursor/skills/product-manager/SKILL.md` 的完整内容**,然后按其规范执行需求分析、文档编写、验收标准制定。
|
||||
|
||||
@@ -26,8 +26,8 @@ alwaysApply: false
|
||||
|
||||
## Skill 加载(必须执行)
|
||||
|
||||
**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\admin-dev\SKILL.md` 的完整内容**,按其规范进行开发。该 Skill 包含代码风格、业务逻辑、API 对接细节等完整约定。
|
||||
**必须使用 Read 工具读取 `.cursor/skills/admin-dev/SKILL.md` 的完整内容**,按其规范进行开发。该 Skill 包含代码风格、业务逻辑、API 对接细节等完整约定。
|
||||
|
||||
接口实现与路由分组的规范在 `e:\Gongsi\Mycontent\.cursor\rules\soul-api.mdc`(编辑 soul-api 时自动加载)。
|
||||
接口实现与路由分组的规范在 `.cursor/rules/soul-api.mdc`(编辑 soul-api 时自动加载)。
|
||||
|
||||
违反上述路径或职责边界即视为「互窜」,需纠正后再提交。
|
||||
|
||||
@@ -6,7 +6,7 @@ alwaysApply: false
|
||||
|
||||
# soul-api 开发规范
|
||||
|
||||
> **Skill 加载**:编辑 soul-api 代码时,**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\api-dev\SKILL.md` 的完整内容**,该 Skill 包含业务对接、与前端边界协同等补充约定。本规则侧重编码规范与路由边界。
|
||||
> **Skill 加载**:编辑 soul-api 代码时,**必须使用 Read 工具读取 `.cursor/skills/api-dev/SKILL.md` 的完整内容**,该 Skill 包含业务对接、与前端边界协同等补充约定。本规则侧重编码规范与路由边界。
|
||||
|
||||
## 1. 路由按使用方归类(强制)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ alwaysApply: false
|
||||
|
||||
- **每次**在 miniprogram、soul-admin、soul-api 内完成一轮修改后,**先过一遍上表 + 二**,再视为本次变更完成。
|
||||
- 若本次变更涉及多端(例如小程序新功能 + 管理端配置页),应在同一次任务内一并完成或明确记录未做项,避免漏改。
|
||||
- 更详细的检查流程:**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\change-checklist\SKILL.md` 的完整内容**,按其「以领域为单位思考」的方法逐项确认。
|
||||
- 更详细的检查流程:**必须使用 Read 工具读取 `.cursor/skills/change-checklist/SKILL.md` 的完整内容**(相对本仓库根),按其「以领域为单位思考」的方法逐项确认。
|
||||
|
||||
## 四、聊天中触发变更检查
|
||||
|
||||
|
||||
22
.cursor/rules/soul-karuo-dialogue.mdc
Normal file
22
.cursor/rules/soul-karuo-dialogue.mdc
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Soul 仓库内对话与卡若 AI 复盘格式对齐(alwaysApply)
|
||||
globs: ["**"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Soul 项目 · 卡若 AI 对话约定
|
||||
|
||||
> 与 `soul-project-boundary.mdc` 中「卡若 AI 对话规范」一致;本文件只强调**对话形态**,不重复三端技术边界。
|
||||
|
||||
## 必须遵守
|
||||
|
||||
0. **默认零提问**:派对/Soul 相关开发、运维、脚本、填表链路,**禁止**反问用户「是否执行」「要不要」。缺信息则读本仓库代码与配置、用合理默认推进;**仅**验证码/密钥缺失/不可逆删除等无法代劳时,用**一句**说明缺什么。
|
||||
1. **语言**:面向用户的说明、结论、按钮文案解释等,默认 **简体中文**。
|
||||
2. **收尾**:每一轮对用户可见的助手回复,**最后一段**必须是完整 **[卡若复盘](YYYY-MM-DD HH:mm)** 块,含五段:**🎯 目标·结果·达成率**(单行 ≤30 字且含达成率 %)·**📌 过程** · **💡 反思** · **📝 总结** · **▶ 下一步执行**。复盘块内**禁止表格**。
|
||||
3. **格式源**:与卡若 AI 仓库内 `运营中枢/参考资料/卡若复盘格式_固定规则.md` 保持同一种写法;若当前工作区已挂载「卡若AI」目录,修改复盘规则时以该文件为唯一真源。
|
||||
4. **需求节奏**:仍服从「需求即执行」——先「好」再改代码再报结果;复盘块放在**全条回复最末**。
|
||||
5. **直接执行**:用户说「直接做、别讲写了什么」时,**正文极短**;**复盘五块不可省**,可压缩过程为 1~2 条要点。
|
||||
|
||||
## 与卡若中枢的衔接
|
||||
|
||||
- Token/API 费用:助手侧通常无 Cursor 账单接口,在 💡 中说明「请用户在 Cursor Usage 自查」即可。
|
||||
@@ -6,7 +6,7 @@ alwaysApply: true
|
||||
|
||||
# Soul 创业派对 - 会议触发器
|
||||
|
||||
当用户表达**开会意图**时(包括但不限于以下触发词),**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md` 的完整内容**,然后严格按其流程主持会议。
|
||||
当用户表达**开会意图**时(包括但不限于以下触发词),**必须使用 Read 工具读取 `.cursor/skills/team-meeting/SKILL.md` 的完整内容**(相对本仓库根),然后严格按其流程主持会议。
|
||||
|
||||
## 语义化触发词(理解意图,不必完全匹配)
|
||||
|
||||
@@ -24,9 +24,9 @@ alwaysApply: true
|
||||
当用户说**「会议结束」「散会」「会开完了」「结束会议」**时:
|
||||
|
||||
1. 助理橙子立即执行收尾流程
|
||||
2. **生成会议纪要**:`e:\Gongsi\Mycontent\.cursor\meeting\YYYY-MM-DD_主题.md`
|
||||
3. **各角色经验入库**:`e:\Gongsi\Mycontent\.cursor\agent\{角色}\evolution\YYYY-MM-DD.md`
|
||||
4. **更新项目索引**:`agent/开发助理/项目索引/{索引名}.md` 开发进度表追加一行
|
||||
5. **更新会议记录索引**:`e:\Gongsi\Mycontent\.cursor\meeting\README.md`
|
||||
2. **生成会议纪要**:`.cursor/meeting/YYYY-MM-DD_主题.md`
|
||||
3. **各角色经验入库**:`.cursor/agent/{角色}/evolution/YYYY-MM-DD.md`
|
||||
4. **更新项目索引**:`.cursor/agent/开发助理/项目索引/{索引名}.md` 开发进度表追加一行
|
||||
5. **更新会议记录索引**:`.cursor/meeting/README.md`
|
||||
|
||||
**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` 执行收尾。**
|
||||
**必须使用 Read 工具读取 `.cursor/skills/assistant-doc-sync/SKILL.md` 执行收尾。**
|
||||
|
||||
@@ -23,8 +23,8 @@ alwaysApply: false
|
||||
|
||||
## Skill 加载(必须执行)
|
||||
|
||||
**必须使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\miniprogram-dev\SKILL.md` 的完整内容**,按其规范进行开发。该 Skill 包含代码风格、业务逻辑、API 对接细节等完整约定。
|
||||
**必须使用 Read 工具读取 `.cursor/skills/miniprogram-dev/SKILL.md` 的完整内容**,按其规范进行开发。该 Skill 包含代码风格、业务逻辑、API 对接细节等完整约定。
|
||||
|
||||
接口实现与路由分组的规范在 `e:\Gongsi\Mycontent\.cursor\rules\soul-api.mdc`(编辑 soul-api 时自动加载)。
|
||||
接口实现与路由分组的规范在 `.cursor/rules/soul-api.mdc`(编辑 soul-api 时自动加载)。
|
||||
|
||||
违反上述路径或职责边界即视为「互窜」,需纠正后再提交。
|
||||
|
||||
@@ -20,17 +20,18 @@ alwaysApply: true
|
||||
| 预览/参考 | next-project/ | 仅预览,非线上 | 不依赖 |
|
||||
| **新版管理端** | **new-soul/soul-admin/** | 新版参考实现,迁移时对照 | soul-api |
|
||||
|
||||
## 需求目录与命名约定
|
||||
|
||||
- **需求目录**:`开发文档/1、需求/`
|
||||
- **命名**:`YYYY-MM-DD-需求.md` 或 `YYYY-MM-DD-简短描述.md`
|
||||
- **主需求**:以日期最新的需求文件为主;同步需求时新建/更新当日文件,并更新 `1、需求/索引.md`
|
||||
- **基准**:`以界面定需求.md` 为界面级需求基准,新增/改版界面或业务规则时先更新该文档
|
||||
|
||||
## 核心原则
|
||||
|
||||
- 小程序只调 `/api/miniprogram/*`;管理端只调 `/api/admin/*`、`/api/db/*`;禁止混用。
|
||||
- 变更完成必过 soul-change-checklist.mdc;聊天中说「变更完成」「检查一下」「准备提交」时主动触发检查。
|
||||
- **需求即执行 + 零提问**:收到需求后**禁止**列出分析表格再问用户选哪个执行,**禁止**「是否帮你执行」类确认。正确做法:内部定方案 → **直接改代码/跑命令** → 回复结果。缺信息:先查仓库与配置推断;**仅**在验证码、缺失密钥、不可逆删除等无法代劳时极简说明。
|
||||
- **卡若 AI 对话规范(与卡若工作区一致)**:在本仓库内协助用户时,**默认使用简体中文**;**每条助手回复末尾**用完整 **卡若复盘块** 收尾(🎯 目标·结果·达成率 · 📌 过程 · 💡 反思 · 📝 总结 · ▶ 下一步执行),标题带 **YYYY-MM-DD HH:mm**,复盘块内不用表格,细则见卡若项目 `运营中枢/参考资料/卡若复盘格式_固定规则.md`(多根工作区时请 Read 该文件)。Mongo 同步、飞书 webhook 等以卡若 `.cursor/rules/karuo-ai.mdc` 为准(本仓库 Agent 在能执行脚本时同样执行对话留存脚本)。
|
||||
|
||||
## 路径约定(Skill / agent / meeting)
|
||||
|
||||
- 下表及本仓库 Skill 中的路径均以 **本 Git 仓库根目录** 为基准(与 `miniprogram/`、`soul-api/` 同级),**不使用盘符或另一台机器上的绝对路径**。
|
||||
- 使用 Read / Write 工具时:填写 **`仓库根/.cursor/...`**,例如 `.cursor/skills/api-dev/SKILL.md`(与规则中的写法一致即可)。
|
||||
- 脚本中的物理路径以 `config/paths.py` 的 `ROOT`、`SKILLS`、`AGENT`、`MEETING` 为准。
|
||||
|
||||
## 角色推断与 Skill 加载(必须执行)
|
||||
|
||||
@@ -38,13 +39,13 @@ alwaysApply: true
|
||||
|
||||
### 按编辑目录
|
||||
|
||||
| 编辑目录 | 推断角色 | 必须 Read 的主 Skill 文件(绝对路径) |
|
||||
|----------|----------|---------------------------------------|
|
||||
| miniprogram/ | 小程序开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\miniprogram-dev\SKILL.md` |
|
||||
| soul-admin/ | 管理端开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\admin-dev\SKILL.md` |
|
||||
| soul-api/ | 后端开发 | `e:\Gongsi\Mycontent\.cursor\skills\api-dev\SKILL.md` |
|
||||
| 开发文档/1、需求/、临时需求池/ | 产品经理 | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md` |
|
||||
| .cursor/ | 助理橙子 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` |
|
||||
| 编辑目录 | 推断角色 | 必须 Read 的主 Skill 文件(相对仓库根) |
|
||||
|----------|----------|----------------------------------------|
|
||||
| miniprogram/ | 小程序开发工程师 | `.cursor/skills/miniprogram-dev/SKILL.md` |
|
||||
| soul-admin/ | 管理端开发工程师 | `.cursor/skills/admin-dev/SKILL.md` |
|
||||
| soul-api/ | 后端开发 | `.cursor/skills/api-dev/SKILL.md` |
|
||||
| 开发文档/1、需求/、临时需求池/ | 产品经理 | `.cursor/skills/product-manager/SKILL.md` |
|
||||
| .cursor/ | 助理橙子 | `.cursor/skills/assistant-doc-sync/SKILL.md` |
|
||||
|
||||
### 按语义触发词(说啥切角色,无需编辑文件)
|
||||
|
||||
@@ -52,26 +53,23 @@ alwaysApply: true
|
||||
|
||||
| 触发词 | 推断角色 | 必须 Read 的 Skill 文件 |
|
||||
|--------|----------|-------------------------|
|
||||
| 后端、API、soul-api、接口、Go、GORM | 后端开发 | `e:\Gongsi\Mycontent\.cursor\skills\api-dev\SKILL.md` |
|
||||
| 管理端、soul-admin、React、后台管理 | 管理端开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\admin-dev\SKILL.md` |
|
||||
| 小程序、miniprogram、C 端、微信小程序 | 小程序开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\miniprogram-dev\SKILL.md` |
|
||||
| 产品、需求、验收、排期、需求文档 | 产品经理 | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md` |
|
||||
| 测试、测试用例、回归测试、功能测试、QA | 测试人员 | `e:\Gongsi\Mycontent\.cursor\skills\testing\SKILL.md` |
|
||||
| 挖矿、安全、服务器操作、部署、miner_guard、xmrig、入侵排查 | 安全工程师 | `e:\Gongsi\Mycontent\.cursor\skills\security-miner-guard\SKILL.md`、`e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md` |
|
||||
| 后端、API、soul-api、接口、Go、GORM | 后端开发 | `.cursor/skills/api-dev/SKILL.md` |
|
||||
| 管理端、soul-admin、React、后台管理 | 管理端开发工程师 | `.cursor/skills/admin-dev/SKILL.md` |
|
||||
| 小程序、miniprogram、C 端、微信小程序 | 小程序开发工程师 | `.cursor/skills/miniprogram-dev/SKILL.md` |
|
||||
| 产品、需求、验收、排期、需求文档 | 产品经理 | `.cursor/skills/product-manager/SKILL.md` |
|
||||
| 测试、测试用例、回归测试、功能测试、QA | 测试人员 | `.cursor/skills/testing/SKILL.md` |
|
||||
|
||||
### 按场景触发词
|
||||
|
||||
| 场景触发词 | 必须 Read 的 Skill 文件(绝对路径) |
|
||||
|------------|-------------------------------------|
|
||||
| 小橙、橙子、橙橙、🍊、讨论完毕、记录一下、记录、同步文档 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` |
|
||||
| 吸收经验、升级 skills、记录经验、保存开发进度、更新项目索引、记录开发进度、任务完成、搞定了、完成了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` |
|
||||
| 跨端功能开发 | `e:\Gongsi\Mycontent\.cursor\skills\role-flow-control\SKILL.md` |
|
||||
| 变更完成、检查一下、准备提交 | `e:\Gongsi\Mycontent\.cursor\skills\change-checklist\SKILL.md` |
|
||||
| 开个会、开会、团队会议、乘风开会、需求评审、方案讨论、大家一起讨论 | `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md`(老板分身/乘风主持) |
|
||||
| 会议结束、散会、会开完了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md`(会议收尾) |
|
||||
| **加个需求**、加个需求:xxx | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md`(产品经理三端分析 → 功能规划 → 指派) |
|
||||
| **新版分析**、版本对比、迁移分析、甲方代码分析、快速分析新版、抽取需求 | `e:\Gongsi\Mycontent\.cursor\skills\new-version-analyze\SKILL.md`(新版快速分析 → 差异清单 → 接口冲突 → 迁移迭代) |
|
||||
| **帮我部署api到线上** | `e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md`(安全工程师 → 执行 soul-api/master.py) |
|
||||
| **管理端帮我部署到xx环境** | `e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md`(安全工程师 → 语义化解析 xx:正式/线上/生产→master.py,测试/dev→deploy.py) |
|
||||
| 场景触发词 | 必须 Read 的 Skill 文件(相对仓库根) |
|
||||
|------------|----------------------------------------|
|
||||
| 小橙、橙子、橙橙、🍊、讨论完毕、记录一下、记录、同步文档 | `.cursor/skills/assistant-doc-sync/SKILL.md` |
|
||||
| 吸收经验、升级 skills、记录经验、保存开发进度、更新项目索引、记录开发进度、任务完成、搞定了、完成了 | `.cursor/skills/assistant-doc-sync/SKILL.md` |
|
||||
| 跨端功能开发 | `.cursor/skills/role-flow-control/SKILL.md` |
|
||||
| 变更完成、检查一下、准备提交 | `.cursor/skills/change-checklist/SKILL.md` |
|
||||
| 开个会、开会、团队会议、乘风开会、需求评审、方案讨论、大家一起讨论 | `.cursor/skills/team-meeting/SKILL.md`(老板分身/乘风主持) |
|
||||
| 会议结束、散会、会开完了 | `.cursor/skills/assistant-doc-sync/SKILL.md`(会议收尾) |
|
||||
| **加个需求**、加个需求:xxx | `.cursor/skills/product-manager/SKILL.md`(需求即执行:回复「好」→ 直接执行代码变更 → 回复结果) |
|
||||
| **新版分析**、版本对比、迁移分析、甲方代码分析、快速分析新版、抽取需求 | `.cursor/skills/new-version-analyze/SKILL.md`(新版快速分析 → 差异清单 → 接口冲突 → 迁移迭代) |
|
||||
|
||||
**注意**:「必须 Read」= 使用 Read 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。
|
||||
**注意**:「必须 Read」= 使用 Read 工具读取上述路径相对于**当前工作区仓库根**的完整文件内容后执行,不可跳过或仅凭记忆。
|
||||
|
||||
@@ -6,7 +6,7 @@ alwaysApply: true
|
||||
# 老板分身 - 能力与约束(Soul 创业派对)
|
||||
|
||||
> **老板分身权限最高**:协调所有智能体(小程序开发工程师、管理端开发工程师、后端工程师、产品经理、开发助理等)。其他 agent 执行任务时遵循本规则;老板分身可调度、协调、指派任一角色。
|
||||
> **激活方式**:用户说「老板」「分身」「乘风」「架构」「帮我协调」时,从旁观者转为主动参与。**开会时**:用户说「开会」「开个会」「团队会议」「乘风开会」「需求评审」「方案讨论」等表达开会意图时,**必须先**用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md` 完整内容,然后由老板分身(乘风)按该协议主持多角色会议,不可仅回复而不执行流程。
|
||||
> **激活方式**:用户说「老板」「分身」「乘风」「架构」「帮我协调」时,从旁观者转为主动参与。**开会时**:用户说「开会」「开个会」「团队会议」「乘风开会」「需求评审」「方案讨论」等表达开会意图时,**必须先**用 Read 工具读取 `.cursor/skills/team-meeting/SKILL.md` 完整内容(相对本仓库根),然后由老板分身(乘风)按该协议主持多角色会议,不可仅回复而不执行流程。
|
||||
> **会话自检**:仅沿用本项目 `.cursor/` 下的 rules、skills、agent;忽略与本项目无关的全局 rules/skills。
|
||||
> **Skill 撰写**:维护或新增 Skill 时参考 `e:\Gongsi\Mycontent\.cursor\docs\skill-writing-principles.md`(写 Claude 不知道的、Gotchas 是灵魂、留灵活空间)。
|
||||
> **角色驱动**:Soul 角色与 agent 映射见 `config/paths.py` 的 ROLE_TO_AGENT。
|
||||
@@ -40,12 +40,11 @@ alwaysApply: true
|
||||
- 产品/需求/config→**产品经理**
|
||||
- 测试/自检/QA→**软件测试**
|
||||
- 架构/选型/路由约定/三端协同→**团队**
|
||||
- 挖矿/安全/服务器操作/部署/入侵排查→**安全工程师**
|
||||
- 无法判断→**通用**(写入开发助理)
|
||||
3. **若可写文件**:
|
||||
- **有明确目标角色**:写入 `.cursor/agent/{角色}/evolution/YYYY-MM-DD-简短描述.md`,并更新该目录下的 `索引.md`
|
||||
- **无法判断角色**:写入 `.cursor/agent/开发助理/evolution/`
|
||||
4. **若无法写文件**:输出 JSON,并提示用户双击 `agent/开发助理/script/一键-添加经验.bat`
|
||||
4. **若无法写文件**:输出 JSON,并提示用户在仓库根执行:`.cursor/agent/开发助理/script/一键-添加经验.bat`(Windows)或同目录下的 `.sh` / Python 入口(macOS/Linux,若已提供)
|
||||
|
||||
### Soul 目标角色与 target_roles 取值
|
||||
|
||||
|
||||
55
.cursor/scripts/README-gitea-sync.md
Normal file
55
.cursor/scripts/README-gitea-sync.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 与 Gitea(192.168.1.201)同步
|
||||
|
||||
## 远程
|
||||
|
||||
- **gitea-local**:`http://192.168.1.201:3000/fnvtk/soul-yongping.git`(拉取 + 推送)
|
||||
|
||||
## 手动同步
|
||||
|
||||
```bash
|
||||
./.cursor/scripts/gitea-sync.sh
|
||||
```
|
||||
|
||||
## 每 10 分钟自动同步(macOS launchd)
|
||||
|
||||
- 已安装:`~/Library/LaunchAgents/com.soul.yongping.gitea-sync.plist`
|
||||
- 每 10 分钟执行一次,登录后自动加载
|
||||
|
||||
**启用:**
|
||||
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/com.soul.yongping.gitea-sync.plist
|
||||
```
|
||||
|
||||
**停用:**
|
||||
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.soul.yongping.gitea-sync.plist
|
||||
```
|
||||
|
||||
**查看是否在跑:**
|
||||
|
||||
```bash
|
||||
launchctl list | grep com.soul.yongping.gitea-sync
|
||||
```
|
||||
|
||||
## 认证(192.168.1.201 需登录时)
|
||||
|
||||
若 push/pull 需要账号密码,定时任务无法弹窗,请把凭证写进 remote URL(勿提交到仓库):
|
||||
|
||||
```bash
|
||||
git remote set-url gitea-local 'http://用户名:token或密码@192.168.1.201:3000/fnvtk/soul-yongping.git'
|
||||
```
|
||||
|
||||
或用系统钥匙串:
|
||||
|
||||
```bash
|
||||
git config --global credential.helper osxkeychain
|
||||
# 然后手动执行一次 gitea-sync.sh,输入一次账号密码,之后由钥匙串记住
|
||||
```
|
||||
|
||||
## 日志
|
||||
|
||||
- 脚本内部:`.cursor/scripts/gitea-sync.log`
|
||||
- launchd 标准输出:`.cursor/scripts/gitea-sync-launchd.log`
|
||||
- launchd 错误:`.cursor/scripts/gitea-sync-launchd.err.log`
|
||||
163
.cursor/scripts/gitea-sync-launchd.err.log
Normal file
163
.cursor/scripts/gitea-sync-launchd.err.log
Normal file
@@ -0,0 +1,163 @@
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
/bin/bash: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/.cursor/scripts/gitea-sync.sh: Operation not permitted
|
||||
0
.cursor/scripts/gitea-sync-launchd.log
Normal file
0
.cursor/scripts/gitea-sync-launchd.log
Normal file
48
.cursor/scripts/gitea-sync.log
Normal file
48
.cursor/scripts/gitea-sync.log
Normal file
@@ -0,0 +1,48 @@
|
||||
[2026-03-19 14:54:01] --- sync start (branch=devlop, remote=gitea-local) ---
|
||||
From http://192.168.1.201:3000/fnvtk/soul-yongping
|
||||
* [new branch] devlop -> gitea-local/devlop
|
||||
* [new branch] main -> gitea-local/main
|
||||
error: cannot pull with rebase: You have unstaged changes.
|
||||
error: Please commit or stash them.
|
||||
[devlop 28a69cbc] sync: 2026-03-19 14:54
|
||||
Committer: 卡若 <karuo@MacBook-Pro.local>
|
||||
Your name and email address were configured automatically based
|
||||
on your username and hostname. Please check that they are accurate.
|
||||
You can suppress this message by setting them explicitly:
|
||||
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email you@example.com
|
||||
|
||||
After doing this, you may fix the identity used for this commit with:
|
||||
|
||||
git commit --amend --reset-author
|
||||
|
||||
26 files changed, 164 insertions(+), 2133 deletions(-)
|
||||
create mode 100644 .cursor/scripts/README-gitea-sync.md
|
||||
create mode 100644 .cursor/scripts/gitea-sync-launchd.err.log
|
||||
create mode 100644 .cursor/scripts/gitea-sync-launchd.log
|
||||
create mode 100644 .cursor/scripts/gitea-sync.log
|
||||
create mode 100755 .cursor/scripts/gitea-sync.sh
|
||||
create mode 100644 project.config.json
|
||||
delete mode 100644 开发文档/1、需求/文章详情-阅读页线框图.md
|
||||
delete mode 100644 开发文档/1、需求/链接人与事-所有同步需求.md
|
||||
delete mode 100644 开发文档/代付功能-美团式方案与场景清单.md
|
||||
delete mode 100644 开发文档/全站测试报告_20260315.md
|
||||
delete mode 100644 开发文档/存客宝对接逻辑图.md
|
||||
delete mode 100644 开发文档/小程序管理/scripts/reports/体验版二维码_soul-party_20260315_2344.png
|
||||
delete mode 100644 开发文档/小程序管理/scripts/reports/体验版二维码_soul-party_20260316_0221.png
|
||||
delete mode 100644 开发文档/小程序管理/scripts/reports/体验版二维码_soul-party_20260316_1804.png
|
||||
delete mode 100644 开发文档/找朋友代付-流程与配置.md
|
||||
delete mode 100644 开发文档/新版管理端迁移到稳定版-需求评估.md
|
||||
delete mode 100644 开发文档/新版迁移-开发方案与清单.md
|
||||
delete mode 100644 开发文档/稳定版-小程序与API对比.md
|
||||
delete mode 100644 开发文档/稳定版-源码质量分析报告.md
|
||||
delete mode 100644 开发文档/稳定版-管理端与小程序对接分析.md
|
||||
delete mode 100644 开发文档/稳定版适配新界面-调整清单.md
|
||||
delete mode 100644 开发文档/管理端两版界面差异-新需求参考.md
|
||||
delete mode 100644 开发文档/管理端迁移分析-基于小程序功能.md
|
||||
delete mode 100644 开发文档/规则引擎迁移-影响分析.md
|
||||
delete mode 100644 开发文档/迁移完成度与待办清单.md
|
||||
remote: Failed to authenticate user
|
||||
fatal: Authentication failed for 'http://192.168.1.201:3000/fnvtk/soul-yongping.git/'
|
||||
[2026-03-19 14:54:02] --- sync end ---
|
||||
46
.cursor/scripts/gitea-sync.sh
Executable file
46
.cursor/scripts/gitea-sync.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# 与 Gitea(192.168.1.201)双向同步:先拉取,有本地变更则提交并推送
|
||||
# 可手动执行,也可由 launchd 每 10 分钟执行
|
||||
|
||||
set -e
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
REMOTE="gitea"
|
||||
LOG_FILE="$REPO_ROOT/.cursor/scripts/gitea-sync.log"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
log "--- sync start (branch=$BRANCH, remote=$REMOTE) ---"
|
||||
|
||||
# 1. 拉取远程更新(若有未提交变更则先 stash,pull 后再 pop)
|
||||
STASHED=""
|
||||
if [ -n "$(git status -s)" ]; then
|
||||
git stash push -u -m "gitea-sync $(date +%s)" 2>/dev/null && STASHED=1 || true
|
||||
fi
|
||||
git fetch "$REMOTE" 2>&1 | tee -a "$LOG_FILE" || true
|
||||
if git ls-remote --exit-code --heads "$REMOTE" "$BRANCH" &>/dev/null; then
|
||||
git pull "$REMOTE" "$BRANCH" --no-edit 2>&1 | tee -a "$LOG_FILE" || log "pull 失败或冲突,继续尝试推送本地变更"
|
||||
fi
|
||||
[ -n "$STASHED" ] && git stash pop 2>/dev/null || true
|
||||
|
||||
# 2. 若有本地未提交变更,则提交并推送
|
||||
STATUS=$(git status -s)
|
||||
if [ -n "$STATUS" ]; then
|
||||
git add -A
|
||||
git commit -m "sync: $(date '+%Y-%m-%d %H:%M')" 2>&1 | tee -a "$LOG_FILE" || log "commit failed (nothing to commit or conflict)"
|
||||
git push "$REMOTE" "$BRANCH" 2>&1 | tee -a "$LOG_FILE" || log "push failed"
|
||||
else
|
||||
# 若有已提交但未推送的提交,也推送(仅当远程有此分支且本地比远程多提交时)
|
||||
if git ls-remote --exit-code --heads "$REMOTE" "$BRANCH" &>/dev/null; then
|
||||
AHEAD=$(git rev-list "refs/remotes/${REMOTE}/${BRANCH}"..HEAD --count 2>/dev/null || echo 0)
|
||||
if [ "${AHEAD:-0}" -gt 0 ]; then
|
||||
git push "$REMOTE" "$BRANCH" 2>&1 | tee -a "$LOG_FILE" || log "push failed"
|
||||
fi
|
||||
else
|
||||
git push -u "$REMOTE" "$BRANCH" 2>&1 | tee -a "$LOG_FILE" || log "push (new branch) failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "--- sync end ---"
|
||||
@@ -35,7 +35,7 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
|
||||
| 要点类型 | 写入位置 | 示例 |
|
||||
|----------|----------|------|
|
||||
| 需求清单项 | `开发文档/1、需求/YYYY-MM-DD-需求.md`(当日文件,以日期最新为主) | 会员分润差异化、VIP 手动设置;同步后更新 `1、需求/索引.md` |
|
||||
| 需求清单项 | `开发文档/1、需求/需求汇总.md` 需求清单表 | 会员分润差异化、VIP 手动设置 |
|
||||
| 近期讨论 | `开发文档/10、项目管理/运营与变更.md` | 第五部分或新增节 |
|
||||
| 技术分析 | `临时需求池/` 或 `开发文档/8、部署/` | 分润需求-技术分析.md |
|
||||
| 项目推进 | `开发文档/10、项目管理/项目落地推进表.md` | 第十二节永平落地表 |
|
||||
@@ -58,7 +58,7 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
|
||||
1. **确定角色**:根据本次对话涉及目录(miniprogram/soul-admin/soul-api/需求文档)推断对应角色
|
||||
2. **提炼进度**:从对话中提取本次开发完成的内容、当前阶段、待续项
|
||||
3. **更新项目索引**:打开 `e:\Gongsi\Mycontent\.cursor\agent\开发助理\项目索引\{索引文件名}.md`(索引文件名见步骤 5.1 映射表)
|
||||
3. **更新项目索引**:打开 `.cursor\agent\开发助理\项目索引\{索引文件名}.md`(索引文件名见步骤 5.1 映射表)
|
||||
- 在「开发进度」表追加一行,**必须写日期**(YYYY-MM-DD)
|
||||
- 视需要更新「项目总结」段落
|
||||
- 文末「最后更新」改为当前日期
|
||||
@@ -66,12 +66,12 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
|
||||
### 4.6 会议收尾(会议结束、散会、会开完了时)
|
||||
|
||||
当用户说**「会议结束」「散会」「会开完了」**时,执行会议收尾流程(完整流程见 `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md` 第 4 节):
|
||||
当用户说**「会议结束」「散会」「会开完了」**时,执行会议收尾流程(完整流程见 `.cursor\skills\team-meeting\SKILL.md` 第 4 节):
|
||||
|
||||
1. **生成会议纪要**:`e:\Gongsi\Mycontent\.cursor\meeting\YYYY-MM-DD_主题.md`,按 `_模板.md` 填写;**必须包含「问题与作答区」**,将待确认/待澄清项列出为问题表,作答列留空
|
||||
2. **各角色经验入库**:追加到 `e:\Gongsi\Mycontent\.cursor\agent\{角色目录}\evolution\YYYY-MM-DD.md`(角色→目录见步骤 5.1 映射表)
|
||||
1. **生成会议纪要**:`.cursor\meeting\YYYY-MM-DD_主题.md`,按 `_模板.md` 填写;**必须包含「问题与作答区」**,将待确认/待澄清项列出为问题表,作答列留空
|
||||
2. **各角色经验入库**:追加到 `.cursor\agent\{角色目录}\evolution\YYYY-MM-DD.md`(角色→目录见步骤 5.1 映射表)
|
||||
3. **更新项目索引**:各参会角色对应的 `agent/开发助理/项目索引/{索引名}.md` 开发进度表追加一行,写日期
|
||||
4. **更新会议记录索引**:`e:\Gongsi\Mycontent\.cursor\meeting\README.md` 索引表追加
|
||||
4. **更新会议记录索引**:`.cursor\meeting\README.md` 索引表追加
|
||||
5. **Skill 升级**(若有影响开发规范的决议):更新对应 SKILL-xxx.md
|
||||
6. **更新 sync-log**:在 `sync-log.md` 末尾追加 `- YYYY-MM-DD 会议收尾:{主题}`
|
||||
7. **回复**:会议收尾已完成,纪要见 `meeting/YYYY-MM-DD_主题.md`
|
||||
@@ -89,7 +89,6 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
| 后端开发 | `agent/后端工程师/evolution/` | `agent/开发助理/项目索引/后端.md` |
|
||||
| 产品经理 | `agent/产品经理/evolution/` | `agent/开发助理/项目索引/产品.md` |
|
||||
| 测试人员 | `agent/软件测试/evolution/` | `agent/开发助理/项目索引/测试.md` |
|
||||
| 安全工程师 | `agent/安全工程师/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
| 助理橙子 | `agent/开发助理/evolution/` | `agent/开发助理/项目索引/助理橙子.md` |
|
||||
| 跨角色/团队 | `agent/团队/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
|
||||
@@ -113,16 +112,16 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
- 应升级的 Skill
|
||||
|
||||
2. **写入经验文件**(绝对路径):
|
||||
- 路径:`e:\Gongsi\Mycontent\.cursor\agent\{角色目录}\evolution\YYYY-MM-DD.md`
|
||||
- 路径:`.cursor\agent\{角色目录}\evolution\YYYY-MM-DD.md`
|
||||
- 当天文件不存在则新建;文件名用日期,经验按天存储
|
||||
|
||||
3. **更新项目索引**(绝对路径):
|
||||
- 路径:`e:\Gongsi\Mycontent\.cursor\agent\开发助理\项目索引\{索引名}.md`(见 5.1 映射)
|
||||
- 路径:`.cursor\agent\开发助理\项目索引\{索引名}.md`(见 5.1 映射)
|
||||
- 在「开发进度」表追加一行,**必须写日期**(YYYY-MM-DD)
|
||||
- 更新文末「最后更新」为当前日期
|
||||
|
||||
4. **更新经验清单**:
|
||||
- 路径:`e:\Gongsi\Mycontent\.cursor\agent\开发助理\经验清单.md`
|
||||
- 路径:`.cursor\agent\开发助理\经验清单.md`
|
||||
- 在索引表追加一行
|
||||
|
||||
5. **升级 Skill**:根据经验类型,更新对应 `.cursor/skills/{skill}/SKILL.md`
|
||||
@@ -158,10 +157,7 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
|
||||
```
|
||||
开发文档/
|
||||
├── 1、需求/
|
||||
│ ├── 索引.md # 主需求 = 日期最新的需求文件
|
||||
│ ├── YYYY-MM-DD-需求.md # 需求文件按日期命名,以最新为主
|
||||
│ └── 以界面定需求.md # 界面级需求基准
|
||||
├── 1、需求/需求汇总.md # 需求清单、业务需求
|
||||
├── 8、部署/ # 技术方案、部署说明
|
||||
├── 10、项目管理/
|
||||
│ ├── 项目落地推进表.md # 里程碑、永平落地
|
||||
@@ -184,7 +180,7 @@ description: Trigger when 小橙/橙子/讨论完毕/记录一下/同步文档/
|
||||
**小橙**执行:
|
||||
|
||||
1. 提炼:VIP 手动设置已完成;会员分润差异化待实现;好友优惠仅针对文章
|
||||
2. 更新 `1、需求/YYYY-MM-DD-需求.md`(当日文件):新增「需求清单」行;同步后更新 `1、需求/索引.md`
|
||||
2. 更新 `需求汇总.md`:新增「需求清单」行
|
||||
3. 更新 `运营与变更.md`:第五部分追加近期讨论
|
||||
4. 回复:已记录并更新开发文档,详见 xxx
|
||||
|
||||
|
||||
159
.cursor/skills/karuo-party/README.md
Normal file
159
.cursor/skills/karuo-party/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# 卡若创业派对运营技能包
|
||||
|
||||
> **打包日期**:2026-03-20
|
||||
> **项目**:一场soul的创业实验-永平
|
||||
> **技能包路径**:`.cursor/skills/karuo-party/`
|
||||
|
||||
---
|
||||
|
||||
## 📦 打包内容
|
||||
|
||||
本技能包整合了卡若创业派对的 4 大核心运营技能:
|
||||
|
||||
1. **运营报表**:Soul派对运营数据全自动写入飞书表格
|
||||
2. **飞书视频文字下载**:从飞书妙记下载视频和文字
|
||||
3. **视频切片**:视频转录、高光识别、批量切片、成片输出
|
||||
4. **多平台分发**:一键分发到抖音/B站/视频号/小红书/快手
|
||||
|
||||
---
|
||||
|
||||
## 🔐 凭证管理
|
||||
|
||||
所有凭证统一存储在 `credentials/` 目录:
|
||||
|
||||
### 飞书凭证
|
||||
|
||||
- **`.feishu_tokens.json`**:飞书访问令牌(自动刷新)
|
||||
|
||||
### 视频平台 Cookies
|
||||
|
||||
- **视频号**:`cookies/视频号_cookies.json`(有效期 ~24-48h)
|
||||
- **B站**:`cookies/B站_cookies.json`(有效期 ~6个月)
|
||||
- **小红书**:`cookies/小红书_cookies.json`(有效期 ~1-3天)
|
||||
- **快手**:`cookies/快手_cookies.json`(有效期 ~7-30天)
|
||||
- **抖音**:`cookies/抖音_cookies.json`(账号封禁中)
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
.cursor/skills/karuo-party/
|
||||
├── SKILL.md # 主技能入口
|
||||
├── README.md # 本文件
|
||||
├── credentials/ # 凭证目录
|
||||
│ ├── .feishu_tokens.json # 飞书Token
|
||||
│ └── cookies/ # 平台Cookies
|
||||
│ ├── 视频号_cookies.json
|
||||
│ ├── B站_cookies.json
|
||||
│ ├── 小红书_cookies.json
|
||||
│ ├── 快手_cookies.json
|
||||
│ └── 抖音_cookies.json
|
||||
└── skills/ # 子技能文档
|
||||
├── 运营报表_SKILL.md
|
||||
├── 飞书视频文字下载_SKILL.md
|
||||
├── 视频切片_SKILL.md
|
||||
└── 多平台分发_SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 运营报表
|
||||
|
||||
```bash
|
||||
FEISHU_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本"
|
||||
cd "$FEISHU_SCRIPT"
|
||||
python3 soul_party_to_feishu_sheet.py 115
|
||||
```
|
||||
|
||||
### 2. 飞书视频文字下载
|
||||
|
||||
```bash
|
||||
JIYAO_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本"
|
||||
python3 "$JIYAO_SCRIPT/feishu_minutes_export_github.py" "<妙记链接>"
|
||||
python3 "$JIYAO_SCRIPT/feishu_minutes_download_video.py" "<妙记链接>"
|
||||
```
|
||||
|
||||
### 3. 视频切片
|
||||
|
||||
```bash
|
||||
VIDEO_SCRIPT="/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频切片/脚本"
|
||||
conda activate mlx-whisper
|
||||
python3 "$VIDEO_SCRIPT/soul_slice_pipeline.py" --video "<原视频.mp4>" --clips 8
|
||||
```
|
||||
|
||||
### 4. 多平台分发
|
||||
|
||||
```bash
|
||||
DIST_SCRIPT="/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本"
|
||||
python3 "$DIST_SCRIPT/distribute_all.py" --video-dir "<成片目录>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用说明
|
||||
|
||||
### 激活方式
|
||||
|
||||
当用户提到以下触发词时,自动激活本技能包:
|
||||
|
||||
- **运营报表**、**派对填表**、**派对截图**
|
||||
- **飞书视频下载**、**妙记下载**、**飞书妙记**
|
||||
- **视频剪辑**、**切片发布**、**视频切片**
|
||||
- **多平台分发**、**一键分发**、**全平台发布**
|
||||
- **卡若创业派对**、**派对运营**
|
||||
|
||||
### 执行流程
|
||||
|
||||
1. **读取对应子技能**:根据用户需求,读取 `skills/` 目录下对应的 SKILL.md
|
||||
2. **检查凭证**:确认 `credentials/` 目录下相关凭证文件存在且有效
|
||||
3. **执行命令**:按子技能文档中的命令执行(路径指向卡若AI原始脚本目录)
|
||||
4. **结果反馈**:执行完成后反馈结果
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **脚本路径**:所有脚本仍在卡若AI原始目录,本技能包仅提供技能文档和凭证管理
|
||||
2. **凭证同步**:凭证更新后需手动复制到本技能包的 `credentials/` 目录
|
||||
3. **跨平台兼容**:macOS 用 `python3`,Windows 用 `python`
|
||||
|
||||
---
|
||||
|
||||
## 🔄 凭证更新
|
||||
|
||||
### 飞书 Token
|
||||
|
||||
```bash
|
||||
cd "/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本"
|
||||
python3 auto_log.py
|
||||
# 更新后,将 .feishu_tokens.json 复制到本技能包的 credentials/ 目录
|
||||
```
|
||||
|
||||
### 平台 Cookies
|
||||
|
||||
各平台 Cookie 文件位于 `credentials/cookies/` 目录。更新方式:
|
||||
|
||||
- **视频号**:浏览器登录后,使用 `cookie_manager.py` 提取
|
||||
- **B站**:使用 `bilibili-api-python` 自动获取
|
||||
- **小红书/快手**:Playwright 自动化登录后提取
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- **主技能入口**:`SKILL.md`
|
||||
- **卡若创业派对项目**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水岸_项目管理/卡若创业派对/README.md`
|
||||
- **运营报表脚本**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/`
|
||||
- **视频切片脚本**:`/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频切片/脚本/`
|
||||
- **多平台分发脚本**:`/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/`
|
||||
|
||||
---
|
||||
|
||||
## 📋 版本记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 1.0 | 2026-03-20 | 初版:整合运营报表、视频切片、多平台分发、飞书视频文字下载 4 大技能,统一凭证管理 |
|
||||
281
.cursor/skills/karuo-party/SKILL.md
Normal file
281
.cursor/skills/karuo-party/SKILL.md
Normal file
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: 卡若创业派对运营
|
||||
description: >
|
||||
卡若创业派对全链路运营技能包。包含运营报表、视频切片、多平台分发、飞书视频文字下载等核心能力。
|
||||
所有凭证(飞书TOKEN、各平台Cookies)统一管理在 credentials/ 目录。
|
||||
当用户提到 运营报表、视频切片、多平台分发、飞书视频下载、派对运营、卡若创业派对 时自动激活。
|
||||
triggers: 运营报表、视频切片、多平台分发、飞书视频下载、派对运营、卡若创业派对、派对填表、视频剪辑、一键分发、妙记下载
|
||||
owner: 水岸
|
||||
group: 运营
|
||||
version: "1.1"
|
||||
updated: "2026-03-21"
|
||||
---
|
||||
|
||||
# 卡若创业派对运营 Skill 包
|
||||
|
||||
> **项目定位**:Soul 创业派对全链路——从派对结束到内容变现
|
||||
> **技能包路径**:`.cursor/skills/karuo-party/`
|
||||
> **凭证目录**:`credentials/`(飞书TOKEN、各平台Cookies)
|
||||
|
||||
---
|
||||
|
||||
## 零、交互默认(派对 AI)
|
||||
|
||||
- **直接操作**:报表、下载、切片、分发、脚本、凭证刷新等,**不向用户反问**「要不要跑」;按本文档路径**直接执行**终端命令(与全局 `karuo-ai.mdc`「默认零提问」一致)。
|
||||
- **缺凭证/Token**:先读 `credentials/` 与脚本内刷新逻辑;失败则**一句**说明缺哪份文件或环境变量,不展开选择题。
|
||||
- **收尾**:技术类任务仍可在会话末跟卡若复盘五块(多根工作区以 `soul-karuo-dialogue.mdc` 为准)。
|
||||
|
||||
---
|
||||
|
||||
## 一、技能包组成
|
||||
|
||||
本技能包包含以下 4 个核心子技能:
|
||||
|
||||
| # | 技能名 | 文件路径 | 触发词 | 用途 |
|
||||
|:--|:---|:---|:---|:---|
|
||||
| ① | Soul派对运营报表 | `skills/运营报表_SKILL.md` | 运营报表、派对填表、派对截图 | 截图→飞书表格→发群 |
|
||||
| ② | 飞书视频文字下载 | `skills/飞书视频文字下载_SKILL.md` | 妙记下载、飞书视频、飞书妙记 | 文字+视频→本地 |
|
||||
| ③ | 视频切片 | `skills/视频切片_SKILL.md` | 视频剪辑、切片发布 | 原视频→转录→高光→成片 |
|
||||
| ④ | 多平台分发 | `skills/多平台分发_SKILL.md` | 一键分发、全平台发布 | 成片→抖音/B站/视频号/小红书/快手 |
|
||||
|
||||
---
|
||||
|
||||
## 二、凭证管理
|
||||
|
||||
所有凭证统一存储在 `credentials/` 目录:
|
||||
|
||||
### 2.1 飞书凭证
|
||||
|
||||
| 文件 | 说明 | 用途 |
|
||||
|:---|:---|:---|
|
||||
| `.feishu_tokens.json` | 飞书访问令牌 | 运营报表、智能纪要、素材库 |
|
||||
|
||||
**Token 自动刷新**:所有脚本遇 401 自动用 refresh_token 刷新,无需手动。
|
||||
|
||||
### 2.2 视频平台 Cookies
|
||||
|
||||
| 平台 | Cookie 文件 | 有效期 | 状态 |
|
||||
|:---|:---|:---|:---|
|
||||
| 视频号 | `cookies/视频号_cookies.json` | ~24-48h | ✅ 可用 |
|
||||
| B站 | `cookies/B站_cookies.json` | ~6个月 | ✅ 可用 |
|
||||
| 小红书 | `cookies/小红书_cookies.json` | ~1-3天 | ✅ 可用 |
|
||||
| 快手 | `cookies/快手_cookies.json` | ~7-30天 | ⚠️ 需检查 |
|
||||
| 抖音 | `cookies/抖音_cookies.json` | ~2-4h | ❌ 账号封禁 |
|
||||
|
||||
**Cookie 管理**:`cookie_manager.py` 统一管理,自动迁移、API 预检、防重复登录。
|
||||
|
||||
---
|
||||
|
||||
## 三、快速使用
|
||||
|
||||
### 3.1 运营报表(派对结束后)
|
||||
|
||||
```bash
|
||||
# 路径指向卡若AI原始脚本目录
|
||||
FEISHU_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本"
|
||||
|
||||
cd "$FEISHU_SCRIPT"
|
||||
python3 auto_log.py # 刷新Token(首次或过期时)
|
||||
python3 soul_party_to_feishu_sheet.py 115 # 填表+发群
|
||||
```
|
||||
|
||||
**详细流程**:见 `skills/运营报表_SKILL.md`
|
||||
|
||||
### 3.2 飞书视频文字下载
|
||||
|
||||
```bash
|
||||
JIYAO_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本"
|
||||
|
||||
# 导出文字
|
||||
python3 "$JIYAO_SCRIPT/feishu_minutes_export_github.py" "<妙记链接>" -o "/Users/karuo/Documents/聊天记录/soul"
|
||||
|
||||
# 下载视频
|
||||
python3 "$JIYAO_SCRIPT/feishu_minutes_download_video.py" "<妙记链接>" -o "/Users/karuo/Movies/soul视频/原视频"
|
||||
```
|
||||
|
||||
**详细流程**:见 `skills/飞书视频文字下载_SKILL.md`
|
||||
|
||||
### 3.3 视频切片
|
||||
|
||||
```bash
|
||||
VIDEO_SCRIPT="/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频切片/脚本"
|
||||
|
||||
eval "$(~/miniforge3/bin/conda shell.zsh hook)"
|
||||
conda activate mlx-whisper
|
||||
python3 "$VIDEO_SCRIPT/soul_slice_pipeline.py" --video "<原视频.mp4>" --clips 8 --two-folders
|
||||
```
|
||||
|
||||
**详细流程**:见 `skills/视频切片_SKILL.md`
|
||||
|
||||
### 3.4 多平台分发
|
||||
|
||||
```bash
|
||||
DIST_SCRIPT="/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本"
|
||||
|
||||
# 定时排期:第1条立即,后续 30-120min 随机间隔
|
||||
python3 "$DIST_SCRIPT/distribute_all.py" --video-dir "<成片目录>"
|
||||
|
||||
# 立即全部发布
|
||||
python3 "$DIST_SCRIPT/distribute_all.py" --now
|
||||
```
|
||||
|
||||
**详细流程**:见 `skills/多平台分发_SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## 四、完整流程(派对结束后)
|
||||
|
||||
### Phase 1:数据入库
|
||||
|
||||
1. **运营报表**:提取效果数据 → 填表 → 发群
|
||||
2. **飞书妙记**:导出文字 → 下载视频
|
||||
|
||||
### Phase 2:智能纪要
|
||||
|
||||
1. 提炼纪要 JSON
|
||||
2. 生成纪要 HTML→PNG
|
||||
3. 纪要图入报表
|
||||
4. 纪要图发群
|
||||
|
||||
### Phase 3:视频生产
|
||||
|
||||
1. **视频切片**:转录 → 高光识别 → 批量切片 → 增强
|
||||
2. **多平台分发**:成片 → 5平台发布(定时排期)
|
||||
|
||||
### Phase 4:文章内容
|
||||
|
||||
1. 写第9章文章
|
||||
2. 上传小程序
|
||||
3. 推送飞书群
|
||||
|
||||
**详细流程**:见卡若AI项目 `02_卡人(水)/水岸_项目管理/卡若创业派对/README.md`
|
||||
|
||||
---
|
||||
|
||||
## 五、凭证更新
|
||||
|
||||
### 5.1 飞书 Token 更新
|
||||
|
||||
```bash
|
||||
cd "/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本"
|
||||
python3 auto_log.py
|
||||
```
|
||||
|
||||
更新后,将 `.feishu_tokens.json` 复制到本技能包的 `credentials/` 目录。
|
||||
|
||||
### 5.2 平台 Cookie 更新
|
||||
|
||||
各平台 Cookie 文件位于 `credentials/cookies/` 目录。更新方式:
|
||||
|
||||
1. **视频号**:浏览器登录后,使用 `cookie_manager.py` 提取
|
||||
2. **B站**:使用 `bilibili-api-python` 自动获取
|
||||
3. **小红书/快手**:Playwright 自动化登录后提取
|
||||
|
||||
更新后,Cookie 文件会自动同步到本技能包。
|
||||
|
||||
---
|
||||
|
||||
## 六、目录结构
|
||||
|
||||
```
|
||||
.cursor/skills/karuo-party/
|
||||
├── SKILL.md # 本文件(主入口)
|
||||
├── README.md # 说明文档
|
||||
├── credentials/ # 凭证目录
|
||||
│ ├── .feishu_tokens.json # 飞书Token
|
||||
│ └── cookies/ # 平台Cookies
|
||||
│ ├── 视频号_cookies.json
|
||||
│ ├── B站_cookies.json
|
||||
│ ├── 小红书_cookies.json
|
||||
│ ├── 快手_cookies.json
|
||||
│ └── 抖音_cookies.json
|
||||
└── skills/ # 子技能文件
|
||||
├── 运营报表_SKILL.md
|
||||
├── 飞书视频文字下载_SKILL.md
|
||||
├── 视频切片_SKILL.md
|
||||
└── 多平台分发_SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、使用说明
|
||||
|
||||
### 7.1 激活方式
|
||||
|
||||
当用户提到以下触发词时,自动激活本技能包:
|
||||
|
||||
- **运营报表**、**派对填表**、**派对截图**
|
||||
- **飞书视频下载**、**妙记下载**、**飞书妙记**
|
||||
- **视频剪辑**、**切片发布**、**视频切片**
|
||||
- **多平台分发**、**一键分发**、**全平台发布**
|
||||
- **卡若创业派对**、**派对运营**
|
||||
|
||||
### 7.2 执行流程
|
||||
|
||||
1. **读取对应子技能**:根据用户需求,读取 `skills/` 目录下对应的 SKILL.md
|
||||
2. **检查凭证**:确认 `credentials/` 目录下相关凭证文件存在且有效
|
||||
3. **执行命令**:按子技能文档中的命令执行(路径指向卡若AI原始脚本目录)
|
||||
4. **结果反馈**:执行完成后反馈结果
|
||||
|
||||
### 7.3 注意事项
|
||||
|
||||
- **脚本路径**:所有脚本仍在卡若AI原始目录,本技能包仅提供技能文档和凭证管理
|
||||
- **凭证同步**:凭证更新后需手动复制到本技能包的 `credentials/` 目录
|
||||
- **跨平台兼容**:macOS 用 `python3`,Windows 用 `python`
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文档
|
||||
|
||||
- **卡若创业派对项目**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水岸_项目管理/卡若创业派对/README.md`
|
||||
- **运营报表脚本**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/`
|
||||
- **视频切片脚本**:`/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频切片/脚本/`
|
||||
- **多平台分发脚本**:`/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/`
|
||||
|
||||
---
|
||||
|
||||
## 九、闭环复盘发群(派对 AI · 强制)
|
||||
|
||||
当本次对话完成**一个可交付的闭环**(需求→实现→自检,或运营链路单条跑通)时,**必须在回复末尾附带完整卡若复盘块**,并**推送到飞书群机器人**。
|
||||
|
||||
### 9.1 复盘正文格式
|
||||
|
||||
严格按卡若AI《卡若复盘格式_固定规则》:**`运营中枢/参考资料/卡若复盘格式_固定规则.md`**(五块 🎯📌💡📝▶,**复盘块内禁止表格**,标题带 **YYYY-MM-DD HH:mm**,**🎯 目标·结果·达成率** 整行 ≤30 字且含达成率百分比)。
|
||||
|
||||
### 9.2 飞书推送(机器人 Webhook v2)
|
||||
|
||||
- **凭证**:Webhook URL **不写死在仓库**;优先环境变量 **`FEISHU_PARTY_CLOSURE_WEBHOOK`**,或用户在对话中临时提供。
|
||||
- **请求体**(必须带 `msg_type`,否则返回 `params error, msg_type need`):
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": "(与回复中复盘块一致的纯文本,可含换行)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **执行方式**(任选其一,由 Agent 直接执行,不向用户索要确认):
|
||||
|
||||
```bash
|
||||
# 将下方 URL 换为 FEISHU_PARTY_CLOSURE_WEBHOOK 或用户提供的 hook
|
||||
TEXT=$(python3 -c "import json,sys; print(json.dumps({'msg_type':'text','content':{'text':sys.argv[1]}}, ensure_ascii=False))" "$(cat /path/to/review.txt)")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$TEXT" "$FEISHU_PARTY_CLOSURE_WEBHOOK"
|
||||
```
|
||||
|
||||
或使用单行 heredoc 时注意对引号转义;**禁止**只 POST 空 JSON 或缺 `msg_type` 的 body。
|
||||
|
||||
### 9.3 触发边界
|
||||
|
||||
- **要发群**:开发闭环(如 soul-admin/soul-api 联调交付)、运营闭环(报表填表+发群)、或用户明确要求「复盘发群」。
|
||||
- **可不发**:单句问答、未改代码的纯咨询(除非用户点名要发群)。
|
||||
|
||||
---
|
||||
|
||||
## 版本记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 1.1 | 2026-03-21 | 新增 §九 闭环复盘发群:卡若五块复盘 + 飞书 Webhook v2(msg_type 必填) |
|
||||
| 1.0 | 2026-03-20 | 初版:整合运营报表、视频切片、多平台分发、飞书视频文字下载 4 大技能,统一凭证管理 |
|
||||
6
.cursor/skills/karuo-party/credentials/README.md
Normal file
6
.cursor/skills/karuo-party/credentials/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 本机凭证目录
|
||||
|
||||
将飞书 token、各平台 cookies 等**仅放在本机**,文件名与结构见上级 `karuo-party` 的 SKILL / README。
|
||||
|
||||
- 本目录已在仓库根 `.gitignore` 中忽略,**不要**把真实密钥提交到 Git。
|
||||
- 若目录为空,按 `skills/karuo-party` 文档从模板复制并重命名即可。
|
||||
170
.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
Normal file
170
.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: 多平台分发
|
||||
description: >
|
||||
一键将视频分发到 5 个平台(抖音、B站、视频号、小红书、快手)。
|
||||
API 优先策略:视频号纯 API、B站 bilibili-api-python、抖音纯 API。
|
||||
支持定时排期(第1条立即发,后续 30-120 分钟随机间隔)、并行分发、去重、失败自动重试。
|
||||
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "4.0"
|
||||
updated: "2026-03-11"
|
||||
---
|
||||
|
||||
# 多平台分发 Skill(v4.0)
|
||||
|
||||
> **核心原则**:API 发布为主,Playwright 为辅。确保确定性地分发到各平台。
|
||||
> **v4.0 变更**:视频号已切换为纯 API、统一元数据生成器、定时排期优化、简介/标签/分区自动填充。
|
||||
|
||||
---
|
||||
|
||||
## 一、平台与实现方式
|
||||
|
||||
| 平台 | 实现方式 | 定时发布 | Cookie 有效期 | 120 场实测 |
|
||||
|------|----------|----------|---------------|------------|
|
||||
| **视频号** | **纯 API**(DFS 上传 + post_create) | API 原生支持 | ~24-48h | 12/12 成功 |
|
||||
| **B站** | **bilibili-api-python** API 优先 → Playwright 兜底 | API `dtime` | ~6 个月 | 12/12 成功 |
|
||||
| **小红书** | Playwright headless 自动化 | UI 定时(降级立即) | ~1-3 天 | 12/12 成功 |
|
||||
| **快手** | Playwright headless 自动化 | UI 定时 | ~7-30 天 | Cookie 过期 |
|
||||
| **抖音** | 纯 API(VOD + bd-ticket-guard) | API `timing_ts` | ~2-4h | 账号封禁中 |
|
||||
|
||||
> **关于视频号官方 API 边界**:
|
||||
> 按《视频号与腾讯相关 API 整理》结论,微信官方目前**没有开放「短视频上传/发布」接口**;本 Skill 中的视频号发布能力,属于对 `https://channels.weixin.qq.com` 视频号助手网页协议的逆向封装(DFS 上传 + `post_create`),仅在你本机使用,需自行承担协议变更与合规风险。
|
||||
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`,如需做直播/橱窗/留资集成,可基于该文档在单独 Skill 中扩展。
|
||||
|
||||
---
|
||||
|
||||
## 二、一键命令
|
||||
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本
|
||||
|
||||
# 定时排期:第1条立即,后续 30-120min 随机间隔
|
||||
python3 distribute_all.py
|
||||
|
||||
# 立即全部发布
|
||||
python3 distribute_all.py --now
|
||||
|
||||
# 只发指定平台
|
||||
python3 distribute_all.py --platforms 视频号 B站
|
||||
|
||||
# 自定义视频目录
|
||||
python3 distribute_all.py --video-dir "/path/to/videos/"
|
||||
|
||||
# 检查 Cookie / 重试失败
|
||||
python3 distribute_all.py --check
|
||||
python3 distribute_all.py --retry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、定时排期(v4.0 优化)
|
||||
|
||||
### 3.1 排期规则
|
||||
- **第 1 条**:立即发布(`first_delay=0`)
|
||||
- **第 2 条起**:前一条 + random(30, 120) 分钟
|
||||
- 若总跨度 > 24h,自动按比例压缩
|
||||
- 12 条视频典型跨度 ~10-14h
|
||||
|
||||
### 3.2 各平台定时实现
|
||||
|
||||
| 平台 | 定时方式 | 参数 |
|
||||
|------|----------|------|
|
||||
| B站 | API `meta.dtime` | Unix 时间戳(秒) |
|
||||
| 视频号 | API 暂不支持原生定时 | 描述中标注时间/手动设置 |
|
||||
| 抖音 | API `timing_ts` | Unix 时间戳 |
|
||||
| 快手 | Playwright UI | `schedule_helper.py` |
|
||||
| 小红书 | Playwright UI | `schedule_helper.py` |
|
||||
|
||||
---
|
||||
|
||||
## 四、元数据自动生成(v4.0 新增)
|
||||
|
||||
`video_metadata.py` 根据文件名自动生成各平台差异化内容:
|
||||
|
||||
```python
|
||||
from video_metadata import VideoMeta
|
||||
meta = VideoMeta.from_filename("AI最大的缺点是上下文太短.mp4")
|
||||
|
||||
meta.title("B站") # 优化后的标题
|
||||
meta.description("B站") # 标题 + 标签 + 品牌标记
|
||||
meta.tags_str("B站") # AI工具,效率提升,Soul派对,...
|
||||
meta.bilibili_meta() # B站投稿完整 meta(含 tid/tag/desc)
|
||||
meta.title_short() # 小红书短标题(≤20字)
|
||||
meta.hashtags("视频号") # #AI工具 #效率提升 ... #小程序 卡若创业派对
|
||||
```
|
||||
|
||||
### 4.1 内容结构
|
||||
- **标题**:手工优化标题库优先,否则从文件名智能提取
|
||||
- **简介**:标题 + 换行 + 话题标签 + `#小程序 卡若创业派对`
|
||||
- **标签**:基于关键词匹配(AI/创业/副业/Soul 等 12 类)+ 通用标签
|
||||
- **分区**:B站 tid=160(生活>日常)
|
||||
- **风控过滤**:`content_filter.py` 自动替换敏感词(70+ 映射,严格/宽松分级)
|
||||
|
||||
---
|
||||
|
||||
## 五、商品链接/小黄车(调研结果)
|
||||
|
||||
| 平台 | 功能 | 实现方式 | 状态 |
|
||||
|------|------|----------|------|
|
||||
| B站 | 花火计划商品链接 | 需企业认证 + 品牌合作授权 | 需手动配置 |
|
||||
| 视频号 | 挂小程序 | 视频号主页 > 设置 > 服务菜单 > 小程序 | 需手动配置 |
|
||||
| 抖音 | 小黄车 | 需开通橱窗(粉丝 ≥1000) | 账号封禁 |
|
||||
| 快手 | 商品卡片 | 需开通快手小店 | 需手动配置 |
|
||||
| 小红书 | 商品笔记 | 需开通小红书店铺 | 需手动配置 |
|
||||
|
||||
**当前做法**:在描述中统一添加 `#小程序 卡若创业派对` 引导用户搜索。
|
||||
|
||||
---
|
||||
|
||||
## 六、Cookie 管理
|
||||
|
||||
`cookie_manager.py` 统一管理:
|
||||
- 中央存储:`多平台分发/cookies/{平台}_cookies.json`
|
||||
- 自动迁移:旧路径 → 中央存储(首次使用时)
|
||||
- API 预检:5 平台各自 auth API 校验有效性
|
||||
- 防重复登录:有效 Cookie 不触发重新获取
|
||||
|
||||
---
|
||||
|
||||
## 七、去重机制
|
||||
|
||||
- 日志:`publish_log.json`(JSON Lines)
|
||||
- 去重键:`(平台名, 视频文件名)`
|
||||
- 双保险:调度器层 + 平台层
|
||||
- `--no-dedup` 跳过,`--retry` 重跑失败
|
||||
|
||||
---
|
||||
|
||||
## 八、目录结构
|
||||
|
||||
```
|
||||
木叶_视频内容/
|
||||
├── 多平台分发/ ← 本 Skill(调度器 + 共享工具)
|
||||
│ ├── SKILL.md
|
||||
│ └── 脚本/
|
||||
│ ├── distribute_all.py # 主调度器 v4
|
||||
│ ├── video_metadata.py # 统一元数据生成器(v4 新增)
|
||||
│ ├── schedule_generator.py # 定时排期(v4: 第1条立即发)
|
||||
│ ├── schedule_helper.py # Playwright 定时 UI 辅助
|
||||
│ ├── publish_result.py # 统一 PublishResult + 去重
|
||||
│ ├── title_generator.py # 标题生成(被 video_metadata 取代)
|
||||
│ ├── content_filter.py # 敏感词过滤(70+ 映射)
|
||||
│ ├── cookie_manager.py # Cookie 统一管理(5 平台 API 预检)
|
||||
│ ├── video_utils.py # 视频处理(封面、元数据)
|
||||
│ └── publish_log.json # 发布日志
|
||||
├── 抖音发布/ ← 纯 API(账号封禁中)
|
||||
├── B站发布/ ← bilibili-api-python API
|
||||
├── 视频号发布/ ← 纯 API(DFS 协议,v5)
|
||||
├── 小红书发布/ ← Playwright headless
|
||||
└── 快手发布/ ← Playwright headless
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、依赖
|
||||
|
||||
- Python 3.10+
|
||||
- httpx, bilibili-api-python, playwright, Pillow
|
||||
- ffmpeg/ffprobe(系统已安装)
|
||||
- `playwright install chromium`
|
||||
583
.cursor/skills/karuo-party/skills/视频切片_SKILL.md
Normal file
583
.cursor/skills/karuo-party/skills/视频切片_SKILL.md
Normal file
@@ -0,0 +1,583 @@
|
||||
---
|
||||
name: 视频切片
|
||||
description: Soul派对视频切片 + 快速混剪 + 切片动效包装(片头/片尾/程序化)+ 剪映思路借鉴(智能剪口播/镜头分割)。触发词含视频剪辑、切片发布、快速混剪、切片动效包装、程序化包装、片头片尾。
|
||||
group: 木
|
||||
triggers: 视频剪辑、切片发布、字幕烧录、**快速混剪、混剪预告、快剪串联、切片动效包装、程序化包装、片头片尾、批量封面、视频包装**、镜头切分、场景检测
|
||||
owner: 木叶
|
||||
version: "1.3"
|
||||
updated: "2026-03-03"
|
||||
---
|
||||
|
||||
# 视频切片
|
||||
|
||||
> **语言**:所有文档、字幕、封面文案统一使用**简体中文**。soul_enhance 自动繁转简。
|
||||
|
||||
> **Soul 视频输出**:Soul 剪辑的成片统一导出到 `/Users/karuo/Movies/soul视频/最终版/`,原视频在 `原视频/`,中间产物在 `其他/`。
|
||||
|
||||
> **联动规则**:每次执行视频切片时,自动检查是否需要「切片动效包装」或「快速混剪」。若用户提到片头/片尾/程序化包装/批量封面,则联动调用 `切片动效包装/10秒视频` 模板渲染,再与切片合成。若用户提到快速混剪/混剪预告/快剪串联,则在切片或成片生成后再调用 `脚本/quick_montage.py` 输出一条节奏版预告。
|
||||
|
||||
## ⭐ Soul派对切片流程(默认)
|
||||
|
||||
```
|
||||
原始视频 → MLX转录 → 字幕转简体 → 高光识别(API 优先/最佳模型,失败则 Ollama→规则) → 批量切片 → soul_enhance → 输出成片
|
||||
↑ ↓
|
||||
提取后立即繁转简+修正错误 封面+字幕(已简体)+加速10%+去语气词
|
||||
```
|
||||
|
||||
**切片时长**:每段为**完整的一个片段**,时长 **30 秒~300 秒**,由该完整片段起止时间决定。**标题**用一句**刺激性观点**(见 `Soul竖屏切片_SKILL.md`)。
|
||||
|
||||
**提问→回答 结构**:若片段内有人提问,前3秒优先展示**提问问题**,再播回答;高光识别填 `question` 且 `hook_3sec` 与之一致,成片整条去语助词。详见 `参考资料/视频结构_提问回答与高光.md`、`参考资料/高光识别提示词.md`。
|
||||
|
||||
**Soul 竖屏专用**:抖音/首页用竖屏成片、完整参数与流程见 → **`Soul竖屏切片_SKILL.md`**(竖屏 498×1080、crop 参数、批量命令)。
|
||||
|
||||
### 最新切片风格(当前默认)
|
||||
|
||||
以后默认按这套风格出切片与成片:
|
||||
|
||||
| 项 | 当前默认风格 |
|
||||
|------|------|
|
||||
| **封面** | **Soul 绿 + 半透明质感 + 深色渐变** |
|
||||
| **前3秒** | **优先提问→回答**,有提问时 Hook = `question` |
|
||||
| **标题** | **一句刺激性观点**,文件名 = 封面标题 = `highlights.title` |
|
||||
| **字幕** | 居中、白字黑描边、关键词亮金黄高亮 |
|
||||
| **节奏** | 去语助词 + 整体加速 10% |
|
||||
| **成片尺寸** | 竖屏 **498×1080** |
|
||||
|
||||
这套风格与 `参考资料/高光识别提示词.md`、`参考资料/热点切片_标准流程.md`、`Soul竖屏切片_SKILL.md` 保持一致。
|
||||
|
||||
### 一键命令(Soul派对专用)
|
||||
|
||||
#### 一体化流水线(推荐)
|
||||
|
||||
```bash
|
||||
cd 03_卡木(木)/木叶_视频内容/视频切片/脚本
|
||||
conda activate mlx-whisper
|
||||
python3 soul_slice_pipeline.py --video "/path/to/soul派对会议第57场.mp4" --clips 6
|
||||
|
||||
# 仅重新烧录(字幕转简体后重跑增强)
|
||||
python3 soul_slice_pipeline.py -v "视频.mp4" -n 6 --skip-transcribe --skip-highlights --skip-clips
|
||||
|
||||
# 切片+成片后,额外生成一条快速混剪
|
||||
python3 soul_slice_pipeline.py -v "视频.mp4" -n 8 --two-folders --quick-montage
|
||||
```
|
||||
|
||||
流程:**转录 → 字幕转简体 → 高光识别 → 批量切片 → 增强**
|
||||
|
||||
#### 分步命令
|
||||
|
||||
```bash
|
||||
# 1. 转录(MLX Whisper,约3分钟/2.5小时视频)
|
||||
eval "$(~/miniforge3/bin/conda shell.zsh hook)"
|
||||
conda activate mlx-whisper
|
||||
mlx_whisper audio.wav --model mlx-community/whisper-small-mlx --language zh --output-format all
|
||||
|
||||
# 2. 高光识别(API 优先,未配置则 Ollama → 规则;流水线会在读取 transcript 前自动转简体)
|
||||
python3 identify_highlights.py -t transcript.srt -o highlights.json -n 6
|
||||
# 需配置 OPENAI_API_KEY 或 OPENAI_API_BASES/KEYS/MODELS,默认模型 gpt-4o
|
||||
|
||||
# 3. 切片
|
||||
python3 batch_clip.py -i 视频.mp4 -l highlights.json -o clips/ -p soul
|
||||
|
||||
# 4. 增强处理(封面+字幕+加速,soul_enhance)
|
||||
python3 soul_enhance.py -c clips/ -l highlights.json -t transcript.srt -o clips_enhanced/
|
||||
```
|
||||
|
||||
### 快速混剪(新增)
|
||||
|
||||
适用场景:已经有 `切片/` 或 `成片/`,需要快速出一条 20~40 秒节奏版预告、招商预热视频、短视频串联版。
|
||||
|
||||
**默认策略**:
|
||||
|
||||
| 项 | 规则 |
|
||||
|------|------|
|
||||
| **顺序** | 优先按 `virality_score` / `rank` 排序;无分数时按序号 |
|
||||
| **取样** | 每条默认截取 **4 秒**高密度片段 |
|
||||
| **成片目录输入** | 自动跳过前 **2.6 秒**封面,避免混剪里全是封面 |
|
||||
| **输出** | 统一分辨率、统一节奏后拼成一条 `快速混剪.mp4` |
|
||||
|
||||
```bash
|
||||
# 从成片目录生成快速混剪(推荐)
|
||||
python3 脚本/quick_montage.py \
|
||||
-i "/path/to/成片" \
|
||||
-o "/path/to/快速混剪.mp4" \
|
||||
-l "/path/to/highlights.json" \
|
||||
--source-kind final \
|
||||
-n 8 \
|
||||
-s 4
|
||||
|
||||
# 一体化流水线里直接附带生成
|
||||
python3 脚本/soul_slice_pipeline.py \
|
||||
-v "/path/to/原视频.mp4" \
|
||||
--two-folders \
|
||||
--quick-montage \
|
||||
--montage-source finals \
|
||||
--montage-max-clips 8 \
|
||||
--montage-seconds 4
|
||||
```
|
||||
|
||||
#### 按章节主题提取(推荐:第9章单场成片)
|
||||
|
||||
以**章节 .md 正文**为来源提取核心主题,再在转录稿中匹配时间,不限于 5 分钟、片段数与章节结构一致。详见 `参考资料/主题片段提取规则.md`。
|
||||
|
||||
```bash
|
||||
# 从章节生成 highlights,再走 batch_clip + soul_enhance
|
||||
python3 chapter_themes_to_highlights.py -c "第112场.md" -t transcript.srt -o highlights_from_chapter.json
|
||||
python3 batch_clip.py -i 视频.mp4 -l highlights_from_chapter.json -o clips/ -p soul112
|
||||
python3 soul_enhance.py -c clips/ -l highlights_from_chapter.json -t transcript.srt -o clips_enhanced/
|
||||
```
|
||||
|
||||
- **主题来源**:章节 .md 按 `---` 分块,每块一个主题;文件名由 batch_clip 按 `前缀_序号_标题` 生成(标题仅保留中文与安全字符)。
|
||||
|
||||
### Soul 竖屏成片(横版源 → 竖屏中段去白边)
|
||||
|
||||
**约定**:以后剪辑 Soul 视频,成片统一做「竖屏中段」裁剪:横版 1920×1080 只保留中间竖条并去掉左右白边,输出 498×1080 竖屏。
|
||||
|
||||
| 步骤 | 说明 |
|
||||
|------|------|
|
||||
| 源 | 横版 1920×1080(soul_enhance 输出) |
|
||||
| 1 | 取竖条 608×1080,起点 **x=483**(相对画面左) |
|
||||
| 2 | 裁掉左侧白边 60px、右侧白边 50px → 内容区宽 498 |
|
||||
| 输出 | **498×1080** 竖屏,仅内容窗口 |
|
||||
|
||||
**FFmpeg 一条命令(固定参数):**
|
||||
|
||||
```bash
|
||||
# 单文件。输入为 1920×1080 的 enhanced 成片
|
||||
ffmpeg -y -i "输入_enhanced.mp4" -vf "crop=608:1080:483:0,crop=498:1080:60:0" -c:a copy "输出_竖屏中段.mp4"
|
||||
```
|
||||
|
||||
**批量对某目录下所有 \*_enhanced.mp4 做竖屏中段:**
|
||||
|
||||
```bash
|
||||
# 脚本目录下执行,或直接调用
|
||||
python3 脚本/soul_vertical_crop.py --dir "/path/to/clips_enhanced" --suffix "_竖屏中段"
|
||||
```
|
||||
|
||||
参数说明见:`参考资料/竖屏中段裁剪参数说明.md`。
|
||||
|
||||
### 增强功能说明
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **封面贴片** | 前2.5秒 Hook,苹方/思源黑体 |
|
||||
| **字幕烧录** | 关键词加粗加大亮金黄突出,去语助词+去空格 |
|
||||
| **加速10%** | 节奏更紧凑,适合短视频 |
|
||||
|
||||
### 时间预估
|
||||
|
||||
| 步骤 | 2.5小时视频 |
|
||||
|------|------------|
|
||||
| MLX转录 | 3分钟 |
|
||||
| 切片10个 | 2分钟 |
|
||||
| 增强处理 | 8分钟 |
|
||||
| **总计** | **约13分钟** |
|
||||
|
||||
---
|
||||
|
||||
## AI 生成与 LTX 可选集成
|
||||
|
||||
在「已有录播 → 转录→高光→切片→成片」主流程外,可选用 **LTX**(GitHub: Lightricks/LTX-Video、LTX-2、LTX-Desktop-MPS)实现:
|
||||
|
||||
| 能力 | 用途 |
|
||||
|------|------|
|
||||
| **Retake**(LTX-2 / LTX Desktop) | 对已有视频**某段时间**重生成,替换口误/补拍,再走成片流程 |
|
||||
| **Text/Image/Audio to video** | AI 生成口播替代、片头片尾、插播片段,生成 mp4 后进 `切片/` 或成片流程 |
|
||||
| **Video extension** | 片段前后自然延长,衔接切片 |
|
||||
| **自动 Prompt 增强** | 高光/标题文案 → 更易被生成模型理解,便于 I2V/Retake |
|
||||
|
||||
**详细能力表与 API/本地/Desktop 接入**:见 `参考资料/LTX_能力与集成说明.md`。
|
||||
**Soul 竖屏场景**:见 `Soul竖屏切片_SKILL.md` 第九节「AI 生成与 LTX 可选集成」。
|
||||
**约定**:LTX 生成的片段统一经 soul_enhance(封面+字幕+竖屏)输出,与录播成片一致。
|
||||
|
||||
---
|
||||
|
||||
## 📹 通用视频处理
|
||||
|
||||
一键处理视频:转录 → 字幕清洗 → 视频增强 → 烧录字幕 → **输出单个成片**
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 一键命令
|
||||
|
||||
```bash
|
||||
# 最简用法 - 输出: 视频名_带字幕.mp4
|
||||
python3 /Users/karuo/Documents/个人/卡若AI/04_效率工具/视频切片/scripts/one_video.py -i "视频.mp4"
|
||||
|
||||
# 指定输出路径
|
||||
python3 scripts/one_video.py -i "视频.mp4" -o "成片.mp4"
|
||||
```
|
||||
|
||||
### 处理流程
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 提取音频 │──▶│MLX转录 │──▶│字幕清洗 │──▶│视频增强 │──▶│烧录字幕 │
|
||||
│ (5秒) │ │(1-3分钟)│ │繁转简 │ │降噪美颜 │ │ │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ 单个带字幕成片 │
|
||||
│ 可直接发布 │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
### 时间预估
|
||||
|
||||
| 视频时长 | 处理时间 |
|
||||
|---------|---------|
|
||||
| 5分钟 | 1-2分钟 |
|
||||
| 30分钟 | 5-8分钟 |
|
||||
| 1小时 | 10-15分钟 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 自动优化项
|
||||
|
||||
脚本自动完成以下优化,无需手动操作:
|
||||
|
||||
| 优化项 | 说明 |
|
||||
|--------|------|
|
||||
| 繁转简 | 自动将繁体字幕转为简体 |
|
||||
| 去语气词 | 删除"嗯"、"啊"、"那个"等 |
|
||||
| 修正错误 | 自动修正常见转录错误 |
|
||||
| 音频降噪 | FFT降噪+高低频过滤 |
|
||||
| 画面美颜 | 亮度+饱和度微调 |
|
||||
| 音量标准化 | 统一音量级别 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 手动分步操作
|
||||
|
||||
如需更精细控制,可分步执行:
|
||||
|
||||
### 1. 转录
|
||||
|
||||
```bash
|
||||
# 激活环境
|
||||
eval "$(~/miniforge3/bin/conda shell.zsh hook)"
|
||||
conda activate mlx-whisper
|
||||
|
||||
# 提取音频
|
||||
ffmpeg -y -i "视频.mp4" -vn -ar 16000 -ac 1 audio.wav
|
||||
|
||||
# MLX Whisper转录
|
||||
mlx_whisper audio.wav --model mlx-community/whisper-small-mlx --language zh --output-format srt
|
||||
```
|
||||
|
||||
### 2. 字幕清洗
|
||||
|
||||
```bash
|
||||
# 繁转简+修正错误
|
||||
python3 scripts/fix_subtitles.py --input transcript.srt --output clean.srt
|
||||
```
|
||||
|
||||
### 3. 视频增强
|
||||
|
||||
```bash
|
||||
# 降噪+美颜
|
||||
ffmpeg -y -i "视频.mp4" \
|
||||
-vf "eq=brightness=0.05:saturation=1.1" \
|
||||
-af "afftdn=nf=-25:nr=10:nt=w,highpass=f=80,lowpass=f=8000,volume=1.2" \
|
||||
-c:v h264_videotoolbox -b:v 5M \
|
||||
-c:a aac -b:a 128k \
|
||||
enhanced.mp4
|
||||
```
|
||||
|
||||
### 4. 烧录字幕
|
||||
|
||||
```bash
|
||||
# Clean版(推荐)
|
||||
python3 scripts/burn_subtitles_clean.py -i enhanced.mp4 -s clean.srt -o 成片.mp4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 字幕样式
|
||||
|
||||
### 默认样式(Clean版)
|
||||
|
||||
| 元素 | 字号 | 颜色 | 效果 |
|
||||
|------|------|------|------|
|
||||
| 内容字幕 | 42px(竖屏)/ 36px(横屏) | 白色 | 黑色描边,无阴影 |
|
||||
| 关键词 | 同上 | 金黄色 | 自动高亮 |
|
||||
|
||||
### 关键词高亮列表
|
||||
|
||||
自动高亮的关键词(金黄色):
|
||||
- 数字:100万、30万、10万、5万、1万
|
||||
- 概念:私域、AI、自动化、矩阵、IP、获客、变现、转化
|
||||
- 平台:抖音、公众号、微信、存客宝
|
||||
|
||||
---
|
||||
|
||||
## 🔊 音频处理参数
|
||||
|
||||
| 滤镜 | 作用 | 参数 |
|
||||
|------|------|------|
|
||||
| highpass | 去低频杂音 | f=80Hz |
|
||||
| lowpass | 去高频噪音 | f=8000Hz |
|
||||
| afftdn | FFT降噪 | nf=-25, nr=10 |
|
||||
| volume | 音量调整 | 1.2倍 |
|
||||
|
||||
---
|
||||
|
||||
## 📁 脚本列表
|
||||
|
||||
| 脚本 | 功能 | 使用频率 |
|
||||
|------|------|---------|
|
||||
| **soul_slice_pipeline.py** | Soul 切片一体化流水线 | ⭐⭐⭐ 最常用 |
|
||||
| **soul_enhance.py** | 封面+字幕(简体)+加速+去语气词 | ⭐⭐⭐ |
|
||||
| **soul_vertical_crop.py** | Soul 竖屏中段批量裁剪(横版→498×1080 去白边) | ⭐⭐⭐ |
|
||||
| **kill_ffmpeg_when_clip_done.py** | 剪辑结束后自动关掉 ffmpeg(监视剪映/PID 或立即杀) | ⭐ 按需 |
|
||||
| **scene_detect_to_highlights.py** | 镜头/场景检测 → highlights.json(PySceneDetect,可接 batch_clip) | ⭐⭐ |
|
||||
| chapter_themes_to_highlights.py | 按章节 .md 主题提取片段(本地模型→highlights.json) | ⭐⭐⭐ |
|
||||
| identify_highlights.py | 高光识别(API 优先→Ollama→规则,默认 gpt-4o) | ⭐⭐ |
|
||||
| batch_clip.py | 批量切片 | ⭐⭐ |
|
||||
| one_video.py | 单视频一键成片 | ⭐⭐ |
|
||||
| burn_subtitles_clean.py | 字幕烧录(无阴影) | ⭐ |
|
||||
| fix_subtitles.py | 字幕清洗(繁转简) | ⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 🛠 环境配置
|
||||
|
||||
### 已安装(默认使用MLX Whisper)
|
||||
|
||||
- **MLX Whisper**: `~/miniforge3/envs/mlx-whisper` ⭐ **默认转录引擎**
|
||||
- Apple Silicon优化,比CPU Whisper快10倍+
|
||||
- 2.5小时视频转录仅需3分钟
|
||||
- **字体**: `03_卡木(木)/木叶_视频内容/视频切片/fonts/`(优先)
|
||||
- **字幕**: 统一简体中文(soul_enhance 自动繁转简)
|
||||
|
||||
### 转录命令(默认)
|
||||
|
||||
```bash
|
||||
# 激活MLX环境
|
||||
eval "$(~/miniforge3/bin/conda shell.zsh hook)"
|
||||
conda activate mlx-whisper
|
||||
|
||||
# MLX Whisper转录(推荐)
|
||||
mlx_whisper audio.wav --model mlx-community/whisper-small-mlx --language zh --output-format all
|
||||
```
|
||||
|
||||
### 高光识别模型(API 优先)
|
||||
|
||||
高光识别默认使用**当前可用最佳模型**:优先走 **OpenAI 兼容 API**(见下),未配置或失败时再用本地 Ollama,最后规则兜底。
|
||||
|
||||
- **单接口**:`OPENAI_API_BASE`、`OPENAI_API_KEY`、`OPENAI_MODEL`(默认 `gpt-4o`)。
|
||||
- **多接口故障切换**:`OPENAI_API_BASES`、`OPENAI_API_KEYS`、`OPENAI_MODELS`(逗号分隔,按顺序尝试)。
|
||||
- 不写死密钥,从环境变量读取;详见 `运营中枢/参考资料/卡若AI异常处理与红线.md` 与 API 稳定性规则。
|
||||
|
||||
### 依赖检查
|
||||
|
||||
```bash
|
||||
# FFmpeg
|
||||
ffmpeg -version
|
||||
|
||||
# MLX环境
|
||||
eval "$(~/miniforge3/bin/conda shell.zsh hook)"
|
||||
conda activate mlx-whisper
|
||||
python -c "import mlx_whisper; print('OK')"
|
||||
|
||||
# Python库
|
||||
pip3 list | grep -E "moviepy|Pillow|opencc|openai"
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pip3 install --break-system-packages moviepy Pillow opencc-python-reimplemented
|
||||
|
||||
# 镜头切分(可选):PySceneDetect
|
||||
pip3 install 'scenedetect[opencv]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 剪辑结束后自动关 ffmpeg
|
||||
|
||||
脚本 **soul_enhance**、**batch_clip**、**soul_slice_pipeline** 在退出时(含 Ctrl+C)会自动结束本进程启动的 ffmpeg 子进程,避免剪辑结束后仍占用 CPU。
|
||||
|
||||
若使用 **剪映/VideoFusion** 等 GUI 剪辑,可先运行监视脚本,剪辑应用退出后自动杀 ffmpeg:
|
||||
|
||||
```bash
|
||||
# 先启动监视,再打开剪映;关掉剪映后会自动结束 ffmpeg
|
||||
python3 脚本/kill_ffmpeg_when_clip_done.py --app VideoFusion
|
||||
|
||||
# 或监视指定 PID
|
||||
python3 脚本/kill_ffmpeg_when_clip_done.py --pid 12345
|
||||
|
||||
# 仅立即杀掉当前所有 ffmpeg
|
||||
python3 脚本/kill_ffmpeg_when_clip_done.py --kill-now
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 转录不准确?
|
||||
A: 使用medium模型:将脚本中的`whisper-small-mlx`改为`whisper-medium-mlx`
|
||||
|
||||
### Q: 字幕太小/太大?
|
||||
A: 修改`one_video.py`第142行的`font_size`值
|
||||
|
||||
### Q: 处理太慢?
|
||||
A:
|
||||
1. 视频已自动使用VideoToolbox GPU加速
|
||||
2. 字幕默认限制80条以内
|
||||
|
||||
### Q: 输出文件太大?
|
||||
A: 降低码率:将`-b:v 5M`改为`-b:v 3M`
|
||||
|
||||
---
|
||||
|
||||
## 🎬 切片动效包装(联动能力)
|
||||
|
||||
用 React 程序化生成片头/片尾/封面,与切片产出一键合成。**每次执行视频切片时,若用户提到片头/片尾/包装/批量封面,则联动本能力**。
|
||||
|
||||
### 联动规则(必守)
|
||||
|
||||
| 场景 | 是否联动 | 操作 |
|
||||
|:---|:---|:---|
|
||||
| 用户说片头/片尾/程序化包装/批量封面 | ✓ | 先执行切片 → 渲染动效模板 → 合成 |
|
||||
| 默认 Soul 切片、单视频成片 | 可选 | 执行后提示可选用切片动效包装 |
|
||||
|
||||
### 10秒视频模板(卡若AI 品牌)
|
||||
|
||||
路径:`视频切片/切片动效包装/10秒视频/`
|
||||
|
||||
| Composition | 说明 |
|
||||
|:---|:---|
|
||||
| Video10s | 简洁版:渐变 + 标题 + 副标题 |
|
||||
| Video10sRich | 内容丰富版:粒子 + 极限环 + 流动线条 |
|
||||
|
||||
规格:竖屏 1080×1920,10 秒,30fps。
|
||||
|
||||
### 一键命令(动效包装)
|
||||
|
||||
```bash
|
||||
cd 03_卡木(木)/木叶_视频内容/视频切片/切片动效包装/10秒视频
|
||||
|
||||
# 预览
|
||||
npm run dev
|
||||
|
||||
# 渲染片头(简洁版)
|
||||
npx remotion render src/index.ts Video10s /Users/karuo/Documents/卡若Ai的文件夹/导出/程序化视频/片头.mp4
|
||||
|
||||
# 渲染片头(丰富版)
|
||||
npx remotion render src/index.ts Video10sRich /Users/karuo/Documents/卡若Ai的文件夹/导出/程序化视频/片头_丰富.mp4
|
||||
```
|
||||
|
||||
### 与切片合成流程
|
||||
|
||||
```
|
||||
切片产出(clips_enhanced/)
|
||||
↓
|
||||
【联动】渲染片头/片尾
|
||||
↓
|
||||
ffmpeg 合成:片头 + 切片 + 片尾
|
||||
```
|
||||
|
||||
### 参考资料
|
||||
|
||||
- 速查:`视频切片/切片动效包装/参考资料/切片动效包装速查.md`
|
||||
- 官方:https://www.remotion.dev/docs
|
||||
|
||||
---
|
||||
|
||||
## 🎞 剪映思路借鉴与自实现(可选能力)
|
||||
|
||||
> 参考 **剪映专业版**(`/Applications/VideoFusion-macOS.app`)内可读配置与流程,用开源方案自实现「智能剪口播」与「智能镜头分割」,不依赖剪映二进制。详见:`参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md`。
|
||||
|
||||
### 智能剪口播(口播稿 → 按文案/时间轴切片段)
|
||||
|
||||
| 剪映逻辑 | 本技能对应实现 |
|
||||
|----------|----------------|
|
||||
| 语音→文字 + 时间戳 | **MLX Whisper** 转录 → `transcript.srt` |
|
||||
| 按文案智能剪、口播稿↔时间轴对齐 | **高光识别**(`identify_highlights` / `chapter_themes_to_highlights`)→ `highlights.json` → `batch_clip` |
|
||||
| 前端配置键 | `script_ai_cut_config`、`transcript_options`(仅作对照,不读写剪映) |
|
||||
|
||||
**结论**:现有流程「转录 → 字幕转简 → 高光识别 → 批量切片 → soul_enhance」已覆盖「智能剪口播」能力;按句/按段细切可与 `transcript.srt` 时间戳结合,在 `highlights.json` 中按句生成条目即可。
|
||||
|
||||
### 智能镜头分割(按镜头/场景切分)
|
||||
|
||||
剪映 **SceneEditDetection** 思路(仅借鉴思路与参数,算法用开源实现):
|
||||
|
||||
- **输入**:帧序列;剪映内部为 96×96 小图 + 数组缓冲。
|
||||
- **算法思路**:图像特征 + 滑动窗口 + 后处理阈值 → 输出镜头边界。
|
||||
- **剪映可读参数**(`SceneEditDetection/config.json`):
|
||||
`sliding_window_size: 7`、`img_feat_dims: 128`、`post_process_threshold: 0.35`、backbone/predhead 模型名(内部用,不引用)。
|
||||
|
||||
**自实现方案**:使用 **PySceneDetect**(ContentDetector/AdaptiveDetector),按阈值与最小场景长度得到切点,再转为与 `batch_clip` 兼容的 `highlights.json`。
|
||||
|
||||
**一键:镜头检测 → highlights → 批量切片 → 增强**
|
||||
|
||||
```bash
|
||||
cd 03_卡木(木)/木叶_视频内容/视频切片/脚本
|
||||
pip install 'scenedetect[opencv]' # 仅首次
|
||||
|
||||
# 镜头检测 → 生成 highlights.json
|
||||
python3 scene_detect_to_highlights.py -i "原视频.mp4" -o "输出目录/highlights_from_scenes.json" -t 27 --min-scene-len 15
|
||||
|
||||
# 用生成的 highlights 做切片 + 增强(与现有流水线一致)
|
||||
python3 batch_clip.py -i "原视频.mp4" -l "输出目录/highlights_from_scenes.json" -o "输出目录/clips/" -p scene
|
||||
python3 soul_enhance.py -c "输出目录/clips/" -l "输出目录/highlights_from_scenes.json" -t "输出目录/transcript.srt" -o "输出目录/clips_enhanced/"
|
||||
```
|
||||
|
||||
**参数速查**:
|
||||
|
||||
| 参数 | 说明 | 建议 |
|
||||
|------|------|------|
|
||||
| `--threshold` / `-t` | 内容变化阈值,越大切点越少 | 27(可试 20~35) |
|
||||
| `--min-scene-len` | 最小场景长度(帧) | 15 |
|
||||
| `--min-duration` | 过滤短于 N 秒的片段 | 按需 |
|
||||
| `--max-clips` / `-n` | 最多保留片段数 | 0=不限制 |
|
||||
|
||||
**与「高光切片」二选一**:
|
||||
- **高光切片**:按话题/金句/提问(需转录 + 高光识别),适合口播、访谈。
|
||||
- **镜头切片**:按画面切换切分,适合多机位、快剪、无稿素材;可先跑 `scene_detect_to_highlights` 再走同一套 `batch_clip` + `soul_enhance`。
|
||||
|
||||
### 参考资料(剪映与流程)
|
||||
|
||||
- **剪映逆向分析**:`03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md`
|
||||
- 智能剪口播 H5 路径、智能片段分割 config 与参数、自实现建议与合规说明。
|
||||
- **热点切片标准流程**:`参考资料/热点切片_标准流程.md`(五步、两目录、命令速查)。
|
||||
- **高光识别提示词**:`参考资料/高光识别提示词.md`(提问→回答、节奏感、快速混剪优先片段规则)。
|
||||
|
||||
---
|
||||
|
||||
## 📊 输出示例
|
||||
|
||||
```
|
||||
输入: 会议录像.mp4 (500MB, 30分钟)
|
||||
↓
|
||||
输出: 会议录像_带字幕.mp4 (200MB)
|
||||
- 中文字幕已烧录
|
||||
- 音频已降噪
|
||||
- 画面已优化
|
||||
- 可直接发布抖音/视频号
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 工作目录
|
||||
|
||||
```
|
||||
03_卡木(木)/木叶_视频内容/视频切片/
|
||||
├── 脚本/
|
||||
│ ├── soul_slice_pipeline.py # ⭐ Soul 一体化
|
||||
│ ├── soul_enhance.py # ⭐ 封面+字幕+加速
|
||||
│ ├── scene_detect_to_highlights.py # 镜头检测→highlights(剪映思路自实现)
|
||||
│ ├── one_video.py # 单视频成片
|
||||
│ └── ...
|
||||
├── 参考资料/
|
||||
│ ├── 剪映_智能剪口播与智能片段分割_逆向分析.md # 剪映思路与参数参考
|
||||
│ ├── 热点切片_标准流程.md
|
||||
│ └── 竖屏中段裁剪参数说明.md
|
||||
├── 切片动效包装/ # 联动能力:片头/片尾/程序化
|
||||
│ ├── 10秒视频/ # React 程序化模板
|
||||
│ └── 参考资料/切片动效包装速查.md
|
||||
├── fonts/
|
||||
└── SKILL.md
|
||||
```
|
||||
522
.cursor/skills/karuo-party/skills/运营报表_SKILL.md
Normal file
522
.cursor/skills/karuo-party/skills/运营报表_SKILL.md
Normal file
@@ -0,0 +1,522 @@
|
||||
---
|
||||
name: Soul派对运营报表
|
||||
description: Soul 派对运营数据全自动写入飞书表格(按月份选 2月/3月 标签)→ 会议纪要图片入表 → 发飞书群(数据+纪要图);与智能纪要联动,一站式可执行。含 Token 自动刷新、写入校验、小程序数据、派对录屏链接。完整流程可复制执行,支持基因胶囊打包。
|
||||
triggers: 运营报表、派对填表、派对截图填表发群、会议纪要上传、本月运营数据、全部月份统计、派对纪要、智能纪要、106场、107场、113场、114场、115场
|
||||
parent: 飞书管理
|
||||
owner: 水桥
|
||||
group: 水
|
||||
version: "3.0"
|
||||
updated: "2026-03-04"
|
||||
---
|
||||
|
||||
# Soul 派对运营报表 · 基因胶囊
|
||||
|
||||
> **一句话**:派对截图 + TXT → 飞书运营报表(按月份选表)→ 填数据 + 填纪要图 + 派对录屏链接 + 发群(文字 + 图片),与**会议纪要**联动,完整流程可复制执行,可打包为基因胶囊。
|
||||
|
||||
---
|
||||
|
||||
## 零、完整流程提取(可复制执行)
|
||||
|
||||
以下为从「派对结束」到「报表+群消息+纪要图」全链路的**逐步清单**与**一键命令**,便于 AI 或人工按序执行。
|
||||
|
||||
### 0.1 流程图
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 输入
|
||||
A1[关闭页截图] --> A2[小助手弹窗]
|
||||
A2 --> A3[派对 TXT]
|
||||
A3 --> A4[飞书妙记链接]
|
||||
end
|
||||
subgraph 步骤
|
||||
B1[1. 注册场次+填数据] --> B2[2. 发群文字]
|
||||
B2 --> B3[3. 生成纪要图]
|
||||
B3 --> B4[4. 纪要图入表]
|
||||
B4 --> B5[5. 纪要图发群]
|
||||
end
|
||||
subgraph 输出
|
||||
C1[飞书运营报表]
|
||||
C2[飞书群消息]
|
||||
end
|
||||
A1 --> B1
|
||||
B1 --> C1
|
||||
B2 --> C2
|
||||
B4 --> C1
|
||||
B5 --> C2
|
||||
```
|
||||
|
||||
### 0.2 前置条件
|
||||
|
||||
| 项 | 说明 |
|
||||
|:---|:---|
|
||||
| Python 3 + requests | `pip3 install requests` |
|
||||
| 飞书 Token | 脚本目录下 `.feishu_tokens.json`,过期时运行 `python3 auto_log.py` |
|
||||
| 场次已注册 | 在 `soul_party_to_feishu_sheet.py` 中已添加 ROWS、SESSION_DATE_COLUMN、SESSION_MONTH、PARTY_VIDEO_LINKS(可选)、MINIPROGRAM_EXTRA / MINIPROGRAM_EXTRA_3(可选) |
|
||||
| 派对 TXT | 如 `soul 派对 115场 20260304.txt`,用于纪要文本/纪要图 |
|
||||
|
||||
### 0.3 逐步命令(以 115 场为例)
|
||||
|
||||
| 步 | 动作 | 输入 | 命令 | 输出/校验 |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| 1 | 填效果数据+小程序+派对录屏+发群 | 场次号 115 | `cd 飞书管理/脚本 && python3 soul_party_to_feishu_sheet.py 115` | 控制台见「已写入」「已同步推送到飞书群」「已写入派对录屏链接」 |
|
||||
| 2 | 纪要文本入表(可选) | TXT 路径、日期列 4 | `python3 write_party_minutes_from_txt.py "/path/to/soul 派对 115场 20260304.txt" 4` | 控制台见「已写入派对智能纪要到今日总结」 |
|
||||
| 3 | 生成纪要图 | 见智能纪要 Skill | JSON→HTML→截图,输出到 `卡若Ai的文件夹/报告/soul_115场_智能纪要_20260304.png` | 得到 PNG 文件 |
|
||||
| 4 | 纪要图入表 | PNG 路径、sheet-id、date-col | `python3 feishu_write_minutes_to_sheet.py --party-image "卡若Ai的文件夹/报告/soul_115场_智能纪要_20260304.png" --sheet-id bJR5sA --date-col 4` | 控制台见「已上传派对智能纪要图片」 |
|
||||
| 5 | 纪要图发群 | PNG 路径 | `cd 智能纪要/脚本 && python3 send_to_feishu.py --image "卡若Ai的文件夹/报告/soul_115场_智能纪要_20260304.png"` | 飞书群收到长图 |
|
||||
|
||||
**路径约定**:飞书管理脚本目录 = `02_卡人(水)/水桥_平台对接/飞书管理/脚本/`;智能纪要脚本 = `02_卡人(水)/水桥_平台对接/智能纪要/脚本/`;报告输出 = `卡若Ai的文件夹/报告/`。
|
||||
|
||||
### 0.4 一键顺序命令块(复制即用)
|
||||
|
||||
```bash
|
||||
# 假设已配置 115 场且 TXT 与报告路径如下,按顺序执行
|
||||
FEISHU_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本"
|
||||
JIYAO_SCRIPT="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本"
|
||||
REPORT="/Users/karuo/Documents/卡若Ai的文件夹/报告"
|
||||
TXT="/Users/karuo/Documents/聊天记录/soul/soul 派对 115场 20260304.txt"
|
||||
|
||||
cd "$FEISHU_SCRIPT"
|
||||
python3 auto_log.py
|
||||
python3 soul_party_to_feishu_sheet.py 115
|
||||
python3 write_party_minutes_from_txt.py "$TXT" 4
|
||||
|
||||
# 纪要图需先按智能纪要 Skill 生成 HTML 再截图得到 PNG,再执行:
|
||||
# python3 feishu_write_minutes_to_sheet.py --party-image "$REPORT/soul_115场_智能纪要_20260304.png" --sheet-id bJR5sA --date-col 4
|
||||
# cd "$JIYAO_SCRIPT" && python3 send_to_feishu.py --image "$REPORT/soul_115场_智能纪要_20260304.png"
|
||||
```
|
||||
|
||||
### 0.5 新场次从零到完成清单
|
||||
|
||||
1. **在 `soul_party_to_feishu_sheet.py` 中**:添加 `ROWS['116']`、`SESSION_DATE_COLUMN['116']`、`SESSION_MONTH['116']`,以及在 `_maybe_send_group` 的 `date_label`、`src_date` 中加 `'116'`;若需派对录屏则填 `PARTY_VIDEO_LINKS['116']`;若需小程序则填 `MINIPROGRAM_EXTRA_3['5']`(3 月 5 日)。
|
||||
2. **执行填表**:`python3 soul_party_to_feishu_sheet.py 116`。
|
||||
3. **可选**:纪要文本 `write_party_minutes_from_txt.py "<txt>" 5`;纪要图按智能纪要生成后 `feishu_write_minutes_to_sheet.py --party-image <png> --sheet-id bJR5sA --date-col 5`,再 `send_to_feishu.py --image <png>`。
|
||||
|
||||
### 0.6 故障排查速查
|
||||
|
||||
| 现象 | 处理 |
|
||||
|:---|:---|
|
||||
| 未找到日期列 | 先 `python3 auto_log.py` 再重试;确认 SESSION_DATE_COLUMN、SESSION_MONTH 与表头一致 |
|
||||
| 90202 wrong range | 单格写入时 range 写成 `E29:E29` 形式 |
|
||||
| 派对录屏未写入 | 检查 PARTY_VIDEO_LINKS 是否非空且格式为完整 URL |
|
||||
| 小程序数据未写入 | 3 月用 MINIPROGRAM_EXTRA_3,键为当月「日期号」如 '4' |
|
||||
| 飞书群未收到 | 检查 Webhook、机器人是否启用 |
|
||||
|
||||
---
|
||||
|
||||
## 一站式完整流程(填数据 → 填图片 → 发群)
|
||||
|
||||
**目标**:同一场派对做完「填运营报表数据 → 把会议纪要图片填进报表 → 把纪要图发到飞书群」,顺序执行、流程清晰。
|
||||
|
||||
| 步骤 | 动作 | 命令 / 说明 |
|
||||
|:---|:---|:---|
|
||||
| **1** | **填数据 + 发群(文字)** | `cd 飞书管理/脚本`<br/>`python3 soul_party_to_feishu_sheet.py 115`<br/>→ 效果数据写入当月表对应日期列,并**自动推送竖状文字到飞书群**(含报表链接) |
|
||||
| **2** | **生成会议纪要图** | 按 **智能纪要 Skill**(`02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md`):txt → JSON → HTML → 截图 PNG,输出到 `卡若Ai的文件夹/报告/` |
|
||||
| **3** | **填图片到报表** | `cd 飞书管理/脚本`<br/>`python3 feishu_write_minutes_to_sheet.py --party-image "<报告路径>/soul_115场_智能纪要_20260304.png" --sheet-id bJR5sA --date-col 4`<br/>→ 纪要图写入运营报表「今日总结」对应列(3 月 115 场 = 第 4 列) |
|
||||
| **4** | **把纪要图发到飞书群** | `cd 智能纪要/脚本`<br/>`python3 send_to_feishu.py --image "<报告路径>/soul_115场_智能纪要_20260304.png"`<br/>→ 默认 Webhook 为**运营报表同一飞书群**,群内会收到纪要长图 |
|
||||
|
||||
**执行顺序**:1 → 2 → 3 → 4,即可完成「数据入表 + 纪要图入表 + 群内先收文字再收纪要图」。
|
||||
|
||||
- **2 月场次**:步骤 3 不传 `--sheet-id`/`--date-col` 时,默认写 2 月表 19/20 列;步骤 4 不变。
|
||||
- **同群**:运营报表发群与纪要图发群使用同一 Webhook(见 1.3),群内先看到场次数据,再看到纪要图。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始(30 秒上手)
|
||||
|
||||
```bash
|
||||
# ❶ 安装依赖(一次性)
|
||||
pip3 install requests
|
||||
|
||||
# ❷ 刷新飞书 Token(每天首次或 Token 过期时)
|
||||
cd 飞书管理/脚本 && python3 auto_log.py
|
||||
|
||||
# ❸ 写入派对效果数据(自动选 2月/3月 工作表 + 发群)
|
||||
python3 soul_party_to_feishu_sheet.py 115
|
||||
|
||||
# ❹ 生成派对智能纪要文本并写入「今日总结」(可选,与纪要图二选一或都做)
|
||||
python3 write_party_minutes_from_txt.py "/path/to/soul 派对 115场 20260304.txt" 4
|
||||
|
||||
# ❺ 批量写入小程序数据(可选)
|
||||
python3 write_miniprogram_batch.py
|
||||
```
|
||||
|
||||
所有脚本路径:飞书管理相关在 `飞书管理/脚本/`,纪要生成与发图在 `智能纪要/脚本/`。**3 月场次**会自动写入 3 月工作表标签,不会误写到 2 月。
|
||||
|
||||
---
|
||||
|
||||
## 一、完整配置清单
|
||||
|
||||
### 1.1 飞书应用
|
||||
|
||||
| 项目 | 值 |
|
||||
|:---|:---|
|
||||
| App ID | `cli_a48818290ef8100d` |
|
||||
| App Secret | `dhjU0qWd5AzicGWTf4cTqhCWJOrnuCk4` |
|
||||
| 授权回调 | `http://localhost:5050/api/auth/callback` |
|
||||
| 权限 | `wiki:wiki` `docx:document` `drive:drive` |
|
||||
|
||||
### 1.2 运营报表(飞书电子表格)
|
||||
|
||||
| 项目 | 值 |
|
||||
|:---|:---|
|
||||
| 表格链接(2月) | https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet=7A3Cy9 |
|
||||
| 表格链接(3月) | https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet=bJR5sA |
|
||||
| spreadsheet_token | `wikcnIgAGSNHo0t36idHJ668Gfd` |
|
||||
| 2 月 sheet_id | `7A3Cy9` |
|
||||
| 3 月 sheet_id | `bJR5sA` |
|
||||
| 表格结构 | A 列=指标名,第 1 行=日期(1、2…),第 2 行可含「113场」「114场」等,第 3~12 行=效果数据,第 15 行=小程序访问,第 28 行=今日总结,第 29 行=派对录屏(飞书妙记链接) |
|
||||
| 月份选择 | 脚本按 `SESSION_MONTH` 自动选 2 月或 3 月工作表,避免串月 |
|
||||
|
||||
### 1.3 飞书群 Webhook(报表数据 + 纪要图同群)
|
||||
|
||||
| 项目 | 值 |
|
||||
|:---|:---|
|
||||
| Webhook URL | `https://open.feishu.cn/open-apis/bot/v2/hook/34b762fc-5b9b-4abb-a05a-96c8fb9599f1` |
|
||||
| 用途 | ① 填表后自动推送竖状格式消息(场次数据+报表链接);② 会议纪要图片发群(智能纪要 `send_to_feishu.py --image` 默认即此 Webhook) |
|
||||
|
||||
### 1.4 Token 管理
|
||||
|
||||
| 项目 | 说明 |
|
||||
|:---|:---|
|
||||
| Token 文件 | 脚本同目录 `.feishu_tokens.json` |
|
||||
| 含字段 | `access_token`、`refresh_token`、`auth_time` |
|
||||
| 自动刷新 | 所有脚本遇 401 自动用 refresh_token 刷新,无需手动 |
|
||||
| 手动刷新 | `python3 auto_log.py` (静默刷新,不需浏览器) |
|
||||
|
||||
---
|
||||
|
||||
## 二、脚本清单与用途
|
||||
|
||||
### 2.1 核心脚本(日常使用)
|
||||
|
||||
| 脚本 | 功能 | 命令 |
|
||||
|:---|:---|:---|
|
||||
| `soul_party_to_feishu_sheet.py` | 按场次写入效果数据到**当月工作表**对应日期列 + 飞书群推送(2 月/3 月自动选标签) | `python3 soul_party_to_feishu_sheet.py 115` |
|
||||
| `write_party_minutes_from_txt.py` | 从 TXT 生成智能纪要**文本**写入「今日总结」行(需指定日期列号) | `python3 write_party_minutes_from_txt.py "<txt路径>" 4` |
|
||||
| `auto_log.py` | Token 刷新 + 飞书日志写入 | `python3 auto_log.py` |
|
||||
|
||||
### 2.2 辅助脚本
|
||||
|
||||
| 脚本 | 功能 | 命令 |
|
||||
|:---|:---|:---|
|
||||
| `feishu_write_minutes_to_sheet.py` | 会议纪要/派对总结**图片**上传到「今日总结」对应日期列(默认 2 月 19/20 日列);3 月某场需指定 sheet 与日期列;**发群**需另执行智能纪要 `send_to_feishu.py --image` | `python3 feishu_write_minutes_to_sheet.py [内部图] [派对图]`<br/>3 月 115 场:`--party-image <png路径> --sheet-id bJR5sA --date-col 4` |
|
||||
| `feishu_sheet_monthly_stats.py` | 月度运营数据统计 | `python3 feishu_sheet_monthly_stats.py 2` 或 `all` |
|
||||
| `write_miniprogram_to_sheet.py` | **单日**写入小程序三核心数据(访问次数、访客、交易金额) | `python3 write_miniprogram_to_sheet.py 23 55 55 0` |
|
||||
| `write_miniprogram_batch.py` | **批量**将 `MINIPROGRAM_EXTRA` 中所有日期的小程序数据写入报表 | `python3 write_miniprogram_batch.py` |
|
||||
|
||||
### 2.3 派对录屏链接(自动写入)
|
||||
|
||||
填表时若在 `soul_party_to_feishu_sheet.py` 中配置了 `PARTY_VIDEO_LINKS[场次]`(飞书妙记完整 URL),会**自动**写入「派对录屏」行对应列(如 115 场 → E29)。新场次需在脚本中补全链接后重新执行该场次填表。
|
||||
|
||||
### 2.4 小程序运营数据(自动写入)
|
||||
|
||||
每日填表时,若在 `soul_party_to_feishu_sheet.py` 中配置了 **2 月** `MINIPROGRAM_EXTRA` 或 **3 月** `MINIPROGRAM_EXTRA_3`,会**自动**把当日小程序三核心数据写入对应日期列。数据需从 **Soul 小程序 / 微信公众平台 → 小程序 → 统计 → 实时访问、概况** 获取后填入配置:
|
||||
|
||||
| 指标 | 数据来源 | 行(A 列关键词) |
|
||||
|:---|:---|:---|
|
||||
| 访问次数 | 微信公众平台 → 小程序 → 统计 → 实时访问 | 小程序访问 |
|
||||
| 访客 | 同上 | 访客 |
|
||||
| 交易金额 | 同上 | 交易金额 |
|
||||
|
||||
**配置方式**(在 `soul_party_to_feishu_sheet.py` 中):
|
||||
|
||||
```python
|
||||
# 派对录屏(飞书妙记链接),写入「派对录屏」行
|
||||
PARTY_VIDEO_LINKS = {
|
||||
'115': 'https://cunkebao.feishu.cn/minutes/obcnxxxx...', # 从飞书妙记复制
|
||||
}
|
||||
|
||||
# 2 月小程序数据
|
||||
MINIPROGRAM_EXTRA = {
|
||||
'23': {'访问次数': 55, '访客': 55, '交易金额': 0}, # 2月23日
|
||||
}
|
||||
# 3 月小程序数据(113/114/115 场填表时自动写 3 月表)
|
||||
MINIPROGRAM_EXTRA_3 = {
|
||||
'4': {'访问次数': 60, '访客': 60, '交易金额': 0}, # 3月4日 115场,从 Soul 小程序后台获取后填入
|
||||
}
|
||||
```
|
||||
|
||||
- 数据来源:Soul 小程序 / 微信公众平台 → 小程序 → 统计,每日手动查看后填入
|
||||
- 填派对表时自动带出:运行 `soul_party_to_feishu_sheet.py` 某场时,2 月用 `MINIPROGRAM_EXTRA`、3 月用 `MINIPROGRAM_EXTRA_3` 同列写入小程序三项,并若有 `PARTY_VIDEO_LINKS` 则写入派对录屏行
|
||||
- 单日写入(仅 2 月表):`python3 write_miniprogram_to_sheet.py 23 55 55 0`(日期列号 访问次数 访客 交易金额)
|
||||
- 历史补全:在 `MINIPROGRAM_EXTRA` 中配齐多日数据后执行 `python3 write_miniprogram_batch.py`
|
||||
|
||||
---
|
||||
|
||||
## 三、完整操作流程
|
||||
|
||||
**整体顺序**:先执行 **[ 一站式完整流程 ]**(本文件开头)中的 ① 填数据发群 → ② 生成纪要图 → ③ 填图片到报表 → ④ 纪要图发群,再按需做小程序或纪要文本。
|
||||
|
||||
### 3.1 每日派对结束后操作(当前流程)
|
||||
|
||||
```
|
||||
输入:派对关闭页截图 + 小助手弹窗截图 + TXT 聊天记录(+ 可选:小程序当日数据)
|
||||
输出:飞书运营报表(当月标签)写入 + 飞书群推送(文字) + 纪要图入表 + 纪要图发群(与会议纪要联动)
|
||||
```
|
||||
|
||||
**月份与工作表**:脚本根据 `SESSION_MONTH` 自动选择 2 月或 3 月工作表,3 月场次(如 113、114、115)写入 3 月标签,不会写入 2 月。
|
||||
|
||||
#### Step 1:提取数据(从截图)
|
||||
|
||||
从派对关闭页和小助手弹窗提取 10 项数据:
|
||||
|
||||
| 序号 | 指标 | 来源 |
|
||||
|:---|:---|:---|
|
||||
| 1 | 主题 | TXT 提炼 ≤12 字 |
|
||||
| 2 | 时长(分钟) | 关闭页「派对时长」 |
|
||||
| 3 | Soul推流人数 | 关闭页「本场获得额外曝光」 |
|
||||
| 4 | 进房人数 | 关闭页「派对成员」或小助手「进房人数」 |
|
||||
| 5 | 人均时长 | 小助手「人均时长」 |
|
||||
| 6 | 互动数量 | 小助手「互动数量」 |
|
||||
| 7 | 礼物 | 关闭页「本场收到礼物」 |
|
||||
| 8 | 灵魂力 | 关闭页「收获灵魂力」 |
|
||||
| 9 | 增加关注 | 关闭页「新增粉丝」或小助手「增加关注」 |
|
||||
| 10 | 最高在线 | 关闭页「最高在线」 |
|
||||
|
||||
#### Step 2:在脚本中注册新场次
|
||||
|
||||
打开 `soul_party_to_feishu_sheet.py`,在 `ROWS` 字典中添加:
|
||||
|
||||
```python
|
||||
# 格式:'场次号': [主题, 时长, 推流, 进房, 人均, 互动, 礼物, 灵魂力, 关注, 最高在线]
|
||||
'107': ['主题关键词 ≤12字', 140, 35000, 400, 8, 90, 3, 25, 10, 45],
|
||||
```
|
||||
|
||||
在 `SESSION_DATE_COLUMN` 和 `SESSION_MONTH` 中添加映射(**按月份选工作表标签**,3 月填 3 月表):
|
||||
|
||||
```python
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4'}
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3}
|
||||
```
|
||||
|
||||
并在 `_maybe_send_group` 的 `date_label`、`src_date` 中为该场次加上对应「X月X日」和 TXT 日期(如 `'115': '3月4日'`、`'115': '20260304'`)。
|
||||
|
||||
#### Step 3:执行写入 + 校验
|
||||
|
||||
```bash
|
||||
# 写入效果数据(自动选 2月/3月 表 + 校验 + 发群)
|
||||
python3 soul_party_to_feishu_sheet.py 115
|
||||
|
||||
# 生成智能纪要文本并写入「今日总结」(日期列号 = 当月几号,如 3月4日 填 4)
|
||||
python3 write_party_minutes_from_txt.py "/path/to/soul 派对 115场 20260304.txt" 4
|
||||
```
|
||||
|
||||
成功输出示例(3 月场次):
|
||||
```
|
||||
✅ 已选 3月 工作表(sheet_id=bJR5sA)
|
||||
✅ 已写入飞书表格:115场 效果数据(竖列 E3:E12,共10格),校验通过
|
||||
✅ 已同步推送到飞书群(竖状格式)
|
||||
✅ 已写入派对智能纪要到「今日总结」→ 2月4日列,校验通过
|
||||
```
|
||||
|
||||
若需将**智能纪要图片**放入「今日总结」并**发到飞书群**:见下节「智能纪要图片上传到报表 + 发群」。
|
||||
|
||||
---
|
||||
|
||||
## 3.2 智能纪要图片上传到报表 + 发群(十步清单)
|
||||
|
||||
与 **智能纪要 Skill**(`02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md`)联动:纪要图写入运营报表「今日总结」对应列,并**发到运营报表同一飞书群**。
|
||||
|
||||
| 序号 | 步骤 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 1 | 准备派对 txt | 如 `soul 派对 115场 20260304.txt`(聊天记录/soul) |
|
||||
| 2 | 智能提炼 JSON | 按智能纪要规范从 txt 提炼分享人、重点片段、干货、行动项,生成 `xxx_meeting.json` |
|
||||
| 3 | 生成 HTML | `智能纪要/脚本/generate_meeting.py --input xxx_meeting.json --output "卡若Ai的文件夹/报告/soul_115场_智能纪要_20260304.html"` |
|
||||
| 4 | 导出目录 | HTML/PNG 一律导出到 `卡若Ai的文件夹/报告/`,不落在 Skill 内 |
|
||||
| 5 | 截图 PNG | `智能纪要/脚本/screenshot.py "<报告路径>.html" --output "<报告路径>.png"` |
|
||||
| 6 | 确认场次与月份 | 115 场 → 3 月表、日期列 4;2 月场次用默认 19/20 列 |
|
||||
| 7 | 上传到报表 | `飞书管理/脚本/feishu_write_minutes_to_sheet.py --party-image "<报告路径>.png" --sheet-id bJR5sA --date-col 4`(3 月) |
|
||||
| 8 | **纪要图发群** | `智能纪要/脚本/send_to_feishu.py --image "<报告路径>.png"`(默认 Webhook = 运营报表群,群内收到纪要长图) |
|
||||
| 9 | 2 月表 | 不指定时默认 `SHEET_ID=7A3Cy9`,派对图→19 列、内部会议→20 列;发群命令同上 |
|
||||
| 10 | 协作 | 纪要内容与样式以智能纪要 Skill 为准;本 Skill 负责写入报表、Token 及与发群流程衔接 |
|
||||
|
||||
**3 月场次参数速查**:`--sheet-id bJR5sA`,`--date-col` = 当月日期(如 4 日填 `4`)。纪要生成与截图命令详见智能纪要 Skill「智能纪要图片上传到运营报表」小节。
|
||||
|
||||
---
|
||||
|
||||
## 四、写入校验机制
|
||||
|
||||
所有写入操作均含**写后读回校验**:
|
||||
|
||||
| 场景 | 校验方式 |
|
||||
|:---|:---|
|
||||
| 效果数据写入 | 写入后读回首格(主题),比对一致才算成功 |
|
||||
| 智能纪要写入 | 写入后读回单元格内容,检查字数 > 0 |
|
||||
| Token 过期 | 自动刷新后重试,不降级为追加行(避免写入错误位置) |
|
||||
| 日期列未找到 | 直接报错退出,不降级为追加行 |
|
||||
|
||||
校验未通过时脚本会打印具体差异信息,方便排查。
|
||||
|
||||
---
|
||||
|
||||
## 五、飞书群推送格式
|
||||
|
||||
**两类推送(同一飞书群)**:① 填表后自动推送竖状文字(场次数据+报表链接);② 纪要图发群需执行智能纪要 `send_to_feishu.py --image <png路径>`,默认即本群 Webhook。一站式顺序见文首「一站式完整流程」。
|
||||
|
||||
写入成功后自动发送到飞书群(竖状格式,每行一项)。链接按当月工作表变化(3 月场次会带 `sheet=bJR5sA`):
|
||||
|
||||
```
|
||||
【Soul 派对运营报表】
|
||||
链接:https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet=bJR5sA
|
||||
|
||||
115场(3月4日)已登记:
|
||||
主题:破产两次 家庭先于事业
|
||||
时长(分钟):156
|
||||
Soul推流人数:36974
|
||||
进房人数:484
|
||||
人均时长(分钟):8
|
||||
互动数量:82
|
||||
礼物:1
|
||||
灵魂力:3
|
||||
增加关注:15
|
||||
最高在线:56
|
||||
数据来源:soul 派对 115场 20260304.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、智能纪要生成规则
|
||||
|
||||
**文本纪要**:`write_party_minutes_from_txt.py` 从派对 TXT 自动提炼结构化纪要:
|
||||
|
||||
| 板块 | 内容 |
|
||||
|:---|:---|
|
||||
| 关键词 | 从 TXT 头部 `关键词:` 行提取 |
|
||||
| 一、核心内容 | 按关键词匹配提取:退伍军人、AI切入、私域、编导对赌、项目切割等 |
|
||||
| 二、金句 | 从对话中提炼可操作的建议 |
|
||||
| 三、下一步 | 行动建议(联系管理、搜索培训等) |
|
||||
|
||||
纪要**文本**写入运营报表「今日总结」行、对应日期列(需传入日期列号,如 3 月 4 日传 `4`)。**纪要图片**上传到同一格并**发群**:见 **§3.2 智能纪要图片上传到报表 + 发群**;3 月用 `--party-image --sheet-id bJR5sA --date-col <日>`,发群用 `智能纪要/脚本/send_to_feishu.py --image <png>`。
|
||||
|
||||
---
|
||||
|
||||
## 七、跨平台兼容
|
||||
|
||||
### macOS(推荐)
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip3 install requests
|
||||
|
||||
# 所有命令直接用 python3
|
||||
python3 soul_party_to_feishu_sheet.py 106
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```cmd
|
||||
# 安装依赖
|
||||
pip install requests
|
||||
|
||||
# Windows 用 python(不是 python3)
|
||||
python soul_party_to_feishu_sheet.py 106
|
||||
python write_party_minutes_from_txt.py "C:\Downloads\soul 派对 106场 20260221.txt" 21
|
||||
python auto_log.py
|
||||
```
|
||||
|
||||
### 路径差异
|
||||
|
||||
| 项目 | macOS | Windows |
|
||||
|:---|:---|:---|
|
||||
| 脚本目录 | `/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/` | `C:\Users\用户名\卡若AI\02_卡人(水)\水桥_平台对接\飞书管理\脚本\` |
|
||||
| Python | `python3` | `python` |
|
||||
| TXT 路径 | `/Users/karuo/Downloads/xxx.txt` | `C:\Users\用户名\Downloads\xxx.txt` |
|
||||
| Token 文件 | 脚本同目录 `.feishu_tokens.json`(两个平台一致) | 同左 |
|
||||
|
||||
### 环境变量覆盖(可选)
|
||||
|
||||
所有配置项均可通过环境变量覆盖,无需改脚本:
|
||||
|
||||
```bash
|
||||
export FEISHU_SPREADSHEET_TOKEN=wikcnIgAGSNHo0t36idHJ668Gfd
|
||||
export FEISHU_SHEET_ID=7A3Cy9
|
||||
export FEISHU_GROUP_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/34b762fc-...
|
||||
export FEISHU_APP_ID=cli_a48818290ef8100d
|
||||
export FEISHU_APP_SECRET=dhjU0qWd5AzicGWTf4cTqhCWJOrnuCk4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题
|
||||
|
||||
| 问题 | 解决 |
|
||||
|:---|:---|
|
||||
| `❌ 无法获取飞书 Token` | 运行 `python3 auto_log.py` 刷新 Token |
|
||||
| `❌ 未找到日期列` | Token 过期导致读表失败,先 `python3 auto_log.py` 再重试 |
|
||||
| `⚠️ 飞书群推送失败` | 检查 Webhook URL 是否有效、群机器人是否被禁用 |
|
||||
| `❌ 写入失败 401` | Token 过期,脚本会自动刷新并重试;若仍失败则 `python3 auto_log.py` |
|
||||
| Windows 中文路径乱码 | 确保终端编码为 UTF-8:`chcp 65001` |
|
||||
| `pip3 not found` | Windows 用 `pip`;macOS 可能需 `pip3` 或 `python3 -m pip` |
|
||||
|
||||
---
|
||||
|
||||
## 九、表格结构参考
|
||||
|
||||
2 月表(sheet=7A3Cy9)、3 月表(sheet=bJR5sA)结构一致:
|
||||
|
||||
```
|
||||
表头第1行: [空] | 3月 | 1 | 2 | 3 | 4 | 5 | 6 | ...
|
||||
第2行: 一、效果数据 | | 113场 | 114场 | 115场 | 116场 | ...
|
||||
第3行: 主题 | | xx | xx | xx | |
|
||||
第4行: 时长 | | xx | xx | xx | |
|
||||
...
|
||||
第12行: 最高在线 | | xx | xx | xx | |
|
||||
...
|
||||
第15行: 小程序访问| | xx | xx | xx | | ← 访问次数、访客、交易金额
|
||||
...
|
||||
第28行: 今日总结 | | xx | xx | xx | | ← 智能纪要(文本或图片)
|
||||
```
|
||||
|
||||
按 `SESSION_DATE_COLUMN` 与 `SESSION_MONTH` 决定写入哪一列、哪张表。
|
||||
|
||||
---
|
||||
|
||||
## 十、新增场次模板
|
||||
|
||||
每次新增场次,在 `soul_party_to_feishu_sheet.py` 中改以下几处:
|
||||
|
||||
```python
|
||||
# 1. ROWS 字典加一行(主题可带冲击性,≤12 字)
|
||||
'116': ['主题≤12字', 时长, 推流, 进房, 人均, 互动, 礼物, 灵魂力, 关注, 最高在线],
|
||||
|
||||
# 2. SESSION_DATE_COLUMN 加日期映射(当月几号)
|
||||
SESSION_DATE_COLUMN = {..., '116': '5'}
|
||||
|
||||
# 3. SESSION_MONTH 加月份(3 月场次必填 3,否则会写入 2 月表)
|
||||
SESSION_MONTH = {..., '116': 3}
|
||||
|
||||
# 4. _maybe_send_group 内 date_label、src_date 加映射(否则不发群)
|
||||
# date_label = {..., '116': '3月5日'}
|
||||
# src_date = {..., '116': '20260305'}
|
||||
|
||||
# 5. 若当日有小程序数据,在 MINIPROGRAM_EXTRA 中加:
|
||||
# MINIPROGRAM_EXTRA = {..., '5': {'访问次数': 60, '访客': 60, '交易金额': 0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、基因胶囊打包入口
|
||||
|
||||
本 Skill 支持打包为基因胶囊,便于继承与分发。打包后产出位于 `卡若Ai的文件夹/导出/基因胶囊/`。
|
||||
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI
|
||||
python3 "05_卡土(土)/土砖_技能复制/基因胶囊/脚本/gene_capsule.py" pack "02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md"
|
||||
# 或按技能名(在 SKILL_REGISTRY 中匹配)
|
||||
python3 "05_卡土(土)/土砖_技能复制/基因胶囊/脚本/gene_capsule.py" pack "Soul派对运营报表"
|
||||
```
|
||||
|
||||
打包后将生成:胶囊 JSON、基因胶囊功能流程图.md、说明文档.md(含解包命令与引用)。
|
||||
|
||||
---
|
||||
|
||||
## 版本记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 1.0 | 2026-02-20 | 初版:截图填表发群 |
|
||||
| 2.0 | 2026-02-22 | 基因胶囊:Token 自动刷新、写入校验、智能纪要、跨平台、完整配置清单 |
|
||||
| 2.1 | 2026-03-04 | 月份路由:2月/3月 工作表分离(7A3Cy9 / bJR5sA),SESSION_MONTH 防串月;支持 113~115 场;小程序批量 write_miniprogram_batch;运营报表 SKILL 与当前流程同步 |
|
||||
| 2.2 | 2026-03-04 | **智能纪要上传到报表**:§3.2 十步清单(txt→JSON→HTML→PNG→feishu_write_minutes_to_sheet);与智能纪要 Skill 联动;3 月用 --party-image --sheet-id bJR5sA --date-col |
|
||||
| 2.3 | 2026-03-04 | **会议纪要 + 运营报表 + 发群一站式**:文首新增「一站式完整流程」四步(①填数据发群 ②生成纪要图 ③填图片到报表 ④纪要图发群);飞书群统一:数据推送与纪要图发群同 Webhook,纪要图发群用智能纪要 `send_to_feishu.py --image`;§3.2 增加「发群」步骤与说明 |
|
||||
| 3.0 | 2026-03-04 | **完整流程提取 + 基因胶囊**:新增「零、完整流程提取」:流程图、前置条件、逐步命令表、一键命令块、新场次清单、故障排查;派对录屏链接写入(E29:E29 范围);§十一 基因胶囊打包入口与 pack 命令 |
|
||||
131
.cursor/skills/karuo-party/skills/飞书视频文字下载_SKILL.md
Normal file
131
.cursor/skills/karuo-party/skills/飞书视频文字下载_SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: 飞书视频和文字下载
|
||||
description: 飞书妙记单条/批量下载视频(mp4)与文字(txt),纯 API+Cookie,不打开浏览器。含 Cookie 获取链、默认输出目录、查找最早长视频。
|
||||
triggers: 飞书视频下载、飞书文字下载、妙记下载视频、妙记导出文字、飞书妙记下载、下载飞书视频、下载飞书文字、飞书视频和文字下载
|
||||
owner: 水桥
|
||||
group: 水
|
||||
version: "1.0"
|
||||
updated: "2026-03-12"
|
||||
parent_skill: 智能纪要
|
||||
---
|
||||
|
||||
# 飞书视频和文字下载
|
||||
|
||||
> **基因能力**:从飞书妙记链接或 object_token,命令行下载**视频(mp4)**与**文字(txt)**,无需打开浏览器。
|
||||
> 归属:水桥 · 智能纪要子能力,可独立打包为基因胶囊复用。
|
||||
|
||||
---
|
||||
|
||||
## 一、默认输出目录
|
||||
|
||||
| 类型 | 默认目录 |
|
||||
|:---|:---|
|
||||
| **文字(txt)** | `/Users/karuo/Documents/聊天记录/soul` |
|
||||
| **视频(mp4)** | `/Users/karuo/Movies/soul视频/原视频` |
|
||||
|
||||
脚本未指定 `-o`/`--output` 时使用上表;解包到其他环境时可修改为本地路径。
|
||||
|
||||
---
|
||||
|
||||
## 二、权限:Cookie 获取链(5 级)
|
||||
|
||||
妙记**文字导出**与**视频下载**均依赖 Web Cookie(Open API 的 tenant_token 无法访问妙记正文/视频)。
|
||||
|
||||
1. **cookie_minutes.txt** 第一行(脚本同目录或 `智能纪要/脚本/`)
|
||||
2. **环境变量** `FEISHU_MINUTES_COOKIE`
|
||||
3. **本机浏览器**(browser_cookie3:Safari/Chrome/Firefox/Edge)
|
||||
4. **Cursor 内置浏览器**:SQLite 明文
|
||||
`~/Library/Application Support/Cursor/Partitions/cursor-browser/Cookies`
|
||||
查询 `host_key LIKE '%feishu%' OR host_key LIKE '%cunkebao%'`
|
||||
5. **手动兜底**:浏览器 F12 → 飞书妙记 list 请求 → 复制 Cookie 到 cookie_minutes.txt
|
||||
|
||||
---
|
||||
|
||||
## 三、命令行用法
|
||||
|
||||
### 3.1 脚本根路径(本基因默认)
|
||||
|
||||
```text
|
||||
SCRIPT_DIR="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本"
|
||||
```
|
||||
|
||||
解包到其他项目时,将上述路径改为本地「智能纪要/脚本」所在路径。
|
||||
|
||||
### 3.2 下载视频(单条)
|
||||
|
||||
```bash
|
||||
# 链接或 object_token 均可
|
||||
python3 "$SCRIPT_DIR/feishu_minutes_download_video.py" "https://cunkebao.feishu.cn/minutes/obcnc53697q9mj6h1go6v25e"
|
||||
python3 "$SCRIPT_DIR/feishu_minutes_download_video.py" obcnc53697q9mj6h1go6v25e -o ~/Downloads/
|
||||
```
|
||||
|
||||
- 输出:默认 `原视频/` 下,文件名含标题与日期。
|
||||
- 依赖:`requests`;Cookie 见第二节。
|
||||
|
||||
### 3.3 导出文字(单条)
|
||||
|
||||
```bash
|
||||
# 导出为 txt(同上,需 Cookie)
|
||||
python3 "$SCRIPT_DIR/feishu_minutes_export_github.py" "https://cunkebao.feishu.cn/minutes/obcnc53697q9mj6h1go6v25e" -o "/Users/karuo/Documents/聊天记录/soul"
|
||||
```
|
||||
|
||||
- 输出:默认 `聊天记录/soul` 下 txt 文件。
|
||||
|
||||
### 3.4 批量文字(按场次范围)
|
||||
|
||||
```bash
|
||||
python3 "$SCRIPT_DIR/download_soul_minutes_101_to_103.py" --from 90 --to 102
|
||||
```
|
||||
|
||||
### 3.5 查找「最早且时长≥1小时且有画面」的妙记
|
||||
|
||||
```bash
|
||||
python3 "$SCRIPT_DIR/find_oldest_long_video_minute.py"
|
||||
python3 "$SCRIPT_DIR/find_oldest_long_video_minute.py" --max-status 200
|
||||
python3 "$SCRIPT_DIR/find_oldest_long_video_minute.py" --list-only
|
||||
```
|
||||
|
||||
- 使用 list API 的 `duration`(毫秒)筛 ≥1 小时,再按 create_time 从早到晚用 status API 筛有 `video_download_url`,输出最早一条的 object_token、标题、日期、时长。
|
||||
|
||||
---
|
||||
|
||||
## 四、核心 API(供二次开发)
|
||||
|
||||
| 能力 | 方法 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| **列表** | `GET /minutes/api/space/list?size=50&space_name=1&last_time={ts}` | 分页用 `last_time` 为上一页最后一条的 create_time |
|
||||
| **文字** | `POST /minutes/api/export` | params: object_token, format=2, add_speaker=true;Header: Cookie + Referer + bv-csrf-token |
|
||||
| **视频** | `GET /minutes/api/status?object_token=xxx` | 返回 `data.video_info.video_download_url`,再 GET 该 URL 流式下载 |
|
||||
|
||||
- 域名:`cunkebao.feishu.cn` 或 `meetings.feishu.cn`(同一套 Cookie)。
|
||||
- list 条目含 `duration`(毫秒)、`create_time`、`topic`、`object_token`。
|
||||
|
||||
---
|
||||
|
||||
## 五、脚本清单(依赖父目录 智能纪要/脚本)
|
||||
|
||||
| 脚本 | 功能 |
|
||||
|:---|:---|
|
||||
| `feishu_minutes_download_video.py` | 单条妙记视频下载(status → mp4) |
|
||||
| `feishu_minutes_export_github.py` | 单条妙记文字导出(export → txt) |
|
||||
| `feishu_auth_helper.py` | tenant_token / Cookie 测试、refresh-cookie |
|
||||
| `cursor_cookie_util.py` | 从 Cursor 浏览器提取 Cookie(feishu/github) |
|
||||
| `download_soul_minutes_101_to_103.py` | 批量场次文字(--from/--to) |
|
||||
| `find_oldest_long_video_minute.py` | 查找最早、时长≥1h、有视频的妙记 |
|
||||
|
||||
---
|
||||
|
||||
## 六、解包后使用(继承本基因)
|
||||
|
||||
1. 将本基因 **unpack** 到目标项目的 `智能纪要/飞书视频文字下载/` 或任意目录。
|
||||
2. 确保目标环境存在「智能纪要/脚本」或等价脚本目录,并安装 `requests`。
|
||||
3. 把本 SKILL 中 `SCRIPT_DIR` 改为目标环境中的脚本路径。
|
||||
4. 配置 Cookie:cookie_minutes.txt 或 FEISHU_MINUTES_COOKIE 或 Cursor Cookie 提取。
|
||||
|
||||
---
|
||||
|
||||
## 七、相关文档
|
||||
|
||||
- 父技能:`02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md`
|
||||
- 权限与排查:`智能纪要/参考资料/飞书妙记下载-权限与排查说明.md`
|
||||
- 账号与 API 索引:`运营中枢/工作台/00_账号与API索引.md`
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: soul-miniprogram-dev
|
||||
description: Trigger when 编辑 miniprogram、小程序、微信原生、C 端、调用 miniprogram 接口、支付/提现/推荐 时必读。WXML/WXSS、app.request、仅用 /api/miniprogram/*。
|
||||
description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑时必遵循。WXML/WXSS/JS、app.request、/api/miniprogram/*、scene、支付、权限。Use when editing miniprogram, 小程序, 微信原生.
|
||||
---
|
||||
# Soul 创业派对 - 小程序开发 Skill
|
||||
|
||||
@@ -79,8 +79,6 @@ description: Trigger when 编辑 miniprogram、小程序、微信原生、C 端
|
||||
## 8. 平台合规与能力检测(2025 起)
|
||||
|
||||
- **隐私按需授权**:涉及用户信息的接口(登录、手机号、位置等)必须在用户**实际触发功能时**再请求授权,禁止在 `app.onLaunch` 中集中请求。需配置《小程序用户隐私保护指引》,使用 `<button open-type="agreePrivacyAuthorization">` 获取同意。
|
||||
- **getPhoneNumber 必须耦合**:`open-type="getPhoneNumber|agreePrivacyAuthorization"`(基础库 2.32.3+),否则无法弹窗获取手机号。同时需在 app.js 的 `onNeedPrivacyAuthorization` 中将该页面加入支持列表,并提供 `showPrivacyModal` + 同意按钮,否则会 resolve(disagree) 导致失败。
|
||||
- **登录弹窗**:使用公用组件 `components/login-modal`,read/my/gift-pay 等页面引入,避免重复实现。
|
||||
- **能力检测**:使用新 API 前用 `wx.canIUse('api.xxx')` 检测,低版本做降级。
|
||||
- **Skyline(可选)**:性能敏感页面可在 `page.json` 中配置 `"renderer": "skyline"`,仍使用 WXML/WXSS。
|
||||
|
||||
@@ -106,41 +104,7 @@ description: Trigger when 编辑 miniprogram、小程序、微信原生、C 端
|
||||
|
||||
---
|
||||
|
||||
## 11. 原生按钮覆盖定位(避免样式干扰)
|
||||
|
||||
- **场景**:在头像、图片等区域需触发 `open-type="chooseAvatar"` 等原生能力时,**禁止用 button 包裹**目标元素,否则会受原生样式影响(灰色矩形、边框等)。
|
||||
- **正确做法**:用 **button 绝对定位覆盖** 在目标区域上方,与展示元素为同级关系。
|
||||
- **结构示例**:
|
||||
```html
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">...</view>
|
||||
<view class="vip-badge">VIP</view>
|
||||
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
|
||||
</view>
|
||||
```
|
||||
- **样式**:`.avatar-wrap { position: relative; }`;`.avatar-overlay-btn { position: absolute; top: 0; left: 0; width/height 与目标一致; background: transparent; border: none; }`;`::after { border: none; }`。
|
||||
- **口诀**:同级覆盖,绝对定位,透明按钮。
|
||||
|
||||
---
|
||||
|
||||
## 12. 常见陷阱(Gotchas)
|
||||
|
||||
> 从实际失败中积累,持续补充。**千万别这么做**——对 AI 信号更强。
|
||||
|
||||
| 陷阱 | 后果 | 正确做法 |
|
||||
|------|------|---------|
|
||||
| 调用 `/api/admin/*`、`/api/db/*` | 路由互窜、鉴权不符 | 仅用 `/api/miniprogram/*` |
|
||||
| 页面里直接 `wx.request` 写死 baseUrl | 配置分散、难切换环境 | 统一 `getApp().request(url, options)` |
|
||||
| 在 input/textarea 自身上设 padding | 光标截断、布局异常 | 外边包 view,padding 写在 view,input `width:100%` |
|
||||
| button 包裹目标元素做 chooseAvatar | 原生样式干扰(灰框、边框) | button 绝对定位覆盖,同级关系,透明背景 |
|
||||
| 分享链路(支付/领取/代付)不判断单页模式 | 朋友圈打开能力受限、报错 | `wx.getSystemInfoSync()?.mode === 'singlePage'` 时引导「前往小程序」 |
|
||||
| 支付前不校验头像/昵称 | 超级个体开通后资料为默认 | 支付前校验,不通过则 `navigateTo('/pages/avatar-nickname/avatar-nickname')` |
|
||||
| app.onLaunch 中集中请求隐私授权 | 平台合规风险、审核不通过 | 按需授权,用户**实际触发功能时**再请求 |
|
||||
| 用 `selectable` 做文本复制 | 已废弃 | 用 `<text user-select>...</text>`(基础库 2.12.1+) |
|
||||
|
||||
---
|
||||
|
||||
## 13. 何时使用本 Skill
|
||||
## 11. 何时使用本 Skill
|
||||
|
||||
- 在 **miniprogram/** 下新增或修改页面、组件、utils 时。
|
||||
- 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。
|
||||
@@ -150,6 +114,5 @@ description: Trigger when 编辑 miniprogram、小程序、微信原生、C 端
|
||||
- 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。
|
||||
- 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 user-select)。
|
||||
- 做编辑资料页分享名片时(遵循 §10)。
|
||||
- 做头像上传、chooseAvatar 等需 button 触发的原生能力时(遵循 §11,用绝对定位覆盖,禁止 button 包裹)。
|
||||
|
||||
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。
|
||||
|
||||
@@ -39,8 +39,9 @@ cd .cursor/scripts/db-exec && npm install
|
||||
|
||||
### 3.2 执行单条 SQL
|
||||
|
||||
在**本仓库根目录**下执行(与 `miniprogram/`、`soul-api/` 同级):
|
||||
|
||||
```bash
|
||||
cd e:\Gongsi\Mycontent
|
||||
node .cursor/scripts/db-exec/run.js "SELECT 1"
|
||||
node .cursor/scripts/db-exec/run.js "DESCRIBE orders"
|
||||
node .cursor/scripts/db-exec/run.js "ALTER TABLE users ADD COLUMN new_field VARCHAR(64) DEFAULT ''"
|
||||
|
||||
@@ -192,10 +192,10 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
2. 排除:技术债、规则不清、与稳定版冲突的部分
|
||||
3. 按**最小功能**拆分,保证每个任务迁移后能完整运行
|
||||
4. 排期顺序:**界面修改优先** → 大逻辑排后;P0(逻辑不通)→ P1(功能缺失)→ P2(优化)
|
||||
5. 写入需求清单(当日需求文件),形成迁移任务清单
|
||||
5. 写入需求汇总,形成迁移任务清单
|
||||
|
||||
**产出**:
|
||||
- `开发文档/1、需求/YYYY-MM-DD-需求.md` 追加需求(当日文件,以日期最新为主;同步后更新 `1、需求/索引.md`)
|
||||
- `开发文档/1、需求/需求汇总.md` 追加需求
|
||||
- `开发文档/新版迁移-开发方案与清单.md` 或等价迁移清单
|
||||
|
||||
---
|
||||
@@ -259,7 +259,7 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
4. **逻辑分层**:每个功能过三层(界面/接口/数据)
|
||||
5. **体验评估**:补充空态、错误态、边界处理
|
||||
6. **接口规范与冲突**:产出接口规范与冲突清单
|
||||
7. **抽取需求**:写入需求清单(当日需求文件),形成迁移任务清单
|
||||
7. **抽取需求**:写入需求汇总,形成迁移任务清单
|
||||
8. **需求评审(迁移前必做)**:列出功能点 + 样式变更,逐一确认,产出评审清单
|
||||
9. **回复用户**:给出分析摘要 + 文档路径 + 建议执行顺序;**迁移须在需求评审通过后开始**
|
||||
|
||||
@@ -273,7 +273,7 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
| 接口规范与冲突 | `开发文档/新版迁移-接口规范与冲突清单.md` |
|
||||
| 迁移方案/清单 | `开发文档/新版迁移-开发方案与清单.md` 或 `新版功能迁移到稳定版方案.md` |
|
||||
| **需求评审清单** | `开发文档/新版迁移-需求评审清单.md`(功能点 + 样式变更,含确认状态) |
|
||||
| 需求清单 | `开发文档/1、需求/YYYY-MM-DD-需求.md`(以日期最新为主) |
|
||||
| 需求汇总 | `开发文档/1、需求/需求汇总.md` |
|
||||
|
||||
若已有同名文档,在其基础上**追加或更新**,不重复创建。
|
||||
|
||||
|
||||
@@ -9,62 +9,57 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
|
||||
---
|
||||
|
||||
## 0. 加个需求流水线(优先执行)
|
||||
## 0. 需求即执行(核心行为准则)
|
||||
|
||||
当用户说**「加个需求:xxxxxxx」**(具体内容)时,产品经理**必须**执行以下流程,确保三端功能闭环。
|
||||
> **绝对禁止**:收到需求后列出分析表格然后问用户「要从哪个开始」。
|
||||
> **正确做法**:收到需求 → 回复「好」→ 立即开始执行代码变更 → 完成后回复结果。
|
||||
|
||||
### 0.1 触发与解析
|
||||
### 0.0 执行模式(最高优先级)
|
||||
|
||||
- **触发词**:`加个需求`、`加个需求:xxx`(理解意图即可)
|
||||
- **解析**:提取用户描述的具体功能或变更点
|
||||
当用户给出需求(需求文档、口头描述、加个需求等任何形式)时:
|
||||
|
||||
### 0.2 三端分析(功能闭环)
|
||||
1. **回复「好」**(一个字,不多说)
|
||||
2. **内部完成三端分析**(不输出给用户,仅作为执行依据)
|
||||
3. **立即按顺序执行代码变更**:后端 → 管理端 → 小程序(按实际依赖关系)
|
||||
4. **执行完成后回复结果**:改了哪些文件、做了什么、验证结果
|
||||
5. **遇到不确定的业务逻辑**时才询问用户,技术实现细节自行判断
|
||||
|
||||
对每个需求,**必须**分析三端各自需要哪些调整:
|
||||
### 0.1 内部三端分析(不输出,仅执行依据)
|
||||
|
||||
| 端 | 分析要点 | 典型产出 |
|
||||
|----|----------|----------|
|
||||
| **小程序** | 新增/改版页面、交互、调用的 miniprogram 接口 | 页面路径、功能要点、接口依赖 |
|
||||
| **管理端** | 是否**确有**管理能力需求? | 新增列表/表单、配置项、审核、统计、开关(无则写「无」) |
|
||||
| **后端** | 接口、数据模型、路由分组 | miniprogram/admin/db 接口契约、表/字段变更 |
|
||||
对每个需求,内部分析三端各自需要哪些调整:
|
||||
|
||||
**判断原则**:
|
||||
|
||||
- 新增功能 → 常伴随:管理端**配置项**(开关、文案、规则)、**列表/审核**(若涉及用户提交)、**统计**(若涉及数据展示)
|
||||
- 若仅小程序展示 → 可能只需 miniprogram 接口,管理端无变更
|
||||
- 若涉及业务规则/开关 → 管理端「系统设置」或独立配置页;后端 config 或专用表
|
||||
| 端 | 分析要点 |
|
||||
|----|----------|
|
||||
| **小程序** | 新增/改版页面、交互、调用的 miniprogram 接口 |
|
||||
| **管理端** | 是否确有管理能力需求(配置/开关/审核/统计) |
|
||||
| **后端** | 接口、数据模型、路由分组 |
|
||||
|
||||
**合理性约束(必守)**:
|
||||
|
||||
- **按实际情况判断**,不因需求表述而过度设计。例如:单纯改文案、改按钮文字、改提示语 → **不需要**新增管理列表、文案管理、配置项;直接改前端代码即可。
|
||||
- 管理端/后端调整**仅在确有管理或数据需求时**才规划:需要运营配置、需要审核、需要统计、需要多端复用同一文案等。
|
||||
- 不确定时,优先给出**最小可行方案**,避免为小改动堆砌管理能力。
|
||||
- **按实际情况判断**,不过度设计。单纯改文案/按钮/提示语 → 直接改前端代码。
|
||||
- 管理端/后端调整**仅在确有管理或数据需求时**才做。
|
||||
- 优先**最小可行方案**,避免为小改动堆砌管理能力。
|
||||
|
||||
### 0.3 功能规划与协调变更
|
||||
### 0.2 执行顺序
|
||||
|
||||
1. **输出需求分析**:写入 `临时需求池/YYYY-MM-DD-需求简述.md` 或追加到 `开发文档/1、需求/YYYY-MM-DD-需求.md` 需求清单(以日期最新为主,同步后更新 `1、需求/索引.md`)
|
||||
2. **三端任务拆分**:按上表列出「小程序任务」「管理端任务」「后端任务」
|
||||
3. **协调变更**:若需更新《以界面定需求》,同步更新界面清单与业务逻辑
|
||||
4. **指派**:明确各任务对应角色(小程序开发工程师、管理端开发工程师、后端工程师),并给出执行顺序建议(通常:后端 → 小程序;管理端视依赖可并行或后置)
|
||||
1. 后端(接口、模型、路由)
|
||||
2. 管理端(页面、组件、交互)
|
||||
3. 小程序(页面、交互、接口调用)
|
||||
4. 联调验证
|
||||
|
||||
### 0.4 产出模板
|
||||
按 `SKILL-role-flow-control` 的协同流程推进。
|
||||
|
||||
```
|
||||
【需求】用户描述
|
||||
【三端分析】
|
||||
- 小程序:xxx
|
||||
- 管理端:xxx(若无则写「无」)
|
||||
- 后端:xxx
|
||||
【任务指派】
|
||||
1. 后端:xxx
|
||||
2. 小程序:xxx
|
||||
3. 管理端:xxx(若需要)
|
||||
【文档更新】以界面定需求 / 需求汇总 / 临时需求池
|
||||
```
|
||||
### 0.25 对话与收尾(卡若 AI)
|
||||
|
||||
### 0.5 与 role-flow-control 的配合
|
||||
- 面向用户的回复默认 **简体中文**。
|
||||
- **每条回复末尾**增加完整 **卡若复盘块**(🎯📌💡📝▶,标题带日期时间,块内无表格),与卡若项目 `运营中枢/参考资料/卡若复盘格式_固定规则.md` 一致;本仓库规则见 `.cursor/rules/soul-karuo-dialogue.mdc`。
|
||||
|
||||
本流水线与 `SKILL-role-flow-control` 协同:产品经理完成三端分析与指派后,开发执行时按 role-flow-control 的协同流程(需求分析 → 并行开发 → 管理端启动 → 联调)推进。
|
||||
### 0.3 执行后文档更新
|
||||
|
||||
代码变更完成后,按需更新:
|
||||
- `临时需求池/YYYY-MM-DD-需求简述.md`
|
||||
- `需求汇总.md`
|
||||
- 《以界面定需求》界面清单
|
||||
|
||||
---
|
||||
|
||||
@@ -73,7 +68,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
| 职责 | 说明 | 产出 |
|
||||
|------|------|------|
|
||||
| 需求分析 | 业务需求拆解、优先级、技术可行性 | 需求分析文档、临时需求池 |
|
||||
| 需求文档 | 需求清单、业务规则、验收标准 | 1、需求/YYYY-MM-DD-需求.md(以最新为主)、运营与变更.md |
|
||||
| 需求文档 | 需求清单、业务规则、验收标准 | 需求汇总.md、运营与变更.md |
|
||||
| 验收 | 功能验收、回归检查 | 验收清单、项目推进表 |
|
||||
| 协调 | 与开发沟通、排期、变更 | 运营与变更、项目落地推进表 |
|
||||
|
||||
@@ -83,7 +78,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| `开发文档/1、需求/` | 需求清单(按日期命名,以最新为主)、业务需求;见 `1、需求/索引.md` |
|
||||
| `开发文档/1、需求/需求汇总.md` | 需求清单、业务需求 |
|
||||
| `临时需求池/` | 需求分析、技术分析 |
|
||||
| `开发文档/10、项目管理/项目落地推进表.md` | 里程碑、永平落地 |
|
||||
| `开发文档/10、项目管理/运营与变更.md` | 近期讨论、变更记录 |
|
||||
@@ -121,7 +116,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
|
||||
## 6. 何时选用
|
||||
|
||||
- 用户说**「加个需求:xxx」**时:执行 §0 加个需求流水线(三端分析 → 功能规划 → 指派)
|
||||
- 用户说**「加个需求:xxx」**时:执行 **§0 需求即执行**(回复「好」→ 三端落地 → 结果回复 + 卡若复盘收尾)
|
||||
- 编辑 `开发文档/1、需求/`、`临时需求池/`、`开发文档/10、项目管理/` 时
|
||||
- 进行需求分析、需求文档编写、验收标准定义时
|
||||
- 用户说「需求分析」「产品经理」「验收」时
|
||||
|
||||
@@ -25,7 +25,7 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
|
||||
```
|
||||
第零步(可选):回顾历史
|
||||
→ 使用 Read 工具读取 `e:\Gongsi\Mycontent\.cursor\meeting\README.md` 的索引表
|
||||
→ 使用 Read 工具读取 `.cursor\meeting\README.md` 的索引表
|
||||
→ 若议题与近期会议相关,Read 最近 1~2 份纪要(如 YYYY-MM-DD_主题.md)
|
||||
→ 便于延续上次讨论、避免重复决议
|
||||
|
||||
@@ -93,10 +93,10 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
### 4.1 生成会议纪要文件
|
||||
|
||||
1. **确定文件名**:`YYYY-MM-DD_议题关键词.md`(日期为今天)
|
||||
2. **创建文件**:`e:\Gongsi\Mycontent\.cursor\meeting\YYYY-MM-DD_议题关键词.md`
|
||||
3. **内容**:按 `e:\Gongsi\Mycontent\.cursor\meeting\_模板.md` 填写完整纪要
|
||||
2. **创建文件**:`.cursor\meeting\YYYY-MM-DD_议题关键词.md`
|
||||
3. **内容**:按 `.cursor\meeting\_模板.md` 填写完整纪要
|
||||
4. **问题与作答区**(必须):将「待确认项」「待澄清项」列出为问题表,责任角色标明谁负责回答,作答列留空供后续填写
|
||||
5. **更新索引**:在 `e:\Gongsi\Mycontent\.cursor\meeting\README.md` 的索引表中追加一行
|
||||
5. **更新索引**:在 `.cursor\meeting\README.md` 的索引表中追加一行
|
||||
|
||||
### 4.2 各角色经验入库(按日期文件)
|
||||
|
||||
@@ -115,7 +115,7 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
```
|
||||
确定今天日期 YYYY-MM-DD
|
||||
↓
|
||||
检查 e:\Gongsi\Mycontent\.cursor\agent\{角色目录}\evolution\YYYY-MM-DD.md 是否存在
|
||||
检查 `.cursor/agent/{角色目录}/evolution/YYYY-MM-DD.md` 是否存在
|
||||
不存在 → 创建,写入文件头(# {角色名} 经验记录 - YYYY-MM-DD)
|
||||
↓
|
||||
从本次会议讨论中,提炼该角色的经验条目追加进去
|
||||
@@ -123,12 +123,12 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
更新 agent/开发助理/项目索引/{索引名}.md(开发进度表追加一行,写日期)
|
||||
```
|
||||
|
||||
若有跨角色共享的经验(架构决策、业务规则、路由约定),**必须**同时写入 `e:\Gongsi\Mycontent\.cursor\agent\团队\evolution\YYYY-MM-DD.md`,并在对应角色文件中注明「详见 agent/团队/evolution/」。
|
||||
若有跨角色共享的经验(架构决策、业务规则、路由约定),**必须**同时写入 `.cursor/agent/团队/evolution/YYYY-MM-DD.md`,并在对应角色文件中注明「详见 agent/团队/evolution/」。
|
||||
|
||||
### 4.3 Skill 升级(重要决议时)
|
||||
|
||||
若本次会议产生了影响开发规范的决议:
|
||||
- 更新对应 `e:\Gongsi\Mycontent\.cursor\skills\SKILL-xxx.md`
|
||||
- 更新对应 `.cursor\skills\SKILL-xxx.md`
|
||||
- 在会议纪要「各角色经验」节标注「已升级 SKILL-xxx.md」
|
||||
|
||||
---
|
||||
@@ -137,11 +137,11 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
|
||||
| 输出物 | 位置 | 负责人 |
|
||||
|--------|------|--------|
|
||||
| 会议纪要 | `e:\Gongsi\Mycontent\.cursor\meeting\YYYY-MM-DD_主题.md` | 助理橙子 |
|
||||
| 各角色经验 | `e:\Gongsi\Mycontent\.cursor\agent\{角色}\evolution\YYYY-MM-DD.md` | 助理橙子 |
|
||||
| 会议纪要 | `.cursor\meeting\YYYY-MM-DD_主题.md` | 助理橙子 |
|
||||
| 各角色经验 | `.cursor/agent/{角色}/evolution/YYYY-MM-DD.md` | 助理橙子 |
|
||||
| 项目索引更新 | agent/开发助理/项目索引/{角色}.md 的开发进度表 | 助理橙子 |
|
||||
| Skill 升级(按需) | `.cursor/skills/{skill}/SKILL.md` | 助理橙子 |
|
||||
| 会议记录索引更新 | `e:\Gongsi\Mycontent\.cursor\meeting\README.md` | 助理橙子 |
|
||||
| 会议记录索引更新 | `.cursor\meeting\README.md` | 助理橙子 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
3
.cursorignore
Normal file
3
.cursorignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# 减轻 Cursor 代码索引噪声(需要时仍可用 @路径 打开)
|
||||
.cursor/scripts/db-exec/node_modules/
|
||||
|
||||
49
.gitignore
vendored
49
.gitignore
vendored
@@ -1 +1,48 @@
|
||||
new-soul
|
||||
# 根目录忽略
|
||||
.DS_Store
|
||||
*.zip
|
||||
.env
|
||||
.env.*
|
||||
!.env.*.example
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
log/
|
||||
tmp/
|
||||
|
||||
# 永不上传到 GitHub
|
||||
开发文档/
|
||||
|
||||
# 二进制/压缩/临时产物
|
||||
*.exe
|
||||
*.gz
|
||||
*.tar
|
||||
*.tgz
|
||||
*.bak
|
||||
|
||||
# 各子项目已有 .gitignore,此处仅补充分支通用项
|
||||
node_modules/
|
||||
|
||||
# 小程序本地配置
|
||||
miniprogram/project.private.config.json
|
||||
|
||||
# 管理端本地构建缓存
|
||||
soul-admin/.vite/
|
||||
|
||||
# API 本地运行产物与上传目录
|
||||
soul-api/uploads/
|
||||
soul-api/wechat/info.log
|
||||
soul-api/soul-api-linux
|
||||
soul-api/soul-api-new
|
||||
|
||||
# 备份文件
|
||||
*.backup
|
||||
|
||||
# 本机运营凭证(karuo-party;保留 credentials/README.md)
|
||||
.cursor/skills/karuo-party/credentials/cookies/
|
||||
.cursor/skills/karuo-party/credentials/.feishu_tokens.json
|
||||
.cursor/skills/karuo-party/credentials/*.json
|
||||
!.cursor/skills/karuo-party/credentials/README.md
|
||||
|
||||
# Cursor 索引减负:db-exec 依赖(仓库根已有 node_modules/ 规则,此处显式强调子路径)
|
||||
.cursor/scripts/db-exec/node_modules/
|
||||
|
||||
237
content_upload.py
Normal file
237
content_upload.py
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
将书稿 md 上传到小程序对应 chapters 表(与 Soul创业实验 Skill「上传」一致)。
|
||||
|
||||
依赖: pip install pymysql
|
||||
数据库配置:复用 scripts/migrate_2026_sections.py 中的 DB_CONFIG(与现网一致)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
PART_2026 = "part-2026-daily"
|
||||
CHAPTER_2026 = "chapter-2026-daily"
|
||||
TITLE_2026 = "2026每日派对干货"
|
||||
|
||||
|
||||
def load_db_config() -> dict:
|
||||
mig = ROOT / "scripts" / "migrate_2026_sections.py"
|
||||
if not mig.is_file():
|
||||
print("缺少 scripts/migrate_2026_sections.py,无法读取 DB_CONFIG", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
spec = importlib.util.spec_from_file_location("_mig_db", mig)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(mod)
|
||||
cfg = getattr(mod, "DB_CONFIG", None)
|
||||
if not isinstance(cfg, dict):
|
||||
print("migrate_2026_sections.py 中无有效 DB_CONFIG", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return cfg
|
||||
|
||||
|
||||
def strip_md_title_line(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
if lines and lines[0].lstrip().startswith("#"):
|
||||
return "\n".join(lines[1:]).lstrip("\n")
|
||||
return text
|
||||
|
||||
|
||||
def for_miniprogram_body(text: str) -> str:
|
||||
"""上传 README:少用 --- 分割线;正文内独立一行的 --- 改为空行分段。"""
|
||||
out_lines: list[str] = []
|
||||
for line in text.splitlines():
|
||||
if line.strip() == "---":
|
||||
out_lines.append("")
|
||||
out_lines.append("")
|
||||
else:
|
||||
out_lines.append(line)
|
||||
return "\n".join(out_lines).strip() + "\n"
|
||||
|
||||
|
||||
def next_10_id(cur) -> str:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM chapters
|
||||
WHERE id REGEXP '^10\\\\.[0-9]+$'
|
||||
ORDER BY CAST(SUBSTRING_INDEX(id, '.', -1) AS UNSIGNED) DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return "10.01"
|
||||
last = row[0]
|
||||
n = int(last.split(".")[-1])
|
||||
return f"10.{n + 1:02d}"
|
||||
|
||||
|
||||
def list_structure(cur):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT DISTINCT part_id, part_title, chapter_id, chapter_title
|
||||
FROM chapters
|
||||
ORDER BY part_id, chapter_id
|
||||
"""
|
||||
)
|
||||
print("篇章结构(distinct part/chapter):")
|
||||
for r in cur.fetchall():
|
||||
print(f" part={r[0]!r} chapter={r[2]!r} | {r[1]} / {r[3]}")
|
||||
|
||||
|
||||
def list_chapters_2026(cur):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, section_title, sort_order
|
||||
FROM chapters
|
||||
WHERE part_id = %s AND chapter_id = %s
|
||||
ORDER BY COALESCE(sort_order, 999999) ASC, id ASC
|
||||
""",
|
||||
(PART_2026, CHAPTER_2026),
|
||||
)
|
||||
print(f"2026每日派对干货 ({PART_2026} / {CHAPTER_2026}):")
|
||||
for r in cur.fetchall():
|
||||
print(f" {r[0]}\torder={r[2]}\t{r[1]}")
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
print("需要: pip install pymysql", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
p = argparse.ArgumentParser(description="上传书稿 md 到 soul_miniprogram.chapters")
|
||||
p.add_argument("--id", help="业务 id,如 10.27;省略则自动取当前最大 10.xx +1")
|
||||
p.add_argument("--title", help="小节标题,如 第128场|主题")
|
||||
p.add_argument("--content-file", type=Path, help="文章 md 绝对或相对路径")
|
||||
p.add_argument("--part", default=PART_2026)
|
||||
p.add_argument("--chapter", default=CHAPTER_2026)
|
||||
p.add_argument("--part-title", default=TITLE_2026)
|
||||
p.add_argument("--chapter-title", default=TITLE_2026)
|
||||
p.add_argument("--price", type=float, default=1.0)
|
||||
p.add_argument("--free", action="store_true", help="标记为免费")
|
||||
p.add_argument("--list-structure", action="store_true")
|
||||
p.add_argument("--list-chapters", action="store_true")
|
||||
p.add_argument("--dry-run", action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
cfg = load_db_config()
|
||||
conn = pymysql.connect(**cfg)
|
||||
cur = conn.cursor()
|
||||
|
||||
if args.list_structure:
|
||||
list_structure(cur)
|
||||
conn.close()
|
||||
return
|
||||
if args.list_chapters:
|
||||
list_chapters_2026(cur)
|
||||
conn.close()
|
||||
return
|
||||
|
||||
if not args.title or not args.content_file:
|
||||
p.error("上传时必须提供 --title 与 --content-file")
|
||||
|
||||
path = args.content_file.expanduser().resolve()
|
||||
if not path.is_file():
|
||||
print(f"文件不存在: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
body = for_miniprogram_body(strip_md_title_line(raw))
|
||||
word_count = len(body)
|
||||
is_free = 1 if args.free else 0
|
||||
price = 0.0 if args.free else float(args.price)
|
||||
|
||||
section_id = args.id
|
||||
if not section_id:
|
||||
section_id = next_10_id(cur)
|
||||
print(f"未指定 --id,使用新 id: {section_id}")
|
||||
|
||||
cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,))
|
||||
row = cur.fetchone()
|
||||
exists = row is not None
|
||||
|
||||
cur.execute("SELECT COALESCE(MAX(sort_order), -1) FROM chapters")
|
||||
max_sort = cur.fetchone()[0]
|
||||
next_sort = int(max_sort) + 1
|
||||
|
||||
if args.dry_run:
|
||||
print(f"id={section_id} exists={exists} next_sort={next_sort} words={word_count}")
|
||||
print(body[:500] + ("..." if len(body) > 500 else ""))
|
||||
conn.close()
|
||||
return
|
||||
|
||||
if exists:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE chapters SET
|
||||
section_title = %s,
|
||||
content = %s,
|
||||
word_count = %s,
|
||||
price = %s,
|
||||
is_free = %s,
|
||||
part_id = %s,
|
||||
part_title = %s,
|
||||
chapter_id = %s,
|
||||
chapter_title = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(
|
||||
args.title,
|
||||
body,
|
||||
word_count,
|
||||
price,
|
||||
is_free,
|
||||
args.part,
|
||||
args.part_title,
|
||||
args.chapter,
|
||||
args.chapter_title,
|
||||
section_id,
|
||||
),
|
||||
)
|
||||
print(f"已更新 {section_id} | {args.title}")
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO chapters (
|
||||
id, part_id, part_title, chapter_id, chapter_title,
|
||||
section_title, content, word_count, is_free, price,
|
||||
sort_order, status, edition_standard, edition_premium,
|
||||
hot_score, created_at, updated_at
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, 'published', 1, 0,
|
||||
0, NOW(), NOW()
|
||||
)
|
||||
""",
|
||||
(
|
||||
section_id,
|
||||
args.part,
|
||||
args.part_title,
|
||||
args.chapter,
|
||||
args.chapter_title,
|
||||
args.title,
|
||||
body,
|
||||
word_count,
|
||||
is_free,
|
||||
price,
|
||||
next_sort,
|
||||
),
|
||||
)
|
||||
print(f"已创建 {section_id} | {args.title} | sort_order={next_sort}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -35,7 +35,8 @@ miniprogram/
|
||||
│ ├── purchases/ # 订单页
|
||||
│ └── settings/ # 设置页
|
||||
├── utils/
|
||||
│ └── util.js # 工具函数
|
||||
│ ├── util.js # 工具函数
|
||||
│ └── payment.js # 支付工具
|
||||
├── assets/
|
||||
│ └── icons/ # 图标资源
|
||||
├── project.config.json # 项目配置
|
||||
|
||||
@@ -9,13 +9,16 @@ const { checkAndExecute } = require('./utils/ruleEngine.js')
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
|
||||
const APP_DISPLAY_VERSION = '1.7.1'
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// API 基础地址:开发时修改下面一行切换环境
|
||||
// baseUrl: "https://soulapi.quwanzhi.com",
|
||||
baseUrl: 'http://localhost:8080', // 开发
|
||||
// baseUrl: 'https://souldev.quwanzhi.com', // 测试
|
||||
// 与微信后台上传版本号一致,供设置页等展示(避免与线上 version 字段混淆)
|
||||
appDisplayVersion: APP_DISPLAY_VERSION,
|
||||
|
||||
// API:仓库默认生产;release 强制生产;develop/trial 可读 storage「apiBaseUrl」或用 env-switch
|
||||
baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: DEFAULT_APP_ID,
|
||||
|
||||
@@ -30,9 +33,14 @@ App({
|
||||
openId: null, // 微信openId,支付必需
|
||||
isLoggedIn: false,
|
||||
|
||||
// 阅读页 @ 解析:/config/read-extras 的 mentionPersons(与后台 persons + token 一致)
|
||||
mentionPersons: [],
|
||||
// 是否已成功拉取过 read-extras(避免仅 linkTags 有缓存时永远拿不到 mentionPersons)
|
||||
readExtrasCacheValid: false,
|
||||
|
||||
// 书籍数据(bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters)
|
||||
bookData: null,
|
||||
totalSections: 62,
|
||||
totalSections: 90,
|
||||
|
||||
// 购买记录
|
||||
purchasedSections: [],
|
||||
@@ -47,6 +55,9 @@ App({
|
||||
// 推荐绑定
|
||||
pendingReferralCode: null, // 待绑定的推荐码
|
||||
|
||||
// 客服微信号(从系统配置加载,默认值兜底)
|
||||
serviceWechat: '28533368',
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
brandColor: '#00CED1',
|
||||
@@ -81,16 +92,41 @@ App({
|
||||
// config 统一缓存(5min),减少重复请求
|
||||
configCache: null,
|
||||
configCacheExpires: 0,
|
||||
// VIP 联系方式检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
// VIP 联系方式检测:上次检测时间戳,onShow 短节流(避免与 launch 重复打满接口)
|
||||
lastVipContactCheck: 0,
|
||||
// 头像昵称检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
// 头像昵称检测:上次检测时间戳(与 VIP 检测同周期刷新)
|
||||
lastAvatarNicknameCheck: 0,
|
||||
// 登录过期:401 后用户点「去登录」时设为 true,我的页 onShow 会检测并自动弹登录
|
||||
pendingLoginAfterExpire: false,
|
||||
},
|
||||
|
||||
|
||||
/** 正式版强制生产 API,避免误传 localhost 导致审核/线上全挂 */
|
||||
initApiBaseUrl() {
|
||||
const PRODUCTION = 'https://soulapi.quwanzhi.com'
|
||||
const KEY = 'apiBaseUrl'
|
||||
try {
|
||||
const info = wx.getAccountInfoSync?.()
|
||||
const env = info?.miniProgram?.envVersion || 'release'
|
||||
if (env === 'release') {
|
||||
this.globalData.baseUrl = PRODUCTION
|
||||
try {
|
||||
const saved = wx.getStorageSync(KEY)
|
||||
if (saved && saved !== PRODUCTION) wx.removeStorageSync(KEY)
|
||||
} catch (_) {}
|
||||
return
|
||||
}
|
||||
const saved = wx.getStorageSync(KEY)
|
||||
if (saved && typeof saved === 'string' && /^https?:\/\//.test(saved)) {
|
||||
this.globalData.baseUrl = String(saved).replace(/\/$/, '')
|
||||
}
|
||||
} catch (_) {
|
||||
this.globalData.baseUrl = PRODUCTION
|
||||
}
|
||||
},
|
||||
|
||||
onLaunch(options) {
|
||||
this.initApiBaseUrl()
|
||||
// 昵称等隐私组件需先授权:input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
|
||||
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
|
||||
wx.onNeedPrivacyAuthorization((resolve) => {
|
||||
@@ -171,10 +207,10 @@ App({
|
||||
this.globalData.lastMpConfigCheck = now
|
||||
this.getAuditMode()
|
||||
}
|
||||
// 从后台切回:先 VIP 强制跳转,再头像/昵称,节流 5 分钟
|
||||
const throttle = 5 * 60 * 1000
|
||||
// 从后台切回:刷新 VIP/头像引导(vipGuideThrottleMs=0 表示不限制间隔)
|
||||
const vipGuideThrottleMs = 0
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
|
||||
if (!this.globalData.lastVipContactCheck || now - this.globalData.lastVipContactCheck > throttle) {
|
||||
if (vipGuideThrottleMs <= 0 || !this.globalData.lastVipContactCheck || now - this.globalData.lastVipContactCheck > vipGuideThrottleMs) {
|
||||
this.globalData.lastVipContactCheck = now
|
||||
this.globalData.lastAvatarNicknameCheck = now
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 500)
|
||||
@@ -560,9 +596,6 @@ App({
|
||||
*/
|
||||
async checkVipContactRequiredAndGuide() {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
const now = Date.now()
|
||||
if (this._lastVipGuideRun && now - this._lastVipGuideRun < 3000) return // 3 秒内不重复执行,避免 onLaunch+onShow 双重触发
|
||||
this._lastVipGuideRun = now
|
||||
const userId = this.globalData.userInfo.id
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
@@ -690,10 +723,11 @@ App({
|
||||
* 获取阅读页扩展配置(linkTags、linkedMiniprograms),懒加载
|
||||
*/
|
||||
async getReadExtras() {
|
||||
if (Array.isArray(this.globalData.linkTagsConfig) && this.globalData.linkTagsConfig.length > 0) {
|
||||
if (this.globalData.readExtrasCacheValid) {
|
||||
return {
|
||||
linkTags: this.globalData.linkTagsConfig,
|
||||
linkedMiniprograms: this.globalData.linkedMiniprograms || []
|
||||
linkTags: this.globalData.linkTagsConfig || [],
|
||||
linkedMiniprograms: this.globalData.linkedMiniprograms || [],
|
||||
mentionPersons: this.globalData.mentionPersons || [],
|
||||
}
|
||||
}
|
||||
try {
|
||||
@@ -701,10 +735,18 @@ App({
|
||||
if (res) {
|
||||
if (Array.isArray(res.linkTags)) this.globalData.linkTagsConfig = res.linkTags
|
||||
if (Array.isArray(res.linkedMiniprograms)) this.globalData.linkedMiniprograms = res.linkedMiniprograms
|
||||
if (Array.isArray(res.mentionPersons)) this.globalData.mentionPersons = res.mentionPersons
|
||||
else this.globalData.mentionPersons = []
|
||||
this.globalData.readExtrasCacheValid = true
|
||||
return res
|
||||
}
|
||||
} catch (e) {}
|
||||
return { linkTags: [], linkedMiniprograms: [] }
|
||||
if (!Array.isArray(this.globalData.mentionPersons)) this.globalData.mentionPersons = []
|
||||
return {
|
||||
linkTags: this.globalData.linkTagsConfig || [],
|
||||
linkedMiniprograms: this.globalData.linkedMiniprograms || [],
|
||||
mentionPersons: this.globalData.mentionPersons,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -761,14 +803,14 @@ App({
|
||||
|
||||
/**
|
||||
* 小程序更新检测(基于 wx.getUpdateManager)
|
||||
* - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求)
|
||||
* - 启动时检测;从后台切回前台时也检测(短间隔即可,避免用户感知「很久才检查更新」)
|
||||
*/
|
||||
checkUpdate() {
|
||||
try {
|
||||
if (!wx.canIUse('getUpdateManager')) return
|
||||
const now = Date.now()
|
||||
const lastCheck = this.globalData.lastUpdateCheck || 0
|
||||
if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测
|
||||
if (lastCheck && now - lastCheck < 60 * 1000) return // 1 分钟内不重复检测
|
||||
this.globalData.lastUpdateCheck = now
|
||||
|
||||
const updateManager = wx.getUpdateManager()
|
||||
@@ -1062,13 +1104,6 @@ App({
|
||||
return null
|
||||
},
|
||||
|
||||
// 模拟登录已废弃 - 不再使用
|
||||
// 现在必须使用真实的微信登录获取openId作为唯一标识
|
||||
mockLogin() {
|
||||
console.warn('[App] mockLogin已废弃,请使用真实登录')
|
||||
return null
|
||||
},
|
||||
|
||||
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
|
||||
async loginWithPhone(phoneCode) {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"pages/avatar-nickname/avatar-nickname",
|
||||
"pages/gift-pay/detail",
|
||||
"pages/gift-pay/list",
|
||||
"pages/gift-pay/redemption-detail",
|
||||
"pages/dev-login/dev-login"
|
||||
"pages/gift-pay/redemption-detail"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
|
||||
8
miniprogram/assets/icons/list-teal.svg
Normal file
8
miniprogram/assets/icons/list-teal.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="8" y1="6" x2="21" y2="6" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 814 B |
7
miniprogram/assets/icons/share-teal.svg
Normal file
7
miniprogram/assets/icons/share-teal.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="5" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="6" cy="12" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="18" cy="19" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
6
miniprogram/assets/icons/unlock-muted-teal.svg
Normal file
6
miniprogram/assets/icons/unlock-muted-teal.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<g stroke="#00CED1" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.36">
|
||||
<path d="M7 10.5V8a5 5 0 0 1 9.6-2"/>
|
||||
<rect x="5" y="10.5" width="14" height="10.5" rx="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
4
miniprogram/assets/icons/wallet-teal.svg
Normal file
4
miniprogram/assets/icons/wallet-teal.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -31,7 +31,7 @@
|
||||
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
|
||||
|
||||
<text class="doc-section">七、联系我们</text>
|
||||
<text class="doc-p">如有疑问,请通过 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -29,6 +29,9 @@ Page({
|
||||
|
||||
// 已加载的篇章章节缓存 { partId: chapters }
|
||||
_loadedChapters: {},
|
||||
|
||||
// 小三角点击动画:当前触发的子章 id(与 chapter.id 比对)
|
||||
_triangleAnimating: '',
|
||||
|
||||
// 固定模块 id -> mid(序言/尾声/附录,供 goToRead 传 mid)
|
||||
fixedSectionsMap: {},
|
||||
@@ -160,6 +163,12 @@ Page({
|
||||
})
|
||||
})
|
||||
const chapters = Array.from(chMap.values())
|
||||
chapters.forEach(ch => ch.sections.reverse())
|
||||
// 目录子章下列表:默认最多展示 5 条,点小三角每次再展开 5 条
|
||||
chapters.forEach((ch) => {
|
||||
const n = ch.sections.length
|
||||
ch.sectionVisibleLimit = n === 0 ? 0 : Math.min(5, n)
|
||||
})
|
||||
const loaded = { ...this.data._loadedChapters, [partId]: chapters }
|
||||
const bookData = this.data.bookData.map(p =>
|
||||
p.id === partId ? { ...p, chapters } : p
|
||||
@@ -235,6 +244,43 @@ Page({
|
||||
if (isExpanding) await this.loadChaptersByPart(partId)
|
||||
},
|
||||
|
||||
expandSectionChapter(e) {
|
||||
const partId = e.currentTarget.dataset.partId
|
||||
const chapterId = e.currentTarget.dataset.chapterId
|
||||
if (!partId || !chapterId) return
|
||||
trackClick('chapters', 'tab_click', '目录_子章展开5条')
|
||||
|
||||
const part = this.data.bookData.find((p) => p.id === partId)
|
||||
const chapter = part && (part.chapters || []).find((c) => c.id === chapterId)
|
||||
if (!chapter || !chapter.sections || chapter.sections.length === 0) return
|
||||
|
||||
const total = chapter.sections.length
|
||||
const cur = typeof chapter.sectionVisibleLimit === 'number' ? chapter.sectionVisibleLimit : Math.min(5, total)
|
||||
const next = Math.min(cur + 5, total)
|
||||
if (next === cur) return
|
||||
|
||||
const bookData = this.data.bookData.map((p) => {
|
||||
if (p.id !== partId) return p
|
||||
return {
|
||||
...p,
|
||||
chapters: (p.chapters || []).map((ch) =>
|
||||
ch.id === chapterId ? { ...ch, sectionVisibleLimit: next } : ch
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
// 先去掉动画 class 再打上,便于连续点击重复触发动画
|
||||
this.setData({ _triangleAnimating: '', bookData })
|
||||
setTimeout(() => {
|
||||
this.setData({ _triangleAnimating: chapterId })
|
||||
setTimeout(() => {
|
||||
if (this.data._triangleAnimating === chapterId) {
|
||||
this.setData({ _triangleAnimating: '' })
|
||||
}
|
||||
}, 480)
|
||||
}, 30)
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
|
||||
<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}}" data-mid="{{section.mid}}">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section" wx:for-index="secIdx">
|
||||
<view class="section-item" wx:if="{{secIdx < chapter.sectionVisibleLimit}}" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<view class="section-lock-wrap">
|
||||
<icon wx:if="{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" name="lock-open" size="24" color="#00CED1" customClass="section-lock lock-open"></icon>
|
||||
@@ -100,12 +100,14 @@
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
<text wx:elif="{{isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已解锁</text>
|
||||
<text wx:else class="section-price">¥{{section.price}}</text>
|
||||
<text wx:elif="{{!(isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1)}}" class="section-price">¥{{section.price}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="section-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="section-expand-trigger" wx:if="{{chapter.sections.length > chapter.sectionVisibleLimit}}" bindtap="expandSectionChapter" data-part-id="{{item.id}}" data-chapter-id="{{chapter.id}}">
|
||||
<view class="latest-expand-triangle {{_triangleAnimating === chapter.id ? 'tri-bounce' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -577,6 +577,49 @@
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 展开三角 ===== */
|
||||
.section-expand-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.latest-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 18rpx solid transparent;
|
||||
border-right: 18rpx solid transparent;
|
||||
border-top: 14rpx solid rgba(0, 206, 209, 0.55);
|
||||
opacity: 0.85;
|
||||
transform-origin: 50% 0;
|
||||
transition: border-top-color 0.15s ease;
|
||||
}
|
||||
|
||||
.section-expand-trigger:active .latest-expand-triangle {
|
||||
border-top-color: #00CED1;
|
||||
}
|
||||
|
||||
@keyframes catalog-tri-nudge {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(10rpx) scale(1.12);
|
||||
opacity: 1;
|
||||
border-top-color: #00CED1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-expand-triangle.tri-bounce {
|
||||
animation: catalog-tri-nudge 0.45s ease-out;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 卡若创业派对 - 开发登录页
|
||||
* 临时:账户=手机号,密码可空,用于切换为对方账号调试
|
||||
* 卡若创业派对 - 开发登录页(仅本地调试)
|
||||
* 勿写入 app.json pages:提审包不得注册本页。
|
||||
* 需要用时在 app.json 的 pages 数组末尾临时加入 "pages/dev-login/dev-login"。
|
||||
*/
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
|
||||
@@ -4,10 +4,23 @@
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser')
|
||||
|
||||
/** 与首页固定「卡若」获客位重复时从横滑列表剔除(含历史误写「卡路」) */
|
||||
function isKaruoHostDuplicateName(displayName) {
|
||||
const s = String(displayName || '').trim()
|
||||
return s === '卡若' || s === '卡路'
|
||||
}
|
||||
|
||||
/** 超级个体无头像占位:仅展示中文首字,避免头像圆里出现英文字母 */
|
||||
function superAvatarLetter(displayName) {
|
||||
const s = String(displayName || '').trim()
|
||||
if (!s) return '会'
|
||||
const ch = s[0]
|
||||
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -31,8 +44,8 @@ Page({
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
|
||||
],
|
||||
|
||||
// 最新章节(动态计算)
|
||||
latestSection: null,
|
||||
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters)
|
||||
bannerSection: null,
|
||||
latestLabel: '最新更新',
|
||||
|
||||
// 内容概览
|
||||
@@ -66,16 +79,13 @@ Page({
|
||||
// 展开状态(首页精选/最新)
|
||||
featuredExpanded: false,
|
||||
latestExpanded: false,
|
||||
featuredSectionsFull: [], // ranking 返回的 10 条,默认展示前 5 条
|
||||
featuredSectionsFull: [], // 精选排行榜全量(最多 50),默认只展示前 3 条
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true,
|
||||
|
||||
// 审核模式:隐藏支付相关入口
|
||||
auditMode: false,
|
||||
|
||||
// 置顶人(链接人与事,管理端设置,小程序首页展示)
|
||||
pinnedPerson: null // { nickname, avatar, token } 或 null
|
||||
auditMode: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -100,10 +110,7 @@ Page({
|
||||
|
||||
onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
const app = getApp()
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
// 每次展示时刷新置顶人(管理端可能已更换置顶)
|
||||
this.loadPinnedPerson()
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
@@ -136,48 +143,27 @@ Page({
|
||||
this.loadBookData()
|
||||
this.loadFeaturedAndLatest()
|
||||
this.loadSuperMembers()
|
||||
this.loadPinnedPerson()
|
||||
},
|
||||
|
||||
async loadPinnedPerson() {
|
||||
const app = getApp()
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/ckb/pinned-person', silent: true })
|
||||
if (res && res.success && res.data) {
|
||||
this.setData({ pinnedPerson: res.data })
|
||||
} else {
|
||||
this.setData({ pinnedPerson: null })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ pinnedPerson: null })
|
||||
}
|
||||
},
|
||||
|
||||
async loadSuperMembers() {
|
||||
this.setData({ superMembersLoading: true })
|
||||
try {
|
||||
// 并行请求 VIP 会员和普通用户,合并后取前 4 个(VIP 优先)
|
||||
const [vipRes, usersRes] = await Promise.all([
|
||||
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
|
||||
app.request({ url: '/api/miniprogram/users?limit=20', silent: true }).catch(() => null)
|
||||
])
|
||||
// 仅走后端 VIP 列表排序(vip_sort、vip_activated_at),不在端上拼普通用户
|
||||
const vipRes = await app.request({ url: '/api/miniprogram/vip/members?limit=24', silent: true }).catch(() => null)
|
||||
let members = []
|
||||
if (vipRes && vipRes.success && Array.isArray(vipRes.data) && vipRes.data.length > 0) {
|
||||
members = vipRes.data.slice(0, 4).map(u => ({
|
||||
id: u.id,
|
||||
name: u.nickname || u.vipName || u.vip_name || '会员',
|
||||
avatar: u.avatar || '',
|
||||
isVip: true
|
||||
}))
|
||||
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '人')
|
||||
}
|
||||
if (members.length < 4 && usersRes && usersRes.success && Array.isArray(usersRes.data)) {
|
||||
const existIds = new Set(members.map(m => m.id))
|
||||
const extra = usersRes.data
|
||||
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
|
||||
.slice(0, 4 - members.length)
|
||||
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
|
||||
members = members.concat(extra)
|
||||
members = vipRes.data.map(u => {
|
||||
const raw = u.name || u.nickname || u.vipName || u.vip_name || '会员'
|
||||
const name = cleanSingleLineField(raw) || '会员'
|
||||
return {
|
||||
id: u.id,
|
||||
name,
|
||||
avatar: u.avatar || '',
|
||||
isVip: true,
|
||||
avatarLetter: superAvatarLetter(name)
|
||||
}
|
||||
}).filter((m) => !isKaruoHostDuplicateName(m.name))
|
||||
console.log('[Index] 超级个体(后端排序):', members.length, '人')
|
||||
}
|
||||
this.setData({ superMembers: members, superMembersLoading: false })
|
||||
} catch (e) {
|
||||
@@ -186,53 +172,79 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 精选推荐 + 最新更新 + 最新列表:一次请求 recommended + latest-chapters,避免重复
|
||||
// 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法,latest=updated_at)
|
||||
async loadFeaturedAndLatest() {
|
||||
try {
|
||||
const excludeFixed = (c) => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
const tagClassForTag = (tag) => (tag === '热门' ? 'tag-hot' : 'tag-rec')
|
||||
const toSectionFromRanking = (s) => {
|
||||
const tag = s.tag || '精选'
|
||||
return {
|
||||
id: s.id || s.section_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,
|
||||
tagClass: tagClassForTag(tag)
|
||||
}
|
||||
}
|
||||
const fallbackTags = ['热门', '推荐', '精选']
|
||||
const toSectionFromHot = (s, i) => {
|
||||
const tag = fallbackTags[i % 3]
|
||||
return {
|
||||
id: s.id || s.section_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,
|
||||
tagClass: tagClassForTag(tag)
|
||||
}
|
||||
}
|
||||
const toSection = (s, i, tagMap = ['热门', '推荐', '精选']) => ({
|
||||
id: s.id || s.section_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: s.tag || tagMap[i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
})
|
||||
|
||||
const [rankRes, latestRes] = await Promise.all([
|
||||
app.request({ url: '/api/miniprogram/book/ranking?limit=10', silent: true }).catch(() => null),
|
||||
const [recRes, latestRes] = await Promise.all([
|
||||
app.request({ url: '/api/miniprogram/book/recommended?limit=50', silent: true }).catch(() => null),
|
||||
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
|
||||
])
|
||||
|
||||
// 1. 精选推荐(10 条,默认展示 5 条,展开显示 10 条)
|
||||
// 1. 精选推荐:一次拉全量(≤50),默认只显示 3 条;点列表下三角展开(与「最新新增」一致)
|
||||
let featuredFull = []
|
||||
if (rankRes && rankRes.success && Array.isArray(rankRes.data) && rankRes.data.length > 0) {
|
||||
featuredFull = rankRes.data.map((s, i) => toSection(s, i))
|
||||
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
|
||||
featuredFull = recRes.data.map((s) => toSectionFromRanking(s))
|
||||
}
|
||||
if (featuredFull.length === 0) {
|
||||
try {
|
||||
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
|
||||
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
|
||||
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
|
||||
if (hotList.length > 0) featuredFull = hotList.slice(0, 10).map((s, i) => toSection(s, i))
|
||||
if (hotList.length > 0) featuredFull = hotList.map((s, i) => toSectionFromHot(s, i))
|
||||
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
|
||||
}
|
||||
if (featuredFull.length > 0) {
|
||||
this.setData({
|
||||
featuredSectionsFull: featuredFull,
|
||||
featuredSections: featuredFull.slice(0, 5)
|
||||
featuredSections: featuredFull.slice(0, 3),
|
||||
featuredExpanded: false
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
featuredSectionsFull: [],
|
||||
featuredSections: [],
|
||||
featuredExpanded: false
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 最新更新 + 最新列表(共用 latest-chapters 数据)
|
||||
// 2. Banner 推荐:优先取 recommended 第一条,回退 latest 第一条
|
||||
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
|
||||
const latestList = rawList.filter(excludeFixed)
|
||||
if (latestList.length > 0) {
|
||||
// 按更新时间倒序,最新在前(与后台展示一致)
|
||||
const latestList = [...rawList].sort((a, b) => {
|
||||
const ta = new Date(a.updatedAt || a.updated_at || 0).getTime()
|
||||
const tb = new Date(b.updatedAt || b.updated_at || 0).getTime()
|
||||
return tb - ta
|
||||
})
|
||||
if (featuredFull.length > 0) {
|
||||
this.setData({ bannerSection: featuredFull[0] })
|
||||
} else if (latestList.length > 0) {
|
||||
const l = latestList[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
bannerSection: {
|
||||
id: l.id,
|
||||
mid: l.mid ?? l.MID ?? 0,
|
||||
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
|
||||
@@ -359,12 +371,6 @@ Page({
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
// 2 分钟内只能点一次(与后端限频一致)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
@@ -464,11 +470,6 @@ Page({
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const app = getApp()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
@@ -526,23 +527,24 @@ Page({
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 精选推荐:展开/折叠(默认 5 条,展开显示 10 条)
|
||||
toggleFeaturedExpanded() {
|
||||
trackClick('home', 'tab_click', this.data.featuredExpanded ? '精选收起' : '精选展开')
|
||||
// 精选推荐:列表下方小三角展开(数据已在 loadFeaturedAndLatest 一次拉齐)
|
||||
expandFeaturedChapters() {
|
||||
if (this.data.featuredExpanded) return
|
||||
const full = this.data.featuredSectionsFull || []
|
||||
if (this.data.featuredExpanded) {
|
||||
this.setData({ featuredExpanded: false, featuredSections: full.slice(0, 5) })
|
||||
} else {
|
||||
this.setData({ featuredExpanded: true, featuredSections: full })
|
||||
}
|
||||
if (full.length <= 3) return
|
||||
trackClick('home', 'tab_click', '精选展开_底部三角')
|
||||
this.setData({ featuredExpanded: true, featuredSections: full })
|
||||
},
|
||||
|
||||
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
|
||||
toggleLatestExpanded() {
|
||||
trackClick('home', 'tab_click', this.data.latestExpanded ? '最新收起' : '最新展开')
|
||||
const expanded = !this.data.latestExpanded
|
||||
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
|
||||
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
|
||||
// 最新新增:列表下方小三角展开(无「收起」,展开后整页向下滚动查看)
|
||||
expandLatestChapters() {
|
||||
if (this.data.latestExpanded) return
|
||||
trackClick('home', 'tab_click', '最新展开_底部三角')
|
||||
const full = this.data.latestChapters || []
|
||||
this.setData({
|
||||
latestExpanded: true,
|
||||
displayLatestChapters: full
|
||||
})
|
||||
},
|
||||
|
||||
goToMemberDetail(e) {
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<!-- 自定义导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部区域(按设计稿:S 图标 + 标题副标题 | 点击链接卡若) -->
|
||||
<!-- 顶部区域:中文标识 + 标题副标题 | 链接卡若 -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="logo-section">
|
||||
<view class="logo-icon">
|
||||
<text class="logo-text">S</text>
|
||||
<text class="logo-text">派</text>
|
||||
</view>
|
||||
<view class="logo-info">
|
||||
<text class="logo-title-text">卡若创业派对</text>
|
||||
@@ -18,8 +18,8 @@
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="contact-btn" bindtap="onLinkKaruo">
|
||||
<image class="contact-avatar" src="{{pinnedPerson && pinnedPerson.avatar ? pinnedPerson.avatar : '/assets/images/author-avatar.png'}}" mode="aspectFill"/>
|
||||
<text class="contact-text">点击链接{{pinnedPerson && pinnedPerson.nickname ? pinnedPerson.nickname : '卡若'}}</text>
|
||||
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
|
||||
<text class="contact-name">点击链接卡若</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -35,13 +35,13 @@
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- Banner卡片 - 最新章节(异步加载) -->
|
||||
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<!-- Banner 推荐卡片(优先 recommended API 第一条) -->
|
||||
<view class="banner-card" wx:if="{{bannerSection}}" bindtap="goToRead" data-id="{{bannerSection.id}}" data-mid="{{bannerSection.mid}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">推荐</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
<view class="banner-title">{{bannerSection.title}}</view>
|
||||
<view class="banner-action">
|
||||
<text class="banner-action-text">点击阅读123</text>
|
||||
<text class="banner-action-text">点击阅读</text>
|
||||
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
@@ -53,10 +53,11 @@
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
|
||||
<!-- 超级个体:与匹配页一致,仅 VIP 横向列表(无首位特例) -->
|
||||
<view class="section" wx:if="{{!auditMode}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">超级个体</text>
|
||||
<text class="section-subtitle">获客入口</text>
|
||||
</view>
|
||||
<!-- 加载中:骨架动画 -->
|
||||
<view wx:if="{{superMembersLoading}}" class="super-loading">
|
||||
@@ -76,12 +77,16 @@
|
||||
wx:key="id"
|
||||
bindtap="goToMemberDetail"
|
||||
data-id="{{item.id}}"
|
||||
hover-class="super-item-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{(item.name && item.name[0]) || '会'}}</text>
|
||||
<view class="super-item-stack">
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.avatarLetter}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -92,14 +97,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选推荐(带 tag,支持展开更多) -->
|
||||
<!-- 精选推荐:默认 3 条,列表下三角展开更多(与最新新增一致) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选推荐</text>
|
||||
<view class="section-more" wx:if="{{featuredSectionsFull.length > 5}}" bindtap="toggleFeaturedExpanded">
|
||||
<text class="more-text">{{featuredExpanded ? '收起' : '展开更多'}}</text>
|
||||
<icon name="{{featuredExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
@@ -111,30 +112,29 @@
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}" wx:if="{{item.tag}}">{{item.tag}}</text>
|
||||
<view class="featured-meta" wx:if="{{item.tag}}">
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.6)" customClass="featured-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="latest-expand-hint"
|
||||
wx:if="{{!featuredExpanded && featuredSectionsFull.length > 3}}"
|
||||
bindtap="expandFeaturedChapters"
|
||||
hover-class="latest-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="latest-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新新增(时间线样式,支持展开更多) -->
|
||||
<!-- 最新新增(时间线样式;超过 5 条时在列表下方点小三角展开,展开后随页面滚动看全部) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header latest-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<view class="section-header-right">
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
</view>
|
||||
<view class="section-more" wx:if="{{latestChapters.length > 5}}" bindtap="toggleLatestExpanded">
|
||||
<text class="more-text">{{latestExpanded ? '收起' : '展开更多'}}</text>
|
||||
<icon name="{{latestExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="timeline-wrap">
|
||||
<view class="timeline-line"></view>
|
||||
@@ -149,6 +149,16 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 仅文案「展开更多」去掉:下方居中轻点小三角,点一次展开剩余条目 -->
|
||||
<view
|
||||
class="latest-expand-hint"
|
||||
wx:if="{{latestChapters.length > 5 && !latestExpanded}}"
|
||||
bindtap="expandLatestChapters"
|
||||
hover-class="latest-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="latest-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -63,26 +63,28 @@
|
||||
|
||||
.contact-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 20rpx 8rpx 12rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 16rpx 10rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.35);
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 24rpx;
|
||||
.contact-name {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
@@ -330,6 +332,12 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.42);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -445,6 +453,7 @@
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin-top: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 内容概览列表 ===== */
|
||||
@@ -608,9 +617,18 @@
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
.super-item-hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
.super-item-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.super-scroll .super-avatar {
|
||||
width: 112rpx;
|
||||
@@ -633,10 +651,11 @@
|
||||
gap: 10rpx;
|
||||
}
|
||||
.super-avatar {
|
||||
position: relative;
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
background: rgba(0,206,209,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -647,10 +666,33 @@
|
||||
border: 3rpx solid #FFD700;
|
||||
box-shadow: 0 0 12rpx rgba(255,215,0,0.3);
|
||||
}
|
||||
.super-avatar-pinned {
|
||||
border: 3rpx solid #38bdac;
|
||||
box-shadow: 0 0 16rpx rgba(56, 189, 172, 0.4);
|
||||
}
|
||||
.super-item-pinned .super-name {
|
||||
color: #38bdac;
|
||||
}
|
||||
.pinned-badge {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: #38bdac;
|
||||
border-radius: 50%;
|
||||
font-size: 18rpx;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.super-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.super-avatar-text {
|
||||
font-size: 40rpx;
|
||||
@@ -688,15 +730,10 @@
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.daily-badge-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 设计稿 1:1:橙底白字 rounded-full */
|
||||
.daily-badge {
|
||||
@@ -706,10 +743,29 @@
|
||||
font-weight: 700;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
margin-left: 8rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(246, 173, 85, 0.3);
|
||||
}
|
||||
|
||||
/* 列表下方:仅小三角,点击展开(替代标题栏「展开更多」) */
|
||||
.latest-expand-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8rpx 0 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.latest-expand-hint-hover {
|
||||
opacity: 0.65;
|
||||
}
|
||||
/* 向下小三角 */
|
||||
.latest-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 20rpx solid rgba(0, 206, 209, 0.85);
|
||||
}
|
||||
|
||||
/* 设计稿 1:1:pl-3 竖线 left-3 top-2 bottom-2 w-[1px] bg-gray-800 */
|
||||
.timeline-wrap {
|
||||
position: relative;
|
||||
@@ -844,6 +900,64 @@
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
/* 展开/收起按钮 */
|
||||
.expand-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 20rpx 0;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.expand-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.expand-icon {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 热度排行 */
|
||||
.hot-list {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.hot-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 28rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.hot-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.hot-rank {
|
||||
width: 48rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.hot-rank-top {
|
||||
color: #38bdac;
|
||||
}
|
||||
.hot-title {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.hot-price {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
|
||||
@@ -6,6 +6,7 @@ Page({
|
||||
title: '链接预览',
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
loadError: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -19,6 +20,26 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onWebViewError() {
|
||||
this.setData({ loadError: true })
|
||||
wx.showModal({
|
||||
title: '无法在小程序内打开',
|
||||
content: '该链接无法在小程序内预览,是否复制链接到浏览器打开?',
|
||||
confirmText: '复制链接',
|
||||
cancelText: '返回',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: this.data.url,
|
||||
success: () => wx.showToast({ title: '链接已复制,请在浏览器打开', icon: 'none', duration: 2000 }),
|
||||
})
|
||||
} else {
|
||||
this.goBack()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
<view class="nav-placeholder" style="height: {{navBarHeight}}px;"></view>
|
||||
|
||||
<!-- 链接预览区域 -->
|
||||
<view class="webview-wrap" wx:if="{{url}}">
|
||||
<web-view src="{{url}}"></web-view>
|
||||
<view class="webview-wrap" wx:if="{{url && !loadError}}">
|
||||
<web-view src="{{url}}" binderror="onWebViewError"></web-view>
|
||||
</view>
|
||||
<view class="error-wrap" wx:elif="{{loadError}}">
|
||||
<text class="error-text">该链接无法在小程序内预览</text>
|
||||
<view class="error-btn" bindtap="copyLink">
|
||||
<text class="error-btn-text">复制链接到浏览器打开</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-wrap" wx:else>
|
||||
<text class="empty-text">暂无链接地址</text>
|
||||
|
||||
@@ -81,3 +81,27 @@ web-view {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.error-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 40rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #38bdac;
|
||||
}
|
||||
|
||||
.error-btn-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,17 @@ Page({
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const avatar = res?.data?.avatarUrl || app.globalData.userInfo?.avatarUrl || ''
|
||||
const isDefaultAvatar = !avatar || avatar.includes('default') || avatar.includes('132')
|
||||
if (isDefaultAvatar) {
|
||||
wx.showModal({
|
||||
title: '完善头像',
|
||||
content: '请先设置头像后再使用匹配功能',
|
||||
confirmText: '去设置',
|
||||
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
|
||||
@@ -311,7 +322,7 @@ Page({
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/catalog/catalog' })
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -475,34 +486,6 @@ Page({
|
||||
}, delay)
|
||||
},
|
||||
|
||||
// 生成模拟匹配数据
|
||||
generateMockMatch() {
|
||||
const nicknames = ['创业先锋', '资源整合者', '私域专家', '导师顾问', '连续创业者']
|
||||
const concepts = [
|
||||
'专注私域流量运营5年,帮助100+品牌实现从0到1的增长。',
|
||||
'连续创业者,擅长商业模式设计和资源整合。',
|
||||
'在Soul分享真实创业故事,希望找到志同道合的合作伙伴。'
|
||||
]
|
||||
const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
|
||||
|
||||
const index = Math.floor(Math.random() * nicknames.length)
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
return {
|
||||
id: `user_${Date.now()}`,
|
||||
nickname: nicknames[index],
|
||||
avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
|
||||
tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
|
||||
matchScore: Math.floor(Math.random() * 20) + 80,
|
||||
concept: concepts[index % concepts.length],
|
||||
wechat: wechats[index % wechats.length],
|
||||
commonInterests: [
|
||||
{ icon: 'book-open', text: '都在读《创业派对》' },
|
||||
{ icon: 'briefcase', text: '对私域运营感兴趣' },
|
||||
{ icon: 'target', text: '相似的创业方向' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
@@ -646,18 +629,10 @@ Page({
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
} else {
|
||||
// 即使API返回失败,也模拟成功(因为已保存本地)
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
// 网络错误时也模拟成功
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isJoining: false })
|
||||
}
|
||||
@@ -734,19 +709,7 @@ Page({
|
||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消', icon: 'none' })
|
||||
} else {
|
||||
// 测试模式
|
||||
wx.showModal({
|
||||
title: '支付服务暂不可用',
|
||||
content: '是否使用测试模式购买?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
}
|
||||
}
|
||||
})
|
||||
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -757,10 +720,6 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 打开资料修改页(找伙伴右上角图标)
|
||||
openSettings() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部留白,让内容往下 -->
|
||||
<view style="height: 30rpx;"></view>
|
||||
<view style="height: 16rpx;"></view>
|
||||
|
||||
<!-- 匹配提示条 - 简化显示 -->
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
* mbti, region, industry, position, businessScale, skills,
|
||||
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
|
||||
* helpOffer→canHelp, helpNeed→needHelp
|
||||
* 点头像:若后台 persons.user_id 已绑定则带 ckbLeadToken,走存客宝 CKBLead(与阅读页 @ 一致)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, member: null, loading: true },
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true },
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
const sb = app.globalData.statusBarHeight || 44
|
||||
this.setData({ statusBarHeight: sb, navBarTotalPx: sb + 44 })
|
||||
if (options.id) this.loadMember(options.id)
|
||||
},
|
||||
|
||||
@@ -44,6 +46,7 @@ Page({
|
||||
storyTurning: u.storyTurning || u.story_turning,
|
||||
helpOffer: u.helpOffer || u.help_offer,
|
||||
helpNeed: u.helpNeed || u.help_need,
|
||||
ckbLeadToken: u.ckbLeadToken || u.ckb_lead_token,
|
||||
}), loading: false })
|
||||
return
|
||||
}
|
||||
@@ -79,7 +82,8 @@ Page({
|
||||
turningPoint: e(raw.turningPoint || raw.storyTurning || raw.story_turning),
|
||||
canHelp: e(raw.canHelp || raw.helpOffer || raw.help_offer),
|
||||
needHelp: e(raw.needHelp || raw.helpNeed || raw.help_need),
|
||||
project: e(raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro)
|
||||
project: e(raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro),
|
||||
ckbLeadToken: String(raw.ckbLeadToken || raw.ckb_lead_token || '').trim()
|
||||
}
|
||||
|
||||
const contact = merged.contactRaw || ''
|
||||
@@ -130,6 +134,189 @@ Page({
|
||||
return d.contact || d.wechat
|
||||
},
|
||||
|
||||
/** VIP 或本会员首次免费:写入解锁;否则弹开通 VIP */
|
||||
_tryFreeUnlock(member, field) {
|
||||
const isVip = app.globalData.isVip
|
||||
const usedFree = this._hasUsedFreeForMember(member.id)
|
||||
if (isVip || !usedFree) {
|
||||
this._addUnlock(member.id, field)
|
||||
return true
|
||||
}
|
||||
wx.showModal({
|
||||
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
|
||||
content: '您的免费解锁次数已用完,开通VIP会员(¥1980/年)可无限解锁',
|
||||
confirmText: '去开通',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* 点头像:有存客宝人物 token 时优先 POST /api/miniprogram/ckb/lead(与阅读页 @ 同链路,匹配 persons.ckb_api_key 计划)
|
||||
* 否则:解锁后复制微信/手机号并引导
|
||||
*/
|
||||
startLinkFlow() {
|
||||
const member = this.data.member
|
||||
if (!member) return
|
||||
const leadTok = (member.ckbLeadToken || '').trim()
|
||||
if (leadTok) {
|
||||
const nickname = ((member.name || 'TA').trim() || 'TA')
|
||||
wx.showModal({
|
||||
title: '添加好友',
|
||||
content: `是否通过获客计划联系 ${nickname}?提交后将按对方在存客宝后台配置的计划执行。`,
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) this._doCkbLeadSubmit(leadTok, nickname)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (member.wechatRaw || member.wechatDisplay) {
|
||||
if (!this._ensureUnlockedForLink('wechat')) return
|
||||
const m = this.data.member
|
||||
if (m.wechatFull) this._copyAndGuideWechat(m.wechatFull)
|
||||
return
|
||||
}
|
||||
if (member.contactRaw || member.contactDisplay) {
|
||||
if (!this._ensureUnlockedForLink('contact')) return
|
||||
const m = this.data.member
|
||||
if (m.contactFull) this._copyAndGuidePhone(m.contactFull)
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '暂未公开联系方式', icon: 'none' })
|
||||
},
|
||||
|
||||
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token */
|
||||
async _doCkbLeadSubmit(targetUserId, targetNickname) {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再添加好友',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号(必填),以便对方通过获客计划联系您',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/ckb/lead',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: myUserId,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechatId || undefined,
|
||||
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
|
||||
targetUserId,
|
||||
targetNickname: targetNickname || undefined,
|
||||
source: 'member_detail_avatar'
|
||||
}
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
_ensureUnlockedForLink(field) {
|
||||
const member = this.data.member
|
||||
if (!member?.id || (field !== 'contact' && field !== 'wechat')) return false
|
||||
if (field === 'wechat' && member.wechatUnlocked) return true
|
||||
if (field === 'contact' && member.contactUnlocked) return true
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: field === 'wechat'
|
||||
? '登录后可解锁并复制对方微信号,再按步骤去微信添加好友。'
|
||||
: '登录后可解锁并复制对方手机号,便于添加好友或回拨。',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
})
|
||||
return false
|
||||
}
|
||||
const d = this._getUnlockData(member.id)
|
||||
if ((field === 'wechat' && d.wechat) || (field === 'contact' && d.contact)) {
|
||||
this.setData({ member: this.enrichAndFormat(member) })
|
||||
return true
|
||||
}
|
||||
if (!this._tryFreeUnlock(member, field)) return false
|
||||
this.setData({ member: this.enrichAndFormat(member) })
|
||||
return true
|
||||
},
|
||||
|
||||
_copyAndGuideWechat(wechatId) {
|
||||
if (!wechatId) return
|
||||
wx.setClipboardData({
|
||||
data: String(wechatId),
|
||||
success: () => {
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '添加微信好友',
|
||||
content: '微信号已复制。\n\n请打开微信 → 右上角「+」→ 添加朋友 → 粘贴搜索并添加。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
fail: () => wx.showToast({ title: '复制失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
_copyAndGuidePhone(phone) {
|
||||
if (!phone) return
|
||||
wx.setClipboardData({
|
||||
data: String(phone),
|
||||
success: () => {
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '联系对方',
|
||||
content: '手机号已复制。\n\n可打开微信「添加朋友」搜索手机号,或使用手机拨号联系对方。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
fail: () => wx.showToast({ title: '复制失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
unlockField(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
if (!field) return
|
||||
@@ -148,22 +335,25 @@ Page({
|
||||
}
|
||||
const d = this._getUnlockData(member.id)
|
||||
if (d[field]) return
|
||||
const isVip = app.globalData.isVip
|
||||
const usedFree = this._hasUsedFreeForMember(member.id)
|
||||
if (isVip || !usedFree) {
|
||||
this._addUnlock(member.id, field)
|
||||
if (this._tryFreeUnlock(member, field)) {
|
||||
const m = this.enrichAndFormat(member)
|
||||
this.setData({ member: m })
|
||||
wx.showToast({ title: field === 'contact' ? '已解锁联系方式' : '已解锁微信号', icon: 'success' })
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
|
||||
content: '您的免费解锁次数已用完,开通VIP会员(¥1980/年)可无限解锁',
|
||||
confirmText: '去开通',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
|
||||
})
|
||||
},
|
||||
|
||||
tapContactRow() {
|
||||
const m = this.data.member
|
||||
if (!m || !(m.contactRaw || m.contactDisplay)) return
|
||||
if (m.contactUnlocked) this.copyContact()
|
||||
else this.unlockField({ currentTarget: { dataset: { field: 'contact' } } })
|
||||
},
|
||||
|
||||
tapWechatRow() {
|
||||
const m = this.data.member
|
||||
if (!m || !(m.wechatRaw || m.wechatDisplay)) return
|
||||
if (m.wechatUnlocked) this.copyWechat()
|
||||
else this.unlockField({ currentTarget: { dataset: { field: 'wechat' } } })
|
||||
},
|
||||
|
||||
copyContact() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!-- 卡若创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<!-- 卡若创业派对 - 超级个体详情(居中头像区 + 低调联系方式 + 信息卡) -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="#5EEAD4" customClass="nav-icon"></icon>
|
||||
@@ -10,135 +9,176 @@
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
|
||||
<!-- 顶部 profile 卡片 -->
|
||||
<view class="card-profile">
|
||||
<view class="profile-deco"></view>
|
||||
<view class="profile-body">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{(member.name && member.name[0]) || '创'}}</text></view>
|
||||
<scroll-view scroll-y class="scroll-wrap" style="height: calc(100vh - {{navBarTotalPx}}px);" wx:if="{{member}}">
|
||||
<!-- 首屏:居中头像 + 昵称 + 标签;点头像走添加微信引导(无独立「链接 TA」大按钮) -->
|
||||
<view class="shell">
|
||||
<view class="shell-glow"></view>
|
||||
<view class="hero-profile">
|
||||
<view class="hero-avatar-block" bindtap="startLinkFlow" hover-class="hero-avatar-block-hover" hover-stay-time="80">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{(member.name && member.name[0]) || '创'}}</text></view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags profile-tags-modern" wx:if="{{member.mbti || member.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{member.region}}">
|
||||
<icon name="map-pin" size="22" color="currentColor" customClass="pin-icon"></icon>
|
||||
<text>{{member.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags" wx:if="{{member.mbti || member.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{member.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="pin-icon"></icon><text>{{member.region}}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="contact-rows contact-rows-subtle">
|
||||
<view
|
||||
class="link-chip link-chip-subtle {{member.contactUnlocked ? 'link-chip-open' : ''}}"
|
||||
wx:if="{{member.contactRaw || member.contactDisplay}}"
|
||||
catchtap="tapContactRow"
|
||||
>
|
||||
<view class="link-chip-icon link-chip-icon-phone link-chip-icon-subtle">
|
||||
<icon name="smartphone" size="26" color="rgba(148,163,184,0.85)" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label link-chip-label-subtle">手机</text>
|
||||
<text class="link-chip-val mono link-chip-val-subtle">{{member.contactDisplay || member.contactRaw}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action link-chip-action-subtle">
|
||||
<text>{{member.contactUnlocked ? '复制' : '解锁'}}</text>
|
||||
<icon wx:if="{{!member.contactUnlocked}}" name="chevron-right" size="22" color="rgba(100,116,139,0.8)" customClass="lc-arr"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="link-chip link-chip-subtle {{member.wechatUnlocked ? 'link-chip-open' : ''}}"
|
||||
wx:if="{{member.wechatRaw || member.wechatDisplay}}"
|
||||
catchtap="tapWechatRow"
|
||||
>
|
||||
<view class="link-chip-icon link-chip-icon-wx link-chip-icon-subtle">
|
||||
<icon name="message-circle" size="26" color="rgba(148,163,184,0.85)" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label link-chip-label-subtle">微信</text>
|
||||
<text class="link-chip-val mono link-chip-val-subtle">{{member.wechatDisplay || member.wechatRaw}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action link-chip-action-subtle">
|
||||
<text>{{member.wechatUnlocked ? '复制' : '解锁'}}</text>
|
||||
<icon wx:if="{{!member.wechatUnlocked}}" name="chevron-right" size="22" color="rgba(100,116,139,0.8)" customClass="lc-arr"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="link-empty link-empty-subtle" wx:if="{{!(member.contactRaw || member.contactDisplay) && !(member.wechatRaw || member.wechatDisplay)}}">
|
||||
<text class="link-empty-txt">暂未公开联系方式</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.contactRaw || member.contactDisplay || member.wechatRaw || member.wechatDisplay}}">
|
||||
<view class="card-head">
|
||||
<icon name="user" size="48" color="#00CED1" customClass="card-icon"></icon>
|
||||
<text class="card-label">基本信息</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="field" wx:if="{{member.industry}}">
|
||||
<text class="f-key">行业</text>
|
||||
<text class="f-val">{{member.industry}}</text>
|
||||
<!-- 一体化信息区(单卡片内分区) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp || member.project}}">
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.position}}">
|
||||
<text class="f-key">职位</text>
|
||||
<text class="f-val">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.businessScale}}">
|
||||
<text class="f-key">业务体量</text>
|
||||
<text class="f-val">{{member.businessScale}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.industry || member.position || member.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{member.skills}}">
|
||||
<text class="f-key">我擅长</text>
|
||||
<text class="f-val">{{member.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.contactRaw || member.contactDisplay}}">
|
||||
<text class="f-key">联系方式</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.contactUnlocked ? member.contactFull : (member.contactDisplay || member.contactRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockField" data-field="contact">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.wechatRaw || member.wechatDisplay}}">
|
||||
<text class="f-key">微信号</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.wechatUnlocked ? member.wechatFull : (member.wechatDisplay || member.wechatRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockField" data-field="wechat">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="card-head">
|
||||
<icon name="lightbulb" size="48" color="#FFD700" customClass="card-icon bulb"></icon>
|
||||
<text class="card-label">个人故事</text>
|
||||
<view class="mono-divider" wx:if="{{(member.industry || member.position || member.businessScale) && member.skills}}"></view>
|
||||
|
||||
<!-- 核心能力 -->
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="story" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月做的是什么</text></view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.bestMonth}}"></view>
|
||||
<view class="story" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最有成就感的一件事</text></view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.achievement}}"></view>
|
||||
<view class="story" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-icon turn"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<!-- 互助 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.canHelp || member.needHelp) && member.project}}"></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.project}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-body proj-body-compact">{{member.project}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="card-head">
|
||||
<icon name="handshake" size="48" color="#00CED1" customClass="card-icon"></icon>
|
||||
<text class="card-label">互助需求</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="help-box help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tag">我能帮你</text>
|
||||
<text class="help-txt">{{member.canHelp}}</text>
|
||||
<!-- 底部:分享 + 双入口(同一视觉块) -->
|
||||
<view class="footer-panel">
|
||||
<view class="footer-pills">
|
||||
<view class="pill pill-gold" bindtap="goToVip">
|
||||
<icon name="sparkles" size="30" color="#FBBF24" customClass="pill-ic"></icon>
|
||||
<text class="pill-txt">成为超级个体</text>
|
||||
</view>
|
||||
<view class="help-box help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tag need">我需要帮助</text>
|
||||
<text class="help-txt">{{member.needHelp}}</text>
|
||||
<view class="pill pill-teal" bindtap="goToMatch">
|
||||
<icon name="users" size="30" color="#5EEAD4" customClass="pill-ic"></icon>
|
||||
<text class="pill-txt">找更多伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="card" wx:if="{{member.project}}">
|
||||
<view class="card-head">
|
||||
<icon name="rocket" size="48" color="#00CED1" customClass="card-icon rocket"></icon>
|
||||
<text class="card-label">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-txt">{{member.project}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-wrap">
|
||||
<view class="btn-super" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="36" color="#F59E0B" customClass="btn-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:160rpx;"></view>
|
||||
<view class="scroll-pad"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载和空状态 -->
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
|
||||
@@ -1,172 +1,629 @@
|
||||
/* 卡若创业派对 - 个人资料页(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: 999;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 32rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.9);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
/* 卡若创业派对 - 超级个体详情(高阶单页 · 深色玻璃态) */
|
||||
.page {
|
||||
background: radial-gradient(120% 80% at 50% -20%, rgba(20, 80, 90, 0.35) 0%, #050B14 45%);
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-back { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5EEAD4; font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
|
||||
.nav-right { display: flex; align-items: center; gap: 16rpx; }
|
||||
.nav-icon-wrap { padding: 8rpx; }
|
||||
.nav-icon-dot { font-size: 28rpx; color: rgba(255,255,255,0.8); }
|
||||
|
||||
.scroll-wrap { height: calc(100vh - 88px); }
|
||||
/* —— 导航 —— */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28rpx;
|
||||
height: 44px;
|
||||
background: rgba(5, 11, 20, 0.72);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.nav-icon {
|
||||
font-size: 44rpx;
|
||||
color: #5eead4;
|
||||
font-weight: 300;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
|
||||
/* ===== 顶部 Profile 卡片 ===== */
|
||||
.card-profile {
|
||||
position: relative; margin: 32rpx 32rpx 0;
|
||||
padding: 64rpx 40rpx 48rpx;
|
||||
.scroll-wrap {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* —— 首屏外壳(头像 + 链接列) —— */
|
||||
.shell {
|
||||
position: relative;
|
||||
margin: 28rpx 24rpx 0;
|
||||
padding: 40rpx 32rpx 36rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(145deg, rgba(22, 36, 48, 0.95) 0%, rgba(12, 20, 32, 0.98) 100%);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.45), inset 0 1rpx 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-deco {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(180deg, rgba(30, 58, 69, 0.3) 0%, transparent 100%);
|
||||
.shell-glow {
|
||||
position: absolute;
|
||||
top: -40%;
|
||||
right: -20%;
|
||||
width: 70%;
|
||||
height: 80%;
|
||||
background: radial-gradient(circle, rgba(45, 212, 191, 0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
|
||||
.hero-profile {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.hero-avatar-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.hero-avatar-block-hover {
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.contact-rows {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.contact-rows-subtle {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.avatar-outer {
|
||||
position: relative;
|
||||
width: 176rpx; height: 176rpx;
|
||||
margin-bottom: 32rpx;
|
||||
width: 168rpx;
|
||||
height: 168rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.avatar-wrap.vip-ring {
|
||||
border: 4rpx solid transparent;
|
||||
background: linear-gradient(135deg, #F59E0B, #5EEAD4, #F59E0B);
|
||||
border: 3rpx solid transparent;
|
||||
background: linear-gradient(135deg, #f59e0b, #5eead4, #f59e0b);
|
||||
background-size: 200% 200%;
|
||||
animation: vipGlow 4s ease infinite;
|
||||
animation: vipGlow 5s ease infinite;
|
||||
}
|
||||
@keyframes vipGlow {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%;
|
||||
background: #17212F;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1a2332;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 52rpx;
|
||||
color: #5eead4;
|
||||
font-weight: 700;
|
||||
}
|
||||
.vip-tag {
|
||||
position: absolute; bottom: -4rpx; right: -4rpx;
|
||||
background: linear-gradient(135deg, #F59E0B, #e8920d);
|
||||
color: #000; font-size: 20rpx; font-weight: 800;
|
||||
padding: 6rpx 14rpx; border-radius: 16rpx;
|
||||
position: absolute;
|
||||
bottom: -2rpx;
|
||||
right: -2rpx;
|
||||
background: linear-gradient(135deg, #fbbf24, #d97706);
|
||||
color: #0f172a;
|
||||
font-size: 18rpx;
|
||||
font-weight: 800;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
z-index: 2;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
|
||||
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 8rpx 24rpx; border-radius: 999rpx; }
|
||||
.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); display: flex; align-items: center; gap: 8rpx; }
|
||||
.pin-icon { color: #EF4444; font-size: 22rpx; }
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.card {
|
||||
margin: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
.link-chip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 22rpx 22rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.15);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.card-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.card-icon { font-size: 40rpx; }
|
||||
.card-icon.bulb { filter: sepia(1) saturate(3) hue-rotate(15deg); }
|
||||
.card-icon.rocket { opacity: 0.9; }
|
||||
.card-label { font-size: 30rpx; font-weight: 700; color: #fff; letter-spacing: 1rpx; }
|
||||
|
||||
.card-body { }
|
||||
.field { margin-bottom: 32rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.f-key { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 12rpx; }
|
||||
.f-val { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.6; }
|
||||
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
|
||||
.f-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
|
||||
.icon-eye-off { display: flex; align-items: center; justify-content: center; }
|
||||
.icon-eye-off .icon-img { width: 40rpx; height: 40rpx; }
|
||||
|
||||
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }
|
||||
|
||||
/* ===== 个人故事 ===== */
|
||||
.story { margin-bottom: 32rpx; }
|
||||
.story:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { font-size: 32rpx; }
|
||||
.story-icon.turn { opacity: 0.9; }
|
||||
.story-q { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 互助需求 ===== */
|
||||
.help-box {
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #17212F;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
.link-chip-subtle {
|
||||
gap: 16rpx;
|
||||
padding: 14rpx 8rpx 14rpx 12rpx;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.help-box:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx; font-weight: 600;
|
||||
padding: 6rpx 16rpx; border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
.link-chip-subtle:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
.help-give .help-tag { color: #5EEAD4; background: #112D2A; }
|
||||
.help-need .help-tag { color: #F59E0B; background: #2D1F0D; }
|
||||
.help-txt { display: block; font-size: 26rpx; color: #fff; line-height: 1.6; letter-spacing: 1rpx; }
|
||||
|
||||
/* ===== 项目介绍 ===== */
|
||||
.proj-txt { font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 底部按钮 ===== */
|
||||
.bottom-wrap {
|
||||
padding: 48rpx 32rpx 0;
|
||||
.link-chip-subtle:active {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.btn-super {
|
||||
.link-chip-open.link-chip-subtle {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.link-chip:active {
|
||||
border-color: rgba(94, 234, 212, 0.35);
|
||||
background: rgba(15, 30, 40, 0.75);
|
||||
}
|
||||
.link-chip-subtle:active {
|
||||
border-color: transparent;
|
||||
}
|
||||
.link-chip-open {
|
||||
border-color: rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.link-chip-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-chip-icon-subtle {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.link-chip-icon-phone {
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.link-chip-icon-wx {
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
}
|
||||
.link-chip-icon-subtle.link-chip-icon-phone,
|
||||
.link-chip-icon-subtle.link-chip-icon-wx {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.lc-ic {
|
||||
display: block;
|
||||
}
|
||||
.link-chip-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.link-chip-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.link-chip-label-subtle {
|
||||
font-size: 18rpx;
|
||||
color: rgba(100, 116, 139, 0.85);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.link-chip-val {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
.link-chip-val-subtle {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(148, 163, 184, 0.92);
|
||||
}
|
||||
.link-chip-val.mono {
|
||||
font-family: ui-monospace, monospace;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.link-chip-action {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #5eead4;
|
||||
}
|
||||
.link-chip-action-subtle {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(100, 116, 139, 0.95);
|
||||
}
|
||||
.lc-arr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link-empty {
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
border: 1rpx dashed rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.link-empty-subtle {
|
||||
padding: 16rpx 8rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.link-empty-txt {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.link-empty-subtle .link-empty-txt {
|
||||
font-size: 22rpx;
|
||||
color: rgba(100, 116, 139, 0.75);
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
.profile-tags {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
width: 100%;
|
||||
padding: 32rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.3);
|
||||
color: #F59E0B;
|
||||
font-size: 30rpx; font-weight: 500;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.btn-arrow { font-size: 36rpx; font-weight: 300; }
|
||||
.profile-tags-modern {
|
||||
gap: 10rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.profile-tags-modern .tag-mbti {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
background: rgba(45, 212, 191, 0.1);
|
||||
color: #7ee8dc;
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
.profile-tags-modern .tag-region {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 16rpx 6rpx 14rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(203, 213, 225, 0.95);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.tag-mbti {
|
||||
background: rgba(19, 78, 74, 0.6);
|
||||
color: #5eead4;
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.tag-region {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
color: #cbd5e1;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.pin-icon {
|
||||
color: #f87171;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* ===== 状态 ===== */
|
||||
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 24rpx; }
|
||||
.state-txt { font-size: 28rpx; color: #64748B; }
|
||||
.state-emoji { font-size: 96rpx; }
|
||||
/* —— 一体化信息卡 —— */
|
||||
.mono-card {
|
||||
margin: 24rpx 24rpx 0;
|
||||
padding: 8rpx 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(15, 23, 34, 0.88);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.07);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mono-card-compact {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
.mono-sec {
|
||||
padding: 28rpx 32rpx 8rpx;
|
||||
}
|
||||
.mono-sec-tight {
|
||||
padding: 18rpx 28rpx 6rpx;
|
||||
}
|
||||
.mono-sec-head {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.mono-sec-head-tight {
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.mono-sec-kicker {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(94, 234, 212, 0.55);
|
||||
letter-spacing: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.mono-sec-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.mono-divider {
|
||||
height: 1rpx;
|
||||
margin: 4rpx 28rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.15), transparent);
|
||||
}
|
||||
|
||||
.kv-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx 20rpx;
|
||||
}
|
||||
.kv-cell {
|
||||
width: calc(50% - 10rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.kv-cell-full {
|
||||
width: 100%;
|
||||
}
|
||||
.kv-cell .kv-k {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.kv-cell .kv-v {
|
||||
font-size: 26rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.kv:last-child {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.kv-k {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.kv-v {
|
||||
font-size: 28rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.65;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skills-showcase .skills-quote {
|
||||
padding: 20rpx 22rpx 20rpx 20rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(105deg, rgba(45, 212, 191, 0.08) 0%, rgba(15, 23, 42, 0.5) 100%);
|
||||
border-left: 6rpx solid #2dd4bf;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.skills-quote-text {
|
||||
font-size: 26rpx;
|
||||
color: #f1f5f9;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.story {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.story-compact .story-head {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.story-compact .story-a {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
padding-left: 0;
|
||||
}
|
||||
.story-gap {
|
||||
height: 28rpx;
|
||||
}
|
||||
.story-gap-tight {
|
||||
height: 16rpx;
|
||||
}
|
||||
.story-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.story-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.story-q {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.story-a {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.7;
|
||||
padding-left: 4rpx;
|
||||
}
|
||||
|
||||
.help-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.help-tile {
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(23, 33, 47, 0.9);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.help-tile-tag {
|
||||
display: inline-block;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tile-tag {
|
||||
color: #5eead4;
|
||||
background: rgba(6, 78, 59, 0.45);
|
||||
border: 1rpx solid rgba(45, 212, 191, 0.2);
|
||||
}
|
||||
.help-need .help-tile-tag.need {
|
||||
color: #fbbf24;
|
||||
background: rgba(69, 47, 8, 0.45);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
.help-tile-txt {
|
||||
font-size: 26rpx;
|
||||
color: #f1f5f9;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.proj-body {
|
||||
font-size: 28rpx;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.75;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
.proj-body-compact {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* —— 底栏:分享 + 双入口 —— */
|
||||
.footer-panel {
|
||||
margin: 32rpx 24rpx 0;
|
||||
padding: 28rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(12, 18, 28, 0.92);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.1);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.footer-pills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.pill {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 22rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.pill:active {
|
||||
background: rgba(51, 65, 85, 0.7);
|
||||
}
|
||||
.pill-gold {
|
||||
border-color: rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
.pill-teal {
|
||||
border-color: rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
.pill-ic {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pill-txt {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(248, 250, 252, 0.88);
|
||||
}
|
||||
|
||||
.scroll-pad {
|
||||
height: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* —— 状态 —— */
|
||||
.state-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.state-txt {
|
||||
font-size: 28rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.state-emoji {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2);
|
||||
border-top-color: #5EEAD4;
|
||||
border-top-color: #5eead4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
const app = getApp()
|
||||
const { formatStatNum } = require('../../utils/util.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
|
||||
/** 是否视为「单章解锁」类订单(排除全书/VIP 等聚合商品名) */
|
||||
function isSectionUnlockOrder(o) {
|
||||
const name = String(o.product_name || o.title || '').trim()
|
||||
if (/全书|全書|VIP|会员|年费|买断/.test(name)) return false
|
||||
const pid = String(o.product_id || o.section_id || o.sectionId || '')
|
||||
if (/^\d+\.\d+/.test(pid)) return true
|
||||
return !!pid && pid.length > 0
|
||||
}
|
||||
|
||||
function parseOrderTimeMs(o) {
|
||||
const raw = o.created_at || o.createdAt || o.pay_time || 0
|
||||
const t = new Date(raw).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -33,6 +49,8 @@ Page({
|
||||
readCountText: '0',
|
||||
totalReadTimeText: '0',
|
||||
matchHistoryText: '0',
|
||||
orderCountText: '0',
|
||||
giftPayCountText: '0',
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
@@ -78,6 +96,11 @@ Page({
|
||||
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 已解锁章节(订单倒序;默认最多展示 5 条,底部倒三角展开)
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -164,6 +187,7 @@ Page({
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadWalletBalance()
|
||||
this.loadUnlockedChapters()
|
||||
} else {
|
||||
const guestReadCount = app.getReadCount()
|
||||
this.setData({
|
||||
@@ -177,6 +201,9 @@ Page({
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: [],
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
totalReadTimeText: '0',
|
||||
@@ -185,6 +212,91 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 已解锁章节:优先订单接口(按支付时间倒序);失败时用 purchasedSections + bookData 兜底
|
||||
*/
|
||||
async loadUnlockedChapters() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo?.id) {
|
||||
this.setData({
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false
|
||||
})
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const expanded = this.data.unlockedExpanded
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
const metaById = (id) => {
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
return {
|
||||
mid: row?.mid ?? row?.MID ?? 0,
|
||||
title: cleanSingleLineField(row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || '')
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`, silent: true })
|
||||
let rows = []
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
rows = res.data
|
||||
.map((item) => ({
|
||||
id: item.product_id || item.section_id,
|
||||
mid: item.section_mid ?? item.mid ?? item.MID ?? 0,
|
||||
title: cleanSingleLineField(item.product_name || ''),
|
||||
_ts: parseOrderTimeMs(item)
|
||||
}))
|
||||
.filter((r) => r.id && isSectionUnlockOrder({ product_id: r.id, product_name: r.title }))
|
||||
}
|
||||
rows.sort((a, b) => b._ts - a._ts)
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const r of rows) {
|
||||
if (seen.has(r.id)) continue
|
||||
seen.add(r.id)
|
||||
const meta = metaById(r.id)
|
||||
deduped.push({
|
||||
id: r.id,
|
||||
mid: r.mid || meta.mid,
|
||||
title: cleanSingleLineField(r.title || meta.title || `章节 ${r.id}`)
|
||||
})
|
||||
}
|
||||
if (deduped.length === 0) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])]
|
||||
ids.reverse()
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
} catch (e) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const id of ids) {
|
||||
if (!id || seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
}
|
||||
},
|
||||
|
||||
expandUnlockedChapters() {
|
||||
if (this.data.unlockedExpanded) return
|
||||
trackClick('my', 'tab_click', '已解锁章节_展开')
|
||||
const full = this.data.unlockedChaptersFull || []
|
||||
this.setData({
|
||||
unlockedExpanded: true,
|
||||
displayUnlockedChapters: full
|
||||
})
|
||||
},
|
||||
|
||||
async loadDashboardStats() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
@@ -212,6 +324,8 @@ Page({
|
||||
const readCount = Number(res.data.readCount || 0)
|
||||
const totalReadTime = Number(res.data.totalReadMinutes || 0)
|
||||
const matchHistory = Number(res.data.matchHistory || 0)
|
||||
const orderCount = Number(res.data.orderCount || 0)
|
||||
const giftPayCount = Number(res.data.giftPayCount || 0)
|
||||
this.setData({
|
||||
readCount,
|
||||
totalReadTime,
|
||||
@@ -219,6 +333,8 @@ Page({
|
||||
readCountText: formatStatNum(readCount),
|
||||
totalReadTimeText: formatStatNum(totalReadTime),
|
||||
matchHistoryText: formatStatNum(matchHistory),
|
||||
orderCountText: formatStatNum(orderCount),
|
||||
giftPayCountText: formatStatNum(giftPayCount),
|
||||
recentChapters
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,20 +49,20 @@
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
<view class="profile-stat" bindtap="goToChapters">
|
||||
<text class="profile-stat-val">{{readCountText}}</text>
|
||||
<text class="profile-stat-val">{{readCountText || '0'}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToMatch">
|
||||
<text class="profile-stat-val">{{matchHistoryText}}</text>
|
||||
<text class="profile-stat-label">匹配伙伴</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<text class="profile-stat-val">{{walletBalanceText}}</text>
|
||||
<text class="profile-stat-label">我的余额</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings || '0.00'}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -92,31 +92,63 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<!-- 快捷入口:我的订单 + 我的代付 -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">阅读统计</text>
|
||||
<text class="card-title">快捷入口</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/book-open-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{readCountText}}</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="orders">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">订单</text>
|
||||
<text class="stat-label">我的订单</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/clock-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{totalReadTimeText}}</text>
|
||||
<text class="stat-label">阅读分钟</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">代付</text>
|
||||
<text class="stat-label">我的代付</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToMatch">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{matchHistoryText}}</text>
|
||||
<text class="stat-label">匹配伙伴</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="wallet">
|
||||
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{walletBalanceText}}</text>
|
||||
<text class="stat-label">我的余额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁:仅低调图标区(无标题文案);默认 5 条 + 倒三角展开;倒序由接口/JS 保证 -->
|
||||
<view class="card recent-card unlocked-card" wx:if="{{unlockedChaptersFull.length > 0}}">
|
||||
<view class="unlocked-section-head">
|
||||
<image class="unlocked-section-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="recent-list">
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{displayUnlockedChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="recent-link">阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="unlocked-expand-hint"
|
||||
wx:if="{{unlockedChaptersFull.length > 5 && !unlockedExpanded}}"
|
||||
bindtap="expandUnlockedChapters"
|
||||
hover-class="unlocked-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="unlocked-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
<view class="card-header">
|
||||
@@ -145,23 +177,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 设置 -->
|
||||
<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"><image class="menu-icon-img" src="/assets/icons/folder-teal.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><icon name="gift" size="32" color="#4FD1C5" customClass="menu-icon"></icon></view>
|
||||
<text class="menu-text">我的代付</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" wx:if="{{showSettingsEntry}}" bindtap="handleMenuTap" data-id="settings">
|
||||
<!-- 设置 -->
|
||||
<view class="card menu-card" wx:if="{{showSettingsEntry}}">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">设置</text>
|
||||
|
||||
@@ -74,10 +74,6 @@
|
||||
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
|
||||
.profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
|
||||
.profile-name-actions { display: flex; align-items: center; gap: 16rpx; flex-shrink: 0; }
|
||||
.profile-edit-btn { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 16rpx; background: rgba(255,255,255,0.08); border-radius: 12rpx; }
|
||||
.profile-edit-icon { width: 28rpx; height: 28rpx; opacity: 0.7; }
|
||||
.profile-edit-text { font-size: 24rpx; color: rgba(255,255,255,0.7); }
|
||||
.user-name {
|
||||
font-size: 44rpx; font-weight: bold; color: #fff;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
|
||||
@@ -166,7 +162,20 @@
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; }
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; }
|
||||
/* 已解锁区块:仅顶部弱对比图标,无标题字 */
|
||||
.unlocked-card { padding-top: 28rpx; }
|
||||
.unlocked-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 8rpx 16rpx 8rpx;
|
||||
}
|
||||
.unlocked-section-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.recent-index { font-size: 28rpx; color: #6B7280; font-family: monospace; }
|
||||
.recent-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.recent-link { font-size: 24rpx; color: #4FD1C5; font-weight: 500; flex-shrink: 0; }
|
||||
@@ -174,6 +183,25 @@
|
||||
.recent-empty-text { font-size: 28rpx; color: #6B7280; display: block; margin-bottom: 24rpx; }
|
||||
.recent-empty-btn { font-size: 28rpx; color: #4FD1C5; }
|
||||
|
||||
/* 已解锁章节列表底部倒三角展开(与首页「最新新增」一致) */
|
||||
.unlocked-expand-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8rpx 0 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.unlocked-expand-hint-hover {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.unlocked-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 20rpx solid rgba(79, 209, 197, 0.85);
|
||||
}
|
||||
|
||||
/* 菜单 */
|
||||
.menu-card { padding: 0; margin-bottom: 48rpx; overflow: hidden; }
|
||||
.menu-item {
|
||||
@@ -197,8 +225,10 @@
|
||||
.icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gray { background: rgba(156,163,175,0.15); }
|
||||
.icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gold { background: rgba(200,161,70,0.2); }
|
||||
.icon-gold .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-amber { background: rgba(245,158,11,0.2); }
|
||||
.menu-icon-emoji { font-size: 28rpx; }
|
||||
.menu-right { display: flex; align-items: center; gap: 12rpx; }
|
||||
.menu-balance { font-size: 26rpx; color: #4FD1C5; font-weight: 500; }
|
||||
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
|
||||
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }
|
||||
|
||||
@@ -272,5 +302,14 @@
|
||||
.modal-btn-cancel { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
.modal-btn-confirm { background: #4FD1C5; color: #000; font-weight: 600; }
|
||||
|
||||
/* 代付链接卡片 */
|
||||
.gift-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.gift-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; }
|
||||
.gift-left { flex: 1; min-width: 0; }
|
||||
.gift-title { display: block; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gift-meta { display: block; font-size: 22rpx; color: #9CA3AF; margin-top: 6rpx; }
|
||||
.gift-share-btn { display: inline-block; padding: 8rpx 28rpx; background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: 600; border-radius: 20rpx; }
|
||||
.gift-done { font-size: 24rpx; color: #6B7280; }
|
||||
|
||||
/* 底部留白:配合 page padding-bottom,避免内容被 TabBar 遮挡 */
|
||||
.bottom-space { height: calc(80rpx + env(safe-area-inset-bottom, 0px)); }
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
|
||||
|
||||
<text class="doc-section">八、联系我们</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
/* 资料编辑 - comprehensive_profile_editor_v1_1 | 配色 enhanced,input/textarea 用 view 包裹 */
|
||||
|
||||
/* 分享名片 canvas:隐藏,仅用于生成图片 */
|
||||
.share-card-canvas {
|
||||
position: fixed; left: -9999px; top: 0; width: 500px; height: 400px;
|
||||
}
|
||||
.page {
|
||||
background: #050B14; min-height: 100vh; color: #fff;
|
||||
width: 100%; box-sizing: border-box; overflow-x: hidden;
|
||||
@@ -206,11 +201,8 @@
|
||||
.btn-choose-avatar {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 30rpx;
|
||||
|
||||
@@ -1,135 +1,175 @@
|
||||
<!-- 个人资料展示页 - enhanced_professional_profile 1:1 重构 -->
|
||||
<!-- 卡若创业派对 - 个人资料展示页(与 member-detail 同一视觉) -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><icon name="chevron-left" size="44" color="#5EEAD4" customClass="back-icon"></icon></view>
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="#5EEAD4" customClass="nav-icon"></icon>
|
||||
</view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-right" bindtap="goToEdit"><text class="nav-more">⋯</text></view>
|
||||
<view class="nav-edit" bindtap="goToEdit">
|
||||
<icon name="edit" size="32" color="#5EEAD4"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></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="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>
|
||||
<view class="tag tag-region" wx:if="{{profile.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="tag-icon"></icon><text>{{profile.region}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<icon name="user" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<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>
|
||||
<scroll-view scroll-y class="scroll-wrap" style="height: calc(100vh - {{statusBarHeight + 44}}px - 120rpx);" wx:if="{{!loading && profile}}">
|
||||
<!-- 首屏壳:头像 + 联系方式 -->
|
||||
<view class="shell">
|
||||
<view class="shell-glow"></view>
|
||||
<view class="hero-row">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar-img" wx:if="{{profile.avatar}}" src="{{profile.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{profile.nickname ? profile.nickname[0] : '?'}}</text></view>
|
||||
</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 class="link-column">
|
||||
<text class="link-column-title">我的联系方式</text>
|
||||
|
||||
<view class="link-chip" wx:if="{{profile.phone}}" bindtap="copyPhone">
|
||||
<view class="link-chip-icon link-chip-icon-phone">
|
||||
<icon name="smartphone" size="34" color="#5EEAD4" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label">手机号</text>
|
||||
<text class="link-chip-val mono">{{profile.phone}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action"><text>复制</text></view>
|
||||
</view>
|
||||
|
||||
<view class="link-chip" wx:if="{{profile.wechat}}" bindtap="copyWechat">
|
||||
<view class="link-chip-icon link-chip-icon-wx">
|
||||
<icon name="message-circle" size="34" color="#34D399" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label">微信号</text>
|
||||
<text class="link-chip-val mono">{{profile.wechat}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action"><text>复制</text></view>
|
||||
</view>
|
||||
|
||||
<view class="link-empty" wx:if="{{!profile.phone && !profile.wechat}}">
|
||||
<text class="link-empty-txt">未填写联系方式</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.phone && !profile.wechat}}">
|
||||
点击右上角 ⋯ 编辑完善资料
|
||||
</view>
|
||||
|
||||
<text class="profile-name">{{profile.nickname || '未设置昵称'}}</text>
|
||||
<view class="profile-tags" wx:if="{{profile.mbti || profile.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{profile.region}}">
|
||||
<icon name="map-pin" size="24" color="currentColor" customClass="pin-icon"></icon>
|
||||
<text>{{profile.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="section" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="section-head">
|
||||
<icon name="lightbulb" size="40" color="#FFD700" customClass="section-icon section-icon-yellow"></icon>
|
||||
<text class="section-title">个人故事</text>
|
||||
<!-- 一体化信息卡 -->
|
||||
<view class="mono-card" wx:if="{{profile.industry || profile.position || profile.businessScale || profile.skills || profile.storyBestMonth || profile.storyAchievement || profile.storyTurning || profile.helpOffer || profile.helpNeed || profile.projectIntro}}">
|
||||
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec" wx:if="{{profile.industry || profile.position || profile.businessScale}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{profile.industry}}</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{profile.position}}</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{profile.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="story-block" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-text">{{profile.storyBestMonth}}</text>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.industry || profile.position || profile.businessScale) && profile.skills}}"></view>
|
||||
|
||||
<!-- 我擅长 -->
|
||||
<view class="mono-sec skills-showcase" wx:if="{{profile.skills}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">我擅长</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"><icon name="star" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最有成就感的一件事</text></view>
|
||||
<text class="story-text">{{profile.storyAchievement}}</text>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{profile.skills}}</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"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">人生的转折点</text></view>
|
||||
<text class="story-text">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{profile.skills && (profile.storyBestMonth || profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{profile.storyBestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
<view class="story" wx:if="{{profile.storyAchievement}}">
|
||||
<view class="story-head"><icon name="star" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{profile.storyAchievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
|
||||
<view class="story" wx:if="{{profile.storyTurning}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.storyBestMonth || profile.storyAchievement || profile.storyTurning) && (profile.helpOffer || profile.helpNeed)}}"></view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="mono-sec" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{profile.helpOffer}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{profile.helpOffer}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{profile.helpNeed}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{profile.helpNeed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.helpOffer || profile.helpNeed) && profile.projectIntro}}"></view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="mono-sec" wx:if="{{profile.projectIntro}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-body">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="section" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="section-head">
|
||||
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<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 class="empty-hint" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.storyBestMonth && !profile.storyAchievement && !profile.storyTurning && !profile.helpOffer && !profile.helpNeed && !profile.projectIntro}}">
|
||||
<icon name="edit" size="48" color="#334155"></icon>
|
||||
<text class="empty-hint-txt">资料尚未完善,点击右上角编辑</text>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="section" wx:if="{{profile.projectIntro}}">
|
||||
<view class="section-head">
|
||||
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text class="section-title">项目介绍</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<text class="project-text">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
<view class="scroll-pad"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 - 设计稿为描边橙色 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="vip-btn-outline" bindtap="goToVip">
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-bar" wx:if="{{!loading}}">
|
||||
<view class="vip-btn" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="36" color="#00CED1" customClass="vip-btn-arrow"></icon>
|
||||
<icon name="chevron-right" size="32" color="#0f172a"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,115 +1,183 @@
|
||||
/* 个人资料展示页 - enhanced_professional_profile 1:1 重构 */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
/* 卡若创业派对 - 个人资料展示(与 member-detail 同一视觉语言) */
|
||||
.page {
|
||||
background: radial-gradient(120% 80% at 50% -20%, rgba(20, 80, 90, 0.35) 0%, #050B14 45%);
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* —— 导航 —— */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
||||
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);
|
||||
padding: 0 28rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.72);
|
||||
backdrop-filter: blur(20rpx); -webkit-backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.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%; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5eead4; font-weight: 300; }
|
||||
.nav-title { font-size: 32rpx; font-weight: 600; color: #f8fafc; letter-spacing: 4rpx; }
|
||||
.nav-edit { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: flex-end; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
.scroll-wrap { box-sizing: border-box; }
|
||||
|
||||
.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;
|
||||
/* —— 首屏外壳 —— */
|
||||
.shell {
|
||||
position: relative; margin: 28rpx 24rpx 0; padding: 40rpx 32rpx 36rpx;
|
||||
border-radius: 32rpx;
|
||||
background: linear-gradient(145deg, rgba(22, 36, 48, 0.95) 0%, rgba(12, 20, 32, 0.98) 100%);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.45), inset 0 1rpx 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.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%);
|
||||
.shell-glow {
|
||||
position: absolute; top: -40%; right: -20%; width: 70%; height: 80%;
|
||||
background: radial-gradient(circle, rgba(45, 212, 191, 0.12) 0%, transparent 70%);
|
||||
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;
|
||||
|
||||
.hero-row { position: relative; z-index: 1; display: flex; align-items: flex-start; gap: 28rpx; }
|
||||
|
||||
.avatar-outer { position: relative; width: 168rpx; height: 168rpx; flex-shrink: 0; }
|
||||
.avatar-wrap {
|
||||
width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.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-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%; background: #1a2332;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 52rpx; color: #5eead4; font-weight: 700;
|
||||
}
|
||||
.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 { display: flex; align-items: center; gap: 8rpx; background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
|
||||
.tag-region .tag-icon { flex-shrink: 0; }
|
||||
|
||||
/* 通用区块 */
|
||||
.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);
|
||||
/* 右侧联系方式列 */
|
||||
.link-column { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.link-column-title { font-size: 26rpx; font-weight: 700; color: #f1f5f9; letter-spacing: 2rpx; }
|
||||
|
||||
.link-chip {
|
||||
display: flex; align-items: center; gap: 20rpx;
|
||||
padding: 22rpx; border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.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;
|
||||
.link-chip:active { background: rgba(15, 30, 40, 0.75); }
|
||||
.link-chip-icon {
|
||||
width: 64rpx; height: 64rpx; border-radius: 16rpx;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.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;
|
||||
.link-chip-icon-phone { background: rgba(45, 212, 191, 0.12); }
|
||||
.link-chip-icon-wx { background: rgba(52, 211, 153, 0.12); }
|
||||
.lc-ic { display: block; }
|
||||
.link-chip-main { flex: 1; min-width: 0; }
|
||||
.link-chip-label { display: block; font-size: 20rpx; color: #94a3b8; margin-bottom: 6rpx; }
|
||||
.link-chip-val { display: block; font-size: 26rpx; font-weight: 600; color: #f8fafc; line-height: 1.35; word-break: break-all; }
|
||||
.link-chip-val.mono { font-family: ui-monospace, monospace; letter-spacing: 1rpx; }
|
||||
.link-chip-action { flex-shrink: 0; font-size: 22rpx; font-weight: 600; color: #5eead4; }
|
||||
|
||||
.link-empty { padding: 24rpx; border-radius: 20rpx; background: rgba(15, 23, 42, 0.4); border: 1rpx dashed rgba(148, 163, 184, 0.2); }
|
||||
.link-empty-txt { font-size: 24rpx; color: #64748b; }
|
||||
|
||||
.profile-name {
|
||||
position: relative; z-index: 1; display: block; text-align: center;
|
||||
margin-top: 36rpx; font-size: 40rpx; font-weight: 700; color: #fff; letter-spacing: 4rpx;
|
||||
}
|
||||
.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; }
|
||||
.profile-tags {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; justify-content: center; gap: 20rpx; flex-wrap: wrap;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 10rpx 26rpx; border-radius: 999rpx; }
|
||||
.tag-mbti { background: rgba(19, 78, 74, 0.6); color: #5eead4; border: 1rpx solid rgba(94, 234, 212, 0.25); }
|
||||
.tag-region {
|
||||
background: rgba(30, 41, 59, 0.8); color: #cbd5e1; border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
display: flex; align-items: center; gap: 8rpx;
|
||||
}
|
||||
.pin-icon { color: #f87171; font-size: 22rpx; }
|
||||
|
||||
.project-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; }
|
||||
/* —— 一体化信息卡 —— */
|
||||
.mono-card {
|
||||
margin: 24rpx 24rpx 0; padding: 8rpx 0 32rpx; border-radius: 32rpx;
|
||||
background: rgba(15, 23, 34, 0.88);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.07);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mono-sec { padding: 28rpx 32rpx 8rpx; }
|
||||
.mono-sec-head { margin-bottom: 24rpx; }
|
||||
.mono-sec-title { font-size: 32rpx; font-weight: 700; color: #f8fafc; }
|
||||
.mono-divider { height: 1rpx; margin: 8rpx 32rpx; background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.15), transparent); }
|
||||
|
||||
.bottom-spacer { height: 180rpx; }
|
||||
.kv { margin-bottom: 28rpx; }
|
||||
.kv:last-child { margin-bottom: 8rpx; }
|
||||
.kv-k { display: block; font-size: 22rpx; color: #64748b; margin-bottom: 10rpx; }
|
||||
.kv-v { font-size: 28rpx; color: #e2e8f0; line-height: 1.65; font-weight: 500; }
|
||||
|
||||
/* 底部按钮 - 设计稿:透明背景 + 橙色描边 */
|
||||
.skills-showcase .skills-quote {
|
||||
padding: 28rpx 28rpx 28rpx 24rpx; border-radius: 20rpx;
|
||||
background: linear-gradient(105deg, rgba(45, 212, 191, 0.08) 0%, rgba(15, 23, 42, 0.5) 100%);
|
||||
border-left: 6rpx solid #2dd4bf;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.skills-quote-text { font-size: 30rpx; color: #f1f5f9; line-height: 1.75; font-weight: 500; }
|
||||
|
||||
.story { margin-bottom: 8rpx; }
|
||||
.story-gap { height: 28rpx; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { flex-shrink: 0; }
|
||||
.story-q { font-size: 24rpx; font-weight: 600; color: #94a3b8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #e2e8f0; line-height: 1.7; padding-left: 4rpx; }
|
||||
|
||||
.help-grid { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.help-tile {
|
||||
padding: 28rpx; border-radius: 22rpx;
|
||||
background: rgba(23, 33, 47, 0.9); border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.help-tile-tag {
|
||||
display: inline-block; font-size: 20rpx; font-weight: 700;
|
||||
padding: 8rpx 18rpx; border-radius: 12rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tile-tag { color: #5eead4; background: rgba(6, 78, 59, 0.45); border: 1rpx solid rgba(45, 212, 191, 0.2); }
|
||||
.help-need .help-tile-tag.need { color: #fbbf24; background: rgba(69, 47, 8, 0.45); border: 1rpx solid rgba(251, 191, 36, 0.2); }
|
||||
.help-tile-txt { font-size: 28rpx; color: #f1f5f9; line-height: 1.65; }
|
||||
|
||||
.proj-body { font-size: 28rpx; color: #cbd5e1; line-height: 1.75; padding-bottom: 8rpx; }
|
||||
|
||||
/* —— 空态 —— */
|
||||
.empty-hint {
|
||||
margin: 48rpx 24rpx; padding: 64rpx 32rpx;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 24rpx;
|
||||
border-radius: 28rpx; background: rgba(15, 23, 34, 0.5);
|
||||
border: 1rpx dashed rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
.empty-hint-txt { font-size: 26rpx; color: #64748b; }
|
||||
|
||||
.scroll-pad { height: calc(80rpx + env(safe-area-inset-bottom)); }
|
||||
|
||||
/* —— 底部 —— */
|
||||
.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);
|
||||
padding: 20rpx 24rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(5, 11, 20, 0.92); backdrop-filter: blur(20rpx);
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.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 {
|
||||
display: flex; align-items: center; justify-content: center; gap: 12rpx;
|
||||
width: 100%; height: 88rpx; border-radius: 999rpx; border: none;
|
||||
background: linear-gradient(135deg, #5eead4 0%, #2dd4bf 50%, #14b8a6 100%);
|
||||
color: #0f172a; font-size: 28rpx; font-weight: 700;
|
||||
box-shadow: 0 8rpx 28rpx rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
.vip-btn-arrow { font-size: 36rpx; }
|
||||
.vip-btn:active { opacity: 0.85; }
|
||||
|
||||
/* —— 加载态 —— */
|
||||
.state-wrap {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 60vh; gap: 24rpx;
|
||||
}
|
||||
.state-txt { font-size: 28rpx; color: #64748b; }
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx; border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2); border-top-color: #5eead4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@@ -23,20 +23,23 @@ Page({
|
||||
if (userId) {
|
||||
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
|
||||
if (res && res.success && res.data) {
|
||||
const orders = (res.data || []).map(item => ({
|
||||
const raw = (res.data || []).map(item => ({
|
||||
id: item.id || item.order_sn,
|
||||
sectionId: item.product_id || item.section_id,
|
||||
sectionMid: item.section_mid ?? item.mid ?? 0,
|
||||
title: item.product_name || `章节 ${item.product_id || ''}`,
|
||||
amount: item.amount || 0,
|
||||
status: item.status || 'completed',
|
||||
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--'
|
||||
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--',
|
||||
_sortMs: new Date(item.created_at || item.pay_time || 0).getTime() || 0
|
||||
}))
|
||||
raw.sort((a, b) => b._sortMs - a._sortMs)
|
||||
const orders = raw.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ orders })
|
||||
return
|
||||
}
|
||||
}
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const orders = purchasedSections.map((id, index) => ({
|
||||
id: `order_${index}`,
|
||||
sectionId: id,
|
||||
@@ -49,7 +52,7 @@ Page({
|
||||
this.setData({ orders })
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
this.setData({
|
||||
orders: purchasedSections.map((id, i) => ({
|
||||
id: `order_${i}`, sectionId: id, sectionMid: 0, title: `章节 ${id}`, amount: 1, status: 'completed',
|
||||
@@ -61,6 +64,14 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
if (!id) return
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onShareAppMessage() {
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
</view>
|
||||
|
||||
<view class="orders-list" wx:elif="{{orders.length > 0}}">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id" bindtap="goToRead" data-id="{{item.sectionId}}" data-mid="{{item.sectionMid}}">
|
||||
<view class="order-info">
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
<view class="order-title-row">
|
||||
<text class="order-unlock-icon">🔓</text>
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="order-time">{{item.createTime}}</text>
|
||||
</view>
|
||||
<view class="order-right">
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
@keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
.orders-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.order-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #1c1c1e; border-radius: 24rpx; }
|
||||
.order-info { flex: 1; }
|
||||
.order-title { font-size: 28rpx; color: #fff; display: block; margin-bottom: 8rpx; }
|
||||
.order-item:active { opacity: 0.92; }
|
||||
.order-info { flex: 1; min-width: 0; }
|
||||
.order-title-row { display: flex; align-items: flex-start; gap: 12rpx; margin-bottom: 8rpx; }
|
||||
.order-unlock-icon { font-size: 26rpx; line-height: 1.35; opacity: 0.55; flex-shrink: 0; }
|
||||
.order-title { font-size: 28rpx; color: #fff; flex: 1; min-width: 0; }
|
||||
.order-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.order-right { text-align: right; }
|
||||
.order-amount { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin-bottom: 4rpx; }
|
||||
|
||||
@@ -13,14 +13,44 @@
|
||||
* - contentSegments 解析每行,mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead
|
||||
*/
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
import readingTracker from '../../utils/readingTracker'
|
||||
const accessManager = require('../../utils/chapterAccessManager')
|
||||
const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
/** 阅读页解析正文用:人物字典 + #标签(与 /config/read-extras 一致) */
|
||||
function getContentParseConfig() {
|
||||
const g = getApp().globalData || {}
|
||||
const raw = Array.isArray(g.mentionPersons) ? g.mentionPersons : []
|
||||
const persons = raw.map((p) => ({
|
||||
personId: p.personId || '',
|
||||
token: p.token || '',
|
||||
name: (p.name || '').trim(),
|
||||
label: (p.label || '').trim(),
|
||||
aliases: p.aliases != null ? String(p.aliases) : '',
|
||||
}))
|
||||
const linkTags = Array.isArray(g.linkTagsConfig) ? g.linkTagsConfig : []
|
||||
return { persons, linkTags }
|
||||
}
|
||||
|
||||
/** 补全 mentionDisplay,避免旧数据无字段;昵称去空白防「@ 名」 */
|
||||
function normalizeMentionSegments(segments) {
|
||||
if (!Array.isArray(segments)) return []
|
||||
return segments.map((row) => {
|
||||
if (!Array.isArray(row)) return row
|
||||
return row.map((seg) => {
|
||||
if (!seg || seg.type !== 'mention') return seg
|
||||
const nick = String(seg.nickname || '')
|
||||
.replace(/^[\s\u00a0\u200b\u3000]+/g, '')
|
||||
.replace(/[\s\u00a0\u200b\u3000]+$/g, '')
|
||||
return { ...seg, nickname: nick, mentionDisplay: '@' + nick }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
@@ -312,6 +342,8 @@ Page({
|
||||
async loadContent(id, accessState, prefetchedChapter) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
try {
|
||||
await app.getReadExtras()
|
||||
const parseCfg = getContentParseConfig()
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
let res = prefetchedChapter
|
||||
if (!res || !res.content) {
|
||||
@@ -328,13 +360,13 @@ Page({
|
||||
// 已解锁用 data.content(完整内容),未解锁用 content(预览);先 determineAccessState 再 loadContent 保证顺序正确
|
||||
const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content
|
||||
if (res && displayContent) {
|
||||
const { lines, segments } = contentParser.parseContent(displayContent)
|
||||
const { lines, segments } = contentParser.parseContent(displayContent, parseCfg)
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const updates = {
|
||||
content: displayContent,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
@@ -355,13 +387,14 @@ Page({
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
const { lines, segments } = contentParser.parseContent(cached.content)
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(cached.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
this.setData({
|
||||
content: cached.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
@@ -460,8 +493,9 @@ Page({
|
||||
},
|
||||
|
||||
// 设置章节内容(兼容纯文本/Markdown 与 TipTap HTML)
|
||||
setChapterContent(res) {
|
||||
const { lines, segments } = contentParser.parseContent(res.content)
|
||||
async setChapterContent(res) {
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(res.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
@@ -478,7 +512,7 @@ Page({
|
||||
content: res.content,
|
||||
previewContent: lines.slice(0, previewCount).join('\n'),
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
// 导航栏、分享等使用的文章标题,同样统一为 sectionTitle
|
||||
@@ -504,7 +538,7 @@ Page({
|
||||
if (currentRetry >= maxRetries) {
|
||||
this.setData({
|
||||
contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
|
||||
contentSegments: contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments,
|
||||
contentSegments: normalizeMentionSegments(contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments),
|
||||
previewParagraphs: ['内容加载失败']
|
||||
})
|
||||
return
|
||||
@@ -514,7 +548,7 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
await this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
return
|
||||
@@ -680,12 +714,6 @@ Page({
|
||||
})
|
||||
return
|
||||
}
|
||||
// 2 分钟内只能点一次(与后端限频一致,与首页链接卡若共用)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
@@ -881,10 +909,8 @@ Page({
|
||||
#创业派对 #私域运营 #商业案例`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({ title: '文案已复制', icon: 'success' })
|
||||
}
|
||||
data: shareText
|
||||
// 不额外 showToast:系统已有「内容已复制」,避免与自定义文案叠两层
|
||||
})
|
||||
},
|
||||
|
||||
@@ -935,12 +961,17 @@ Page({
|
||||
wx.setClipboardData({
|
||||
data: copyText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请点击右上角「···」菜单,选择「分享到朋友圈」即可发布',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
// 系统会在 setClipboardData 成功后自动 toast「内容已复制」,与下方引导弹窗重复,先关掉再弹窗
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '分享到朋友圈',
|
||||
content: '文案已复制。\n\n请点击右上角「···」菜单,选择「分享到朋友圈」即可发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
|
||||
@@ -1099,7 +1130,28 @@ Page({
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
// 检查金额是否有效
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
const avatar = userInfo.avatarUrl || ''
|
||||
const nickname = userInfo.nickname || userInfo.nickName || ''
|
||||
const needProfile = !avatar || avatar.includes('default') || avatar.includes('132') || !nickname || nickname === '微信用户'
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '购买前请先完善头像和昵称',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: resolve
|
||||
})
|
||||
})
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
console.error('[Pay] 金额无效:', amount)
|
||||
wx.showToast({ title: '价格信息错误', icon: 'none' })
|
||||
@@ -1246,13 +1298,13 @@ Page({
|
||||
// 支付接口失败时,显示客服联系方式
|
||||
wx.showModal({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
|
||||
content: '微信支付正在审核中,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => {
|
||||
wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
}
|
||||
@@ -1294,13 +1346,13 @@ Page({
|
||||
// 支付失败,可能是参数错误或权限问题
|
||||
wx.showModal({
|
||||
title: '支付失败',
|
||||
content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
|
||||
content: '微信支付暂不可用,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
@@ -1453,18 +1505,22 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报
|
||||
// 生成海报(Canvas 2D API)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true }, () => resolve())
|
||||
})
|
||||
await new Promise((resolve) => {
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(resolve)
|
||||
else setTimeout(resolve, 50)
|
||||
})
|
||||
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数,优先 mid 与新链接一致)
|
||||
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
@@ -1473,109 +1529,102 @@ Page({
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
if (qrRes.success && qrRes.image) qrcodeImage = qrRes.image
|
||||
} catch (_) {}
|
||||
|
||||
const canvasNode = await new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (res && res[0] && res[0].node) resolve(res[0])
|
||||
else reject(new Error('canvas node not found'))
|
||||
})
|
||||
})
|
||||
|
||||
const canvas = canvasNode.node
|
||||
const ctx = canvas.getContext('2d')
|
||||
let dpr = 2
|
||||
try {
|
||||
if (typeof wx.getWindowInfo === 'function') {
|
||||
dpr = wx.getWindowInfo().pixelRatio || 2
|
||||
} else if (wx.getSystemInfoSync) {
|
||||
dpr = wx.getSystemInfoSync().pixelRatio || 2
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
} catch (_) {
|
||||
dpr = 2
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
const titleLines = this.wrapText2d(ctx, title, width - 40)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
titleLines.forEach(line => { ctx.fillText(line, 20, y); y += 26 })
|
||||
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.1)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
// 内容摘要
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
||||
y += 30
|
||||
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
|
||||
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
// 底部区域背景
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
const summaryLines = this.wrapText2d(ctx, summary, width - 40)
|
||||
summaryLines.slice(0, 6).forEach(line => { ctx.fillText(line, 20, y); y += 20 })
|
||||
|
||||
ctx.fillStyle = 'rgba(0,206,209,0.1)'
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
// 绘制小程序码或占位符
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeImage) {
|
||||
// 下载base64图片并绘制
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
if (qrcodeImage) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
fs.writeFileSync(filePath, base64Data, 'base64')
|
||||
const img = canvas.createImage()
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
img.src = filePath
|
||||
})
|
||||
ctx.drawImage(img, width - 85, height - 85, 70, 70)
|
||||
} catch (_) {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
@@ -1583,21 +1632,19 @@ Page({
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
drawQRPlaceholder(ctx, width, height) {
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
drawQRPlaceholder2d(ctx, width, height) {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.beginPath()
|
||||
ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.setFontSize(9)
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.font = '9px sans-serif'
|
||||
ctx.fillText('扫码', width - 57, height - 52)
|
||||
ctx.fillText('阅读', width - 57, height - 40)
|
||||
},
|
||||
|
||||
// 文字换行处理
|
||||
wrapText(ctx, text, maxWidth, fontSize) {
|
||||
|
||||
wrapText2d(ctx, text, maxWidth) {
|
||||
const lines = []
|
||||
let line = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
@@ -1619,39 +1666,47 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
// 保存海报到相册(Canvas 2D)
|
||||
savePoster() {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const canvas = res[0].node
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
success: (r2) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: r2.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (m) => {
|
||||
if (m.confirm) wx.openSetting()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
}, this)
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
|
||||
@@ -54,11 +54,9 @@
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
|
||||
<text user-select wx:if="{{!(item.length === 1 && item[0].type === 'image')}}"><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<text wx:if="{{seg.type === 'text'}}" user-select>{{seg.text}}</text>
|
||||
<text wx:elif="{{seg.type === 'mention'}}" class="mention" user-select bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">@{{seg.nickname}}</text>
|
||||
<text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" user-select bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text>
|
||||
<image wx:elif="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -94,23 +92,17 @@
|
||||
<!-- 分享操作区 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<view class="action-btn-inline" bindtap="onShareTimelineTap">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-share-inline" bindtap="onShareTimelineTap">
|
||||
<icon name="megaphone" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</view>
|
||||
<view class="action-btn-inline" bindtap="generatePoster">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/image.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<icon name="image" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<view class="action-btn-inner">
|
||||
<image class="action-btn-icon" src="/assets/icons/gift.svg" mode="aspectFit"></image>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
@@ -130,15 +122,23 @@
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<!-- 付费墙 - 未登录:显示购买按钮(朋友圈/分享场景) -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读{{previewPercent}}%,登录后查看完整内容</text>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并购买后阅读全文</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">手机号一键登录</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal" style="margin-top:12px">
|
||||
<text class="login-btn-text">手机号登录后购买</text>
|
||||
</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -185,7 +185,7 @@
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读{{previewPercent}}%,购买后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</text>
|
||||
|
||||
<!-- 购买选项(审核模式隐藏) -->
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
@@ -280,7 +280,7 @@
|
||||
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
<canvas type="2d" id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
|
||||
<view class="poster-actions">
|
||||
@@ -359,6 +359,6 @@
|
||||
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<image class="fab-circle-icon" src="/assets/icons/circle.svg" mode="aspectFit"></image>
|
||||
<icon name="share" size="44" color="#0f172a" customClass="fab-share-icon"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -479,10 +479,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
@@ -492,28 +493,31 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-text-small {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.share-tip-inline {
|
||||
@@ -655,6 +659,9 @@
|
||||
}
|
||||
|
||||
/* ===== 代付分享 ===== */
|
||||
.btn-gift-inline {
|
||||
/* 与 btn-share-inline 同风格 */
|
||||
}
|
||||
.gift-share-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1248,15 +1255,20 @@
|
||||
.fab-share {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
width:70rpx!important;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 70rpx;
|
||||
border-radius: 60rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
@@ -1265,10 +1277,18 @@
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.fab-circle-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
.fab-icon {
|
||||
padding:16rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-share-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,7 @@ Page({
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
posterCaseCount: 62,
|
||||
|
||||
posterCaseCount: 62
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -94,28 +93,14 @@ Page({
|
||||
// 生成邀请码
|
||||
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
|
||||
|
||||
console.log('[Referral] 开始加载分销数据,userId:', userInfo.id)
|
||||
|
||||
// 从API获取真实数据
|
||||
let realData = null
|
||||
try {
|
||||
// app.request 第一个参数是 URL 字符串(会自动拼接 baseUrl)
|
||||
const res = await app.request('/api/miniprogram/referral/data?userId=' + userInfo.id)
|
||||
console.log('[Referral] API返回:', JSON.stringify(res).substring(0, 200))
|
||||
|
||||
if (res && res.success && res.data) {
|
||||
realData = res.data
|
||||
console.log('[Referral] ✅ 获取推广数据成功')
|
||||
console.log('[Referral] - bindingCount:', realData.bindingCount)
|
||||
console.log('[Referral] - paidCount:', realData.paidCount)
|
||||
console.log('[Referral] - earnings:', realData.earnings)
|
||||
console.log('[Referral] - expiringCount:', realData.stats?.expiringCount)
|
||||
} else {
|
||||
console.log('[Referral] ❌ API返回格式错误:', res?.error || 'unknown')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Referral] ❌ API调用失败:', e.message || e)
|
||||
console.log('[Referral] 错误详情:', e)
|
||||
console.error('[Referral] API调用失败:', e.message || e)
|
||||
}
|
||||
|
||||
// 使用真实数据或默认值
|
||||
@@ -123,14 +108,10 @@ Page({
|
||||
let convertedBindings = realData?.convertedUsers || []
|
||||
let expiredBindings = realData?.expiredUsers || []
|
||||
|
||||
console.log('[Referral] activeBindings:', activeBindings.length)
|
||||
console.log('[Referral] convertedBindings:', convertedBindings.length)
|
||||
console.log('[Referral] expiredBindings:', expiredBindings.length)
|
||||
|
||||
// 计算即将过期的数量(7天内)
|
||||
const expiringCount = realData?.stats?.expiringCount || activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
|
||||
|
||||
console.log('[Referral] expiringCount:', expiringCount)
|
||||
|
||||
// 计算各类统计
|
||||
const bindingCount = realData?.bindingCount || activeBindings.length
|
||||
@@ -153,7 +134,6 @@ Page({
|
||||
purchaseCount: user.purchaseCount || 0,
|
||||
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
|
||||
}
|
||||
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
|
||||
return formatted
|
||||
}
|
||||
|
||||
@@ -169,15 +149,6 @@ Page({
|
||||
const availableEarningsNum = Math.max(0, totalCommissionNum - withdrawnNum - pendingWithdrawNum)
|
||||
const minWithdrawAmount = realData?.minWithdrawAmount || 10
|
||||
|
||||
console.log('=== [Referral] 收益计算(完整版)===')
|
||||
console.log('累计佣金 (totalCommission):', totalCommissionNum)
|
||||
console.log('已提现金额 (withdrawnEarnings):', withdrawnNum)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', pendingWithdrawNum)
|
||||
console.log('可提现金额 = 累计 - 已提现 - 待审核 =', totalCommissionNum, '-', withdrawnNum, '-', pendingWithdrawNum, '=', availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', minWithdrawAmount)
|
||||
console.log('按钮判断:', availableEarningsNum, '>=', minWithdrawAmount, '=', availableEarningsNum >= minWithdrawAmount)
|
||||
console.log('✅ 按钮应该:', availableEarningsNum >= minWithdrawAmount ? '🟢 启用(绿色)' : '⚫ 禁用(灰色)')
|
||||
|
||||
const hasWechatId = !!(userInfo?.wechat || userInfo?.wechatId || wx.getStorageSync('user_wechat'))
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
@@ -234,20 +205,6 @@ Page({
|
||||
})
|
||||
|
||||
|
||||
console.log('[Referral] ✅ 数据设置完成')
|
||||
console.log('[Referral] - 绑定中:', this.data.bindingCount)
|
||||
console.log('[Referral] - 即将过期:', this.data.expiringCount)
|
||||
console.log('[Referral] - 收益:', this.data.earnings)
|
||||
|
||||
console.log('=== [Referral] 按钮状态验证 ===')
|
||||
console.log('累计佣金 (totalCommission):', this.data.totalCommission)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', this.data.pendingWithdrawAmount)
|
||||
console.log('可提现金额 (availableEarnings 显示):', this.data.availableEarnings)
|
||||
console.log('可提现金额 (availableEarningsNum 判断):', this.data.availableEarningsNum, typeof this.data.availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', this.data.minWithdrawAmount, typeof this.data.minWithdrawAmount)
|
||||
console.log('按钮启用条件:', this.data.availableEarningsNum, '>=', this.data.minWithdrawAmount, '=', this.data.availableEarningsNum >= this.data.minWithdrawAmount)
|
||||
console.log('✅ 最终结果: 按钮应该', this.data.availableEarningsNum >= this.data.minWithdrawAmount ? '🟢 启用' : '⚫ 禁用')
|
||||
|
||||
// 隐藏加载提示
|
||||
wx.hideLoading()
|
||||
} else {
|
||||
|
||||
@@ -3,20 +3,13 @@
|
||||
* 账号绑定功能
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
version: '1.0.0',
|
||||
isDevMode: false, // 是否开发版(用于显示切换账号入口)
|
||||
|
||||
// 切换账号(开发)
|
||||
showSwitchAccountModal: false,
|
||||
switchAccountUserId: '',
|
||||
switchAccountLoading: false,
|
||||
version: '',
|
||||
|
||||
// 绑定信息
|
||||
phoneNumber: '',
|
||||
@@ -38,16 +31,27 @@ Page({
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : null
|
||||
const envVersion = accountInfo?.miniProgram?.envVersion || ''
|
||||
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
|
||||
const displayVersion =
|
||||
wxPkgVersion ||
|
||||
(app.globalData.appDisplayVersion || '1.7.1')
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
isLoggedIn: app.globalData.isLoggedIn,
|
||||
userInfo: app.globalData.userInfo,
|
||||
isDevMode: envVersion === 'develop'
|
||||
isDevMode: envVersion === 'develop',
|
||||
version: displayVersion
|
||||
})
|
||||
this.loadBindingInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : null
|
||||
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
|
||||
const displayVersion =
|
||||
wxPkgVersion ||
|
||||
(app.globalData.appDisplayVersion || '1.7.1')
|
||||
this.setData({ version: displayVersion })
|
||||
this.loadBindingInfo()
|
||||
},
|
||||
|
||||
@@ -247,92 +251,6 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 获取微信头像(新版授权)
|
||||
async getWechatAvatar() {
|
||||
try {
|
||||
const res = await wx.getUserProfile({
|
||||
desc: '用于完善会员资料'
|
||||
})
|
||||
|
||||
if (res.userInfo) {
|
||||
const { nickName, avatarUrl: tempAvatarUrl } = res.userInfo
|
||||
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
// 1. 先上传图片到服务器
|
||||
console.log('[Settings] 开始上传头像:', tempAvatarUrl)
|
||||
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: {
|
||||
folder: 'avatars'
|
||||
},
|
||||
success: (uploadResult) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadResult.data)
|
||||
if (data.success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.error || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[Settings] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地
|
||||
this.setData({
|
||||
userInfo: {
|
||||
...this.data.userInfo,
|
||||
nickname: nickName,
|
||||
avatar: avatarUrl
|
||||
}
|
||||
})
|
||||
|
||||
// 4. 同步到服务器数据库(只保存路径,不含域名)
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, nickname: nickName, avatar: toAvatarPath(avatarUrl) }
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 更新全局
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.nickname = nickName
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[Settings] 获取头像失败:', e)
|
||||
wx.showToast({
|
||||
title: e.message || '获取头像失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
@@ -402,71 +320,6 @@ Page({
|
||||
this.setData({ showBindModal: false })
|
||||
},
|
||||
|
||||
// 跳转账户密码登录页(开发)
|
||||
goToDevLogin() {
|
||||
wx.navigateTo({ url: '/pages/dev-login/dev-login' })
|
||||
},
|
||||
|
||||
// 打开切换账号弹窗(开发)
|
||||
openSwitchAccountModal() {
|
||||
this.setData({
|
||||
showSwitchAccountModal: true,
|
||||
switchAccountUserId: app.globalData.userInfo?.id || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭切换账号弹窗
|
||||
closeSwitchAccountModal() {
|
||||
if (this.data.switchAccountLoading) return
|
||||
this.setData({ showSwitchAccountModal: false, switchAccountUserId: '' })
|
||||
},
|
||||
|
||||
// 切换账号 userId 输入
|
||||
onSwitchAccountUserIdInput(e) {
|
||||
this.setData({ switchAccountUserId: e.detail.value.trim() })
|
||||
},
|
||||
|
||||
// 确认切换账号
|
||||
async confirmSwitchAccount() {
|
||||
const userId = this.data.switchAccountUserId.trim()
|
||||
if (!userId || this.data.switchAccountLoading) return
|
||||
this.setData({ switchAccountLoading: true })
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/dev/login-as', {
|
||||
method: 'POST',
|
||||
data: { userId }
|
||||
})
|
||||
if (res && res.success && res.data) {
|
||||
const { token, user } = res.data
|
||||
const openId = res.data.openId || ''
|
||||
wx.setStorageSync('token', token)
|
||||
wx.setStorageSync('userInfo', user)
|
||||
app.globalData.userInfo = user
|
||||
app.globalData.isLoggedIn = true
|
||||
app.globalData.purchasedSections = user.purchasedSections || []
|
||||
app.globalData.hasFullBook = user.hasFullBook || false
|
||||
app.globalData.isVip = user.isVip || false
|
||||
app.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
if (openId) {
|
||||
app.globalData.openId = openId
|
||||
wx.setStorageSync('openId', openId)
|
||||
}
|
||||
this.setData({
|
||||
showSwitchAccountModal: false,
|
||||
switchAccountUserId: '',
|
||||
switchAccountLoading: false
|
||||
})
|
||||
this.loadBindingInfo()
|
||||
wx.showToast({ title: '已切换为 ' + (user.nickname || userId), icon: 'success' })
|
||||
} else {
|
||||
throw new Error(res?.error || '切换失败')
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ switchAccountLoading: false })
|
||||
wx.showToast({ title: e.message || '切换失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
wx.showModal({
|
||||
|
||||
@@ -113,21 +113,7 @@
|
||||
<text class="tip-text">提示:绑定微信号才能使用提现功能</text>
|
||||
</view>
|
||||
|
||||
<!-- 开发专用:切换账号(仅开发版显示) -->
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="openSwitchAccountModal">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="wrench" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">切换账号(开发)</text>
|
||||
<text class="dev-switch-desc">输入 userId 切换为其他账号调试</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="goToDevLogin">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="smartphone" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">账户密码登录</text>
|
||||
<text class="dev-switch-desc">输入对方手机号登录,密码可留空</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 开发专用入口已移除 -->
|
||||
|
||||
<view class="logout-btn" wx:if="{{isLoggedIn}}" bindtap="handleLogout">退出登录</view>
|
||||
</view>
|
||||
@@ -141,10 +127,10 @@
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<view class="form-input-wrap">
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="{{bindType === 'phone' ? 'number' : 'text'}}"
|
||||
class="form-input"
|
||||
placeholder="{{bindType === 'phone' ? '请输入11位手机号' : bindType === 'wechat' ? '请输入微信号' : '请输入支付宝账号'}}"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{bindValue}}"
|
||||
@@ -164,28 +150,4 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 切换账号弹窗(开发) -->
|
||||
<view class="modal-overlay" wx:if="{{showSwitchAccountModal}}" bindtap="closeSwitchAccountModal">
|
||||
<view class="modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">切换账号(开发)</text>
|
||||
<view class="modal-close" bindtap="closeSwitchAccountModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
placeholder="请输入目标用户的 userId(如 ogpTW5fmXRGNpoUbXB3UEqnVe5Tg)"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{switchAccountUserId}}"
|
||||
bindinput="onSwitchAccountUserIdInput"
|
||||
/>
|
||||
</view>
|
||||
<text class="bind-tip">从管理端或数据库获取要调试的用户 ID,切换后将以该用户身份操作</text>
|
||||
<view class="btn-primary {{!switchAccountUserId || switchAccountLoading ? 'btn-disabled' : ''}}" bindtap="confirmSwitchAccount">
|
||||
{{switchAccountLoading ? '切换中...' : '确认切换'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
.modal-title { font-size: 36rpx; font-weight: 700; color: #fff; }
|
||||
.modal-close { width: 64rpx; height: 64rpx; background: rgba(255,255,255,0.08); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: rgba(255,255,255,0.5); }
|
||||
.modal-body { padding: 16rpx 40rpx 48rpx; }
|
||||
/* 弹窗 input:外边包 view,padding 写在 view 上,避免光标截断 */
|
||||
.form-input-wrap { padding: 16rpx 24rpx; background: #1F2937; border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; margin-bottom: 32rpx; }
|
||||
.form-input-inner { width: 100%; font-size: 28rpx; background: transparent; color: #fff; }
|
||||
.input-wrapper { margin-bottom: 32rpx; }
|
||||
.form-input { width: 100%; padding: 32rpx 24rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; font-size: 32rpx; color: #fff; box-sizing: border-box; transition: all 0.2s; }
|
||||
.form-input:focus { border-color: rgba(0,206,209,0.5); background: rgba(0,206,209,0.05); }
|
||||
.input-placeholder { color: rgba(255,255,255,0.25); }
|
||||
.bind-tip { font-size: 24rpx; color: rgba(255,255,255,0.4); margin-bottom: 40rpx; display: block; line-height: 1.6; text-align: center; }
|
||||
.btn-primary { padding: 32rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 28rpx; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
const accessManager = require('../../utils/chapterAccessManager')
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
@@ -12,16 +12,16 @@ Page({
|
||||
originalPrice: 6980,
|
||||
/* 按 premium_membership_landing_v1 设计稿 */
|
||||
contentRights: [
|
||||
{ title: '解锁全部章节', desc: '365天全案精读', icon: 'book-open' },
|
||||
{ title: '案例库', desc: '100+创业实战案例', icon: 'book-open' },
|
||||
{ title: '智能纪要', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ title: '会议纪要库', desc: '往期完整沉淀', icon: 'folder' }
|
||||
{ title: '匹配伙伴', desc: '精准匹配创业伙伴', icon: 'users' },
|
||||
{ title: '派对专属', desc: '创业派对房专享', icon: 'star' },
|
||||
{ title: '老板排行', desc: '创业老板排行榜', icon: 'bar-chart' },
|
||||
{ title: '轮流置顶', desc: '首页获客曝光位', icon: 'arrow-up' }
|
||||
],
|
||||
socialRights: [
|
||||
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: 'users' },
|
||||
{ title: '创业老板排行', desc: '项目曝光展示', icon: 'bar-chart' },
|
||||
{ title: '链接资源', desc: '深度私域资源池', icon: 'link' },
|
||||
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: 'check' }
|
||||
{ title: '案例宝库', desc: '100+赚钱案例库', icon: 'book-open' },
|
||||
{ title: '全书解锁', desc: '365天全案精读', icon: 'folder' },
|
||||
{ title: '每日总结', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ title: '获取客资', desc: '文章@你即可获客', icon: 'link' }
|
||||
],
|
||||
purchasing: false
|
||||
},
|
||||
|
||||
@@ -3,12 +3,15 @@ const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
statusBarHeight: 0,
|
||||
balance: 0,
|
||||
balanceText: '0.00',
|
||||
totalRecharged: '0.00',
|
||||
totalGifted: '0.00',
|
||||
totalRefunded: '0.00',
|
||||
transactions: [],
|
||||
loading: true,
|
||||
rechargeAmounts: [10, 30, 50, 100],
|
||||
rechargeAmounts: [10, 30, 50, 1000],
|
||||
selectedAmount: 30,
|
||||
auditMode: false,
|
||||
},
|
||||
@@ -35,6 +38,9 @@ Page({
|
||||
this.setData({
|
||||
balance: res.data.balance || 0,
|
||||
balanceText: (res.data.balance || 0).toFixed(2),
|
||||
totalRecharged: (res.data.totalRecharged || 0).toFixed(2),
|
||||
totalGifted: (res.data.totalGifted || 0).toFixed(2),
|
||||
totalRefunded: (res.data.totalRefunded || 0).toFixed(2),
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
@@ -51,10 +57,9 @@ Page({
|
||||
if (res && res.data) {
|
||||
const list = (res.data || []).map(t => ({
|
||||
...t,
|
||||
amountText: Math.abs(t.amount || 0).toFixed(2),
|
||||
amountSign: (t.amount || 0) >= 0 ? '+' : '-',
|
||||
description: t.type === 'recharge' ? '充值' : t.type === 'consume' ? '阅读消费' : t.type === 'refund' ? '退款' : '其他',
|
||||
createdAt: t.createdAt ? new Date(t.createdAt).toLocaleString('zh-CN') : '--',
|
||||
amountText: (t.amount || 0).toFixed(2),
|
||||
amountSign: (t.amount || 0) >= 0 ? '+' : '',
|
||||
description: t.description || (t.type === 'recharge' ? '充值' : t.type === 'gift' ? '赠送' : t.type === 'refund' ? '退款' : t.type === 'consume' ? '阅读消费' : '其他'),
|
||||
}))
|
||||
this.setData({ transactions: list })
|
||||
}
|
||||
@@ -112,6 +117,7 @@ Page({
|
||||
wx.requestPayment({
|
||||
...params,
|
||||
success: async () => {
|
||||
// Confirm the recharge
|
||||
await app.request({
|
||||
url: '/api/miniprogram/balance/recharge/confirm',
|
||||
method: 'POST',
|
||||
@@ -126,16 +132,53 @@ Page({
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: payRes?.error || '创建支付失败', icon: 'none' })
|
||||
wx.showToast({ title: '创建支付失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[Wallet] recharge error', e)
|
||||
console.error('[Wallet] recharge error:', e)
|
||||
wx.showToast({ title: '充值失败:' + (e.message || e.errMsg || '网络异常'), icon: 'none', duration: 3000 })
|
||||
}
|
||||
},
|
||||
|
||||
async handleRefund() {
|
||||
trackClick('wallet', 'btn_click', '退款')
|
||||
if (this.data.balance <= 0) {
|
||||
wx.showToast({ title: '余额为零', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const balance = this.data.balance
|
||||
|
||||
wx.showModal({
|
||||
title: '余额退款',
|
||||
content: `退回全部余额 ¥${balance.toFixed(2)}\n\n退款将在1-3个工作日内原路返回`,
|
||||
confirmText: '确认退款',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
wx.showLoading({ title: '处理中...' })
|
||||
try {
|
||||
const result = await app.request({
|
||||
url: '/api/miniprogram/balance/refund',
|
||||
method: 'POST',
|
||||
data: { userId, amount: balance }
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (result && result.data) {
|
||||
wx.showToast({ title: result.data.message || '退款成功', icon: 'success' })
|
||||
this.loadBalance()
|
||||
this.loadTransactions()
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '退款失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
padding-bottom: 64rpx;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -45,6 +46,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 余额卡片 - 渐变背景 */
|
||||
.balance-card {
|
||||
margin: 24rpx 24rpx 32rpx;
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, rgba(56, 189, 172, 0.15) 100%);
|
||||
@@ -90,6 +92,7 @@
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.section {
|
||||
margin: 0 24rpx 32rpx;
|
||||
}
|
||||
@@ -110,6 +113,7 @@
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* 金额选择卡片 */
|
||||
.amount-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -170,6 +174,7 @@
|
||||
color: rgba(213, 255, 250, 0.72);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
@@ -189,7 +194,13 @@
|
||||
background: #38bdac;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
.btn-refund {
|
||||
background: #1c1c1e;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.4);
|
||||
}
|
||||
|
||||
/* 交易记录 */
|
||||
.transactions {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "卡若创业派对 - 来自派对房的真实商业故事",
|
||||
"appid": "wxb8bbb2b10dec74aa",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"name": "开发登录",
|
||||
"pathName": "pages/dev-login/dev-login",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/member-detail/member-detail",
|
||||
|
||||
@@ -206,4 +206,4 @@ class ChapterAccessManager {
|
||||
|
||||
// 导出单例
|
||||
const accessManager = new ChapterAccessManager()
|
||||
export default accessManager
|
||||
module.exports = accessManager
|
||||
|
||||
@@ -27,6 +27,29 @@ function decodeEntities(str) {
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
/**
|
||||
* 单行展示用:昵称、#标签文案、章节外标题类字段 — 合并换行、<br>、连续空白(避免 TipTap/粘贴带入异常断行)
|
||||
*/
|
||||
function cleanSingleLineField(s) {
|
||||
if (!s && s !== 0) return ''
|
||||
let t = decodeEntities(String(s))
|
||||
.replace(/<br\s*\/?>/gi, ' ')
|
||||
.replace(/\r\n|\r|\n/g, ' ')
|
||||
.replace(/[\s\u00a0\u200b\u200c\u200d\ufeff\u3000]+/g, ' ')
|
||||
.trim()
|
||||
return t
|
||||
}
|
||||
|
||||
/** @提及昵称:去首尾空白、零宽、全角空格;合并内部换行/<br> */
|
||||
function cleanMentionNickname(n) {
|
||||
return cleanSingleLineField(n)
|
||||
}
|
||||
|
||||
/** 纯文本在 mention 节点前若已有「@」,去掉末尾 @,避免渲染成「找@@阿浪」 */
|
||||
function stripTrailingAtForMention(before) {
|
||||
return before.replace(/[@@][\s\u00a0\u200b]*$/u, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个 HTML block 字符串解析为 segments 数组
|
||||
* 处理三种内联元素:mention / linkTag(span) / linkTag(a) / img
|
||||
@@ -39,20 +62,25 @@ function parseBlockToSegments(block) {
|
||||
let m
|
||||
|
||||
while ((m = tokenRe.exec(block)) !== null) {
|
||||
// 前置纯文本
|
||||
const before = decodeEntities(block.slice(lastEnd, m.index).replace(/<[^>]+>/g, ''))
|
||||
// 前置纯文本(mention 紧挨手写「找@」时去掉重复 @)
|
||||
let before = decodeEntities(block.slice(lastEnd, m.index).replace(/<[^>]+>/g, ''))
|
||||
const tag = m[0]
|
||||
if (/data-type="mention"/i.test(tag)) {
|
||||
before = stripTrailingAtForMention(before)
|
||||
}
|
||||
if (before.trim()) segs.push({ type: 'text', text: before })
|
||||
|
||||
const tag = m[0]
|
||||
|
||||
if (/data-type="mention"/i.test(tag)) {
|
||||
// @mention — TipTap mention span
|
||||
// @mention — TipTap mention span(span 内常见「@ 昵称」多空格,统一紧挨显示)
|
||||
const idMatch = tag.match(/data-id="([^"]*)"/)
|
||||
const labelMatch = tag.match(/data-label="([^"]*)"/)
|
||||
const innerText = tag.replace(/<[^>]+>/g, '')
|
||||
const userId = idMatch ? idMatch[1].trim() : ''
|
||||
const nickname = labelMatch ? labelMatch[1].trim() : innerText.replace(/^@/, '').trim()
|
||||
if (userId || nickname) segs.push({ type: 'mention', userId, nickname })
|
||||
let nickname = labelMatch ? labelMatch[1] : innerText.replace(/^[@@]\s*/, '')
|
||||
nickname = cleanMentionNickname((nickname || '').trim())
|
||||
if (userId || nickname) {
|
||||
segs.push({ type: 'mention', userId, nickname, mentionDisplay: '@' + nickname })
|
||||
}
|
||||
|
||||
} else if (/data-type="linkTag"/i.test(tag)) {
|
||||
// #linkTag — 自定义 span 格式(data-type="linkTag" data-url="..." data-tag-type="..." data-page-path="..." data-app-id="...")
|
||||
@@ -62,7 +90,7 @@ function parseBlockToSegments(block) {
|
||||
const tagIdMatch = tag.match(/data-tag-id="([^"]*)"/)
|
||||
const appIdMatch = tag.match(/data-app-id="([^"]*)"/)
|
||||
const mpKeyMatch = tag.match(/data-mp-key="([^"]*)"/)
|
||||
const innerText = tag.replace(/<[^>]+>/g, '').replace(/^#/, '').trim()
|
||||
const innerText = cleanSingleLineField(tag.replace(/<[^>]+>/g, '').replace(/^#/, ''))
|
||||
const url = urlMatch ? urlMatch[1] : ''
|
||||
const tagType = tagTypeMatch ? tagTypeMatch[1] : 'url'
|
||||
const pagePath = pagePathMatch ? pagePathMatch[1] : ''
|
||||
@@ -75,7 +103,7 @@ function parseBlockToSegments(block) {
|
||||
// #linkTag — 旧格式 <a href>(insertLinkTag 旧版产生,url 可能为空)
|
||||
// m[1] = href, m[2] = innerText(以 # 开头)
|
||||
const url = m[1] || ''
|
||||
const label = (m[2] || '').replace(/^#/, '').trim()
|
||||
const label = cleanSingleLineField((m[2] || '').replace(/^#/, ''))
|
||||
// 旧格式没有 tagType,在 onLinkTagTap 中会按 label 匹配缓存的 linkTags 配置降级处理
|
||||
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
|
||||
|
||||
@@ -100,8 +128,10 @@ function parseBlockToSegments(block) {
|
||||
|
||||
/**
|
||||
* 从 HTML 中解析出 lines(纯文本行)和 segments(含富文本片段)
|
||||
* @param {string} html
|
||||
* @param {object} [config] - { persons: [], linkTags: [] },用于对 text 段自动匹配 @人名 / #标签
|
||||
*/
|
||||
function parseHtmlToSegments(html) {
|
||||
function parseHtmlToSegments(html, config) {
|
||||
const lines = []
|
||||
const segments = []
|
||||
|
||||
@@ -125,7 +155,7 @@ function parseHtmlToSegments(html) {
|
||||
for (const block of blocks) {
|
||||
if (!block.trim()) continue
|
||||
|
||||
const blockSegs = parseBlockToSegments(block)
|
||||
let blockSegs = parseBlockToSegments(block)
|
||||
if (!blockSegs.length) continue
|
||||
|
||||
// 纯图片行独立成段
|
||||
@@ -135,6 +165,20 @@ function parseHtmlToSegments(html) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 对 text 段再跑一遍 @人名 / #标签 自动匹配(处理未用 TipTap 插入而是手打的 @xxx)
|
||||
if (config && (config.persons?.length || config.linkTags?.length)) {
|
||||
const expanded = []
|
||||
for (const seg of blockSegs) {
|
||||
if (seg.type === 'text' && seg.text) {
|
||||
const sub = matchLineToSegments(seg.text, config)
|
||||
expanded.push(...sub)
|
||||
} else {
|
||||
expanded.push(seg)
|
||||
}
|
||||
}
|
||||
blockSegs = expanded
|
||||
}
|
||||
|
||||
// 行纯文本用于 lines(previewParagraphs 降级展示)
|
||||
const lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
lines.push(lineText)
|
||||
@@ -144,29 +188,150 @@ function parseHtmlToSegments(html) {
|
||||
return { lines, segments }
|
||||
}
|
||||
|
||||
/** 纯文本按行解析(无 HTML 标签) */
|
||||
function parsePlainTextToSegments(text) {
|
||||
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
||||
const segments = lines.map(line => [{ type: 'text', text: line }])
|
||||
/** 清理 Markdown 格式标记(**加粗** *斜体* __加粗__ _斜体_ ~~删除线~~ `代码` 等)*/
|
||||
function stripMarkdownFormatting(text) {
|
||||
if (!text) return text
|
||||
let s = text
|
||||
s = s.replace(/^#{1,6}\s+/gm, '')
|
||||
s = s.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
s = s.replace(/__(.+?)__/g, '$1')
|
||||
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '$1')
|
||||
s = s.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '$1')
|
||||
s = s.replace(/~~(.+?)~~/g, '$1')
|
||||
s = s.replace(/`([^`]+)`/g, '$1')
|
||||
s = s.replace(/^>\s+/gm, '')
|
||||
s = s.replace(/^---$/gm, '')
|
||||
s = s.replace(/^\* /gm, '• ')
|
||||
s = s.replace(/^- /gm, '• ')
|
||||
s = s.replace(/^\d+\.\s/gm, '')
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* 对一行纯文本进行 @人名 / #标签 自动匹配,返回 segments 数组
|
||||
* config: { persons: [{ personId, token, name, label, aliases }], linkTags: [...] }
|
||||
* 点击加好友时须传 persons.token(与 CKB lead 的 targetUserId 一致),不能用 personId。
|
||||
*/
|
||||
function matchLineToSegments(line, config) {
|
||||
if (!config || (!config.persons?.length && !config.linkTags?.length)) {
|
||||
return [{ type: 'text', text: line }]
|
||||
}
|
||||
// 编辑器/系统在 @ 与人名之间插入的普通空格,合并为紧挨 @(避免「找@ 阿浪」无法匹配人名)
|
||||
line = line.replace(/([@@])\s+(?=[\u4e00-\u9fffA-Za-z0-9_\u00b7])/g, '$1')
|
||||
const normalize = s => (s || '').trim().toLowerCase()
|
||||
const personMap = {}
|
||||
const tagMap = {}
|
||||
for (const p of (config.persons || [])) {
|
||||
const token = (p.token || '').trim()
|
||||
if (!token) continue
|
||||
const display = (p.name || p.label || '').trim()
|
||||
const aliasStr = p.aliases != null ? String(p.aliases) : ''
|
||||
const keys = [display, p.label, ...(aliasStr ? aliasStr.split(',') : [])]
|
||||
.map((x) => (x != null ? String(x) : '').trim())
|
||||
.filter(Boolean)
|
||||
.map(normalize)
|
||||
.filter(Boolean)
|
||||
for (const k of keys) {
|
||||
if (!personMap[k]) personMap[k] = p
|
||||
}
|
||||
}
|
||||
for (const t of (config.linkTags || [])) {
|
||||
const keys = [t.label, ...(t.aliases ? t.aliases.split(',') : [])].map(normalize).filter(Boolean)
|
||||
for (const k of keys) { if (!tagMap[k]) tagMap[k] = t }
|
||||
}
|
||||
const esc = n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const personNames = Object.keys(personMap).sort((a, b) => b.length - a.length).map(esc)
|
||||
const tagLabels = Object.keys(tagMap).sort((a, b) => b.length - a.length).map(esc)
|
||||
if (!personNames.length && !tagLabels.length) return [{ type: 'text', text: line }]
|
||||
|
||||
const parts = []
|
||||
if (personNames.length) parts.push('[@@]\\s*(' + personNames.join('|') + ')')
|
||||
if (tagLabels.length) parts.push('[##]\\s*(' + tagLabels.join('|') + ')')
|
||||
const pattern = new RegExp(parts.join('|'), 'gi')
|
||||
|
||||
const segs = []
|
||||
let lastEnd = 0
|
||||
let m
|
||||
while ((m = pattern.exec(line)) !== null) {
|
||||
if (m.index > lastEnd) {
|
||||
segs.push({ type: 'text', text: line.slice(lastEnd, m.index) })
|
||||
}
|
||||
const full = m[0]
|
||||
if (/^[@@]/u.test(full)) {
|
||||
const body = full.replace(/^[@@]\s*/u, '')
|
||||
const person = personMap[normalize(body)]
|
||||
if (person) {
|
||||
const nick = cleanSingleLineField(person.name || person.label || body)
|
||||
const uid = (person.token || '').trim()
|
||||
if (uid) {
|
||||
segs.push({ type: 'mention', userId: uid, nickname: nick, mentionDisplay: '@' + nick })
|
||||
} else {
|
||||
segs.push({ type: 'text', text: full })
|
||||
}
|
||||
} else {
|
||||
segs.push({ type: 'text', text: full })
|
||||
}
|
||||
} else {
|
||||
const body = full.replace(/^[##]\s*/u, '')
|
||||
const tag = tagMap[normalize(body)]
|
||||
if (tag) {
|
||||
segs.push({
|
||||
type: 'linkTag',
|
||||
label: tag.label || body,
|
||||
url: tag.url || '',
|
||||
tagType: tag.type || 'url',
|
||||
pagePath: tag.pagePath || '',
|
||||
tagId: tag.tagId || '',
|
||||
appId: tag.appId || '',
|
||||
mpKey: tag.mpKey || ''
|
||||
})
|
||||
} else {
|
||||
segs.push({ type: 'text', text: full })
|
||||
}
|
||||
}
|
||||
lastEnd = m.index + full.length
|
||||
}
|
||||
if (lastEnd < line.length) {
|
||||
segs.push({ type: 'text', text: line.slice(lastEnd) })
|
||||
}
|
||||
return segs.length ? segs : [{ type: 'text', text: line }]
|
||||
}
|
||||
|
||||
/** 纯文本/Markdown 按行解析 */
|
||||
function parsePlainTextToSegments(text, config) {
|
||||
const cleaned = stripMarkdownFormatting(text)
|
||||
const lines = cleaned.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
||||
const segments = lines.map(line => matchLineToSegments(line, config))
|
||||
return { lines, segments }
|
||||
}
|
||||
|
||||
/** 清理残留的 Markdown 图片引用文本(如 "image.png" ) */
|
||||
function stripOrphanImageRefs(text) {
|
||||
if (!text) return text
|
||||
text = text.replace(/[^\s]*\.(?:png|jpg|jpeg|gif|webp|svg|bmp)!\[[^\]]*\]\([^)]*\)/gi, '')
|
||||
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始内容解析为 contentSegments(用于阅读页展示)
|
||||
* @param {string} rawContent
|
||||
* @param {object} [config] - { persons: [], linkTags: [] }
|
||||
* @returns {{ lines: string[], segments: Array<Array<segment>> }}
|
||||
*/
|
||||
function parseContent(rawContent) {
|
||||
function parseContent(rawContent, config) {
|
||||
if (!rawContent || typeof rawContent !== 'string') {
|
||||
return { lines: [], segments: [] }
|
||||
}
|
||||
if (isHtmlContent(rawContent)) {
|
||||
return parseHtmlToSegments(rawContent)
|
||||
let content = stripOrphanImageRefs(rawContent)
|
||||
if (isHtmlContent(content)) {
|
||||
return parseHtmlToSegments(content, config)
|
||||
}
|
||||
return parsePlainTextToSegments(rawContent)
|
||||
return parsePlainTextToSegments(content, config)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseContent,
|
||||
isHtmlContent
|
||||
isHtmlContent,
|
||||
cleanSingleLineField,
|
||||
}
|
||||
|
||||
@@ -170,10 +170,10 @@ class ReadingTracker {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
|
||||
// 计算本次上报的时长(仅发送增量 delta,后端会累加,避免重复累加导致阅读分钟数异常)
|
||||
// 计算本次上报的时长
|
||||
const now = Date.now()
|
||||
const delta = Math.round((now - this.activeTracker.lastScrollTime) / 1000)
|
||||
this.activeTracker.totalDuration += delta
|
||||
const duration = Math.round((now - this.activeTracker.lastScrollTime) / 1000)
|
||||
this.activeTracker.totalDuration += duration
|
||||
this.activeTracker.lastScrollTime = now
|
||||
|
||||
try {
|
||||
@@ -183,7 +183,7 @@ class ReadingTracker {
|
||||
userId,
|
||||
sectionId: this.activeTracker.sectionId,
|
||||
progress: this.activeTracker.maxProgress,
|
||||
duration: Math.max(0, delta),
|
||||
duration: this.activeTracker.totalDuration,
|
||||
status: this.activeTracker.isCompleted ? 'completed' : 'reading',
|
||||
completedAt: this.activeTracker.completedAt
|
||||
}
|
||||
@@ -246,4 +246,4 @@ class ReadingTracker {
|
||||
|
||||
// 导出单例
|
||||
const readingTracker = new ReadingTracker()
|
||||
export default readingTracker
|
||||
module.exports = readingTracker
|
||||
|
||||
@@ -26,7 +26,8 @@ function getAppInstance() {
|
||||
}
|
||||
|
||||
const RULE_COOLDOWN_KEY = 'rule_engine_cooldown'
|
||||
const COOLDOWN_MS = 60 * 1000
|
||||
// 0 = 关闭冷却(需求:去掉「操作频繁 / N 分钟」类体感限制)
|
||||
const COOLDOWN_MS = 0
|
||||
let _cachedRules = null
|
||||
let _cacheTs = 0
|
||||
const CACHE_TTL = 5 * 60 * 1000
|
||||
@@ -45,6 +46,7 @@ const TRIGGER_SCENE_MAP = {
|
||||
}
|
||||
|
||||
function isInCooldown(ruleId) {
|
||||
if (!COOLDOWN_MS || COOLDOWN_MS <= 0) return false
|
||||
try {
|
||||
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
|
||||
const ts = map[ruleId]
|
||||
@@ -57,6 +59,7 @@ function isInCooldown(ruleId) {
|
||||
}
|
||||
|
||||
function setCooldown(ruleId) {
|
||||
if (!COOLDOWN_MS || COOLDOWN_MS <= 0) return
|
||||
try {
|
||||
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
|
||||
map[ruleId] = Date.now()
|
||||
|
||||
@@ -9,11 +9,11 @@ const app = getApp()
|
||||
*/
|
||||
function trackClick(module, action, target, extra) {
|
||||
const userId = app.globalData.userInfo?.id || ''
|
||||
app.request({
|
||||
url: '/api/miniprogram/track',
|
||||
if (!userId) return
|
||||
app.request('/api/miniprogram/track', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: userId || undefined,
|
||||
userId,
|
||||
action,
|
||||
target,
|
||||
extraData: Object.assign({ module, page: module }, extra || {})
|
||||
|
||||
@@ -32,18 +32,6 @@ const formatMoney = (amount, decimals = 2) => {
|
||||
return Number(amount).toFixed(decimals)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化统计数字:≥1万显示 x.xw,≥1千显示 x.xk,否则原样
|
||||
* @param {number} n - 原始数字
|
||||
* @returns {string}
|
||||
*/
|
||||
const formatStatNum = n => {
|
||||
const num = Number(n) || 0
|
||||
if (num >= 10000) return (num / 10000).toFixed(1).replace(/\.0$/, '') + 'w'
|
||||
if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
|
||||
return String(num)
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (fn, delay = 300) => {
|
||||
let timer = null
|
||||
@@ -163,6 +151,15 @@ const hideLoading = () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
|
||||
// 修复图片 URL 中 protocol 缺少冒号的问题(如 "https//..." → "https://...")
|
||||
const normalizeImageUrl = (url) => {
|
||||
if (!url || typeof url !== 'string') return ''
|
||||
let s = url.trim()
|
||||
if (!s) return ''
|
||||
s = s.replace(/^(https?)\/\//, '$1://')
|
||||
return s
|
||||
}
|
||||
|
||||
// 显示确认框
|
||||
const showConfirm = (title, content) => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -192,7 +189,6 @@ module.exports = {
|
||||
formatTime,
|
||||
formatDate,
|
||||
formatMoney,
|
||||
formatStatNum,
|
||||
formatNumber,
|
||||
debounce,
|
||||
throttle,
|
||||
@@ -201,6 +197,7 @@ module.exports = {
|
||||
isValidWechat,
|
||||
deepClone,
|
||||
getQueryParams,
|
||||
normalizeImageUrl,
|
||||
storage,
|
||||
showToast,
|
||||
showLoading,
|
||||
|
||||
14
new-soul/miniprogram/.gitignore
vendored
Normal file
14
new-soul/miniprogram/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
138
new-soul/miniprogram/README.md
Normal file
138
new-soul/miniprogram/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Soul创业实验 - 微信小程序
|
||||
|
||||
> 一场SOUL的创业实验场 - 来自Soul派对房的真实商业故事
|
||||
|
||||
## 📱 项目简介
|
||||
|
||||
本项目是《一场SOUL的创业实验场》的微信小程序版本,完整还原了Web端的所有UI界面和功能。
|
||||
|
||||
## 🎨 设计特点
|
||||
|
||||
- **主题色**: Soul青色 (#00CED1)
|
||||
- **设计风格**: 深色主题 + 毛玻璃效果
|
||||
- **1:1还原**: 完全复刻Web端的UI设计
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
```
|
||||
miniprogram/
|
||||
├── app.js # 应用入口
|
||||
├── app.json # 应用配置
|
||||
├── app.wxss # 全局样式
|
||||
├── custom-tab-bar/ # 自定义TabBar组件
|
||||
│ ├── index.js
|
||||
│ ├── index.json
|
||||
│ ├── index.wxml
|
||||
│ └── index.wxss
|
||||
├── pages/
|
||||
│ ├── index/ # 首页
|
||||
│ ├── chapters/ # 目录页
|
||||
│ ├── match/ # 找伙伴页
|
||||
│ ├── my/ # 我的页面
|
||||
│ ├── read/ # 阅读页
|
||||
│ ├── about/ # 关于作者
|
||||
│ ├── referral/ # 推广中心
|
||||
│ ├── purchases/ # 订单页
|
||||
│ └── settings/ # 设置页
|
||||
├── utils/
|
||||
│ ├── util.js # 工具函数
|
||||
│ └── payment.js # 支付工具
|
||||
├── assets/
|
||||
│ └── icons/ # 图标资源
|
||||
├── project.config.json # 项目配置
|
||||
└── sitemap.json # 站点地图
|
||||
```
|
||||
|
||||
## 🚀 功能列表
|
||||
|
||||
### 核心功能
|
||||
- ✅ 首页 - 书籍展示、推荐章节、阅读进度
|
||||
- ✅ 目录 - 完整章节列表、篇章折叠展开
|
||||
- ✅ 找伙伴 - 匹配动画、匹配类型选择
|
||||
- ✅ 我的 - 个人信息、订单、推广中心
|
||||
- ✅ 阅读 - 付费墙、章节导航、分享功能
|
||||
|
||||
### 特色功能
|
||||
- ✅ 自定义TabBar(中间突出的找伙伴按钮)
|
||||
- ✅ 阅读进度条
|
||||
- ✅ 匹配动画效果
|
||||
- ✅ 付费墙与购买流程
|
||||
- ✅ 分享海报功能
|
||||
- ✅ 推广佣金系统
|
||||
|
||||
## 🛠 开发指南
|
||||
|
||||
### 环境要求
|
||||
- 微信开发者工具 >= 1.06.2308310
|
||||
- 基础库版本 >= 3.3.4
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **下载微信开发者工具**
|
||||
- 前往 [微信开发者工具下载页面](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
|
||||
|
||||
2. **导入项目**
|
||||
- 打开微信开发者工具
|
||||
- 选择"导入项目"
|
||||
- 项目目录选择 `miniprogram` 文件夹
|
||||
- AppID 使用: `wx432c93e275548671`
|
||||
|
||||
3. **编译运行**
|
||||
- 点击"编译"按钮
|
||||
- 在模拟器中预览效果
|
||||
|
||||
### 真机调试
|
||||
|
||||
1. 点击工具栏的"预览"按钮
|
||||
2. 使用微信扫描二维码
|
||||
3. 在真机上测试所有功能
|
||||
|
||||
## 📝 配置说明
|
||||
|
||||
### API配置
|
||||
在 `app.js` 中修改 `globalData.baseUrl`:
|
||||
|
||||
```javascript
|
||||
globalData: {
|
||||
baseUrl: 'https://soul.ckb.fit', // 你的API地址
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### AppID配置
|
||||
在 `project.config.json` 中修改:
|
||||
|
||||
```json
|
||||
{
|
||||
"appid": "你的小程序AppID"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 上线发布
|
||||
|
||||
1. **准备工作**
|
||||
- 确保所有功能测试通过
|
||||
- 检查API接口是否正常
|
||||
- 确认支付功能已配置
|
||||
|
||||
2. **上传代码**
|
||||
- 在开发者工具中点击"上传"
|
||||
- 填写版本号和项目备注
|
||||
|
||||
3. **提交审核**
|
||||
- 登录[微信公众平台](https://mp.weixin.qq.com)
|
||||
- 进入"版本管理"
|
||||
- 提交审核
|
||||
|
||||
4. **发布上线**
|
||||
- 审核通过后点击"发布"
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- **Web版本**: https://soul.ckb.fit
|
||||
- **作者微信**: 28533368
|
||||
- **技术支持**: 存客宝
|
||||
|
||||
## 📄 版权信息
|
||||
|
||||
© 2024 卡若. All rights reserved.
|
||||
67
new-soul/miniprogram/RESTORE-ANALYSIS.md
Normal file
67
new-soul/miniprogram/RESTORE-ANALYSIS.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# miniprogram 功能还原分析报告
|
||||
|
||||
## 一、对比结论
|
||||
|
||||
| 项目 | miniprogram(甲方) | miniprogram2(你写的) |
|
||||
|------|-------------------|------------------------|
|
||||
| 页面分享 | 仅 read、referral 有 | 几乎所有页面都有 |
|
||||
| scene 解析 | 无 | 有 utils/scene.js |
|
||||
| 推荐码获取 | 分散(userInfo?.referralCode 等) | 统一 app.getMyReferralCode() |
|
||||
| 书籍 API | /api/book/all-chapters | /api/miniprogram/book/all-chapters |
|
||||
| 特有页面 | vip、member-detail | scan、profile-edit |
|
||||
|
||||
## 二、已完成的还原项
|
||||
|
||||
### 1. 基础能力(app.js + utils/scene.js)
|
||||
|
||||
- **新增** `utils/scene.js`:扫码 scene 参数编解码,支持 `mid`、`id`、`ref`
|
||||
- **app.js**:
|
||||
- 引入 `parseScene`,`handleReferralCode` 支持 `options.scene` 解析
|
||||
- 新增 `getMyReferralCode()`:统一获取邀请码
|
||||
- 新增 `getSectionMid(sectionId)`:根据 id 查 mid
|
||||
- `loadBookData` 改为 `/api/miniprogram/book/all-chapters`
|
||||
|
||||
### 2. 页面分享(onShareAppMessage)
|
||||
|
||||
已为以下页面补充分享,路径统一带 `ref` 参数:
|
||||
|
||||
- index、chapters、match、my
|
||||
- read、referral(原有,已统一用 getMyReferralCode)
|
||||
- search、settings、purchases、privacy
|
||||
- withdraw-records、addresses、addresses/edit
|
||||
- agreement、about、vip、member-detail
|
||||
|
||||
### 3. read.js 分享逻辑与 mid 支持
|
||||
|
||||
- 使用 `app.getMyReferralCode()` 替代 `userInfo?.referralCode || wx.getStorageSync('referralCode')`
|
||||
- **mid 支持**:onLoad 支持 `options.scene`、`options.mid`;mid 有值无 id 时从 bookData 或 `/api/miniprogram/book/chapter/by-mid/:mid` 解析 id
|
||||
- 分享 path 优先用 `mid=`(扫码/海报闭环),无则用 `id=`;API 返回 `res.mid` 时写入 `sectionMid`
|
||||
|
||||
### 4. API 路径修正
|
||||
|
||||
- `app.loadBookData`:`/api/book/all-chapters` → `/api/miniprogram/book/all-chapters`
|
||||
- `index.loadBookData`、`loadFeaturedFromServer`、`loadLatestChapters`:同上
|
||||
- `chapters.loadDailyChapters`:同上
|
||||
- **VIP 接口**:`/api/vip/*` → `/api/miniprogram/vip/*`(vip.js、my.js、index.js、member-detail.js)
|
||||
|
||||
## 三、已处理项
|
||||
|
||||
- **vip 相关接口**:已改为 `/api/miniprogram/vip/*`(members、status、profile),符合项目边界。**后端需在 soul-api 的 miniprogram 组下挂对应路由**,可复用现有 handler。
|
||||
- **页面结构**:保留 vip、member-detail,未引入 scan、profile-edit
|
||||
|
||||
## 四、后端已补全(soul-api)
|
||||
|
||||
已在 miniprogram 组下新增以下路由:
|
||||
|
||||
| 路径 | 方法 | 用途 |
|
||||
|------|------|------|
|
||||
| `/api/miniprogram/vip/status` | GET | 查询用户 VIP 状态(按 fullbook/vip 订单判断) |
|
||||
| `/api/miniprogram/vip/profile` | GET/POST | 获取/更新 VIP 资料(映射 users 表 nickname/phone) |
|
||||
| `/api/miniprogram/vip/members` | GET | VIP 会员列表(无 id)或单个(?id=) |
|
||||
| `/api/miniprogram/users` | GET | 用户列表(?limit=)或单个(?id=),首页超级个体、会员详情回退 |
|
||||
|
||||
## 五、后续建议
|
||||
|
||||
1. **soul-api 路由**:确认 `/api/miniprogram/book/all-chapters` 已注册;VIP 接口见「四、后端待办」。
|
||||
2. **referral.js**:已统一使用 `app.getMyReferralCode()` 作为回退。
|
||||
3. **read.js mid 支持**:已完成。若 soul-api 未提供 `/api/miniprogram/book/chapter/by-mid/:mid`,扫码带 mid 时需依赖 bookData 已加载完成;建议后端补充 by-mid 接口。
|
||||
750
new-soul/miniprogram/app.js
Normal file
750
new-soul/miniprogram/app.js
Normal file
@@ -0,0 +1,750 @@
|
||||
/**
|
||||
* Soul创业派对 - 小程序入口
|
||||
* 开发: 卡若
|
||||
*/
|
||||
|
||||
const { parseScene } = require('./utils/scene.js')
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
|
||||
function getRuntimeBootstrapConfig() {
|
||||
try {
|
||||
const extCfg = wx.getExtConfigSync ? (wx.getExtConfigSync() || {}) : {}
|
||||
return {
|
||||
baseUrl: extCfg.apiBaseUrl || wx.getStorageSync('apiBaseUrl') || DEFAULT_BASE_URL,
|
||||
appId: extCfg.appId || DEFAULT_APP_ID,
|
||||
mchId: extCfg.mchId || DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: extCfg.withdrawSubscribeTmplId || DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
} catch (_) {
|
||||
return {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
appId: DEFAULT_APP_ID,
|
||||
mchId: DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapConfig = getRuntimeBootstrapConfig()
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// 运行配置:优先外部配置/缓存,其次默认值
|
||||
baseUrl: bootstrapConfig.baseUrl,
|
||||
appId: bootstrapConfig.appId,
|
||||
|
||||
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
||||
withdrawSubscribeTmplId: bootstrapConfig.withdrawSubscribeTmplId,
|
||||
|
||||
// 微信支付配置
|
||||
mchId: bootstrapConfig.mchId,
|
||||
|
||||
// 用户信息
|
||||
userInfo: null,
|
||||
openId: null, // 微信openId,支付必需
|
||||
isLoggedIn: false,
|
||||
|
||||
// 书籍数据
|
||||
bookData: null,
|
||||
totalSections: 0,
|
||||
supportWechat: '',
|
||||
|
||||
// 购买记录
|
||||
purchasedSections: [],
|
||||
hasFullBook: false,
|
||||
// VIP 会员(365天,包含增值版免费;与 hasFullBook=9.9 买断不同)
|
||||
isVip: false,
|
||||
vipExpireDate: '',
|
||||
|
||||
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
|
||||
readSectionIds: [],
|
||||
|
||||
// 推荐绑定
|
||||
pendingReferralCode: null, // 待绑定的推荐码
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
brandColor: '#00CED1',
|
||||
brandSecondary: '#20B2AA',
|
||||
goldColor: '#FFD700',
|
||||
bgColor: '#000000',
|
||||
cardBg: '#1c1c1e'
|
||||
},
|
||||
|
||||
// 系统信息
|
||||
systemInfo: null,
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// TabBar相关
|
||||
currentTab: 0,
|
||||
|
||||
// 是否处于「单页模式」(如朋友圈文章里的单页预览)
|
||||
// 用于在受限环境下给出引导文案,提示用户点击底部「前往小程序」进入完整体验
|
||||
isSinglePageMode: false,
|
||||
|
||||
// 更新检测:上次检测时间戳,避免频繁请求
|
||||
lastUpdateCheck: 0
|
||||
},
|
||||
|
||||
onLaunch(options) {
|
||||
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
||||
// 获取系统信息
|
||||
this.getSystemInfo()
|
||||
|
||||
// 场景值兜底:1154 为「朋友圈单页模式」进入
|
||||
try {
|
||||
const launchOpts = wx.getLaunchOptionsSync ? wx.getLaunchOptionsSync() : null
|
||||
if (launchOpts && launchOpts.scene === 1154) {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[App] 读取 LaunchOptions 失败:', e)
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
this.loadRuntimeConfig()
|
||||
|
||||
// 加载书籍数据
|
||||
this.loadBookData()
|
||||
|
||||
// 检查更新
|
||||
this.checkUpdate()
|
||||
|
||||
// 处理分享参数(推荐码绑定)
|
||||
this.handleReferralCode(options)
|
||||
},
|
||||
|
||||
// 小程序显示时:处理分享参数、检测更新(从后台切回时)
|
||||
onShow(options) {
|
||||
this.handleReferralCode(options)
|
||||
this.checkUpdate()
|
||||
},
|
||||
|
||||
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
|
||||
handleReferralCode(options) {
|
||||
const query = options?.query || {}
|
||||
let refCode = query.ref || query.referralCode
|
||||
const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || ''
|
||||
if (sceneStr) {
|
||||
const parsed = parseScene(sceneStr)
|
||||
if (parsed.mid) this.globalData.initialSectionMid = parsed.mid
|
||||
if (parsed.id) this.globalData.initialSectionId = parsed.id
|
||||
if (parsed.ref) refCode = parsed.ref
|
||||
}
|
||||
if (refCode) {
|
||||
console.log('[App] 检测到推荐码:', refCode)
|
||||
|
||||
// 立即记录访问(不需要登录,用于统计"通过链接进的人数")
|
||||
this.recordReferralVisit(refCode)
|
||||
|
||||
// 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺)
|
||||
this.globalData.pendingReferralCode = refCode
|
||||
wx.setStorageSync('pendingReferralCode', refCode)
|
||||
// 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
|
||||
wx.setStorageSync('referral_code', refCode)
|
||||
|
||||
// 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
|
||||
this.bindReferralCode(refCode)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 记录推荐访问(不需要登录,用于统计)
|
||||
async recordReferralVisit(refCode) {
|
||||
try {
|
||||
// 获取openId(如果有)
|
||||
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
|
||||
const userId = this.globalData.userInfo?.id || ''
|
||||
|
||||
await this.request('/api/miniprogram/referral/visit', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
referralCode: refCode,
|
||||
visitorOpenId: openId,
|
||||
visitorId: userId,
|
||||
source: 'miniprogram',
|
||||
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
|
||||
},
|
||||
silent: true
|
||||
})
|
||||
console.log('[App] 记录推荐访问成功')
|
||||
} catch (e) {
|
||||
console.log('[App] 记录推荐访问失败:', e.message)
|
||||
// 忽略错误,不影响用户体验
|
||||
}
|
||||
},
|
||||
|
||||
// 绑定推荐码到用户(自己的推荐码不请求接口,避免 400 与控制台报错)
|
||||
async bindReferralCode(refCode) {
|
||||
try {
|
||||
const userId = this.globalData.userInfo?.id
|
||||
if (!userId || !refCode) return
|
||||
|
||||
const myCode = this.getMyReferralCode()
|
||||
if (myCode && this._normalizeReferralCode(refCode) === this._normalizeReferralCode(myCode)) {
|
||||
console.log('[App] 跳过绑定:不能使用自己的推荐码')
|
||||
this.globalData.pendingReferralCode = null
|
||||
wx.removeStorageSync('pendingReferralCode')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
|
||||
|
||||
const res = await this.request('/api/miniprogram/referral/bind', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
referralCode: refCode
|
||||
},
|
||||
silent: true
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
console.log('[App] 推荐码绑定成功')
|
||||
wx.setStorageSync('boundReferralCode', refCode)
|
||||
this.globalData.pendingReferralCode = null
|
||||
wx.removeStorageSync('pendingReferralCode')
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) ? String(e.message) : ''
|
||||
if (msg.indexOf('不能使用自己的推荐码') !== -1) {
|
||||
console.log('[App] 跳过绑定:不能使用自己的推荐码')
|
||||
this.globalData.pendingReferralCode = null
|
||||
wx.removeStorageSync('pendingReferralCode')
|
||||
} else {
|
||||
console.error('[App] 绑定推荐码失败:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 推荐码归一化后比较(忽略大小写、短横线等)
|
||||
_normalizeReferralCode(code) {
|
||||
if (!code || typeof code !== 'string') return ''
|
||||
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
|
||||
},
|
||||
|
||||
// 判断用户资料是否完善(昵称 + 头像)
|
||||
_isProfileIncomplete(user) {
|
||||
if (!user) return true
|
||||
const nickname = (user.nickname || '').trim()
|
||||
const avatar = (user.avatar || '').trim()
|
||||
const isDefaultNickname = !nickname || nickname === '微信用户'
|
||||
const noAvatar = !avatar
|
||||
return isDefaultNickname || noAvatar
|
||||
},
|
||||
|
||||
// 登录后若资料未完善,引导跳转到资料编辑页
|
||||
_ensureProfileCompletedAfterLogin(user) {
|
||||
try {
|
||||
if (!user || !this._isProfileIncomplete(user)) return
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1]
|
||||
// 避免在资料页内重复跳转
|
||||
if (current && current.route === 'pages/profile-edit/profile-edit') return
|
||||
wx.showToast({ title: '请先完善头像和昵称', icon: 'none', duration: 2000 })
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
} catch (e) {
|
||||
console.warn('[App] 跳转资料编辑页失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 根据业务 id 从 bookData 查 mid(用于跳转)
|
||||
getSectionMid(sectionId) {
|
||||
const list = this.globalData.bookData || []
|
||||
const ch = list.find(c => c.id === sectionId)
|
||||
return ch?.mid || 0
|
||||
},
|
||||
|
||||
// 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串)
|
||||
getMyReferralCode() {
|
||||
const user = this.globalData.userInfo
|
||||
if (!user) return ''
|
||||
if (user.referralCode) return user.referralCode
|
||||
if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
|
||||
return ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 自定义导航栏「返回」:有上一页则返回,否则跳转首页(解决从分享进入时点返回无效的问题)
|
||||
*/
|
||||
goBackOrToHome() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length <= 1) {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
} else {
|
||||
wx.navigateBack()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
try {
|
||||
const systemInfo = wx.getSystemInfoSync()
|
||||
this.globalData.systemInfo = systemInfo
|
||||
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
|
||||
|
||||
// 微信在单页模式下会在 systemInfo.mode 标记 singlePage
|
||||
if (systemInfo.mode === 'singlePage') {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
|
||||
// 计算导航栏高度
|
||||
const menuButton = wx.getMenuButtonBoundingClientRect()
|
||||
if (menuButton) {
|
||||
this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取系统信息失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 若当前处于朋友圈等「单页模式」,在尝试登录/购买前给用户友好提示,
|
||||
* 引导用户点击底部「前往小程序」进入完整小程序再操作。
|
||||
* 返回 false 表示应中断当前操作。
|
||||
*/
|
||||
ensureFullAppForAuth() {
|
||||
// 每次调用时再做一次兜底检测,避免全局标记遗漏
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
if (sys && sys.mode === 'singlePage') {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[App] ensureFullAppForAuth getSystemInfoSync error:', e)
|
||||
}
|
||||
|
||||
if (!this.globalData.isSinglePageMode) return true
|
||||
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为朋友圈单页,仅支持部分浏览。如需登录和解锁内容,请点击底部「前往小程序」后再操作。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
try {
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
if (userInfo && token) {
|
||||
this.globalData.userInfo = userInfo
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = userInfo.purchasedSections || []
|
||||
this.globalData.hasFullBook = userInfo.hasFullBook || false
|
||||
this.globalData.isVip = userInfo.isVip || false
|
||||
this.globalData.vipExpireDate = userInfo.vipExpireDate || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查登录状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadRuntimeConfig() {
|
||||
try {
|
||||
const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
|
||||
const mpConfig = res?.mpConfig || {}
|
||||
this.globalData.baseUrl = mpConfig.apiDomain || this.globalData.baseUrl
|
||||
this.globalData.appId = mpConfig.appId || this.globalData.appId
|
||||
this.globalData.mchId = mpConfig.mchId || this.globalData.mchId
|
||||
this.globalData.withdrawSubscribeTmplId = mpConfig.withdrawSubscribeTmplId || this.globalData.withdrawSubscribeTmplId
|
||||
this.globalData.supportWechat = mpConfig.supportWechat || mpConfig.customerWechat || mpConfig.serviceWechat || ''
|
||||
try {
|
||||
wx.setStorageSync('apiBaseUrl', this.globalData.baseUrl)
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
console.warn('[App] 加载运行配置失败,继续使用默认配置:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载书籍数据
|
||||
async loadBookData() {
|
||||
try {
|
||||
// 先从缓存加载
|
||||
const cachedData = wx.getStorageSync('bookData')
|
||||
if (cachedData) {
|
||||
this.globalData.bookData = cachedData
|
||||
}
|
||||
|
||||
// 从服务器获取最新数据
|
||||
const res = await this.request('/api/miniprogram/book/all-chapters')
|
||||
if (res && (res.data || res.chapters)) {
|
||||
const chapters = res.data || res.chapters || []
|
||||
this.globalData.bookData = chapters
|
||||
this.globalData.totalSections = res.total || chapters.length || 0
|
||||
wx.setStorageSync('bookData', chapters)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 小程序更新检测(基于 wx.getUpdateManager)
|
||||
* - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求)
|
||||
*/
|
||||
checkUpdate() {
|
||||
try {
|
||||
if (!wx.canIUse('getUpdateManager')) return
|
||||
const now = Date.now()
|
||||
const lastCheck = this.globalData.lastUpdateCheck || 0
|
||||
if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测
|
||||
this.globalData.lastUpdateCheck = now
|
||||
|
||||
const updateManager = wx.getUpdateManager()
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
if (res.hasUpdate) {
|
||||
console.log('[App] 发现新版本,正在下载...')
|
||||
}
|
||||
})
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已准备好,重启后即可使用',
|
||||
confirmText: '立即重启',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
updateManager.onUpdateFailed(() => {
|
||||
wx.showToast({
|
||||
title: '更新失败,请稍后重试',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[App] checkUpdate failed:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段)
|
||||
*/
|
||||
_getApiErrorMsg(data, defaultMsg = '请求失败') {
|
||||
if (!data || typeof data !== 'object') return defaultMsg
|
||||
const msg = data.message || data.error
|
||||
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
|
||||
},
|
||||
|
||||
/**
|
||||
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
|
||||
* @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
|
||||
* @param {object} options - { method, data, header, silent }
|
||||
* @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计)
|
||||
*/
|
||||
request(urlOrOptions, options = {}) {
|
||||
let url
|
||||
if (typeof urlOrOptions === 'string') {
|
||||
url = urlOrOptions
|
||||
} else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) {
|
||||
url = urlOrOptions.url
|
||||
options = { ...urlOrOptions, url: undefined }
|
||||
} else {
|
||||
url = ''
|
||||
}
|
||||
const silent = !!options.silent
|
||||
const showError = (msg) => {
|
||||
if (!silent && msg) {
|
||||
wx.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
wx.request({
|
||||
url: this.globalData.baseUrl + url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || 15000,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
...options.header
|
||||
},
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
if (res.statusCode === 200) {
|
||||
// 业务失败:success === false,soul-api 用 message 或 error 返回原因
|
||||
if (data && data.success === false) {
|
||||
const msg = this._getApiErrorMsg(data, '操作失败')
|
||||
// 登录态不一致:本地有 token/userInfo,但后端查不到该用户
|
||||
// 典型原因:切换环境(baseUrl)、换库/清库、用户被删除、token 与用户不匹配
|
||||
if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
|
||||
this.logout()
|
||||
}
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 401) {
|
||||
this.logout()
|
||||
showError('未授权,请重新登录')
|
||||
reject(new Error('未授权'))
|
||||
return
|
||||
}
|
||||
// 4xx/5xx:优先用返回体的 message/error
|
||||
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
},
|
||||
fail: (err) => {
|
||||
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
|
||||
async login() {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes || !loginRes.code) {
|
||||
console.warn('[App] wx.login 未返回 code')
|
||||
wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const res = await this.request('/api/miniprogram/login', {
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 保存openId
|
||||
if (res.data.openId) {
|
||||
this.globalData.openId = res.data.openId
|
||||
wx.setStorageSync('openId', res.data.openId)
|
||||
console.log('[App] 获取openId成功')
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
if (res.data.user) {
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token || '')
|
||||
|
||||
// 登录成功后,检查待绑定的推荐码并执行绑定
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
console.log('[App] 登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
}
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.log('[App] API登录失败:', apiError.message)
|
||||
// 不使用模拟登录,提示用户网络问题
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (e) {
|
||||
console.error('[App] 登录失败:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 获取openId (支付必需)
|
||||
async getOpenId() {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
// 先检查缓存
|
||||
const cachedOpenId = wx.getStorageSync('openId')
|
||||
if (cachedOpenId) {
|
||||
this.globalData.openId = cachedOpenId
|
||||
return cachedOpenId
|
||||
}
|
||||
|
||||
// 没有缓存则登录获取
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
|
||||
const res = await this.request('/api/miniprogram/login', {
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code }
|
||||
})
|
||||
|
||||
if (res.success && res.data?.openId) {
|
||||
this.globalData.openId = res.data.openId
|
||||
wx.setStorageSync('openId', res.data.openId)
|
||||
// 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
|
||||
if (res.data.user) {
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token || '')
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
}
|
||||
return res.data.openId
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[App] 获取openId失败:', e)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
|
||||
async loginWithPhone(phoneCode) {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes.code) {
|
||||
wx.showToast({ title: '获取登录态失败', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
const res = await this.request('/api/miniprogram/phone-login', {
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code, phoneCode }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
|
||||
// 登录成功后绑定推荐码
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[App] 手机号登录失败:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout() {
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.isLoggedIn = false
|
||||
this.globalData.purchasedSections = []
|
||||
this.globalData.hasFullBook = false
|
||||
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.removeStorageSync('token')
|
||||
},
|
||||
|
||||
// 检查是否已购买章节
|
||||
hasPurchased(sectionId) {
|
||||
if (this.globalData.hasFullBook) return true
|
||||
return this.globalData.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
// 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计)
|
||||
markSectionAsRead(sectionId) {
|
||||
if (!sectionId) return
|
||||
const list = this.globalData.readSectionIds || []
|
||||
if (list.includes(sectionId)) return
|
||||
list.push(sectionId)
|
||||
this.globalData.readSectionIds = list
|
||||
wx.setStorageSync('readSectionIds', list)
|
||||
},
|
||||
|
||||
// 已读章节数(用于首页展示)
|
||||
getReadCount() {
|
||||
return (this.globalData.readSectionIds || []).length
|
||||
},
|
||||
|
||||
// 获取章节总数
|
||||
getTotalSections() {
|
||||
return this.globalData.totalSections
|
||||
},
|
||||
|
||||
// 切换TabBar
|
||||
switchTab(index) {
|
||||
this.globalData.currentTab = index
|
||||
},
|
||||
|
||||
// 显示Toast
|
||||
showToast(title, icon = 'none') {
|
||||
wx.showToast({
|
||||
title,
|
||||
icon,
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示Loading
|
||||
showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏Loading
|
||||
hideLoading() {
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
71
new-soul/miniprogram/app.json
Normal file
71
new-soul/miniprogram/app.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/chapters/chapters",
|
||||
"pages/index/index",
|
||||
"pages/match/match",
|
||||
"pages/my/my",
|
||||
"pages/read/read",
|
||||
"pages/link-preview/link-preview",
|
||||
"pages/about/about",
|
||||
"pages/agreement/agreement",
|
||||
"pages/privacy/privacy",
|
||||
"pages/referral/referral",
|
||||
"pages/purchases/purchases",
|
||||
"pages/settings/settings",
|
||||
"pages/search/search",
|
||||
"pages/addresses/addresses",
|
||||
"pages/addresses/edit",
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail",
|
||||
"pages/mentors/mentors",
|
||||
"pages/mentor-detail/mentor-detail",
|
||||
"pages/profile-show/profile-show",
|
||||
"pages/profile-edit/profile-edit",
|
||||
"pages/wallet/wallet"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTitleText": "Soul创业派对",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000",
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#8e8e93",
|
||||
"selectedColor": "#00CED1",
|
||||
"backgroundColor": "#1c1c1e",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/chapters/chapters",
|
||||
"text": "目录"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/match/match",
|
||||
"text": "找伙伴"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/my",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"navigateToMiniProgramAppIdList": [
|
||||
"wx6489c26045912fe1",
|
||||
"wx3d15ed02e98b04e3"
|
||||
],
|
||||
"__usePrivacyCheck__": true,
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user