现在有的接口对接测试完成

This commit is contained in:
2026-07-25 19:19:36 +08:00
parent 8870136d1b
commit 817d85e117
59 changed files with 26623 additions and 1576 deletions
+112 -63
View File
@@ -1,20 +1,21 @@
# PC 接口对接规划(Apifox 79 接口版)
# PC 接口对接规划(Apifox 85 接口版)
> 状态:当前范围已确认
> 日期:2026-07-24
> 当前阶段:已直接在 Apifox 核验验证码中心、认证登录、文件上传、行政区划、家族圈、字辈谱和世系人物共 57 条接口;施工只以这些 PC 详情为准,其余目录继续逐组核验后再接入
> 日期:2026-07-25
> 当前阶段:桌面版 Apifox 的 PC 目录为 85 条,已逐条在 PC 详情复核并与客户端映射比对。后续仅在该目录出现新增或变更时,按 PC 详情重新核验;不据旧记录猜测施工
> 交接入口:[交接文档.md](交接文档.md)
## 1. 已确认的基线
1. **Apifox 的 PC 目录是 PC 前端接口的唯一正式契约源。**
2. 当前 Apifox PC 共 79 个操作:
2. 当前 Apifox PC 共 85 个操作:
| 分组 | 数量 |
| --- | ---: |
| 认证登录 | 11 |
| 验证中心 | 4 |
| 文件上传 | 6 |
| 认证登录 | 12 |
| 验证中心 | 3 |
| 文件上传 | 3 |
| 家谱 | 1 |
| 行政区划 | 4 |
| 家族圈 | 14 |
| 字辈谱 | 6 |
@@ -22,9 +23,11 @@
| 内容文章 | 1 |
| 相册 | 2 |
| 视频 | 1 |
| 贺礼邀约 | 4 |
| 祭祀 | 2 |
| 族务记录 | 16 |
| **合计** | **79** |
| 消息通知 | 4 |
| **合计** | **85** |
3. 仓库中的旧 OpenAPI 文件不再作为新增功能依据。Apifox 客户端中实时的 PC 目录是最终验收源;导出文件最多作为本地自动测试快照,不能替代在 Apifox 中逐项核对。
4. APP 接口不允许在 PC 页面中直接复用。PC 缺少的能力应先补入 Apifox 的 PC 目录,再开发页面。
@@ -33,7 +36,7 @@
## 2. 当前最重要的结论
现有 79 个接口不能覆盖整个 PC 管理端闭环,主要问题不是前端页面,而是接口目录不完整:
现有 85 个接口不能覆盖整个 PC 管理端闭环,主要问题不是前端页面,而是接口目录不完整:
- 没有“我的家谱、家谱详情、创建/加入家谱、家谱成员”接口,PC 无法自行取得真实 `genealogyId`
- 内容文章只有删除接口。
@@ -46,13 +49,13 @@
因此对接分成三类:
1. **当前可以形成可用流程:**认证、验证、文件上传、区划、家族圈、字辈谱、世系人物成长记录、亲友往来、备忘录其中家谱内接口必须先由 PC 运行入口提供真实 `genealogyId`
1. **当前可以形成可用流程:**认证、验证、文件上传、区划、家族圈、字辈谱、世系人物,以及成长记录、亲友往来、备忘录的新增;三类列表仅能按未展开 DTO 原样展示。其中家谱内接口必须先由 PC 运行入口提供真实 `genealogyId`
2. **当前只做契约映射、不开放页面操作:**文章、相册、视频、祭祀和功德录目前只有删除接口,不能在没有真实列表和详情数据时单独启用删除。
3. **后端补充 PC 接口后再规划:**家谱上下文、成员,以及上述资源缺少的读写操作、世系关系解除和字辈删除/停用。
### 2.1 当前范围规则
- 当前只分析和对接 Apifox PC 中已经存在的 79 个操作。
- 当前只分析和对接 Apifox PC 中已经存在的 85 个操作。
- 不读取 APP 接口补 PC 缺口,不把 `/genealogy/app` 改前缀后用于 PC。
- 后端公开文档中存在、但 Apifox PC 当前没有的接口,不进入本轮对接;也不以导出文件缺失为由跳过 Apifox 中已有的接口。
- PC 当前缺少的业务接口只登记为后端待办,相关页面继续显示待开发状态。
@@ -71,13 +74,15 @@
所有家谱内业务必须使用真实 `genealogyId`
1. 当前 79 个接口没有“我的家谱”入口,本轮不从 APP 获取 `genealogyId`
1. 当前 85 个接口没有“我的家谱”入口;新增的家谱配额接口也不能提供真实 `genealogyId`,本轮不从 APP 获取
2. 当前家谱业务页面只接受 PC 运行入口通过 URL 传入的真实 `genealogyId`
3. `profile-common.js` 作为 PC 页面家谱上下文的唯一 owner,负责读取、校验和传播 `genealogyId`
4. `ApiClient` 负责把 `genealogyId` 放入路径。
5. 页面缺少 `genealogyId` 时阻止请求并显示“等待家谱入口接口”,不使用示例编号。
后端补充 PC 家谱入口接口后,再规划“我的家谱 → 选择家谱 → 进入业务页面”的完整导航闭环
当前 `GET /genealogy/pc/genealogies/quota` 已映射为 `genealogyQuota()`;它只返回创建/加入家谱的已用数量、上限、剩余和 `canCreate`/`canJoin`,不能作为家谱列表、详情、创建或加入接口,也不能生成或替代真实 `genealogyId`
5. 页面缺少 `genealogyId` 时,统一先跳转 `profile-families.html`;该入口页只读取配额并说明 PC 尚不能选择家谱,阻止业务请求且不使用示例编号。
个人中心、内容发布和家谱内导航已统一指向该入口页;后端补充 PC 家谱入口接口后,再由入口页实现“我的家谱 → 选择家谱 → 进入业务页面”的完整导航闭环。
### 3.3 请求头与登录状态
@@ -162,16 +167,16 @@ Apifox 还必须正式声明 Bearer security scheme、`Authorization` 和 `clien
替换或删除业务对象后解除旧文件引用。上传成功但业务保存失败时,也要提供清理策略。
## 4. 79 个接口的页面用途
## 4. 已完成施工接口与当前迁移
### 4.1 认证登录(11
### 4.1 认证登录(当前目录 12
| 接口 | 使用页面 | 用途 |
| --- | --- | --- |
| `POST /genealogy/pc/auth/register` | `register.html` | 用户注册 |
| `POST /genealogy/pc/auth/login` | `login.html` | 密码登录 |
| `POST /genealogy/pc/auth/login/sms` | `login.html` | 短信登录 |
| `POST /genealogy/pc/auth/sms/code` | 短信登录、注册、找回密码、换绑手机号、注销账号 | 发送短信验证码 |
| `POST /genealogy/pc/auth/sms/{operationCode}/code` | 短信登录、注册、找回密码、换绑手机号、注销账号 | 按认证动作发送短信验证码 |
| `GET /genealogy/pc/auth/profile` | `profile.html``profile-data.html``profile-security.html` | 获取当前用户资料/已绑定手机号 |
| `PUT /genealogy/pc/auth/profile` | `profile-data.html` | 修改个人资料 |
| `PUT /genealogy/pc/auth/password` | `profile-security.html` | 修改密码 |
@@ -180,51 +185,50 @@ Apifox 还必须正式声明 Bearer security scheme、`Authorization` 和 `clien
| `POST /genealogy/pc/auth/account/deactivate` | `profile-security.html` | 注销账号 |
| `DELETE /genealogy/pc/auth/logout` | 所有登录后页面 | 退出登录 |
状态:第一轮施工已接入密码登录、短信登录、注册、找回密码、换绑手机号、注销和退出。注册必须先取得短信码;找回密码由 `ApiClient` 统一补齐 `grantType``tenantId``clientId`。Apifox 的 `LoginVo` 明确允许 `token``accessToken``tokenValue` 三种响应字段,客户端按该顺序读取第一个非空值;资料响应未给出字段 Schema,页面只读取 PC 页面已使用的明确字段,不增加 APP 字段猜测。
状态:当前 PC 认证登录目录 12 条已逐条复核:第一轮已接入密码登录、短信登录、注册、找回密码、换绑手机号、注销和退出;本轮已把短信发送迁移到当前 PC 的 `operationCode` 路径。密码登录也先按 `password-login` 查询验证策略,策略要求时完成 TAC 后提交返回的 `validToken``ApiClient` 只补 DTO 明确要求的 `grantType``tenantId``clientid` 仅由请求头统一注入,body 不得传 `clientId`。目录内两条短信发送定义为相同路径和契约。资料响应未给出字段 Schema,页面只读取 PC 页面已使用的明确字段,不增加 APP 字段猜测。
已在 Apifox 直接核验的认证约束:
- 发送短信验证码只允许 `PC_SMS_LOGIN``PC_REGISTER``PC_FORGOT_PASSWORD``PC_PHONE_CHANGE``PC_ACCOUNT_DEACTIVATE` 五个场景
- 发送短信码请求体为 `clientId``grantType``tenantId``sceneCode``phone``validToken``validToken` 来自验证中心且只能单次消费。
- 换绑手机号请求体为 `clientId``phone``smsCode`;注销账号请求体为 `clientId``smsCode`。二者不附带 `tenantId`
- 密码登录注册使用 `grantType: "password"`;短信登录与发送短信码使用 `grantType: "sms"`;所有密码字段均为 32 位 MD5。
- 发送短信验证码使用路径 `operationCode``sms-login``register``forgot-password``phone-change``account-deactivate`
- 发送短信码请求体为 `grantType``tenantId``phone``validToken``validToken` 来自验证中心且只能单次消费。不得传 `sceneCode` 或 body `clientId`
- 换绑手机号请求体为 `phone``smsCode`;注销账号请求体为 `smsCode`。二者不附带 `tenantId``clientid` 仅通过请求头传递
- 密码登录注册与找回密码使用 `grantType: "password"`;短信登录与发送短信码使用 `grantType: "sms"`;所有密码字段均为 32 位 MD5。密码登录的 `validToken``password-login` 验证策略决定,有策略要求时随本次登录提交。上述认证 DTO 都禁止 body `clientId`
第一批页面与接口顺序:
| 页面 | 页面流程 | 对接接口 |
| --- | --- | --- |
| `login.html` | 密码登录 | `POST /genealogy/pc/auth/login` |
| `login.html` | 获取短信码 → 短信登录 | `POST /genealogy/pc/auth/sms/code``POST /genealogy/pc/auth/login/sms` |
| `register.html` | 获取短信码 → 注册 | `POST /genealogy/pc/auth/sms/code``POST /genealogy/pc/auth/register` |
| `forgot-password.html` | 获取短信码 → 重设密码 | `POST /genealogy/pc/auth/sms/code``PUT /genealogy/pc/auth/password/reset` |
| `login.html` | 获取短信码 → 短信登录 | `POST /genealogy/pc/auth/sms/sms-login/code``POST /genealogy/pc/auth/login/sms` |
| `register.html` | 获取短信码 → 注册 | `POST /genealogy/pc/auth/sms/register/code``POST /genealogy/pc/auth/register` |
| `forgot-password.html` | 获取短信码 → 重设密码 | `POST /genealogy/pc/auth/sms/forgot-password/code``PUT /genealogy/pc/auth/password/reset` |
| `profile-security.html` | 修改密码 | `PUT /genealogy/pc/auth/password` |
| `profile-security.html` | 获取短信码 → 换绑手机号 | `POST /genealogy/pc/auth/sms/code``PUT /genealogy/pc/auth/phone` |
| `profile-security.html` | 读取绑定手机号 → 获取短信码 → 注销 | `GET /genealogy/pc/auth/profile``POST /genealogy/pc/auth/sms/code``POST /genealogy/pc/auth/account/deactivate` |
| `profile-security.html` | 获取短信码 → 换绑手机号 | `POST /genealogy/pc/auth/sms/phone-change/code``PUT /genealogy/pc/auth/phone` |
| `profile-security.html` | 读取绑定手机号 → 获取短信码 → 注销 | `GET /genealogy/pc/auth/profile``POST /genealogy/pc/auth/sms/account-deactivate/code``POST /genealogy/pc/auth/account/deactivate` |
| 所有登录后页面 | 退出登录 | `DELETE /genealogy/pc/auth/logout` |
### 4.2 验证中心(4
### 4.2 验证中心(当前目录 3
| 接口 | 使用位置 | 用途 |
| --- | --- | --- |
| `GET /captcha/require` | 发送登录、注册、找回、换绑、注销短信前 | 判断当前场景是否需要验证 |
| `POST /captcha/challenge` | 同上 | 获取滑块或图形验证挑战 |
| `POST /captcha/verify` | 同上 | 校验挑战并换取 `validToken` |
| `GET /auth/code` | 仅兼容旧图形验证码 | 旧验证方案 |
| `GET /genealogy/pc/auth/verification/{operationCode}/require` | 密码登录及发送短信登录、注册、找回、换绑、注销短信前 | 判断当前认证动作是否需要验证 |
| `POST /genealogy/pc/auth/verification/{operationCode}/challenge` | 同上 | 获取滑块或图形验证挑战 |
| `POST /genealogy/pc/auth/verification/{operationCode}/verify` | 同上 | 校验挑战并换取 `validToken` |
已在 Apifox 直接核验的验证码约束:
- `GET /captcha/require` 使用 `tenantId``clientId``sceneCode``subject` 查询场景策略
- `POST /captcha/challenge` 必须携带同一组 `tenantId``clientId``sceneCode``subject`;天爱策略返回行为验证数据,系统图形策略返回 `uuid``img`
- `POST /captcha/verify` 除上述字段外还必须携带 `challengeId`,并以 `providerCode``captchaType``payload` 提交验证结果;成功后取得 `validToken`
- `validToken` 与租户、PC 客户端、场景、手机号主体、挑战和请求 IP 绑定,只能随发送短信码接口单次提交。
- `operationCode` 只允许 `password-login``sms-login``register``forgot-password``phone-change``account-deactivate`。服务端按它解析激活场景,前端不得传 `sceneCode`
- `require` query 为必填 `tenantId` 和可选 `subject``challenge` body 为必填 `tenantId``subject``verify` 增加必填 `challengeId`,可提交 `providerCode``captchaType``payload``clientid` 只从请求头读取,query/body 不得传 `clientId`
- `validToken` 与租户、PC 客户端、认证动作、手机号主体、挑战和请求 IP 绑定。它只用于触发它的当前认证动作:发送短信码时随发送请求提交,密码登录被策略要求时随本次登录提交
规划:
- 新流程统一使用 `/captcha/*` 三个接口
- `/auth/code` 标记为兼容接口,确定所有场景迁移完成后从 Apifox 和代码一起删除
- `validToken` 只用于发送短信验证码的请求;发送完成后立即从表单清除,不写日志、不长期存储,也不混入登录、注册、重置或换绑的业务 DTO。
- 新流程统一使用 `/genealogy/pc/auth/verification/{operationCode}/*` 三个接口;页面通过 `ApiClient` 的业务方法和 URL helper 驱动 TAC,不拼路径
- `validToken` 不长期存储或写日志;发送短信码或密码登录完成后立即从表单清除。页面只会在验证中心对该认证动作明确要求时,将本次返回的值混入对应请求 DTO
### 4.3 文件上传(6
### 4.3 文件上传(当前目录 3
> 下表是 2026-07-24 的 6 条历史映射。当前桌面版目录只显示 3 条,且三条详情已复核;表中未列的旧单文件上传、文件引用路径已从 `ApiClient` 删除。
| 接口 | 使用位置 | 用途 |
| --- | --- | --- |
@@ -235,7 +239,7 @@ Apifox 还必须正式声明 Bearer security scheme、`Authorization` 和 `clien
| `POST /genealogy/pc/files/reference` | 所有业务保存成功后 | 绑定业务引用 |
| `DELETE /genealogy/pc/files/reference` | 替换或删除业务文件后 | 解除业务引用 |
状态:6 个文件接口已直接在 Apifox 核验,`ApiClient` 的 PC 路径、认证头和请求形状一致。单文件上传只提交 multipart `file`,返回 `data.ossId``url``thumbnailUrl``fileName``originalName``profile-data.html` 使用它回填 `avatarOssId`,再随 `PUT /genealogy/pc/auth/profile` 保存
状态:当前三条均为分片初始化、上传分片、完成分片。初始化要求 `fileName``fileSize``fileMd5``chunkSize``totalChunks`,可附 `contentType``bizType``usageScene`;分片为 multipart 的 `uploadId``chunkIndex``chunkMd5``file`;完成要求 `uploadId``fileMd5``fileSize`。所有文件先初始化,普通小文件可根据 `instant=true` 直接使用 OSS 信息,但初始化响应 Schema 未展开,页面不猜测 `uploadId`/`ossId`/`instant`,头像上传保持阻止
已核验的分片/引用约束:
@@ -284,7 +288,7 @@ Apifox 还必须正式声明 Bearer security scheme、`Authorization` 和 `clien
| `GET .../comments/{commentId}/replies` | 动态详情 | 直接回复列表 |
| `GET .../comments/{commentId}/replies/page` | 动态详情 | 直接回复分页 |
状态:14 个操作已在 Apifox PC 详情中核验并完成客户端路径映射`profile-feed.html` 已接入动态分页、点赞、一级评论、发表回复和按需展开直接回复;评论及回复 ID 在路径中按字符串传递。
状态:14 个操作已在本轮 Apifox PC 详情中逐条复核,现有 `ApiClient` 路径、分页 query 和页面调用均一致`profile-feed.html` 已接入动态分页、点赞、一级评论、发表回复和按需展开直接回复;评论及回复 ID 在路径中按字符串传递。
已核验的评论约束:
@@ -372,6 +376,8 @@ DELETE /genealogy/pc/genealogies/{genealogyId}/articles/{articleId}
- 发布、下线和公开可见性
- 官网公开文章详情
状态:删除操作已核验并映射为 `ApiClient.deleteArticle(genealogyId, articleId)`。两个路径参数均为必填 int64,登录和 `clientid` 必填,响应是 `VoidResult`;没有列表、详情或稳定 `articleId` 来源,页面不开放删除。
规划:接口补齐前,`profile-article.html``profile-article-edit.html``article-detail.html` 保持设计预览,不单独接入删除接口。
### 4.9 相册(2
@@ -387,6 +393,8 @@ DELETE /genealogy/pc/genealogies/{genealogyId}/albums/{albumId}/photos/{photoId}
当前缺少相册列表、详情、新增、修改,以及照片列表、新增、修改和文件引用。接口补齐前不启用删除按钮。
状态:两个删除操作已核验并映射为 `deleteAlbum(genealogyId, albumId)``deleteAlbumPhoto(genealogyId, albumId, photoId)`。所有路径 ID 是必填 int64,登录和 `clientid` 必填,响应是 `VoidResult`;删除相册会由后端逻辑删除相册及照片,并释放封面和照片文件引用。没有列表/详情或稳定 ID 来源,页面不开放删除。
### 4.10 视频(1
现有接口:
@@ -399,6 +407,8 @@ DELETE /genealogy/pc/genealogies/{genealogyId}/videos/{videoId}
当前缺少视频列表、详情、新增、修改、发布状态、公开播放详情以及文件引用。本轮只映射删除契约;后端补齐 PC 读写接口后,再结合分片上传实施完整视频流程。
状态:删除操作已核验并映射为 `deleteVideo(genealogyId, videoId)`。两个路径参数是必填 int64,登录和 `clientid` 必填,响应是 `VoidResult`;后端逻辑删除视频并释放视频和封面文件引用。没有列表/详情或稳定 ID 来源,页面不开放删除。
### 4.11 祭祀/典礼(2
现有接口:
@@ -412,12 +422,36 @@ DELETE /genealogy/pc/genealogies/{genealogyId}/ceremonies/{ceremonyId}/gifts/{gi
当前缺少活动列表、详情、新增、修改,以及献礼列表和新增。接口补齐前不启用删除。
状态:两个删除操作已核验并映射为 `deleteCeremony(genealogyId, ceremonyId)``deleteCeremonyGift(genealogyId, ceremonyId, giftId)`。所有路径 ID 是必填 int64,登录和 `clientid` 必填,响应是 `VoidResult`;删除祭祀活动会由后端逻辑删除活动及祭品,并释放活动封面文件引用。没有列表/详情或稳定 ID 来源,页面不开放删除。
命名需先拍板:
- 如果只处理祖先祭祀,DTO 和页面只保留祭祀类型。
- 如果还处理婚礼、生日、升学等活动,Apifox 目录应改为“典礼/贺礼”,避免接口名称和产品含义不一致。
### 4.12 族务记录(16
### 4.12 贺礼邀约(4
```text
PUT /genealogy/pc/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitees
GET /genealogy/pc/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitations
PUT /genealogy/pc/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitations/me
GET /genealogy/pc/genealogies/ceremony-invitations/mine
```
状态:四条已逐条核验并映射为 `replaceCeremonyInvitees``ceremonyInvitations``respondCeremonyInvitation``myCeremonyInvitations`。替换受邀人只接受必填 `inviteeUserIds`(空数组取消全部待响应邀请);当前用户响应只接受 `inviteStatus: ACCEPTED|DECLINED`。前三条需要真实 `genealogyId``ceremonyId`,最后一条只查询当前用户。当前仍没有活动列表、创建、详情或献礼接口,`profile-gift.html` / `profile-gift-edit.html` 不开启操作。
### 4.13 消息通知(4
```text
GET /genealogy/pc/notifications?readStatus=0|1
GET /genealogy/pc/notifications/unread-count
POST /genealogy/pc/notifications/{notificationId}/read
POST /genealogy/pc/notifications/read-all
```
状态:四条已逐条核验并映射为 `notifications``unreadNotificationCount``markNotificationRead``markAllNotificationsRead`。列表的元素 DTO 在当前 PC 详情未展开,不能假设通知标题、正文、时间或 `notificationId` 的响应字段。`profile-messages.html` 因此只安全展示原始记录,并开放无歧义的未读数、刷新和全部已读;单条已读继续等待列表元素 Schema。
### 4.14 族务记录(16
#### 成长记录(5
@@ -433,6 +467,14 @@ DELETE .../growth-records/{recordId}
用途:列表、新增、详情、编辑、删除,可关联世系人物和附件。
状态:5 个操作均已在 Apifox PC 详情中逐条核验,`ApiClient` 已映射为 `growthRecords``createGrowthRecord``growthRecordDetail``updateGrowthRecord``deleteGrowthRecord`。全部需要登录,使用必填 `clientid` 请求头;`genealogyId``recordId` 都是 int64 路径参数。
已核验的请求/响应约束:
- 新增和修改使用 `GrowthRecordBody``recordTitle` 必填;`lineagePersonId``recordType``recordContent``recordDate``remindTime``mediaOssIds``sortOrder``status` 可选。`mediaOssIds` 为英文逗号分隔的正整数 OSS ID。
- 列表响应是 `ListResult`,但元素 DTO 未在 Apifox 展开;详情、新增和修改是元素 DTO 未展开的 `ObjectResult`,删除是 `VoidResult`。因此不能假设响应含有 `recordId`、标题、权限或文件引用字段。
- `profile-growth.html` 已请求列表并安全地原样展示数组元素;`profile-growth-edit.html` 已开放新增和写入防重。因为没有可安全使用的响应 ID 或详情字段,详情、编辑和删除入口保持关闭,等待 Apifox 补齐响应 DTO。
#### 亲友往来(5
```text
@@ -445,6 +487,8 @@ DELETE .../relative-records/{relativeId}
当前没有独立页面。现有 `profile-memo.html` 同时写了“人情往来”和“备忘提醒”,会导致两个资源边界混乱。
状态:5 个操作已在 Apifox PC 详情中逐条核验,`ApiClient` 已映射为 `relativeRecords``createRelativeRecord``relativeRecordDetail``updateRelativeRecord``deleteRelativeRecord`;全部需要登录和必填 `clientid` 请求头。`RelativeRecordBody``relativeName` 必填,`relationName``eventName``eventTime``giftAmount``recordContent``mediaOssIds``sortOrder``status` 可选。列表/详情元素 DTO 未展开,故新增 `profile-relative.html` / `profile-relative-edit.html` 仅开放原始列表展示和新增,不猜测 `relativeId` 后开放详情、编辑或删除。
规划:将“人亲簿/亲友往来”和“备忘录”拆成两个 Tab 或两组页面:
- 亲友往来:亲友、关系、事项、时间、礼金、说明。
@@ -464,6 +508,8 @@ DELETE .../memos/{memoId}
用途:家族事务、纪念事项、待办提醒和完成状态。
状态:5 个操作已在 Apifox PC 详情中逐条核验,`ApiClient` 已映射为 `memos``createMemo``memoDetail``updateMemo``deleteMemo`;全部需要登录、必填 `clientid` 请求头,`genealogyId``memoId` 都是 int64 路径参数。新增和修改使用已核验的备忘录请求体:`memoTitle` 必填,`memoContent``remindTime``completed``mediaOssIds``sortOrder``status` 可选;`completed` 是 string`mediaOssIds` 是英文逗号分隔的正整数 OSS ID。列表响应是元素 DTO 未展开的 `ListResult`,详情/新增/修改是元素 DTO 未展开的 `ObjectResult`,删除是 `VoidResult`,因此 `profile-memo.html` / `profile-memo-edit.html` 仅开放原始列表展示和新增,不猜测 `memoId` 后开放详情、编辑或删除。
#### 功德记录(1
```text
@@ -474,9 +520,11 @@ DELETE .../merit-records/{meritId}
当前缺少列表、新增、详情和修改。接口补齐前,`profile-merit.html``profile-merit-edit.html` 保持设计预览。
状态:该删除操作已在 Apifox PC 详情中核验,`ApiClient.deleteMeritRecord(genealogyId, meritId)` 映射 `DELETE /genealogy/pc/genealogies/{genealogyId}/merit-records/{meritId}``genealogyId``meritId` 是必填 int64 路径参数,登录和 `clientid` 必填,响应为 `VoidResult`。没有真实列表、详情或稳定 `meritId` 来源,页面不开放删除。
## 5. 后端后续补充清单(当前不对接)
本节只登记当前 PC 79 个接口之外的业务缺口,不借用 APP 路径、参数或 DTO。后端把新接口正式加入 Apifox PC 后,再更新接口数量和页面对接计划。
本节只登记当前 PC 85 个接口之外的业务缺口,不借用 APP 路径、参数或 DTO。后端把新接口正式加入 Apifox PC 后,再更新接口数量和页面对接计划。
### 后续优先级 1:家谱上下文与成员
@@ -519,14 +567,14 @@ DELETE .../merit-records/{meritId}
目标:
- 直接在 Apifox 客户端中核对当前 PC 目录。
- 锁定 12 个目录、79 个操作及各目录数量,作为本轮唯一对接清单。
- 锁定 15 个目录、85 个操作及各目录数量,作为当前唯一对接清单。
- 为每个操作在 Apifox 中确认 method、path、请求 DTO、响应 DTO、权限和错误码;可选导出仅用于生成本地契约测试快照。
- 排除所有未进入 Apifox PC 目录的接口,不引用 APP 或其他公开文档补充本轮范围。
- 统一 token、ID、`ossId`、分页、日期和枚举。
验收:
- Apifox PC 操作数等于 79,目录数量与第 1 节一致。
- Apifox PC 操作数等于 85,目录数量与第 1 节一致。
- 第 4 节中的每个接口都能在 Apifox 客户端中找到,且没有 APP 或其他目录的接口混入当前清单。
- 同一接口只有一套 path 和 DTO。
- 当前计划不再引用 APP 路径或 APP DTO。
@@ -535,10 +583,10 @@ DELETE .../merit-records/{meritId}
范围:
1. 认证登录 11 个。
2. 验证中心 4 个。
1. 认证登录 12 个。
2. 验证中心 3 个。
3. 行政区划 4 个。
4. 文件上传 6 个。
4. 文件上传 3 个。
5. 家族圈 14 个。
执行重点:
@@ -549,10 +597,11 @@ DELETE .../merit-records/{meritId}
当前施工进度:
1. 已完成认证、验证码与账号安全流程,包含登录、短信登录、注册、找回密码、换绑、注销和退出。
2. 已完成个人资料读取与保存、头像单文件上传回填、行政区划三级联动/搜索/回显;资料和区划请求统一携带登录态,`ossId` 在浏览器端保持字符串。
3. 已完成文件分片初始化、分片完成、绑定和解绑的客户端契约映射;待后端补齐初始化响应和各业务引用值后,再开通页面上传闭环。
4. 已完成家族圈回复、字辈谱和世系人物的 Apifox 核验与首轮页面施工;下一步直接核验族务记录的完整路径、DTO 与响应字段,再决定成长记录、亲友往来备忘录页面能否开放
1. 已完成认证、验证码与账号安全流程,包含登录、短信登录、注册、找回密码、换绑、注销和退出;本轮已按当前 PC 契约将验证码与短信发送迁移至 `operationCode` 路径并移除 `sceneCode`/body `clientId`。密码登录已接入 `password-login` 的验证策略和 TAC `validToken` 提交
2. 已完成个人资料读取与保存、行政区划三级联动/搜索/回显;资料和区划请求统一携带登录态,`ossId` 在浏览器端保持字符串。头像上传等待当前 PC 分片初始化响应 Schema 补齐后再开放回填。
3. 当前仅保留文件分片初始化、分片完成三条客户端契约;旧单文件上传、文件引用路径已删除。待后端补齐初始化响应 Schema 后,再开通头像上传闭环。
4. 历史 79 条映射不能代替当前 85 条复核。本轮已完成当前 85/85 条的逐项 PC 详情复核:验证中心 3 条、认证登录 12 条、文件上传 3 条、家谱配额、家族圈 14 条、贺礼邀约、族务记录 16 条、消息通知、行政区划 4 条、字辈谱 6 条、世系人物 12 条和文章/相册/视频/祭祀 6 条。成长记录、亲友往来备忘录的响应 DTO 仍待后端补齐,文章、相册、视频、祭祀和功德记录均只有删除孤岛接口,页面保持关闭
5. 已去除个人中心中的示例家谱卡片;所有无上下文的家谱业务入口先进入 `profile-families.html`,实际调用 PC 配额接口并在缺少真实 `genealogyId` 时保持阻止。
验收:
@@ -566,9 +615,9 @@ DELETE .../merit-records/{meritId}
1. 字辈谱 6 个。
2. 世系人物 12 个。
3. 成长记录 5 个。
4. 亲友往来 5 个。
5. 备忘录 5 个。
3. 成长记录 5 个(已完成契约映射、列表/新增页面;响应 DTO 缺口使详情/编辑/删除保持关闭)
4. 亲友往来 5 个(已完成契约映射、列表/新增页面;响应 DTO 缺口使详情/编辑/删除保持关闭)
5. 备忘录 5 个(已完成契约映射、列表/新增页面;响应 DTO 缺口使详情/编辑/删除保持关闭)
执行前提:
@@ -587,11 +636,11 @@ DELETE .../merit-records/{meritId}
范围:
1. 内容文章 1 个。
2. 相册与照片 2 个。
3. 视频 1 个。
4. 祭祀/典礼与献礼 2 个。
5. 功德记录 1 个。
1. 内容文章 1 个(已完成删除契约映射;缺少列表/详情,不开放页面删除)
2. 相册与照片 2 个(已完成删除契约映射;缺少列表/详情,不开放页面删除)
3. 视频 1 个(已完成删除契约映射;缺少列表/详情,不开放页面删除)
4. 祭祀/典礼与献礼 2 个(已完成删除契约映射;缺少列表/详情,不开放页面删除)
5. 功德记录 1 个(已完成删除契约映射;缺少列表/详情,不开放页面删除)
处理方式:
@@ -607,7 +656,7 @@ DELETE .../merit-records/{meritId}
### 阶段 4:接收后端新增 PC 接口
本阶段不属于当前 79 个接口的对接范围。后端每次新增 PC 接口后:
每次后端新增或调整 PC 接口后:
1. 先确认接口已正式进入 Apifox PC 目录。
2. 更新基线数量、第 4 节用途映射和第 5 节缺口。
@@ -635,7 +684,7 @@ DELETE .../merit-records/{meritId}
- 把静态示例数据当成接口成功结果。
- 只有删除接口时先开放删除按钮。
## 8. 后续业务待确认项(不阻塞当前 79
## 8. 后续业务待确认项(不阻塞当前 85
以下问题只影响后端后续新增 PC 接口,不改变本轮范围:
+87 -22
View File
@@ -1,7 +1,7 @@
# PC 接口对接交接文档
> 更新:2026-07-24
> 状态:已完成已核验 PC 接口的首轮页面施工;其余 PC 目录尚未核验、尚未接入
> 更新:2026-07-25
> 状态:桌面版 Apifox 的 PC 目录现有 85 条接口,已于本轮逐条直接复核并与客户端映射比对。2026-07-24 的 79 条施工记录仅保留为历史;后续只在 PC 目录发生变动时,重新核验受影响的接口后再修改代码
## 一、必须遵守的范围
@@ -12,7 +12,30 @@
5. 家谱业务只能使用真实 `?genealogyId=...`;不写死编号、不从 APP 获取。
6. 页面不直接调用 Axios、不自行拼请求路径,统一通过 `utils/ApiClient.js`
## 二、已直接在 Apifox PC 核验并施工的接口
## 二、当前目录与历史施工范围
### 当前 PC 目录(2026-07-25
| 分组 | 条数 | 当前处理状态 |
| --- | ---: | --- |
| 验证中心 | 3 | 已逐条复核并迁移 |
| 认证登录 | 12 | 已逐条复核并迁移;其中两条短信发送定义同为当前 `operationCode` 路径 |
| 文件上传 | 3 | 已逐条复核、`ApiClient` 仅保留分片三条;初始化响应未展开,头像上传保持阻止 |
| 家谱 | 1 | 已核验并映射配额查询;入口页仅展示配额并阻止进入需 `genealogyId` 的业务页,不能伪造编号 |
| 家族圈 | 14 | 已逐条复核;动态、点赞、评论、直接回复及两类分页均与当前实现一致 |
| 行政区划 | 4 | 已逐条复核;当前 PC 目录下实际路径均为 `/genealogy/region/*`,与 `ApiClient` 一致 |
| 字辈谱 | 6 | 已逐条复核;正常查询、维护、新增、批量预览/保存和修改/停用/恢复均与当前实现一致 |
| 世系人物 | 12 | 已逐条复核;人物列表/分页/选项/树、CRUD/停用和四种新增关系均与当前实现一致 |
| 内容文章 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
| 相册 | 2 | 已核验;仅删除,页面继续关闭危险入口 |
| 视频 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
| 贺礼邀约 | 4 | 已逐条核验并映射;缺少活动列表/创建/详情,页面保持预览 |
| 祭祀 | 2 | 已核验;仅删除活动/祭品,页面继续关闭危险入口 |
| 族务记录 | 16 | 已逐条复核;成长记录、亲友往来、备忘录各 5 条与功德删除 1 条均保持当前契约 |
| 消息通知 | 4 | 已逐条核验并映射;消息页已接入原始列表、未读数和全部已读 |
| 合计 | 85 | 以桌面版 PC 目录为准 |
### 历史 79 条施工清单(2026-07-24,已被当前目录基线替代)
| 分组 | 条数 | 状态 |
| --- | ---: | --- |
@@ -23,22 +46,48 @@
| 家族圈 | 14 | 已核验、已接入 |
| 字辈谱 | 6 | 已核验、已接入 |
| 世系人物 | 12 | 已核验、已接入 |
| 合计 | 57 | |
| 族务记录·成长记录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·亲友往来 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·备忘录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·功德记录 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 内容文章 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 相册 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 视频 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 祭祀 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 合计 | 79 | |
完整映射见 [PC接口对接规划.md](PC接口对接规划.md)。
### 已核验关键规则
- 短信接口为 `POST /genealogy/pc/auth/sms/code`;场景只允许 `PC_SMS_LOGIN``PC_REGISTER``PC_FORGOT_PASSWORD``PC_PHONE_CHANGE``PC_ACCOUNT_DEACTIVATE``validToken` 来自 `/captcha/*`,只能单次消费
- 验证中心现为三条 PC 接口:`GET /genealogy/pc/auth/verification/{operationCode}/require``POST .../{operationCode}/challenge``POST .../{operationCode}/verify``operationCode` 仅允许 `password-login``sms-login``register``forgot-password``phone-change``account-deactivate`;前端不得提交旧 `sceneCode`
- `require` 的 query 为必填 `tenantId` 和可选 `subject`challenge 的 body 为必填 `tenantId``subject`verify 的 body 在此基础上增加 `challengeId`,可附 `providerCode``captchaType``payload`。三条都由请求头传 `clientid`,不得在 query/body 传 `clientId`
- 短信发送为 `POST /genealogy/pc/auth/sms/{operationCode}/code`,支持除 `password-login` 外的五个短信动作。body 使用 `grantType: "sms"``tenantId``phone` 和策略开启时的 `validToken``clientid` 只走请求头。
- 当前认证登录的上述已核验接口一律要求 `clientid` 请求头,且 body 明确禁止 `clientId`。注册/密码登录/找回密码的 body 为 `grantType: "password"``tenantId` 加业务字段;密码登录在验证中心策略要求时还提交本次验证取得的 `validToken`;短信登录为 `grantType: "sms"``tenantId``phone``smsCode`;换绑为 `phone``smsCode`;注销为 `smsCode`
- 族务记录当前 16 条由成长记录、亲友往来、备忘录三套完整 CRUD(各 5 条)和 `DELETE .../merit-records/{meritId}` 组成。三套 CRUD 都要求 `genealogyId`,详情/修改/删除还要求各自记录 ID;列表元素与新增/详情响应 DTO 仍未展开,页面只展示原始记录并保持无稳定 ID 的编辑、详情、删除入口关闭。
- 行政区划四条当前路径为 `GET /genealogy/region/children`(可选 `parentCode`)、`GET /genealogy/region/path/{regionCode}``GET /genealogy/region/search`(必填 `keyword`,可选 `level``limit`)、`GET /genealogy/region/{regionCode}`;均已与 `ApiClient` 核对一致。
- 字辈谱 6 条当前路径分别为 `GET .../generation-poems``GET .../generation-poems/management``POST .../generation-poems``POST .../generation-poems/batch/preview``POST .../generation-poems/batch/save``PUT .../generation-poems/{poemId}`;实现仍只使用其明确 DTO 字段。
- 世系人物 12 条当前包含 `persons` 列表/新增、`persons/page``persons/options``tree`、人物详情/修改/停用,以及 `children``parents``siblings``spouses` 四种以完整 `LineagePersonBody` 新建关系人物的接口;`genealogyId``personId` 均保持真实入口传入。
- 内容文章、相册、视频和祭祀当前 6 条均为删除孤岛:`DELETE .../articles/{articleId}``.../albums/{albumId}``.../albums/{albumId}/photos/{photoId}``.../videos/{videoId}``.../ceremonies/{ceremonyId}``.../ceremonies/{ceremonyId}/gifts/{giftId}`。它们已映射但无当前 PC 列表/详情来源,不能开放页面删除。
- 文件上传现仅有 `POST /genealogy/pc/files/resumable/init``POST .../chunk``POST .../complete`。所有文件统一先初始化;普通小文件可按初始化响应的 `instant=true` 直接取 OSS 信息,但该响应的字段 Schema 尚未展开,页面不得猜测 `uploadId``ossId``instant`,头像上传继续阻止。
- 家谱当前只提供 `GET /genealogy/pc/genealogies/quota`,返回创建/加入的已用数量、上限、剩余额度以及 `canCreate`/`canJoin`。它不返回家谱对象或 `genealogyId`;只映射为 `genealogyQuota`,不据此开放家谱业务页面。
- 贺礼邀约四条为替换受邀人、查询活动邀请名单、当前用户接受/拒绝、查询我的邀请。受邀人 body 只允许 `inviteeUserIds`,当前用户响应 body 只允许 `inviteStatus: ACCEPTED|DECLINED`。缺少活动列表/创建/详情,`profile-gift*.html` 继续为明确的待开发预览。
- 消息通知为列表(可选 `readStatus: 0|1`)、未读数量、单条已读和全部已读。列表元素 DTO 未展开,`profile-messages.html` 只安全展示服务端原始记录,不猜测标题、内容、时间或单条 `notificationId`;未读数和全部已读已开放。
- 认证相关均使用 PC 路径;密码为 32 位 MD5。改绑/注销只使用 PC DTO 所需字段,不擅自加 `tenantId`
- 单文件上传为 `POST /genealogy/pc/files/upload`multipart 仅提交 `file`;返回 `ossId``url``thumbnailUrl``fileName``originalName`
- 家族圈新评论只提交 `commentContent` 和可选 `parentCommentId`,不能发旧字段 `content``replyUserId`
- 家族圈 14 条当前路径为动态列表/分页/详情/增改删、点赞/取消点赞、一级评论列表/分页/发表/删除和直接回复列表/分页;均要求真实 `genealogyId`,现有 `ApiClient` 与页面调用已逐项比对一致。新评论只提交必填 `commentContent` 和可选 `parentCommentId`,不能发旧字段 `content``replyUserId`
- 字辈管理列表使用 `GET .../generation-poems/management`;批量操作使用 `poemText` 和可选 `disableMissing`。单项最多 50 字符、一次最多 500 代、总长最多 26000。
- 世系分页 query`pageNum``pageSize``keyword``generation``personStatus``keyword` 只查姓名、别名、人物编号。
- 世系写入字段是 `name``generation``biography`,不是旧字段 `personName``generationNo``introduction``sortOrder``relationName` 是可选字段。
- `DELETE .../lineage/persons/{personId}` 是逻辑停用,不是物理删除;有正常子女时后端会拒绝。
- 新增父母/配偶/兄弟姐妹/子女四个关系接口均接收完整 `LineagePersonBody` 来新建关系人物,并非绑定两个已有 ID。
- 已末次回到 Apifox 复核:世系分页 query、创建人物 body、`POST .../lineage/persons/{personId}/spouses``relationName`
- 成长记录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/growth-records``/{recordId}``genealogyId``recordId` 均是必填 int64 路径参数,`clientid` 为必填请求头。
- 成长记录的 `GrowthRecordBody` 只有 `recordTitle` 必填;可选字段为 `lineagePersonId``recordType``recordContent``recordDate``remindTime``mediaOssIds``sortOrder``status``mediaOssIds` 只接受英文逗号分隔的正整数 OSS ID。
- 成长记录列表响应是未展开元素 DTO 的 `ListResult`,详情/新增/修改为未展开元素 DTO 的 `ObjectResult`,删除为 `VoidResult`。不能据此猜测 `recordId`、标题或权限字段;页面仅作原始列表展示和新增,不开放编辑、详情或删除入口。
- 亲友往来 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/relative-records``/{relativeId}``RelativeRecordBody` 只有 `relativeName` 必填,可选 `relationName``eventName``eventTime``giftAmount``recordContent``mediaOssIds``sortOrder``status`。列表/详情元素 DTO 同样未展开,页面不开放编辑、详情或删除入口。
- 备忘录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/memos``/{memoId}``genealogyId``memoId` 均是必填 int64 路径参数,`clientid` 为必填请求头。备忘录请求体只有 `memoTitle` 必填,可选 `memoContent``remindTime``completed``mediaOssIds``sortOrder``status``completed` 在 Apifox 中是 string,不能擅自改成布尔值。列表/详情元素 DTO 未展开,页面仅作原始列表展示和新增。
- 功德记录在 PC 目录中当前只存在 `DELETE /genealogy/pc/genealogies/{genealogyId}/merit-records/{meritId}`;两个路径参数均为必填 int64,登录和 `clientid` 必填,响应为 `VoidResult``deleteMeritRecord` 已映射,但页面没有真实列表、详情或稳定 `meritId` 来源,必须保持删除入口关闭。
- 其余 6 个删除孤岛接口也已核验并映射:`deleteArticle``deleteAlbum``deleteAlbumPhoto``deleteVideo``deleteCeremony``deleteCeremonyGift`。它们都要求登录、`clientid` 和 int64 路径 ID,响应均为 `VoidResult`;相册、视频与祭祀删除会由后端释放相应文件引用。由于没有 PC 列表/详情和稳定 ID 来源,所有相关页面继续保持预览,不能触发删除。
## 三、已经落地的页面
@@ -48,13 +97,16 @@
- `register.html`:短信注册。
- `forgot-password.html`:短信重置密码。
- `profile-security.html`:改密码、改绑手机、注销账号。
- `profile-data.html` / `profile.html`:资料读取保存、头像单文件上传、行政区划。
- `profile-data.html` / `profile.html`:资料读取保存、行政区划;头像上传等待当前 PC 分片初始化响应 Schema 补齐
### 家谱业务
- `profile-feed.html` / `profile-feed-edit.html`:家族圈列表、详情、发布/修改、点赞、评论、回复。
- `profile-generation.html` + `public/js/generation-pages.js`:字辈管理列表、新增、编辑、停用/恢复、批量预览/保存。所有写操作有防重复提交锁;批量示例要求用空格或支持的标点分隔。
- `profile-tree.html` + `public/js/lineage-pages.js`:成员总览、世系树、分页搜索/翻页、下拉选择、详情、新增/编辑、逻辑停用、父母/配偶/兄弟姐妹/子女新增。所有写操作有防重复提交锁;响应 int64 ID 只以安全字符串用于后续请求。
- `profile-growth.html` / `profile-growth-edit.html` + `public/js/growth-pages.js`:成长记录列表和新增;写入仅使用 `GrowthRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
- `profile-relative.html` / `profile-relative-edit.html` + `public/js/relative-pages.js`:亲友往来列表和新增;写入仅使用 `RelativeRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
- `profile-memo.html` / `profile-memo-edit.html` + `public/js/memo-pages.js`:备忘录列表和新增;写入仅使用已核验的备忘录请求体字段,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
### 关键代码文件
@@ -62,28 +114,31 @@
| --- | --- |
| `utils/ApiClient.js` | 已核验 PC 路径、请求头、DTO |
| `public/js/profile-common.js` | `genealogyId` 读取和链接传播的唯一 owner |
| `public/js/genealogy-entry-pages.js` | 无真实家谱上下文时的统一入口页;仅查询 PC 配额并阻止业务跳转 |
| `public/js/auth-pages.js``captcha-pages.js``security-pages.js` | 登录、注册、验证码、账号安全 |
| `public/js/profile-pages.js``region-pages.js``upload-pages.js` | 资料、区划、头像 |
| `public/js/feed-pages.js` | 家族圈 |
| `public/js/generation-pages.js` | 字辈谱 |
| `public/js/lineage-pages.js` | 世系人物 |
| `public/js/growth-pages.js` | 成长记录 |
| `public/js/relative-pages.js` | 亲友往来 |
| `public/js/memo-pages.js` | 备忘录 |
## 四、当前阻塞和未完成范围
### PC 家谱入口缺失
PC 目录没有“我的家谱、家谱详情、创建/加入/选择家谱”接口。前端无法自行得到真实 `genealogyId`,因此当前正确行为是:缺少 ID 时提示用户从具体家谱进入并且不发请求。不要伪造编号,也不要用 APP 补这个缺口。
PC 目录没有“我的家谱、家谱详情、创建/加入/选择家谱”接口。前端无法自行得到真实 `genealogyId`,因此当前正确行为是:个人中心和内容入口先统一进入 `profile-families.html`,该页只读取 `genealogyQuota()` 并明确说明当前不能选择家谱;缺少 ID 时阻止业务请求。静态家谱卡片已移除,不要伪造编号,也不要用 APP 补这个缺口。
### 尚未核验、尚未施工的 22 条 PC 接口
### 已核验但仍缺业务闭环的页面范围
| 分组 | 条数 | 当前原则 |
| 分组 | 已核验条数 | 当前原则 |
| --- | ---: | --- |
| 内容文章 | 1 | 目前只见删除能力;没有真实列表/详情来源时不开放删除 |
| 相册 | 2 | 目前只见删除能力;不猜 DTO |
| 视频 | 1 | 目前只见删除能力;不猜 DTO |
| 祭祀 | 2 | 目前只见删除能力;不猜实体和献礼字段 |
| 族务记录 | 16 | 尚未逐条核验;先在 Apifox 看完再映射成长记录、亲友往来、备忘录等页面 |
| 合计 | 22 | |
| 内容文章 | 1 | 仅删除;没有真实列表/详情来源时不开放删除 |
| 相册 | 2 | 仅删除;不猜 DTO 或文件引用来源 |
| 视频 | 1 | 仅删除;不猜 DTO 或文件引用来源 |
| 祭祀 | 2 | 仅删除;不猜实体和献礼字段 |
| 功德记录 | 1 | 仅删除;不猜实体和 `meritId` 来源 |
## 五、通用行为和安全约束
@@ -95,23 +150,33 @@ PC 目录没有“我的家谱、家谱详情、创建/加入/选择家谱”接
## 六、验证结果
最近一次聚焦验证已通过:52/52 测试通过,`git diff --check` 通过
本轮已直接完成当前 PC 目录 85/85 条的详情复核,并逐项比对 `ApiClient`、对应页面和契约测试;历史 79 条记录不作为本轮验收依据。完整测试结果以本轮末次运行记录为准
已运行的检查包括:
- `node --check utils/ApiClient.js`
- `node --check public/js/profile-common.js`
- `node --check public/js/genealogy-entry-pages.js`
- `node --check public/js/generation-pages.js`
- `node --check public/js/lineage-pages.js`
- `node --test tests/auth-pages.test.js tests/api-client-contract.test.js tests/profile-pages.test.js tests/security-upload-scope.test.js tests/feed-pages.test.js tests/generation-pages.test.js tests/lineage-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
- `node --check public/js/growth-pages.js`
- `node --check public/js/memo-pages.js`
- `node --check public/js/captcha-pages.js`
- `node --check public/js/auth-pages.js`
- `node --check public/js/security-pages.js`
- `node --test tests/api-client-contract.test.js tests/captcha-pages-contract.test.js tests/auth-pages.test.js tests/security-upload-scope.test.js tests/profile-pages.test.js`
- `node --test tests/auth-pages.test.js tests/api-client-contract.test.js tests/profile-pages.test.js tests/security-upload-scope.test.js tests/feed-pages.test.js tests/generation-pages.test.js tests/lineage-pages.test.js tests/growth-pages.test.js tests/relative-pages.test.js tests/memo-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
- `npm.cmd test`91/91 通过)
- `git diff --check`
轮未运行无关的完整历史测试集
地运行态检查已使用 Chrome DevTools 逐页打开 `profile-feed*`、成长记录、亲友往来、备忘录、消息、资料、字辈和世系页面:12 个静态页面均返回 200;无登录态时需鉴权的页面转到登录且没有发送业务请求,三类族务编辑页缺少真实 `genealogyId` 时保持阻止且不发请求。已使用测试账号完成密码登录的真实滑块验证与登录;资料页实际修改昵称后已恢复原值,页面两次均显示保存成功;消息页实际完成列表和未读数读取,服务端返回空列表。个人中心到家谱入口页的真实点击已验证,入口页仅调用配额接口且在缺少真实 `genealogyId` 时不进入家族业务。当前仍没有 PC 家谱列表/详情返回的真实家谱上下文,因此未对需 `genealogyId` 的写接口发送提交
当前完整测试集已运行并通过。
## 七、下一位 GPT 的执行顺序
1. 读本文和 `docs/PC接口对接规划.md`
2. 打开 Apifox**只看 PC 目录**,从“族务记录”16 条开始逐条核验
3. 每次只处理一个完整小分组:Apifox 核验 → `ApiClient` → 对应页面 → 聚焦测试。
2. 先检查桌面版 Apifox PC 目录是否新增或变更;只逐条复核受影响接口,不要借用 APP 接口或猜 DTO
3. 每次只处理一个完整小分组:Apifox 核验 → `ApiClient` → 对应页面 → 聚焦测试;家谱配额接口不是入口,继续不写死 `genealogyId`
4. 只有删除能力、没有真实列表/详情来源的模块,不开放危险操作,只登记后端缺口。
5. 每完成一组,更新本文、`PC接口对接规划.md` 和相关测试,并报告改动、验证和剩余阻塞。
+1
View File
@@ -46,6 +46,7 @@
autocomplete="current-password"
placeholder="密码"
/>
<input type="hidden" name="validToken" />
<button class="btn primary" type="submit">登录</button>
</form>
<form
+2 -1
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-article-edit-page data-feature-status="pending" data-feature-message="谱文服务正在开发中,当前页面仅保留设计预览。">
<header class="site-header">
@@ -114,7 +115,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="config.js"></script>
+12 -7
View File
@@ -53,7 +53,7 @@
<aside class="module-nav">
<a href="profile-families.html">我的家谱</a
><a href="profile-messages.html">消息中心</a
><a href="profile-family-admin.html">家族管理</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a class="active" href="profile-content.html">内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
@@ -87,20 +87,25 @@
<h3>贺礼邀请</h3>
<p>创建婚礼、升学、生日等贺礼邀请。</p></a
>
<a class="module-card tilt-card" href="profile-feed.html" data-feature-link="available"
<a class="module-card tilt-card" href="profile-feed.html" data-feature-link="available" data-genealogy-context-link
><span class="icon"></span>
<h3>发布动态</h3>
<p>向家族圈发布图文动态,可设置置顶和精华。</p></a
>
<a class="module-card tilt-card" href="profile-growth.html"
<a class="module-card tilt-card" href="profile-growth.html" data-feature-link="available" data-genealogy-context-link
><span class="icon"></span>
<h3>成长日志</h3>
<p>记录个人成长节点,支持照片附件。</p></a
>
<a class="module-card tilt-card" href="profile-memo.html"
<a class="module-card tilt-card" href="profile-relative.html" data-feature-link="available" data-genealogy-context-link
><span class="icon"></span>
<h3>亲友往来</h3>
<p>记录亲友关系、事项和礼金。</p></a
>
<a class="module-card tilt-card" href="profile-memo.html" data-feature-link="available" data-genealogy-context-link
><span class="icon"></span>
<h3>人亲簿 / 备忘录</h3>
<p>保存人情往来、家族事务和纪念事项</p></a
<h3>备忘录</h3>
<p>记录家族事务、提醒和完成状态</p></a
>
</div>
</section>
@@ -118,7 +123,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+2 -2
View File
@@ -87,11 +87,11 @@
accept="image/*"
data-upload-target="#profileAvatarOssId"
data-upload-status="#profileAvatarStatus"
data-upload-mode="auto"
data-upload-mode="resumable"
data-upload-biz-type="avatar"
data-upload-usage-scene="profile_avatar"
/>
<p id="profileAvatarStatus" class="upload-status">未选择文件</p>
<p id="profileAvatarStatus" class="upload-status">当前 PC 文件上传响应未定义头像回填字段,暂不能上传头像</p>
</div>
</div>
<div class="editor-two">
+7 -19
View File
@@ -38,7 +38,7 @@
<div>
<h1>我的家谱</h1>
<p>
集中管理你创建或加入的家谱,进入家谱主页后可继续处理世系图、相册、家族圈和成员协作
从这里进入你的家谱。当前 PC 只提供家谱额度查询,列表和详情入口等待后端补齐后再开放
</p>
</div>
<a class="btn primary magnetic" href="profile-create-family.html"
@@ -52,30 +52,17 @@
<aside class="module-nav">
<a class="active" href="profile-families.html">我的家谱</a
><a href="profile-messages.html">消息中心</a
><a href="profile-family-admin.html">家族管理</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a href="profile-content.html">内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>家谱列表</h2>
<p>对应 APP 的“我的家谱”和“家谱主页”</p>
<h2>家谱入口</h2>
<p>对应 PC 的“我的家谱”入口;进入具体业务前必须取得真实家谱编号</p>
<div class="module-list" data-genealogy-list="mine">
<a class="module-row" href="profile-family-home.html"
><div>
<h3>四川武胜汤氏族</h3>
<p>创建者 · 共修入 7 人 · 最近更新:家族圈动态</p>
</div>
<span class="pill">进入主页</span></a
>
<a class="module-row" href="profile-family-home.html"
><div>
<h3>四川达州刘氏族</h3>
<p>管理员 · 共修入 7 人 · 2 条内容待查看</p>
</div>
<span class="pill">进入主页</span></a
>
<div class="api-empty" data-genealogy-entry-status>正在读取家谱入口状态…</div>
</div>
</section>
<section class="module-panel">
@@ -107,8 +94,9 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/genealogy-entry-pages.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+1 -1
View File
@@ -111,7 +111,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+8 -8
View File
@@ -16,14 +16,14 @@
>
<nav class="nav-links">
<a class="active" href="profile-families.html">我的家谱</a
><a href="profile-family-admin.html">家族管理</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a href="profile-content.html">内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
</nav>
<div class="nav-actions">
<a href="profile.html">返回中心</a
><a class="btn primary magnetic" href="profile-feed.html" data-feature-link="available">发布动态</a>
><a class="btn primary magnetic" href="profile-feed.html" data-feature-link="available" data-genealogy-context-link>发布动态</a>
</div>
</div>
</header>
@@ -38,7 +38,7 @@
后台家谱主页,用于进入世系、相册、家族圈、内容管理和成员协作。
</p>
</div>
<a class="btn ghost magnetic" href="profile-family-admin.html"
<a class="btn ghost magnetic" href="profile-family-admin.html" data-genealogy-context-link
>管理家谱</a
>
</div>
@@ -47,11 +47,11 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a class="active" href="profile-family-home.html">家谱主页</a
><a href="profile-tree.html">世系图</a
<a class="active" href="profile-family-home.html" data-genealogy-context-link>家谱主页</a
><a href="profile-tree.html" data-genealogy-context-link>世系图</a
><a href="profile-album.html">相册</a
><a href="profile-content.html">内容发布</a
><a href="profile-family-admin.html">家族管理</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a href="profile-messages.html">消息中心</a>
</aside>
<div class="module-main">
@@ -63,7 +63,7 @@
<h3>家谱编号</h3>
<p data-family-home-summary>家谱信息加载中...</p>
</div>
<a class="module-card tilt-card" href="profile-tree.html"
<a class="module-card tilt-card" href="profile-tree.html" data-genealogy-context-link
><span class="icon"></span>
<h3>世系图</h3>
<p>查看树谱模式和成员关系。</p></a
@@ -106,7 +106,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+2 -1
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-feed-edit-page>
<header class="site-header">
@@ -95,7 +96,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="config.js"></script>
+1 -1
View File
@@ -101,7 +101,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+4 -3
View File
@@ -7,8 +7,9 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-ceremony-edit-page data-feature-status="pending" data-feature-message="贺礼服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-ceremony-edit-page data-feature-status="pending" data-feature-message="当前 PC 未提供活动创建或编辑接口,页面保持设计预览。">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
@@ -94,7 +95,7 @@
<div class="editor-field">
<label for="giftCoverOssId">封面 OSS ID</label>
<input id="giftCoverOssId" name="coverOssId" type="text" placeholder="上传封面后自动回填" />
<!-- 公共上传脚本会调用 /files/upload 并回填封面 OSS ID -->
<!-- 当前 PC 文件初始化响应未展开,封面上传保持阻止。 -->
<label class="upload-control" for="giftCoverFile">选择封面图片</label>
<input id="giftCoverFile" class="upload-input" type="file" accept="image/*" data-upload-target="#giftCoverOssId" data-upload-status="#giftCoverStatus" />
<p id="giftCoverStatus" class="upload-status">未选择文件</p>
@@ -114,7 +115,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="config.js"></script>
+4 -4
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-ceremony-page data-feature-status="pending" data-feature-message="贺礼服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-ceremony-page data-feature-status="pending" data-feature-message="当前 PC 仅提供活动邀约接口,缺少活动列表、创建和详情,页面保持设计预览。">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
@@ -34,7 +34,7 @@
<div>
<h1>贺礼邀请</h1>
<p>
管理婚礼、升学、生日和其他家族贺礼邀请,可进入管理模式删除或编辑
当前仅核验活动邀约、响应及“我的邀请”;活动列表、创建和详情接口尚未提供
</p>
</div>
</div>
@@ -51,14 +51,14 @@
<div class="module-main">
<section class="module-panel">
<h2>邀请列表</h2>
<!-- 贺礼/祭祀列表由 /ceremonies 接口渲染 -->
<!-- 当前 PC 未提供活动列表接口,保持待开发状态。 -->
<div class="module-list" data-ceremony-list>
<div class="api-empty">贺礼邀请加载中...</div>
</div>
</section>
<section class="module-panel">
<h2>献礼记录</h2>
<!-- 选择贺礼后由 /ceremonies/{ceremonyId}/gifts 接口渲染 -->
<!-- 当前 PC 未提供活动详情或献礼记录接口,保持待开发状态。 -->
<div class="module-list" data-ceremony-gift-list>
<div class="api-empty">请选择贺礼查看献礼记录</div>
</div>
+35 -28
View File
@@ -3,12 +3,13 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新建成长日志 - 个人中心 - 代代相传</title>
<title>新建成长记录 - 个人中心 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-growth-edit-page data-feature-status="pending" data-feature-message="成长日志服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-growth-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
@@ -16,10 +17,10 @@
>
<nav class="nav-links">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-growth.html">成长日志</a>
><a class="active" href="profile-growth.html" data-genealogy-context-link>成长记录</a>
</nav>
<div class="nav-actions">
<a href="profile-growth.html">返回日志</a
<a href="profile-growth.html" data-genealogy-context-link>返回记录</a
><button class="btn primary magnetic" type="submit" form="growth-edit-form">保存</button>
</div>
</div>
@@ -27,11 +28,11 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Growth Editor</div>
<div class="module-kicker">Growth Record</div>
<div class="module-title-row">
<div>
<h1>新建成长日志</h1>
<p>记录成员成长节点、特殊情况、照片附件和补充资料</p>
<h1>新建成长记录</h1>
<p>仅提交 Apifox PC 的 GrowthRecordBody 字段</p>
</div>
</div>
</div>
@@ -41,12 +42,12 @@
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-feed.html">发布动态</a
><a class="active" href="profile-growth.html">成长日志</a
><a class="active" href="profile-growth.html" data-genealogy-context-link>成长记录</a
><a href="profile-memo.html">备忘录</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>日志内容</h2>
<h2>记录内容</h2>
<form id="growth-edit-form" class="editor-form layui-form" data-growth-form>
<div class="editor-two">
<div class="editor-field">
@@ -55,7 +56,7 @@
id="growthPersonId"
name="lineagePersonId"
type="text"
placeholder="请输入世系人物 ID"
placeholder="可选;请输入世系人物 ID"
/>
</div>
<div class="editor-field">
@@ -63,7 +64,7 @@
><input
id="growthDate"
name="recordDate"
type="text"
type="date"
placeholder="请选择日期"
/>
</div>
@@ -75,12 +76,7 @@
</div>
<div class="editor-field">
<label for="growthType">类型</label>
<select id="growthType" name="recordType">
<option value="growth">成长</option>
<option value="birth">出生</option>
<option value="education">教育</option>
<option value="family">家庭</option>
</select>
<input id="growthType" name="recordType" type="text" placeholder="可选;按后端定义填写类型" />
</div>
</div>
<div class="editor-field">
@@ -89,22 +85,33 @@
id="growthContent"
name="recordContent"
class="js-rich-editor"
placeholder="请输入成长日志内容"
placeholder="请输入成长记录内容"
></textarea>
</div>
<div class="editor-field">
<label for="growthMediaOssIds">附件 OSS ID</label>
<input id="growthMediaOssIds" name="mediaOssIds" type="text" placeholder="多个附件 ID 用英文逗号分隔" />
<!-- 公共上传脚本会调用 /files/upload 并追加 OSS ID -->
<label class="upload-control" for="growthMediaFile">添加照片</label>
<input id="growthMediaFile" class="upload-input" type="file" accept="image/*" data-upload-target="#growthMediaOssIds" data-upload-status="#growthMediaStatus" data-upload-multiple="true" />
<p id="growthMediaStatus" class="upload-status">未选择文件</p>
<label for="growthRemindTime">提醒时间</label>
<input id="growthRemindTime" name="remindTime" type="text" placeholder="可选;使用带时区的 ISO 8601 时间" />
</div>
<div class="editor-two">
<div class="editor-field">
<label for="growthMediaOssIds">附件 OSS ID</label>
<input id="growthMediaOssIds" name="mediaOssIds" type="text" placeholder="多个附件 ID 用英文逗号分隔" />
</div>
<div class="editor-field">
<label for="growthSortOrder">排序值</label>
<input id="growthSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" />
</div>
</div>
<div class="editor-field">
<label for="growthStatus">状态</label>
<input id="growthStatus" name="status" type="text" placeholder="可选;按后端定义填写状态" />
</div>
<div class="bottom-actions">
<button class="btn primary magnetic" type="submit">保存</button
><a class="btn ghost magnetic" href="profile-growth.html"
><a class="btn ghost magnetic" href="profile-growth.html" data-genealogy-context-link
>取消</a
>
<span class="form-status" data-growth-form-status></span>
</div>
</form>
</section>
@@ -115,16 +122,16 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+14 -27
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-growth-page data-feature-status="pending" data-feature-message="成长日志服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-growth-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
@@ -16,12 +16,12 @@
>
<nav class="nav-links">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-growth.html">成长日志</a>
><a class="active" href="profile-growth.html" data-genealogy-context-link>成长记录</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-growth-edit.html"
>新建日志</a
><a class="btn primary magnetic" href="profile-growth-edit.html" data-genealogy-context-link
>新建记录</a
>
</div>
</div>
@@ -29,11 +29,11 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Growth</div>
<div class="module-kicker">Growth Records</div>
<div class="module-title-row">
<div>
<h1>成长日志</h1>
<p>记录成员成长节点、特殊情况、继承关系和个人资料补充</p>
<h1>成长记录</h1>
<p>记录成员成长节点、日期、提醒和附件 OSS ID</p>
</div>
</div>
</div>
@@ -43,33 +43,20 @@
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-feed.html">发布动态</a
><a class="active" href="profile-growth.html">成长日志</a
><a class="active" href="profile-growth.html" data-genealogy-context-link>成长记录</a
><a href="profile-memo.html">备忘录</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>日志列表</h2>
<!-- 成长记录由 /growth-records 接口渲染 -->
<h2>记录列表</h2>
<!-- 成长记录由 ApiClient.growthRecords 渲染 -->
<div class="module-list" data-growth-list>
<div class="api-empty">成长记录加载中...</div>
</div>
</section>
<section class="module-panel">
<h2>新建成长日志</h2>
<div class="form-like">
<p>
<span>绑定账号</span><b>点击绑定</b
><a class="link-btn" href="profile-data.html">绑定</a>
</p>
<p>
<span>姓名</span><b>待输入</b
><a class="link-btn" href="profile-growth-edit.html">填写</a>
</p>
<p>
<span>内容</span><b>待输入</b
><a class="link-btn" href="profile-growth-edit.html">编辑</a>
</p>
</div>
<h2>接口状态</h2>
<p>Apifox 当前未展开成长记录列表和详情的元素 DTO,页面不猜测 <code>recordId</code>、标题或权限字段,因此不开放编辑和删除入口。</p>
</section>
</div>
</div>
@@ -78,13 +65,13 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+18 -76
View File
@@ -3,97 +3,42 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>添加备忘 - 个人中心 - 代代相传</title>
<title>新建备忘 - 个人中心 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-memo-edit-page data-feature-status="pending" data-feature-message="备忘录服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-memo-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a
>
<nav class="nav-links">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-memo.html">备忘录</a>
</nav>
<div class="nav-actions">
<a href="profile-memo.html">返回备忘录</a
><button class="btn primary magnetic" type="submit" form="memo-edit-form">保存</button>
</div>
<a class="brand magnetic" href="profile.html"><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a>
<nav class="nav-links"><a href="profile-content.html">内容发布</a><a class="active" href="profile-memo.html" data-genealogy-context-link>备忘录</a></nav>
<div class="nav-actions"><a href="profile-memo.html" data-genealogy-context-link>返回记录</a><button class="btn primary magnetic" type="submit" form="memo-edit-form">保存</button></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Memo Editor</div>
<div class="module-title-row">
<div>
<h1>添加备忘</h1>
<p>保存人情往来、家族事务、纪念事项和附件图片。</p>
</div>
</div>
</div>
<div class="container"><div class="module-kicker">Memo Editor</div><div class="module-title-row"><div><h1>新建备忘录</h1><p>仅提交 Apifox PC 已核验的请求体字段。</p></div></div></div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-growth.html">成长日志</a
><a class="active" href="profile-memo.html">备忘录</a
><a href="profile-gift.html">贺礼邀请</a>
</aside>
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a class="active" href="profile-memo.html" data-genealogy-context-link>备忘录</a></aside>
<div class="module-main">
<section class="module-panel">
<h2>备忘内容</h2>
<form id="memo-edit-form" class="editor-form layui-form" data-memo-form>
<div class="editor-field">
<label for="memoTitle">姓名 / 标题</label
><input
id="memoTitle"
name="memoTitle"
type="text"
placeholder="请输入姓名或标题"
required
/>
</div>
<div class="editor-field">
<label for="memoContent">内容</label
><textarea
id="memoContent"
name="memoContent"
class="js-rich-editor"
placeholder="请输入内容"
></textarea>
</div>
<div class="editor-field">
<label for="memoRemindTime">提醒时间</label>
<input id="memoRemindTime" name="remindTime" type="text" placeholder="例如 2026-08-01 09:00:00" />
<div class="editor-field"><label for="memoTitle">备忘标题</label><input id="memoTitle" name="memoTitle" type="text" required /></div>
<div class="editor-field"><label for="memoContent">备忘内容</label><textarea id="memoContent" name="memoContent" placeholder="可选"></textarea></div>
<div class="editor-two">
<div class="editor-field"><label for="memoRemindTime">提醒时间</label><input id="memoRemindTime" name="remindTime" type="text" placeholder="可选;按后端定义填写" /></div>
<div class="editor-field"><label for="memoCompleted">是否已完成</label><input id="memoCompleted" name="completed" type="text" placeholder="可选;按后端定义填写" /></div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="memoCompleted">完成状态</label>
<select id="memoCompleted" name="completed">
<option value="0">未完成</option>
<option value="1">已完成</option>
</select>
</div>
<div class="editor-field">
<label for="memoMediaOssIds">附件 OSS ID</label>
<input id="memoMediaOssIds" name="mediaOssIds" type="text" placeholder="多个附件 ID 用英文逗号分隔" />
<!-- 公共上传脚本会调用 /files/upload 并追加 OSS ID -->
<label class="upload-control" for="memoMediaFile">添加图片</label>
<input id="memoMediaFile" class="upload-input" type="file" accept="image/*" data-upload-target="#memoMediaOssIds" data-upload-status="#memoMediaStatus" data-upload-multiple="true" />
<p id="memoMediaStatus" class="upload-status">未选择文件</p>
</div>
</div>
<div class="bottom-actions">
<button class="btn primary magnetic" type="submit">保存</button
><a class="btn ghost magnetic" href="profile-memo.html"
>取消</a
>
<div class="editor-field"><label for="memoMediaOssIds">附件 OSS ID</label><input id="memoMediaOssIds" name="mediaOssIds" type="text" placeholder="多个附件 ID 用英文逗号分隔" /></div>
<div class="editor-field"><label for="memoSortOrder">排序值</label><input id="memoSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" /></div>
</div>
<div class="editor-field"><label for="memoStatus">状态</label><input id="memoStatus" name="status" type="text" placeholder="可选;按后端定义填写" /></div>
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">保存</button><a class="btn ghost magnetic" href="profile-memo.html" data-genealogy-context-link>取消</a><span class="form-status" data-memo-form-status></span></div>
</form>
</section>
</div>
@@ -103,16 +48,13 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+14 -54
View File
@@ -3,78 +3,38 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>人情薄 / 备忘录 - 个人中心 - 代代相传</title>
<title>备忘录 - 个人中心 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-memo-page data-feature-status="pending" data-feature-message="备忘录服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-memo-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a
>
<nav class="nav-links">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-memo.html">备忘录</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-memo-edit.html"
>发布备忘录</a
>
</div>
<a class="brand magnetic" href="profile.html"><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a>
<nav class="nav-links"><a href="profile-content.html">内容发布</a><a class="active" href="profile-memo.html" data-genealogy-context-link>备忘录</a></nav>
<div class="nav-actions"><a href="profile-content.html">返回发布中心</a><a class="btn primary magnetic" href="profile-memo-edit.html" data-genealogy-context-link>新建备忘</a></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Memo</div>
<div class="module-title-row">
<div>
<h1>人情薄 / 备忘录</h1>
<p>保存亲友往来、家族事务、纪念事项和待办提醒。</p>
</div>
</div>
<div class="module-title-row"><div><h1>备忘录</h1><p>记录家族事务、纪念事项、提醒时间和完成状态。</p></div></div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-growth.html">成长日志</a
><a class="active" href="profile-memo.html">备忘录</a
><a href="profile-gift.html">贺礼邀请</a>
</aside>
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a class="active" href="profile-memo.html" data-genealogy-context-link>备忘录</a></aside>
<div class="module-main">
<section class="module-panel">
<h2>备忘</h2>
<!-- 备忘录列表由 /memos 接口渲染 -->
<div class="module-list" data-memo-list>
<div class="api-empty">备忘录加载中...</div>
</div>
<h2>备忘列表</h2>
<!-- 备忘录由 ApiClient.memos 渲染 -->
<div class="module-list" data-memo-list><div class="api-empty">备忘录加载中...</div></div>
</section>
<section class="module-panel">
<h2>添加备忘</h2>
<div class="form-like">
<p>
<span>姓名 / 标题</span><b>待输入</b
><a class="link-btn" href="profile-memo-edit.html">填写</a>
</p>
<p>
<span>内容</span><b>待输入</b
><a class="link-btn" href="profile-memo-edit.html">编辑</a>
</p>
<p>
<span>附件</span><b>可添加图片</b
><a
class="link-btn"
href="#"
data-layer-msg="请选择要上传的附件"
>添加</a
>
</p>
</div>
<h2>接口状态</h2>
<p>Apifox 当前未展开备忘录列表和详情的元素 DTO,页面不猜测 <code>memoId</code>、标题或权限字段,因此不开放编辑和删除入口。</p>
</section>
</div>
</div>
@@ -83,13 +43,13 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+2 -1
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-merit-edit-page data-feature-status="pending" data-feature-message="功德录服务正在开发中,当前页面仅保留设计预览。">
<header class="site-header">
@@ -113,7 +114,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="config.js"></script>
+9 -32
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-feature-status="pending" data-feature-message="消息与通知服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
@@ -38,7 +38,7 @@
<div>
<h1>消息中心</h1>
<p>
处理入谱申请、贺礼邀请、资料提醒和系统通知,减少家族协作中的遗漏
查看当前账号的消息通知,并同步未读数量
</p>
</div>
<a class="btn ghost magnetic" href="profile.html">返回个人中心</a>
@@ -57,34 +57,11 @@
</aside>
<div class="module-main">
<section class="module-panel">
<h2>待处理</h2>
<h2>通知列表</h2>
<p class="api-empty" data-notification-status>正在加载消息…</p>
<p class="module-note">未读数量:<b data-notification-unread-count>0</b></p>
<div class="module-list" data-notification-list>
<div class="module-row">
<div>
<h3>某某人申请加入四川武胜汤氏族</h3>
<p>2026.05.25 09:20 · 需要管理员审核</p>
</div>
<div class="bottom-actions">
<a class="btn ghost" href="profile-messages.html">拒绝</a
><a class="btn primary" href="profile-messages.html"
>同意</a
>
</div>
</div>
<a class="module-row" href="profile-content.html"
><div>
<h3>张三&里斯于 2023年3月23日举办婚礼</h3>
<p>贺礼邀请 · 待确认公开状态</p>
</div>
<span class="pill">查看</span></a
>
<a class="module-row" href="profile-data-reminders.html"
><div>
<h3>2 位成员资料待完善</h3>
<p>头像、出生日期、亲属关系信息不完整</p>
</div>
<span class="pill">处理</span></a
>
<div class="api-empty">正在加载消息…</div>
</div>
</section>
<section class="module-panel">
@@ -93,12 +70,12 @@
<button class="module-card module-action-card" type="button" data-notification-read-all>
<span class="icon"></span>
<h3>全部标记已读</h3>
<p>清理已处理提醒,保留重要审核消息</p>
<p>使用当前 PC 的全部已读接口</p>
</button>
<button class="module-card module-action-card" type="button" data-notification-refresh>
<span class="icon"></span>
<h3>刷新消息</h3>
<p>同步最新入谱申请、评论和内容通知</p>
<p>重新读取通知列表与未读数量</p>
</button>
</div>
</section>
@@ -115,7 +92,7 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/notification-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新建亲友往来 - 个人中心 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-relative-edit-page>
<header class="site-header"><div class="container nav">
<a class="brand magnetic" href="profile.html"><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a>
<nav class="nav-links"><a href="profile-content.html">内容发布</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a></nav>
<div class="nav-actions"><a href="profile-relative.html" data-genealogy-context-link>返回记录</a><button class="btn primary magnetic" type="submit" form="relative-edit-form">保存</button></div>
</div></header>
<main>
<section class="module-hero"><div class="container"><div class="module-kicker">Relative Record</div><div class="module-title-row"><div><h1>新建亲友往来</h1><p>仅提交 Apifox PC 的 RelativeRecordBody 字段。</p></div></div></div></section>
<section class="section"><div class="container module-layout">
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a href="profile-memo.html">备忘录</a></aside>
<div class="module-main"><section class="module-panel"><h2>记录内容</h2>
<form id="relative-edit-form" class="editor-form layui-form" data-relative-form>
<div class="editor-two"><div class="editor-field"><label for="relativeName">亲友姓名</label><input id="relativeName" name="relativeName" type="text" required /></div><div class="editor-field"><label for="relationName">关系名称</label><input id="relationName" name="relationName" type="text" placeholder="可选" /></div></div>
<div class="editor-two"><div class="editor-field"><label for="eventName">事件名称</label><input id="eventName" name="eventName" type="text" placeholder="可选" /></div><div class="editor-field"><label for="eventTime">事件时间</label><input id="eventTime" name="eventTime" type="text" placeholder="可选;使用带时区的 ISO 8601 时间" /></div></div>
<div class="editor-two"><div class="editor-field"><label for="giftAmount">礼金金额</label><input id="giftAmount" name="giftAmount" type="number" step="any" placeholder="可选" /></div><div class="editor-field"><label for="relativeSortOrder">排序值</label><input id="relativeSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" /></div></div>
<div class="editor-field"><label for="relativeContent">记录内容</label><textarea id="relativeContent" name="recordContent" placeholder="可选"></textarea></div>
<div class="editor-two"><div class="editor-field"><label for="relativeMediaOssIds">附件 OSS ID</label><input id="relativeMediaOssIds" name="mediaOssIds" type="text" placeholder="多个附件 ID 用英文逗号分隔" /></div><div class="editor-field"><label for="relativeStatus">状态</label><input id="relativeStatus" name="status" type="text" placeholder="可选;按后端定义填写" /></div></div>
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">保存</button><a class="btn ghost magnetic" href="profile-relative.html" data-genealogy-context-link>取消</a><span class="form-status" data-relative-form-status></span></div>
</form>
</section></div>
</div></section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><script src="public/js/profile-common.js?v=20260725-entry"></script><script src="config.js"></script><script src="utils/StorageUtil.js"></script><script src="utils/axios.js"></script><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/relative-pages.js"></script><script src="public/js/page-effects.js"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>亲友往来 - 个人中心 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-relative-page>
<header class="site-header"><div class="container nav">
<a class="brand magnetic" href="profile.html"><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a>
<nav class="nav-links"><a href="profile-content.html">内容发布</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a></nav>
<div class="nav-actions"><a href="profile-content.html">返回发布中心</a><a class="btn primary magnetic" href="profile-relative-edit.html" data-genealogy-context-link>新建记录</a></div>
</div></header>
<main>
<section class="module-hero"><div class="container"><div class="module-kicker">Relative Records</div><div class="module-title-row"><div><h1>亲友往来</h1><p>记录亲友关系、事项、时间、礼金和附件 OSS ID。</p></div></div></div></section>
<section class="section"><div class="container module-layout">
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a href="profile-memo.html">备忘录</a></aside>
<div class="module-main">
<section class="module-panel"><h2>记录列表</h2><div class="module-list" data-relative-list><div class="api-empty">亲友往来加载中...</div></div></section>
<section class="module-panel"><h2>接口状态</h2><p>Apifox 未展开列表和详情元素 DTO,页面不猜测 <code>relativeId</code>、展示字段或权限字段,因此不开放编辑和删除入口。</p></section>
</div>
</div></section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><script src="public/js/profile-common.js?v=20260725-entry"></script><script src="config.js"></script><script src="utils/StorageUtil.js"></script><script src="utils/axios.js"></script><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/relative-pages.js"></script><script src="public/js/page-effects.js"></script>
</body>
</html>
+1 -1
View File
@@ -185,7 +185,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+10 -24
View File
@@ -124,30 +124,16 @@
<div class="family-grid">
<a
class="family-card tilt-card"
href="profile-family-home.html"
href="profile-families.html"
>
<div class="book-cover">汤氏<br />家谱</div>
<div class="book-cover">我 的<br /> </div>
<div>
<h3>四川武胜汤氏族</h3>
<h3>进入我的家谱</h3>
<p>
共修入 7 人,激活 7 人。你是创建者,可管理内容和成员权限
从家谱入口选择真实家谱后,再查看主页、成员、世系图和家族内容
</p>
<div class="mini-actions">
<span>家谱主页</span><span>家族圈</span
><span>管理员</span>
</div>
</div>
</a>
<a
class="family-card tilt-card"
href="profile-family-home.html"
>
<div class="book-cover alt">刘氏<br />家谱</div>
<div>
<h3>四川达州刘氏族</h3>
<p>共修入 7 人,近期有 2 条内容更新等待查看。</p>
<div class="mini-actions">
<span>世系图</span><span>相册</span><span>消息</span>
<span>家谱主页</span><span>家族圈</span><span>成员管理</span>
</div>
</div>
</a>
@@ -162,7 +148,7 @@
把 APP 里的世代、管理员、权限、入谱审核集中到一个管理区。
</p>
</div>
<a class="link-btn" href="profile-family-admin.html"
<a class="link-btn" href="profile-family-admin.html" data-genealogy-context-link
>进入管理</a
>
</div>
@@ -241,17 +227,17 @@
<h3>贺礼邀请</h3>
<p>管理婚礼、生日、升学等贺礼。</p></a
>
<a class="content-tile tilt-card" href="profile-feed.html"
<a class="content-tile tilt-card" href="profile-families.html?next=profile-feed.html"
><span>动态</span>
<h3>发布动态</h3>
<p>面向家族圈发布图文近况。</p></a
>
<a class="content-tile tilt-card" href="profile-growth.html"
<a class="content-tile tilt-card" href="profile-families.html?next=profile-growth.html"
><span>日志</span>
<h3>成长日志</h3>
<p>记录成员成长和人生节点。</p></a
>
<a class="content-tile tilt-card" href="profile-memo.html"
<a class="content-tile tilt-card" href="profile-families.html?next=profile-memo.html"
><span>备忘</span>
<h3>人亲簿 / 备忘录</h3>
<p>保存亲友往来和家族事务。</p></a
@@ -369,7 +355,7 @@
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/profile-pages.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+27
View File
@@ -125,6 +125,21 @@
padding: 28px;
}
.rich-editor-host {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--white);
}
.rich-editor-toolbar {
border-bottom: 1px solid var(--line);
}
.rich-editor-content {
height: 320px;
}
.module-panel h2 {
margin-bottom: 10px;
font-family: "SimSun", "Songti SC", serif;
@@ -523,6 +538,18 @@
gap: 14px;
}
.notification-raw {
margin: 0;
padding: 16px;
overflow-wrap: anywhere;
white-space: pre-wrap;
border: 1px solid var(--line);
border-radius: 15px;
background: #fffdf8;
color: var(--ink);
line-height: 1.6;
}
.module-row {
display: grid;
grid-template-columns: 1fr auto;
+29 -25
View File
@@ -18,22 +18,22 @@
var FORM_CONFIG = {
login: {
selector: '#login-password-form',
captchaScene: 'PC_SMS_LOGIN',
operationCode: 'password-login',
successUrl: 'profile.html'
},
'sms-login': {
selector: '#login-sms-form',
captchaScene: 'PC_SMS_LOGIN',
operationCode: 'sms-login',
successUrl: 'profile.html'
},
register: {
selector: '#register-form',
captchaScene: 'PC_REGISTER',
operationCode: 'register',
successUrl: 'login.html'
},
'password-reset': {
selector: '#password-reset-form',
captchaScene: 'PC_FORGOT_PASSWORD',
operationCode: 'forgot-password',
successUrl: 'login.html'
}
};
@@ -47,16 +47,19 @@
}
function buildLoginBody(values, hashFn) {
// 这里只组装登录 DTO 字段,grantType、tenantId、clientId 由 api-client 统一补齐。
// 这里只组装登录 DTO 字段,grantType、tenantId 由 api-client 统一补齐clientid 只走请求头
var hash = hashFn || hashPassword;
return {
var body = {
phone: values.phone,
password: hash(values.password)
};
if (values.validToken) body.validToken = values.validToken;
return body;
}
function buildSmsLoginBody(values) {
// 短信登录接口只需要页面字段,grantType、tenantId、clientId 由 api-client 统一补齐。
// 短信登录接口只需要页面字段,grantType、tenantId 由 api-client 统一补齐clientid 只走请求头
return {
phone: values.phone,
smsCode: values.smsCode
@@ -88,11 +91,11 @@
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function getCaptchaScene(formType) {
// PC/H5 认证场景来自后台验证码配置,客户端 Key 为 web_pc
function getCaptchaOperation(formType) {
// PC 验证中心按业务动作解析服务端已激活的验证场景
var config = FORM_CONFIG[formType] || {};
return config.captchaScene || '';
return config.operationCode || '';
}
function getSuccessUrl(formType) {
@@ -101,16 +104,16 @@
return config.successUrl || '';
}
function getFormCaptchaScene(form, formType) {
// 场景码由当前脚本统一管理,避免散落在 HTML 属性中。
var sceneCode = getCaptchaScene(formType);
function getFormCaptchaOperation(form, formType) {
// 业务动作由当前脚本统一管理,避免散落在 HTML 属性中。
var operationCode = getCaptchaOperation(formType);
return sceneCode;
return operationCode;
}
async function ensureCaptcha(form, sceneCode, subject) {
// 未配置 PC/H5 验证场景时不调用验证中心,也不伪造 validToken。
if (!sceneCode) return true;
async function ensureCaptcha(form, operationCode, subject) {
// 未配置 PC 认证动作时不调用验证中心,也不伪造 validToken。
if (!operationCode) return true;
if (!root.CaptchaPages || !root.CaptchaPages.ensureToken) {
showMessage('验证码组件未加载,请刷新后重试');
@@ -118,7 +121,7 @@
}
return Boolean(await root.CaptchaPages.ensureToken(form, {
sceneCode: sceneCode,
operationCode: operationCode,
subject: subject
}));
}
@@ -310,6 +313,8 @@
return;
}
if (!validateBeforeSubmit('login', values)) return;
if (!await ensureCaptcha(form, getFormCaptchaOperation(form, 'login'), values.phone)) return;
values = readForm(form);
setBusy(button, true);
try {
await api.login(buildLoginBody(values));
@@ -364,26 +369,25 @@
}
async function sendCode(button) {
// 短信验证码场景由表单配置决定,不在按钮属性中重复声明。
// 短信验证码认证动作由表单配置决定,不在按钮属性中重复声明。
var api = getApi();
var form = button.closest('form');
var values = readForm(form);
var formType = getFormType(form);
var sceneCode = getFormCaptchaScene(form, formType);
var operationCode = getFormCaptchaOperation(form, formType);
if (!api) {
showMessage('接口初始化失败,请刷新后重试');
return;
}
if (!validateBeforeSendCode(values)) return;
if (!await ensureCaptcha(form, sceneCode, values.phone)) return;
if (!await ensureCaptcha(form, operationCode, values.phone)) return;
values = readForm(form);
setBusy(button, true);
try {
await api.sendSmsCode({
sceneCode: sceneCode,
await api.sendSmsCode(operationCode, {
phone: values.phone,
validToken: values.validToken || ''
});
@@ -473,8 +477,8 @@
buildPasswordResetBody: buildPasswordResetBody,
clearCaptchaToken: clearCaptchaToken,
startSmsCooldown: startSmsCooldown,
getCaptchaScene: getCaptchaScene,
getFormCaptchaScene: getFormCaptchaScene,
getCaptchaOperation: getCaptchaOperation,
getFormCaptchaOperation: getFormCaptchaOperation,
getSuccessUrl: getSuccessUrl,
validateAuthValues: validateAuthValues,
init: init
+7 -12
View File
@@ -9,7 +9,6 @@
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var DEFAULT_SCENE_CODE = 'PC_SMS_LOGIN';
var CAPTCHA_LAYER_AREA = ['318px', '318px'];
var captchaLayerSeed = 0;
@@ -31,13 +30,11 @@
}
function buildChallengeBody(options, api) {
// 请求体字段严格对应 PC YAML 的 VerificationChallengeBody
// operationCode 属于路径,PC 验证请求体只提交租户和验证主体
var settings = options || {};
return {
tenantId: settings.tenantId || api.tenantId,
clientId: settings.clientId || api.clientId,
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
subject: settings.subject || ''
};
}
@@ -74,8 +71,6 @@
return {
tenantId: settings.tenantId || api.tenantId,
clientId: settings.clientId || api.clientId,
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
subject: settings.subject || '',
challengeId: settings.challengeId || source.challengeId || source.id,
providerCode: settings.providerCode || 'tianai',
@@ -87,7 +82,7 @@
}
function normalizeVerifyTrack(data) {
// 后端 /captcha/verify 要求轨迹统一放在 payload.track,并显式携带 left/top。
// PC 验证接口要求轨迹统一放在 payload.track,并显式携带 left/top。
var track = Object.assign({}, data || {});
var trackList = Array.isArray(track.trackList) ? track.trackList : [];
var first = trackList[0] || {};
@@ -184,7 +179,7 @@
}
function shouldRequireCaptcha(result) {
// /captcha/require 返回 required=false 时允许直接继续业务请求。
// PC require 返回 required=false 时允许直接继续业务请求。
if (!result) return true;
return result.required !== false;
}
@@ -213,8 +208,8 @@
config = new root.CaptchaConfig({
bindEl: box,
requestCaptchaDataUrl: api.buildApiUrl('/captcha/challenge'),
validCaptchaUrl: api.buildApiUrl('/captcha/verify'),
requestCaptchaDataUrl: api.captchaChallengeUrl(settings.operationCode),
validCaptchaUrl: api.captchaVerifyUrl(settings.operationCode),
requestHeaders: {
clientid: api.clientId
},
@@ -301,10 +296,10 @@
if (!api) return '';
if (field && field.value) return field.value;
if (!settings.sceneCode) return '';
if (!settings.operationCode) return '';
try {
requirement = await api.captchaRequirement(buildChallengeBody(settings, api));
requirement = await api.captchaRequirement(settings.operationCode, buildChallengeBody(settings, api));
if (!shouldRequireCaptcha(requirement)) return 'CAPTCHA_NOT_REQUIRED';
} catch (error) {
// 查询策略失败时仍尝试直接拉起验证,避免认证接口缺少 validToken。
+1 -1
View File
@@ -377,7 +377,7 @@
genealogyId = requireGenealogyId();
if (!genealogyId) return;
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
if (root.KindEditor && root.KindEditor.sync) root.KindEditor.sync('#feedContent');
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
body = buildFeedBody(getFormValues(form));
if (!body.feedContent) {
showMessage('请填写动态内容');
+67
View File
@@ -0,0 +1,67 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GenealogyEntryPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GenealogyEntryPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var TARGET_LABELS = {
'profile-family-home.html': '家谱主页',
'profile-feed.html': '家族圈动态',
'profile-growth.html': '成长记录',
'profile-relative.html': '亲友往来',
'profile-memo.html': '备忘录',
'profile-tree.html': '世系图',
'profile-generation.html': '字辈谱'
};
function getTargetPage(search) {
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
var target = params.get('next') || '';
return /^profile-[a-z-]+\.html$/.test(target) ? target : 'profile-family-home.html';
}
function buildStatus(quota, target) {
var source = quota || {};
var details = [];
var label = TARGET_LABELS[target] || '该家谱业务';
if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + ' 部');
return '当前 PC 接口尚未提供“我的家谱”列表或家谱详情,不能伪造家谱编号进入“' + label + '”。' +
(details.length ? ' 已读取额度:' + details.join('') + '。' : '');
}
async function init() {
var status = root.document && root.document.querySelector('[data-genealogy-entry-status]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var target = getTargetPage();
if (!status) return;
if (!api || !api.genealogyQuota) {
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
return;
}
try {
status.textContent = buildStatus(await api.genealogyQuota(), target);
} catch (error) {
status.textContent = '无法读取家谱额度,请稍后重试。当前 PC 接口仍未提供家谱列表或详情,不能伪造家谱编号。';
}
}
return {
getTargetPage: getTargetPage,
buildStatus: buildStatus,
init: init
};
});
+301
View File
@@ -0,0 +1,301 @@
(function (root, factory) {
// 成长记录页面只消费 ApiClient 中已在 Apifox PC 目录核验的方法。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GrowthPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GrowthPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toSafeIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId(search) {
var page = query('[data-growth-page], [data-growth-edit-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function buildGrowthRecordBody(values) {
var source = values || {};
var body = {
recordTitle: String(source.recordTitle || '').trim()
};
var lineagePersonId = trimOrUndefined(source.lineagePersonId);
var recordType = trimOrUndefined(source.recordType);
var recordContent = trimOrUndefined(source.recordContent);
var recordDate = trimOrUndefined(source.recordDate);
var remindTime = trimOrUndefined(source.remindTime);
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
var status = trimOrUndefined(source.status);
var sortOrderText = trimOrUndefined(source.sortOrder);
if (lineagePersonId !== undefined) body.lineagePersonId = lineagePersonId;
if (recordType !== undefined) body.recordType = recordType;
if (recordContent !== undefined) body.recordContent = recordContent;
if (recordDate !== undefined) body.recordDate = recordDate;
if (remindTime !== undefined) body.remindTime = remindTime;
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (sortOrderText !== undefined) body.sortOrder = toSafeIntegerOrUndefined(sortOrderText);
if (status !== undefined) body.status = status;
return body;
}
function validateGrowthRecordBody(body) {
if (!body || !body.recordTitle) return '请填写记录标题';
if (body.lineagePersonId !== undefined && !/^\d+$/.test(body.lineagePersonId)) return '世系人物 ID 必须是整数';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
}
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
return '';
}
function normalizeGrowthList(data) {
return Array.isArray(data) ? data : [];
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function stringifyRecord(record) {
try {
return typeof record === 'string' ? record : JSON.stringify(record);
} catch (error) {
return String(record);
}
}
function renderGrowthRecords(data) {
var container = query('[data-growth-list]');
var records = normalizeGrowthList(data);
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">成长记录响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
return;
}
// ListResult 的元素 DTO 尚未展开,不能假设 recordId、标题或权限字段。
container.innerHTML = records.map(function (record, index) {
return '<article class="module-row"><div><h3>成长记录 ' + (index + 1) + '</h3><p>' +
escapeHtml(stringifyRecord(record)) + '</p></div></article>';
}).join('');
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (!root.location) return true;
if (typeof root.location.replace === 'function') {
root.location.replace('login.html');
return true;
}
root.location.href = 'login.html';
return true;
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function setFormStatus(message) {
var status = query('[data-growth-form-status]');
if (status) status.textContent = message || '';
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-growth-form] button, [data-growth-form] input, [data-growth-form] textarea').forEach(function (control) {
control.disabled = writePending;
});
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
return;
}
queryAll('[data-genealogy-context-link]').forEach(function (link) {
var href = link.getAttribute('href');
var base;
if (!href || href === '#') return;
base = href.split('?')[0];
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
});
}
function renderNoContext() {
var list = query('[data-growth-list]');
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入成长记录。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入成长记录。');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
showMessage('等待家谱入口接口;请从具体家谱进入成长记录。');
}
return genealogyId;
}
function buildListUrl(genealogyId) {
return 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
}
async function loadGrowthRecords() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
renderGrowthRecords(await api.growthRecords(genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
renderGrowthRecords(null);
showMessage(error.message || '成长记录加载失败');
}
}
async function submitGrowthRecord(form) {
var api = getApi();
var genealogyId;
var body;
var validation;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
body = buildGrowthRecordBody(getFormValues(form));
validation = validateGrowthRecordBody(body);
if (validation) {
setFormStatus(validation);
return;
}
setWritePending(true);
setFormStatus('正在保存成长记录...');
try {
await api.createGrowthRecord(genealogyId, body);
if (root.location) root.location.href = buildListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFormStatus(error.message || '成长记录保存失败');
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-growth-form]');
if (!form) return;
event.preventDefault();
submitGrowthRecord(form);
});
}
function init() {
var genealogyId;
if (!documentRef) return;
bindActions();
if (query('[data-growth-page]')) loadGrowthRecords();
if (query('[data-growth-edit-page]')) {
genealogyId = requireGenealogyId();
if (genealogyId) syncGenealogyLinks(genealogyId);
}
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
buildGrowthRecordBody: buildGrowthRecordBody,
validateGrowthRecordBody: validateGrowthRecordBody,
normalizeGrowthList: normalizeGrowthList,
shouldRedirectToLogin: shouldRedirectToLogin,
init: init
};
});
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

-948
View File
@@ -1,948 +0,0 @@
/* common */
.ke-inline-block {
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-clearfix {
zoom: 1;
}
.ke-clearfix:after {
content: ".";
display: block;
clear: both;
font-size: 0;
height: 0;
line-height: 0;
visibility: hidden;
}
.ke-shadow {
box-shadow: 1px 1px 3px #A0A0A0;
-moz-box-shadow: 1px 1px 3px #A0A0A0;
-webkit-box-shadow: 1px 1px 3px #A0A0A0;
filter: progid:DXImageTransform.Microsoft.Shadow(color='#A0A0A0', Direction=135, Strength=3);
background-color: #F0F0EE;
}
.ke-menu a,
.ke-menu a:hover,
.ke-dialog a,
.ke-dialog a:hover {
color: #337FE5;
text-decoration: none;
}
/* icons */
.ke-icon-source {
background-position: 0px 0px;
width: 16px;
height: 16px;
}
.ke-icon-preview {
background-position: 0px -16px;
width: 16px;
height: 16px;
}
.ke-icon-print {
background-position: 0px -32px;
width: 16px;
height: 16px;
}
.ke-icon-undo {
background-position: 0px -48px;
width: 16px;
height: 16px;
}
.ke-icon-redo {
background-position: 0px -64px;
width: 16px;
height: 16px;
}
.ke-icon-cut {
background-position: 0px -80px;
width: 16px;
height: 16px;
}
.ke-icon-copy {
background-position: 0px -96px;
width: 16px;
height: 16px;
}
.ke-icon-paste {
background-position: 0px -112px;
width: 16px;
height: 16px;
}
.ke-icon-selectall {
background-position: 0px -128px;
width: 16px;
height: 16px;
}
.ke-icon-justifyleft {
background-position: 0px -144px;
width: 16px;
height: 16px;
}
.ke-icon-justifycenter {
background-position: 0px -160px;
width: 16px;
height: 16px;
}
.ke-icon-justifyright {
background-position: 0px -176px;
width: 16px;
height: 16px;
}
.ke-icon-justifyfull {
background-position: 0px -192px;
width: 16px;
height: 16px;
}
.ke-icon-insertorderedlist {
background-position: 0px -208px;
width: 16px;
height: 16px;
}
.ke-icon-insertunorderedlist {
background-position: 0px -224px;
width: 16px;
height: 16px;
}
.ke-icon-indent {
background-position: 0px -240px;
width: 16px;
height: 16px;
}
.ke-icon-outdent {
background-position: 0px -256px;
width: 16px;
height: 16px;
}
.ke-icon-subscript {
background-position: 0px -272px;
width: 16px;
height: 16px;
}
.ke-icon-superscript {
background-position: 0px -288px;
width: 16px;
height: 16px;
}
.ke-icon-date {
background-position: 0px -304px;
width: 25px;
height: 16px;
}
.ke-icon-time {
background-position: 0px -320px;
width: 25px;
height: 16px;
}
.ke-icon-formatblock {
background-position: 0px -336px;
width: 25px;
height: 16px;
}
.ke-icon-fontname {
background-position: 0px -352px;
width: 21px;
height: 16px;
}
.ke-icon-fontsize {
background-position: 0px -368px;
width: 23px;
height: 16px;
}
.ke-icon-forecolor {
background-position: 0px -384px;
width: 20px;
height: 16px;
}
.ke-icon-hilitecolor {
background-position: 0px -400px;
width: 23px;
height: 16px;
}
.ke-icon-bold {
background-position: 0px -416px;
width: 16px;
height: 16px;
}
.ke-icon-italic {
background-position: 0px -432px;
width: 16px;
height: 16px;
}
.ke-icon-underline {
background-position: 0px -448px;
width: 16px;
height: 16px;
}
.ke-icon-strikethrough {
background-position: 0px -464px;
width: 16px;
height: 16px;
}
.ke-icon-removeformat {
background-position: 0px -480px;
width: 16px;
height: 16px;
}
.ke-icon-image {
background-position: 0px -496px;
width: 16px;
height: 16px;
}
.ke-icon-flash {
background-position: 0px -512px;
width: 16px;
height: 16px;
}
.ke-icon-media {
background-position: 0px -528px;
width: 16px;
height: 16px;
}
.ke-icon-div {
background-position: 0px -544px;
width: 16px;
height: 16px;
}
.ke-icon-formula {
background-position: 0px -576px;
width: 16px;
height: 16px;
}
.ke-icon-hr {
background-position: 0px -592px;
width: 16px;
height: 16px;
}
.ke-icon-emoticons {
background-position: 0px -608px;
width: 16px;
height: 16px;
}
.ke-icon-link {
background-position: 0px -624px;
width: 16px;
height: 16px;
}
.ke-icon-unlink {
background-position: 0px -640px;
width: 16px;
height: 16px;
}
.ke-icon-fullscreen {
background-position: 0px -656px;
width: 16px;
height: 16px;
}
.ke-icon-about {
background-position: 0px -672px;
width: 16px;
height: 16px;
}
.ke-icon-plainpaste {
background-position: 0px -704px;
width: 16px;
height: 16px;
}
.ke-icon-wordpaste {
background-position: 0px -720px;
width: 16px;
height: 16px;
}
.ke-icon-table {
background-position: 0px -784px;
width: 16px;
height: 16px;
}
.ke-icon-tablemenu {
background-position: 0px -768px;
width: 16px;
height: 16px;
}
.ke-icon-tableinsert {
background-position: 0px -784px;
width: 16px;
height: 16px;
}
.ke-icon-tabledelete {
background-position: 0px -800px;
width: 16px;
height: 16px;
}
.ke-icon-tablecolinsertleft {
background-position: 0px -816px;
width: 16px;
height: 16px;
}
.ke-icon-tablecolinsertright {
background-position: 0px -832px;
width: 16px;
height: 16px;
}
.ke-icon-tablerowinsertabove {
background-position: 0px -848px;
width: 16px;
height: 16px;
}
.ke-icon-tablerowinsertbelow {
background-position: 0px -864px;
width: 16px;
height: 16px;
}
.ke-icon-tablecoldelete {
background-position: 0px -880px;
width: 16px;
height: 16px;
}
.ke-icon-tablerowdelete {
background-position: 0px -896px;
width: 16px;
height: 16px;
}
.ke-icon-tablecellprop {
background-position: 0px -912px;
width: 16px;
height: 16px;
}
.ke-icon-tableprop {
background-position: 0px -928px;
width: 16px;
height: 16px;
}
.ke-icon-checked {
background-position: 0px -944px;
width: 16px;
height: 16px;
}
.ke-icon-code {
background-position: 0px -960px;
width: 16px;
height: 16px;
}
.ke-icon-map {
background-position: 0px -976px;
width: 16px;
height: 16px;
}
.ke-icon-baidumap {
background-position: 0px -976px;
width: 16px;
height: 16px;
}
.ke-icon-lineheight {
background-position: 0px -992px;
width: 16px;
height: 16px;
}
.ke-icon-clearhtml {
background-position: 0px -1008px;
width: 16px;
height: 16px;
}
.ke-icon-pagebreak {
background-position: 0px -1024px;
width: 16px;
height: 16px;
}
.ke-icon-insertfile {
background-position: 0px -1040px;
width: 16px;
height: 16px;
}
.ke-icon-quickformat {
background-position: 0px -1056px;
width: 16px;
height: 16px;
}
.ke-icon-template {
background-position: 0px -1072px;
width: 16px;
height: 16px;
}
.ke-icon-tablecellsplit {
background-position: 0px -1088px;
width: 16px;
height: 16px;
}
.ke-icon-tablerowmerge {
background-position: 0px -1104px;
width: 16px;
height: 16px;
}
.ke-icon-tablerowsplit {
background-position: 0px -1120px;
width: 16px;
height: 16px;
}
.ke-icon-tablecolmerge {
background-position: 0px -1136px;
width: 16px;
height: 16px;
}
.ke-icon-tablecolsplit {
background-position: 0px -1152px;
width: 16px;
height: 16px;
}
.ke-icon-anchor {
background-position: 0px -1168px;
width: 16px;
height: 16px;
}
.ke-icon-search {
background-position: 0px -1184px;
width: 16px;
height: 16px;
}
.ke-icon-new {
background-position: 0px -1200px;
width: 16px;
height: 16px;
}
.ke-icon-specialchar {
background-position: 0px -1216px;
width: 16px;
height: 16px;
}
.ke-icon-multiimage {
background-position: 0px -1232px;
width: 16px;
height: 16px;
}
.ke-icon-zijianju {
background-position: 0px -1248px;
width: 16px;
height: 16px;
}
.ke-icon-duansuo {
background-position: 0px -1264px;
width: 16px;
height: 16px;
}
/* container */
.ke-container {
display: block;
/*border: 1px solid #CCCCCC;*/
background-color: #FFF;
overflow: hidden;
padding: 0;
}
/* toolbar */
.ke-toolbar {
border-bottom: 1px solid #CCC;
background-color: #F0F0EE;
text-align: left;
overflow: hidden;
zoom: 1;
}
.ke-toolbar-icon {
background-repeat: no-repeat;
font-size: 0;
line-height: 0;
overflow: hidden;
display: block;
}
.ke-toolbar-icon-url {
background-image: url(default.png);
}
.ke-toolbar .ke-outline {
border: 1px solid #F0F0EE;
/*margin: 1px;*/
margin: 8px 9px;
padding: 1px 2px;
font-size: 0;
line-height: 0;
overflow: hidden;
cursor: pointer;
display: block;
float: left;
}
.ke-toolbar .ke-on {
border: 1px solid #5690D2;
}
.ke-toolbar .ke-selected {
border: 1px solid #5690D2;
background-color: #E9EFF6;
}
.ke-toolbar .ke-disabled {
cursor: default;
}
.ke-toolbar .ke-separator {
height: 16px;
margin: 2px 3px;
border-left: 1px solid #A0A0A0;
border-right: 1px solid #FFFFFF;
border-top:0;
border-bottom:0;
width: 0;
font-size: 0;
line-height: 0;
overflow: hidden;
display: block;
float: left;
}
.ke-toolbar .ke-hr {
overflow: hidden;
height: 1px;
clear: both;
}
/* edit */
.ke-edit {
padding: 0;
}
.ke-edit-iframe,
.ke-edit-textarea {
border: 0;
margin: 0;
padding: 0;
overflow: auto;
}
.ke-edit-textarea {
font: 12px/1.5 "Consolas", "Monaco", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
color: #000;
overflow: auto;
resize: none;
}
.ke-edit-textarea:focus {
outline: none;
}
/* statusbar */
.ke-statusbar {
position: relative;
background-color: #F0F0EE;
border-top: 1px solid #CCCCCC;
font-size: 0;
line-height: 0;
*height: 12px;
overflow: hidden;
text-align: center;
cursor: s-resize;
}
.ke-statusbar-center-icon {
background-position: -0px -754px;
width: 15px;
height: 11px;
background-image: url(default.png);
}
.ke-statusbar-right-icon {
position: absolute;
right: 0;
bottom: 0;
cursor: se-resize;
background-position: -5px -741px;
width: 11px;
height: 11px;
background-image: url(default.png);
}
/* menu */
.ke-menu {
border: 1px solid #A0A0A0;
background-color: #F1F1F1;
color: #222222;
padding: 2px;
font-family: "sans serif",tahoma,verdana,helvetica;
font-size: 12px;
text-align: left;
overflow: hidden;
}
.ke-menu-item {
border: 1px solid #F1F1F1;
background-color: #F1F1F1;
color: #222222;
height: 24px;
overflow: hidden;
cursor: pointer;
}
.ke-menu-item-on {
border: 1px solid #5690D2;
background-color: #E9EFF6;
}
.ke-menu-item-left {
width: 27px;
text-align: center;
overflow: hidden;
}
.ke-menu-item-center {
width: 0;
height: 24px;
border-left: 1px solid #E3E3E3;
border-right: 1px solid #FFFFFF;
border-top: 0;
border-bottom: 0;
}
.ke-menu-item-center-on {
border-left: 1px solid #E9EFF6;
border-right: 1px solid #E9EFF6;
}
.ke-menu-item-right {
border: 0;
padding: 0 0 0 5px;
line-height: 24px;
text-align: left;
overflow: hidden;
}
.ke-menu-separator {
margin: 2px 0;
height: 0;
overflow: hidden;
border-top: 1px solid #CCCCCC;
border-bottom: 1px solid #FFFFFF;
border-left: 0;
border-right: 0;
}
/* colorpicker */
.ke-colorpicker {
border: 1px solid #A0A0A0;
background-color: #F1F1F1;
color: #222222;
padding: 2px;
}
.ke-colorpicker-table {
border:0;
margin:0;
padding:0;
border-collapse: separate;
}
.ke-colorpicker-cell {
font-size: 0;
line-height: 0;
border: 1px solid #F0F0EE;
cursor: pointer;
margin:3px;
padding:0;
}
.ke-colorpicker-cell-top {
font-family: "sans serif",tahoma,verdana,helvetica;
font-size: 12px;
line-height: 24px;
border: 1px solid #F0F0EE;
cursor: pointer;
margin:0;
padding:0;
text-align: center;
}
.ke-colorpicker-cell-on {
border: 1px solid #5690D2;
}
.ke-colorpicker-cell-selected {
border: 1px solid #2446AB;
}
.ke-colorpicker-cell-color {
width: 14px;
height: 14px;
margin: 3px;
padding: 0;
border: 0;
}
/* dialog */
.ke-dialog {
position: absolute;
margin: 0;
padding: 0;
}
.ke-dialog .ke-header {
width: 100%;
margin-bottom: 10px;
}
.ke-dialog .ke-header .ke-left {
float: left;
}
.ke-dialog .ke-header .ke-right {
float: right;
}
.ke-dialog .ke-header label {
margin-right: 0;
cursor: pointer;
font-weight: normal;
display: inline;
vertical-align: top;
}
.ke-dialog-content {
background-color: #FFF;
width: 100%;
height: 100%;
color: #333;
border: 1px solid #A0A0A0;
}
.ke-dialog-shadow {
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: 3px 3px 7px #999;
-moz-box-shadow: 3px 3px 7px #999;
-webkit-box-shadow: 3px 3px 7px #999;
filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius='3', MakeShadow='true', ShadowOpacity='0.4');
background-color: #F0F0EE;
}
.ke-dialog-header {
border:0;
margin:0;
padding: 0 10px;
background: url(background.png) repeat scroll 0 0 #F0F0EE;
border-bottom: 1px solid #CFCFCF;
height: 24px;
font: 12px/24px "sans serif",tahoma,verdana,helvetica;
text-align: left;
color: #222;
cursor: move;
}
.ke-dialog-icon-close {
display: block;
background: url(default.png) no-repeat scroll 0px -688px;
width: 16px;
height: 16px;
position: absolute;
right: 6px;
top: 6px;
cursor: pointer;
}
.ke-dialog-body {
font: 12px/1.5 "sans serif",tahoma,verdana,helvetica;
text-align: left;
overflow: hidden;
width: 100%;
}
.ke-dialog-body textarea {
display: block;
overflow: auto;
padding: 0;
resize: none;
}
.ke-dialog-body textarea:focus,
.ke-dialog-body input:focus,
.ke-dialog-body select:focus {
outline: none;
}
.ke-dialog-body label {
margin-right: 10px;
cursor: pointer;
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-dialog-body img {
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-dialog-body select {
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
width: auto;
}
.ke-dialog-body .ke-textarea {
display: block;
width: 408px;
height: 260px;
font-family: "sans serif",tahoma,verdana,helvetica;
font-size: 12px;
border-color: #848484 #E0E0E0 #E0E0E0 #848484;
border-style: solid;
border-width: 1px;
}
.ke-dialog-body .ke-form {
margin: 0;
padding: 0;
}
.ke-dialog-loading {
position: absolute;
top: 0;
left: 1px;
z-index: 1;
text-align: center;
}
.ke-dialog-loading-content {
background: url("../common/loading.gif") no-repeat;
color: #666;
font-size: 14px;
font-weight: bold;
height: 31px;
line-height: 31px;
padding-left: 36px;
}
.ke-dialog-row {
margin-bottom: 10px;
}
.ke-dialog-footer {
font: 12px/1 "sans serif",tahoma,verdana,helvetica;
text-align: right;
padding:0 0 5px 0;
background-color: #FFF;
width: 100%;
}
.ke-dialog-preview,
.ke-dialog-yes {
margin: 5px;
}
.ke-dialog-no {
margin: 5px 10px 5px 5px;
}
.ke-dialog-mask {
background-color:#FFF;
filter:alpha(opacity=50);
opacity:0.5;
}
.ke-button-common {
background: url(background.png) no-repeat;
cursor: pointer;
height: 23px;
line-height: 23px;
overflow: visible;
display: inline-block;
vertical-align: top;
cursor: pointer;
}
.ke-button-outer {
background-position: 0 -25px;
padding: 0;
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-button {
background-position: right -25px;
padding: 0 14px 0 12px;
margin: 0 0 0 2px;
font-family: "sans serif",tahoma,verdana,helvetica;
border: 0 none;
color: #333;
font-size: 12px;
text-decoration: none;
}
/* inputbox */
.ke-input-text {
background-color:#FFFFFF;
font-family: "sans serif",tahoma,verdana,helvetica;
font-size: 12px;
line-height: 17px;
height: 17px;
padding: 2px 4px;
border-color: #848484 #E0E0E0 #E0E0E0 #848484;
border-style: solid;
border-width: 1px;
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-input-number {
width: 50px;
}
.ke-input-color {
border: 1px solid #A0A0A0;
background-color: #FFFFFF;
font-size: 12px;
width: 60px;
height: 20px;
line-height: 20px;
padding-left: 5px;
overflow: hidden;
cursor: pointer;
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
zoom: 1;
*display: inline;
}
.ke-upload-button {
position: relative;
}
.ke-upload-area {
position: relative;
overflow: hidden;
margin: 0;
padding: 0;
*height: 25px;
}
.ke-upload-area .ke-upload-file {
position: absolute;
font-size: 60px;
top: 0;
right: 0;
padding: 0;
margin: 0;
z-index: 811212;
border: 0 none;
opacity: 0;
filter: alpha(opacity=0);
}
/* tabs */
.ke-tabs {
font: 12px/1 "sans serif",tahoma,verdana,helvetica;
border-bottom:1px solid #A0A0A0;
padding-left:5px;
margin-bottom:20px;
}
.ke-tabs-ul {
list-style-image:none;
list-style-position:outside;
list-style-type:none;
margin:0;
padding:0;
}
.ke-tabs-li {
position: relative;
border: 1px solid #A0A0A0;
background-color: #F0F0EE;
margin: 0 2px -1px 0;
padding: 0 20px;
float: left;
line-height: 25px;
text-align: center;
color: #555555;
cursor: pointer;
}
.ke-tabs-li-selected {
background-color: #FFF;
border-bottom: 1px solid #FFF;
color: #000;
cursor: default;
}
.ke-tabs-li-on {
background-color: #FFF;
color: #000;
}
/* progressbar */
.ke-progressbar {
position: relative;
margin: 0;
padding: 0;
}
.ke-progressbar-bar {
border: 1px solid #6FA5DB;
width: 80px;
height: 5px;
margin: 10px 10px 0 10px;
padding: 0;
}
.ke-progressbar-bar-inner {
width: 0;
height: 5px;
background-color: #6FA5DB;
overflow: hidden;
margin: 0;
padding: 0;
}
.ke-progressbar-percent {
position: absolute;
top: 0;
left: 40%;
display: none;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

+289
View File
@@ -0,0 +1,289 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MemoPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MemoPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toSafeIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId(search) {
var page = query('[data-memo-page], [data-memo-edit-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function buildMemoBody(values) {
var source = values || {};
var body = { memoTitle: String(source.memoTitle || '').trim() };
var fields = ['memoContent', 'remindTime', 'completed', 'mediaOssIds', 'status'];
var sortOrderText = trimOrUndefined(source.sortOrder);
fields.forEach(function (field) {
var value = trimOrUndefined(source[field]);
if (value !== undefined) body[field] = value;
});
if (sortOrderText !== undefined) body.sortOrder = toSafeIntegerOrUndefined(sortOrderText);
return body;
}
function validateMemoBody(body) {
if (!body || !body.memoTitle) return '请填写备忘标题';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
}
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
return '';
}
function normalizeMemoList(data) {
return Array.isArray(data) ? data : [];
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function stringifyMemo(memo) {
try {
return typeof memo === 'string' ? memo : JSON.stringify(memo);
} catch (error) {
return String(memo);
}
}
function renderMemos(data) {
var container = query('[data-memo-list]');
var memos = normalizeMemoList(data);
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">备忘录响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!memos.length) {
container.innerHTML = '<div class="api-empty">暂无备忘录</div>';
return;
}
// ListResult 的元素 DTO 尚未展开,不能假设 memoId、标题或权限字段。
container.innerHTML = memos.map(function (memo, index) {
return '<article class="module-row"><div><h3>备忘录 ' + (index + 1) + '</h3><p>' +
escapeHtml(stringifyMemo(memo)) + '</p></div></article>';
}).join('');
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (!root.location) return true;
if (typeof root.location.replace === 'function') {
root.location.replace('login.html');
return true;
}
root.location.href = 'login.html';
return true;
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function setFormStatus(message) {
var status = query('[data-memo-form-status]');
if (status) status.textContent = message || '';
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-memo-form] button, [data-memo-form] input, [data-memo-form] textarea, [data-memo-form] select').forEach(function (control) {
control.disabled = writePending;
});
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
return;
}
queryAll('[data-genealogy-context-link]').forEach(function (link) {
var href = link.getAttribute('href');
var base;
if (!href || href === '#') return;
base = href.split('?')[0];
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
});
}
function renderNoContext() {
var list = query('[data-memo-list]');
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入备忘录。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入备忘录。');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
showMessage('等待家谱入口接口;请从具体家谱进入备忘录。');
}
return genealogyId;
}
function buildListUrl(genealogyId) {
return 'profile-memo.html?genealogyId=' + encodeURIComponent(genealogyId);
}
async function loadMemos() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
renderMemos(await api.memos(genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
renderMemos(null);
showMessage(error.message || '备忘录加载失败');
}
}
async function submitMemo(form) {
var api = getApi();
var genealogyId;
var body;
var validation;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
body = buildMemoBody(getFormValues(form));
validation = validateMemoBody(body);
if (validation) {
setFormStatus(validation);
return;
}
setWritePending(true);
setFormStatus('正在保存备忘录...');
try {
await api.createMemo(genealogyId, body);
if (root.location) root.location.href = buildListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFormStatus(error.message || '备忘录保存失败');
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-memo-form]');
if (!form) return;
event.preventDefault();
submitMemo(form);
});
}
function init() {
var genealogyId;
if (!documentRef) return;
bindActions();
if (query('[data-memo-page]')) loadMemos();
if (query('[data-memo-edit-page]')) {
genealogyId = requireGenealogyId();
if (genealogyId) syncGenealogyLinks(genealogyId);
}
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
buildMemoBody: buildMemoBody,
validateMemoBody: validateMemoBody,
normalizeMemoList: normalizeMemoList,
shouldRedirectToLogin: shouldRedirectToLogin,
init: init
};
});
+132
View File
@@ -0,0 +1,132 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.NotificationPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.NotificationPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function normalizeNotifications(data) {
return Array.isArray(data) ? data : [];
}
function formatNotification(data) {
return JSON.stringify(data || {}, null, 2);
}
function getUnreadCount(data) {
var count = Number(data);
return Number.isFinite(count) && count >= 0 ? count : 0;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
}
function renderNotifications(container, notifications) {
if (!container) return;
container.innerHTML = '';
if (!notifications.length) {
container.textContent = '暂无通知';
return;
}
notifications.forEach(function (notification) {
var row = documentRef.createElement('pre');
row.className = 'notification-raw';
row.textContent = formatNotification(notification);
container.appendChild(row);
});
}
function updateStatus(message) {
var status = query('[data-notification-status]');
if (status) status.textContent = message;
}
async function loadNotifications() {
var api = getApi();
var container = query('[data-notification-list]');
var countTarget = query('[data-notification-unread-count]');
var result;
if (!api || !api.notifications || !api.unreadNotificationCount) {
updateStatus('消息接口初始化失败,请刷新后重试');
return;
}
updateStatus('正在同步消息…');
try {
result = await Promise.all([api.notifications(), api.unreadNotificationCount()]);
renderNotifications(container, normalizeNotifications(result[0]));
if (countTarget) countTarget.textContent = String(getUnreadCount(result[1]));
updateStatus('已同步消息。列表元素 DTO 尚未在 PC 契约展开,以下保留服务端原始记录。');
} catch (error) {
updateStatus(error.message || '消息同步失败');
}
}
async function markAllRead(button) {
var api = getApi();
if (!api || !api.markAllNotificationsRead) {
updateStatus('消息接口初始化失败,请刷新后重试');
return;
}
button.disabled = true;
try {
await api.markAllNotificationsRead();
showMessage('已全部标记为已读');
await loadNotifications();
} catch (error) {
updateStatus(error.message || '全部标记已读失败');
} finally {
button.disabled = false;
}
}
function init() {
var readAll = query('[data-notification-read-all]');
var refresh = query('[data-notification-refresh]');
if (!documentRef || !query('[data-notification-list]')) return;
if (readAll) readAll.addEventListener('click', function () { markAllRead(readAll); });
if (refresh) refresh.addEventListener('click', loadNotifications);
loadNotifications();
}
return {
normalizeNotifications: normalizeNotifications,
formatNotification: formatNotification,
getUnreadCount: getUnreadCount,
loadNotifications: loadNotifications,
init: init
};
});
+10 -2
View File
@@ -147,14 +147,21 @@
return parts[0] + '?' + params.toString() + hash;
}
function getGenealogyEntryUrl(href) {
var value = String(href || '');
if (!value || value.charAt(0) === '#' || /^(?:https?:|mailto:|tel:)/i.test(value)) return value;
if (value.indexOf('profile-families.html') === 0) return value;
return 'profile-families.html?next=' + encodeURIComponent(value.split('?')[0]);
}
function syncGenealogyContextLinks() {
var genealogyId = getGenealogyId();
if (!genealogyId) return;
$('[data-genealogy-context-link]').each(function () {
var $link = $(this);
$link.attr('href', withGenealogyId($link.attr('href'), genealogyId));
$link.attr('href', genealogyId ? withGenealogyId($link.attr('href'), genealogyId) : getGenealogyEntryUrl($link.attr('href')));
});
}
@@ -272,6 +279,7 @@
initLayui: initLayui,
getGenealogyId: getGenealogyId,
withGenealogyId: withGenealogyId,
getGenealogyEntryUrl: getGenealogyEntryUrl,
syncGenealogyContextLinks: syncGenealogyContextLinks
};
})(window, window.jQuery || (window.layui && window.layui.$));
+168
View File
@@ -0,0 +1,168 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.RelativePages = factory(root);
if (root.document) root.document.addEventListener('DOMContentLoaded', function () { root.RelativePages.init(); });
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var writePending = false;
function getApi() { return root.GenealogyApi && root.GenealogyApi.defaultClient; }
function query(selector, node) { return documentRef ? (node || documentRef).querySelector(selector) : null; }
function queryAll(selector, node) { return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : []; }
function trim(value) { var text = String(value === undefined || value === null ? '' : value).trim(); return text || undefined; }
function queryParam(search, name) { return new URLSearchParams(String(search || '').replace(/^\?/, '')).get(name) || ''; }
function getCurrentGenealogyId(search) {
var page = query('[data-relative-page], [data-relative-edit-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
}
return queryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function toSafeNumber(value) {
var text = trim(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isFinite(number) ? number : undefined;
}
function toSafeInteger(value) {
var number = toSafeNumber(value);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function buildRelativeRecordBody(values) {
var source = values || {};
var body = { relativeName: String(source.relativeName || '').trim() };
var fields = ['relationName', 'eventName', 'eventTime', 'recordContent', 'mediaOssIds', 'status'];
var amountText = trim(source.giftAmount);
var sortOrderText = trim(source.sortOrder);
fields.forEach(function (field) {
var value = trim(source[field]);
if (value !== undefined) body[field] = value;
});
if (amountText !== undefined) body.giftAmount = toSafeNumber(amountText);
if (sortOrderText !== undefined) body.sortOrder = toSafeInteger(sortOrderText);
return body;
}
function validateRelativeRecordBody(body) {
if (!body || !body.relativeName) return '请填写亲友姓名';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
}
if (Object.prototype.hasOwnProperty.call(body, 'giftAmount') && body.giftAmount === undefined) return '礼金金额必须是数字';
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
return '';
}
function normalizeRelativeList(data) { return Array.isArray(data) ? data : []; }
function escapeHtml(value) { return String(value === undefined || value === null ? '' : value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
function stringify(value) { try { return typeof value === 'string' ? value : JSON.stringify(value); } catch (error) { return String(value); } }
function renderRelativeRecords(data) {
var container = query('[data-relative-list]');
var records = normalizeRelativeList(data);
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">亲友往来响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无亲友往来记录</div>';
return;
}
container.innerHTML = records.map(function (record, index) {
return '<article class="module-row"><div><h3>亲友往来 ' + (index + 1) + '</h3><p>' + escapeHtml(stringify(record)) + '</p></div></article>';
}).join('');
}
function showMessage(message) { if (root.layui && root.layui.layer) root.layui.layer.msg(message); else if (root.alert) root.alert(message); }
function shouldRedirectToLogin(api, error) { var status = error && (error.status || error.code); return !api || !api.getToken || !api.getToken() || Number(status) === 401; }
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (root.location && typeof root.location.replace === 'function') root.location.replace('login.html');
else if (root.location) root.location.href = 'login.html';
return true;
}
function setFormStatus(message) { var target = query('[data-relative-form-status]'); if (target) target.textContent = message || ''; }
function setWritePending(value) { writePending = Boolean(value); queryAll('[data-relative-form] button, [data-relative-form] input, [data-relative-form] textarea').forEach(function (control) { control.disabled = writePending; }); }
function syncLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) { root.ProfileUI.syncGenealogyContextLinks(); return; }
queryAll('[data-genealogy-context-link]').forEach(function (link) { var href = link.getAttribute('href'); if (href) link.href = href.split('?')[0] + '?genealogyId=' + encodeURIComponent(genealogyId); });
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
var list = query('[data-relative-list]');
if (genealogyId) return genealogyId;
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入亲友往来。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入亲友往来。');
showMessage('等待家谱入口接口;请从具体家谱进入亲友往来。');
return '';
}
async function loadRelativeRecords() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks(genealogyId);
try { renderRelativeRecords(await api.relativeRecords(genealogyId)); }
catch (error) { if (!redirectUnauthorized(api, error)) { renderRelativeRecords(null); showMessage(error.message || '亲友往来加载失败'); } }
}
async function submitRelativeRecord(form) {
var api = getApi();
var genealogyId;
var values = {};
var body;
var validation;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; });
body = buildRelativeRecordBody(values);
validation = validateRelativeRecordBody(body);
if (validation) { setFormStatus(validation); return; }
setWritePending(true);
setFormStatus('正在保存亲友往来...');
try {
await api.createRelativeRecord(genealogyId, body);
if (root.location) root.location.href = 'profile-relative.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
if (!redirectUnauthorized(api, error)) setFormStatus(error.message || '亲友往来保存失败');
} finally { setWritePending(false); }
}
function init() {
var genealogyId;
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) { var form = event.target.closest('[data-relative-form]'); if (form) { event.preventDefault(); submitRelativeRecord(form); } });
if (query('[data-relative-page]')) loadRelativeRecords();
if (query('[data-relative-edit-page]')) { genealogyId = requireGenealogyId(); if (genealogyId) syncLinks(genealogyId); }
}
return { getCurrentGenealogyId: getCurrentGenealogyId, buildRelativeRecordBody: buildRelativeRecordBody, validateRelativeRecordBody: validateRelativeRecordBody, normalizeRelativeList: normalizeRelativeList, shouldRedirectToLogin: shouldRedirectToLogin, init: init };
});
+122 -72
View File
@@ -1,93 +1,143 @@
(function (window, $) {
(function (root) {
'use strict';
var defaultItems = [
'source', '|',
'undo', 'redo', '|',
'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
'plainpaste', 'wordpaste', '|',
'justifyleft', 'justifycenter', 'justifyright', 'justifyfull',
'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', '|',
'formatblock', 'fontname', 'fontsize', '|',
'forecolor', 'hilitecolor', 'bold', 'italic', 'underline',
'strikethrough', 'lineheight', 'removeformat', '|',
'image', 'multiimage', 'table', 'hr', 'emoticons', 'baidumap',
'pagebreak', 'anchor', 'link', 'unlink', '|',
'about'
var documentRef = root.document;
var editors = {};
var submitBound = false;
var toolbarKeys = [
'headerSelect', 'fontSize', 'color', 'bgColor', '|',
'bold', 'italic', 'underline', 'through', '|',
'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyJustify', '|',
'insertTable', 'clearStyle', 'insertLink', 'undo', 'redo'
];
function getJquery() {
return $ || window.jQuery || (window.layui && window.layui.$);
function findTextarea(target) {
if (!target || !documentRef) return null;
if (typeof target === 'string') return documentRef.querySelector(target);
if (target.nodeType === 1) return target;
return target[0] || null;
}
function normalizeSelector(target) {
var jq = getJquery();
if (!target) return '';
if (typeof target === 'string') return target;
if (jq && target instanceof jq) target = target.get(0);
if (target && target.id) return '#' + target.id;
return target;
function ensureId(textarea) {
if (!textarea.id) textarea.id = 'rich-editor-' + Math.floor(Math.random() * 1000000);
return textarea.id;
}
function initRichEditor(target, options) {
var jq = getJquery();
var selector = normalizeSelector(target);
var $textarea;
function fieldValue(editor) {
return String(editor.getText() || '').trim() ? editor.getHtml() : '';
}
if (!jq) return null;
$textarea = jq(selector).first();
if (!$textarea.length || !window.KindEditor) return null;
function syncEditor(instance) {
instance.textarea.value = fieldValue(instance.editor);
}
if (!$textarea.attr('id')) {
$textarea.attr('id', 'rich-editor-' + Math.floor(Math.random() * 1000000));
selector = '#' + $textarea.attr('id');
function createRichEditor(target, options) {
var textarea = findTextarea(target);
var settings = options || {};
var id;
var host;
var toolbar;
var content;
var editor;
var wangEditor = root.wangEditor;
var instance;
if (!textarea || !wangEditor || !wangEditor.createEditor || !wangEditor.createToolbar) return null;
if (textarea.dataset.wangEditorInitialized === 'true') return editors[textarea.id] || null;
id = ensureId(textarea);
host = documentRef.createElement('div');
host.className = 'rich-editor-host';
toolbar = documentRef.createElement('div');
toolbar.className = 'rich-editor-toolbar';
toolbar.id = id + '-toolbar';
content = documentRef.createElement('div');
content.className = 'rich-editor-content';
content.id = id + '-content';
host.appendChild(toolbar);
host.appendChild(content);
textarea.insertAdjacentElement('afterend', host);
try {
editor = wangEditor.createEditor({
selector: '#' + content.id,
html: textarea.value || '',
config: Object.assign({
placeholder: textarea.getAttribute('placeholder') || '请输入内容',
onChange: function (currentEditor) {
textarea.value = fieldValue(currentEditor);
},
onBlur: function (currentEditor) {
textarea.value = fieldValue(currentEditor);
}
}, settings.editorConfig || {}),
mode: settings.mode || 'default'
});
wangEditor.createToolbar({
editor: editor,
selector: '#' + toolbar.id,
config: Object.assign({ toolbarKeys: toolbarKeys }, settings.toolbarConfig || {}),
mode: settings.mode || 'default'
});
} catch (error) {
host.remove();
return null;
}
return window.KindEditor.create(selector, jq.extend({
width: '100%',
minWidth: '100%',
height: '460px',
resizeType: 1,
allowPreviewEmoticons: true,
allowImageUpload: true,
filterMode: false,
newlineTag: 'p',
items: defaultItems,
afterBlur: function () {
this.sync();
}
}, options || {}));
textarea.hidden = true;
textarea.setAttribute('aria-hidden', 'true');
textarea.dataset.wangEditorInitialized = 'true';
instance = {
textarea: textarea,
editor: editor,
sync: function () { syncEditor(instance); },
getHtml: function () { return fieldValue(editor); },
getText: function () { return editor.getText(); }
};
editors[id] = instance;
instance.sync();
return instance;
}
function initRichEditors(root, options) {
var jq = getJquery();
var editors = {};
var $root;
function initRichEditors(rootNode, options) {
var scope = rootNode || documentRef;
var instances = {};
if (!jq) return editors;
$root = root ? jq(root) : jq(document);
$root.find('.js-rich-editor').each(function (index) {
var $textarea = jq(this);
var id = $textarea.attr('id') || ('rich-editor-' + (index + 1));
if (!scope || !scope.querySelectorAll) return instances;
Array.prototype.forEach.call(scope.querySelectorAll('.js-rich-editor'), function (textarea) {
var instance = createRichEditor(textarea, options);
$textarea.attr('id', id);
editors[id] = initRichEditor('#' + id, options);
if (instance) instances[textarea.id] = instance;
});
return editors;
return instances;
}
window.AppRichEditor = {
init: initRichEditor,
initAll: initRichEditors
function bindFormSync() {
if (submitBound || !documentRef) return;
submitBound = true;
documentRef.addEventListener('submit', function (event) {
Object.keys(editors).forEach(function (id) {
var instance = editors[id];
if (instance.textarea.form === event.target) instance.sync();
});
});
}
function initAll() {
bindFormSync();
return initRichEditors(documentRef);
}
root.AppRichEditor = {
init: createRichEditor,
initAll: initRichEditors,
syncAll: function () {
Object.keys(editors).forEach(function (id) { editors[id].sync(); });
}
};
if (getJquery()) {
getJquery()(function () {
if (getJquery()('.js-rich-editor').length) {
initRichEditors(document);
}
});
}
})(window, window.jQuery || (window.layui && window.layui.$));
if (!documentRef) return;
if (documentRef.readyState === 'loading') documentRef.addEventListener('DOMContentLoaded', initAll);
else initAll();
})(window);
+13 -15
View File
@@ -15,15 +15,15 @@
'use strict';
var documentRef = root.document;
var CAPTCHA_SCENES = {
phone: 'PC_PHONE_CHANGE',
deactivate: 'PC_ACCOUNT_DEACTIVATE'
var CAPTCHA_OPERATIONS = {
phone: 'phone-change',
deactivate: 'account-deactivate'
};
var SMS_CODE_COOLDOWN_SECONDS = 60;
var boundPhone = '';
function getCaptchaScene(formType) {
return CAPTCHA_SCENES[formType] || '';
function getCaptchaOperation(formType) {
return CAPTCHA_OPERATIONS[formType] || '';
}
function hashPassword(value) {
@@ -100,18 +100,18 @@
}
async function ensureCaptcha(form, formType, subject) {
// 发送敏感业务短信前,先取得与当前场景绑定的验证码票据。
// 发送敏感业务短信前,先取得与当前认证动作绑定的验证码票据。
var captcha = root.CaptchaPages;
var sceneCode = getCaptchaScene(formType);
var operationCode = getCaptchaOperation(formType);
if (!sceneCode) return true;
if (!operationCode) return true;
if (!captcha || !captcha.ensureToken) {
showMessage('验证码组件未加载,请刷新后重试');
return false;
}
return Boolean(await captcha.ensureToken(form, {
sceneCode: sceneCode,
operationCode: operationCode,
subject: subject || ''
}));
}
@@ -295,7 +295,7 @@
}
async function sendPhoneCode(button) {
// 换绑手机号使用独立短信场景,避免复用登录或找回密码票据。
// 换绑手机号使用独立认证动作,避免复用登录或找回密码票据。
var api = getApi();
var form = query('[data-security-form="phone"]');
var values;
@@ -311,9 +311,8 @@
button.disabled = true;
try {
await api.sendSmsCode({
await api.sendSmsCode(getCaptchaOperation('phone'), {
phone: values.phone,
sceneCode: getCaptchaScene('phone'),
validToken: values.validToken || ''
});
clearCaptchaToken(form);
@@ -342,9 +341,8 @@
button.disabled = true;
try {
await api.sendSmsCode({
await api.sendSmsCode(getCaptchaOperation('deactivate'), {
phone: boundPhone,
sceneCode: getCaptchaScene('deactivate'),
validToken: values.validToken || ''
});
clearCaptchaToken(form);
@@ -460,7 +458,7 @@
buildPasswordChangeBody: buildPasswordChangeBody,
buildPhoneChangeBody: buildPhoneChangeBody,
buildDeactivateBody: buildDeactivateBody,
getCaptchaScene: getCaptchaScene,
getCaptchaOperation: getCaptchaOperation,
isDangerConfirmed: isDangerConfirmed,
isPhone: isPhone,
isSmsCode: isSmsCode,
+4 -4
View File
@@ -32,8 +32,8 @@
}
function getUploadMode() {
// 本期只保留头像单文件上传,不再根据文件大小切换分片流程。
return 'single';
// 当前 PC 目录只定义统一分片上传流程。
return 'resumable';
}
function getApi() {
@@ -72,9 +72,9 @@
}
function uploadFileForPage(api, file) {
// 上传结果仅用于回填资料表单中的 avatarOssId
// 当前初始化响应未展开 uploadId、instant 与 OSS 字段,不能猜测分片闭环
if (!api || !file) return Promise.reject(new Error('请选择文件'));
return api.uploadFile(file);
return Promise.reject(new Error('当前 PC 文件上传响应未定义头像回填字段,暂不能上传头像'));
}
async function uploadFromInput(input) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+309 -46
View File
@@ -25,10 +25,12 @@ test('API client exposes latest document-defined PC operations', () => {
'clientId', 'tenantId', 'tokenKey', 'getToken', 'setToken', 'clearToken', 'buildApiUrl',
'register', 'login', 'loginBySms', 'sendSmsCode',
'currentProfile', 'updateProfile', 'changePassword', 'resetPassword', 'changePhone',
'deactivateAccount', 'logout', 'uploadFile', 'initResumableUpload', 'uploadResumableChunk',
'completeResumableUpload', 'createFileReference', 'deleteFileReference',
'captchaRequirement', 'captchaChallenge', 'captchaVerify',
'deactivateAccount', 'logout', 'initResumableUpload', 'uploadResumableChunk',
'completeResumableUpload',
'captchaRequirement', 'captchaChallenge', 'captchaVerify', 'captchaChallengeUrl', 'captchaVerifyUrl',
'regionChildren', 'regionPath', 'regionSearch', 'regionDetail',
'genealogyQuota',
'notifications', 'unreadNotificationCount', 'markNotificationRead', 'markAllNotificationsRead',
'feeds', 'feedsPage', 'feedDetail', 'createFeed', 'updateFeed', 'deleteFeed',
'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
'feedCommentReplies', 'feedCommentRepliesPage', 'deleteFeedComment',
@@ -36,7 +38,12 @@ test('API client exposes latest document-defined PC operations', () => {
'previewGenerationPoems', 'saveGenerationPoems',
'lineagePersons', 'lineagePersonsPage', 'lineagePersonOptions', 'lineageTree',
'createLineagePerson', 'lineagePersonDetail', 'updateLineagePerson', 'disableLineagePerson',
'createLineageChild', 'createLineageParent', 'createLineageSibling', 'createLineageSpouse'
'createLineageChild', 'createLineageParent', 'createLineageSibling', 'createLineageSpouse',
'growthRecords', 'createGrowthRecord', 'growthRecordDetail', 'updateGrowthRecord', 'deleteGrowthRecord',
'relativeRecords', 'createRelativeRecord', 'relativeRecordDetail', 'updateRelativeRecord', 'deleteRelativeRecord',
'memos', 'createMemo', 'memoDetail', 'updateMemo', 'deleteMemo', 'deleteMeritRecord',
'deleteArticle', 'deleteAlbum', 'deleteAlbumPhoto', 'deleteVideo', 'deleteCeremony', 'deleteCeremonyGift',
'replaceCeremonyInvitees', 'ceremonyInvitations', 'respondCeremonyInvitation', 'myCeremonyInvitations'
].sort();
assert.deepEqual(Object.keys(client).sort(), allowed);
@@ -66,34 +73,80 @@ test('password login stores only the returned token', async () => {
assert.equal(client.getToken(), 'access-token');
});
test('sending an SMS code uses the latest documented request fields', async () => {
test('sending an SMS code uses the PC operation path and documented request body', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.url, '/genealogy/pc/auth/sms/code');
assert.equal(config.url, '/genealogy/pc/auth/sms/sms-login/code');
assert.deepEqual(config.data, {
grantType: 'sms',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
sceneCode: 'PC_SMS_LOGIN',
validToken: 'captcha-ticket'
});
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.sendSmsCode({
await client.sendSmsCode('sms-login', {
phone: '13800000000',
sceneCode: 'PC_SMS_LOGIN',
validToken: 'captcha-ticket'
});
});
test('password reset adds the documented grant, tenant and client fields', async () => {
test('PC verification operations keep operationCode in the path and clientid out of query and body', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: { required: false } } });
}
}
});
const challenge = { tenantId: '000000', subject: '13800000000' };
const verification = {
tenantId: '000000',
subject: '13800000000',
challengeId: 'challenge-1',
providerCode: 'tianai',
captchaType: 'SLIDER',
payload: { track: { left: 120, top: 0, trackList: [] } }
};
await client.captchaRequirement('sms-login', { subject: '13800000000' });
await client.captchaChallenge('sms-login', challenge);
await client.captchaVerify('sms-login', verification);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
['get', '/genealogy/pc/auth/verification/sms-login/require', {
tenantId: '000000',
subject: '13800000000'
}, undefined],
['post', '/genealogy/pc/auth/verification/sms-login/challenge', undefined, challenge],
['post', '/genealogy/pc/auth/verification/sms-login/verify', undefined, verification]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal(config.headers.Authorization, undefined);
});
assert.equal(
client.captchaChallengeUrl('sms-login'),
'https://api.example.test/genealogy/pc/auth/verification/sms-login/challenge'
);
assert.equal(
client.captchaVerifyUrl('sms-login'),
'https://api.example.test/genealogy/pc/auth/verification/sms-login/verify'
);
});
test('password reset adds the documented grant and tenant fields without body clientId', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
@@ -105,7 +158,6 @@ test('password reset adds the documented grant, tenant and client fields', async
assert.deepEqual(config.data, {
grantType: 'password',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456',
newPassword: 'password-md5'
@@ -151,7 +203,6 @@ test('registration and SMS login use the PC auth endpoints with SMS fields', asy
grantType: 'password',
registerSource: 'PC',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456',
nickName: '小李',
@@ -160,14 +211,17 @@ test('registration and SMS login use the PC auth endpoints with SMS fields', asy
['post', '/genealogy/pc/auth/login/sms', {
grantType: 'sms',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456'
}]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal('clientId' in config.data, false);
});
});
test('phone change and account deactivation retain their PC request shapes', async () => {
test('phone change and account deactivation retain PC bodies without clientId', async () => {
let stored = 'access-token';
const calls = [];
const client = GenealogyApi.createClient({
@@ -191,14 +245,16 @@ test('phone change and account deactivation retain their PC request shapes', asy
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['put', '/genealogy/pc/auth/phone', {
phone: '13900000000',
smsCode: '123456',
clientId: 'ced7e5f0498645c6ec642dcf450b036f'
smsCode: '123456'
}],
['post', '/genealogy/pc/auth/account/deactivate', {
smsCode: '654321',
clientId: 'ced7e5f0498645c6ec642dcf450b036f'
smsCode: '654321'
}]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal('clientId' in config.data, false);
});
assert.equal(stored, '');
});
@@ -233,7 +289,7 @@ test('region methods use PC paths and retain the logged-in authorization header'
]);
});
test('remaining PC file methods use the documented paths, request forms and login header', async () => {
test('current PC file methods use the three documented resumable paths and request forms', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
@@ -268,19 +324,6 @@ test('remaining PC file methods use the documented paths, request forms and logi
fileMd5: 'file-md5',
fileSize: 8388608
});
await client.createFileReference({
bizType: 'family_feed',
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids',
ossId: '2060000000000000000'
});
await client.deleteFileReference({
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids'
});
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
@@ -289,13 +332,7 @@ test('remaining PC file methods use the documented paths, request forms and logi
]), [
['post', '/genealogy/pc/files/resumable/init', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/resumable/chunk', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/resumable/complete', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/reference', undefined, 'Bearer access-token'],
['delete', '/genealogy/pc/files/reference', {
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids'
}, 'Bearer access-token']
['post', '/genealogy/pc/files/resumable/complete', undefined, 'Bearer access-token']
]);
assert.deepEqual(calls[0].data, {
fileName: 'video.mp4',
@@ -316,12 +353,55 @@ test('remaining PC file methods use the documented paths, request forms and logi
fileMd5: 'file-md5',
fileSize: 8388608
});
assert.deepEqual(calls[3].data, {
bizType: 'family_feed',
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids',
ossId: '2060000000000000000'
});
test('genealogy quota uses the current PC path without manufacturing a genealogy ID', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.method, 'get');
assert.equal(config.url, '/genealogy/pc/genealogies/quota');
assert.equal(config.params, undefined);
assert.equal(config.data, undefined);
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: { canCreate: true, canJoin: true } } });
}
}
});
await client.genealogyQuota();
});
test('notification methods use the current PC list, unread and read paths', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.notifications({ readStatus: '0' });
await client.unreadNotificationCount();
await client.markNotificationRead('2060000000000000001');
await client.markAllNotificationsRead();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
['get', '/genealogy/pc/notifications', { readStatus: '0' }, undefined],
['get', '/genealogy/pc/notifications/unread-count', undefined, undefined],
['post', '/genealogy/pc/notifications/2060000000000000001/read', undefined, undefined],
['post', '/genealogy/pc/notifications/read-all', undefined, undefined]
]);
calls.forEach((config) => {
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
});
});
@@ -474,3 +554,186 @@ test('lineage person methods use the Apifox PC paths, query and LineagePersonBod
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002/spouses', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
test('growth record methods use the Apifox PC paths and GrowthRecordBody', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
const genealogyId = '2060000000000000001';
const recordId = '2060000000000000002';
const record = {
lineagePersonId: '2060000000000000003',
recordType: 'birth',
recordTitle: '出生记录',
recordContent: '平安出生',
recordDate: '2026-07-24',
remindTime: '2026-07-24T09:00:00+08:00',
mediaOssIds: '101,102',
sortOrder: 1,
status: 'enabled'
};
await client.growthRecords(genealogyId);
await client.createGrowthRecord(genealogyId, record);
await client.growthRecordDetail(genealogyId, recordId);
await client.updateGrowthRecord(genealogyId, recordId, record);
await client.deleteGrowthRecord(genealogyId, recordId);
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.data,
config.headers.Authorization,
config.headers.clientid
]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/growth-records', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/growth-records', undefined, record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['put', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
test('relative record methods use the Apifox PC paths and RelativeRecordBody', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: {} } }); } }
});
const genealogyId = '2060000000000000001';
const relativeId = '2060000000000000002';
const record = { relativeName: '王叔', relationName: '长辈', eventName: '寿宴', eventTime: '2026-07-24T09:00:00+08:00', giftAmount: 500, recordContent: '出席', mediaOssIds: '101', sortOrder: 1, status: 'enabled' };
await client.relativeRecords(genealogyId);
await client.createRelativeRecord(genealogyId, record);
await client.relativeRecordDetail(genealogyId, relativeId);
await client.updateRelativeRecord(genealogyId, relativeId, record);
await client.deleteRelativeRecord(genealogyId, relativeId);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/relative-records', undefined],
['post', '/genealogy/pc/genealogies/2060000000000000001/relative-records', record],
['get', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', record],
['delete', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', undefined]
]);
});
test('memo methods use the Apifox PC paths and documented request fields', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: {} } }); } }
});
const genealogyId = '2060000000000000001';
const memoId = '2060000000000000002';
const memo = {
memoTitle: '祭祖提醒',
memoContent: '准备供品',
remindTime: '2026-07-24T09:00:00+08:00',
completed: '0',
mediaOssIds: '101,102',
sortOrder: 1,
status: 'enabled'
};
await client.memos(genealogyId);
await client.createMemo(genealogyId, memo);
await client.memoDetail(genealogyId, memoId);
await client.updateMemo(genealogyId, memoId, memo);
await client.deleteMemo(genealogyId, memoId);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/memos', undefined],
['post', '/genealogy/pc/genealogies/2060000000000000001/memos', memo],
['get', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', memo],
['delete', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', undefined]
]);
});
test('merit record deletion uses the only documented PC merit operation', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.method, 'delete');
assert.equal(config.url, '/genealogy/pc/genealogies/2060000000000000001/merit-records/2060000000000000002');
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.deleteMeritRecord('2060000000000000001', '2060000000000000002');
});
test('remaining document-defined deletion operations use PC paths only', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: null } }); } }
});
const genealogyId = '2060000000000000001';
await client.deleteArticle(genealogyId, '2060000000000000002');
await client.deleteAlbum(genealogyId, '2060000000000000003');
await client.deleteAlbumPhoto(genealogyId, '2060000000000000003', '2060000000000000004');
await client.deleteVideo(genealogyId, '2060000000000000005');
await client.deleteCeremony(genealogyId, '2060000000000000006');
await client.deleteCeremonyGift(genealogyId, '2060000000000000006', '2060000000000000007');
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data, config.headers.clientid]), [
['delete', '/genealogy/pc/genealogies/2060000000000000001/articles/2060000000000000002', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000003', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000003/photos/2060000000000000004', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/videos/2060000000000000005', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000006', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000006/gifts/2060000000000000007', undefined, 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
test('ceremony invitation methods use the current PC invitation contracts', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: [] } }); } }
});
const genealogyId = '2060000000000000001';
const ceremonyId = '2060000000000000002';
await client.replaceCeremonyInvitees(genealogyId, ceremonyId, { inviteeUserIds: ['900000001', '900000002'] });
await client.ceremonyInvitations(genealogyId, ceremonyId);
await client.respondCeremonyInvitation(genealogyId, ceremonyId, { inviteStatus: 'ACCEPTED' });
await client.myCeremonyInvitations();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['put', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitees', {
inviteeUserIds: ['900000001', '900000002']
}],
['get', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitations', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitations/me', {
inviteStatus: 'ACCEPTED'
}],
['get', '/genealogy/pc/genealogies/ceremony-invitations/mine', undefined]
]);
calls.forEach((config) => {
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
});
});
+30 -5
View File
@@ -5,6 +5,25 @@ const test = require('node:test');
const AuthPages = require('../public/js/auth-pages.js');
test('password login includes a verification token only when the PC strategy returns one', () => {
assert.deepEqual(
AuthPages.buildLoginBody({
phone: '13800000000',
password: 'plain-password',
validToken: 'captcha-ticket'
}, (value) => 'md5-' + value),
{
phone: '13800000000',
password: 'md5-plain-password',
validToken: 'captcha-ticket'
}
);
assert.deepEqual(
AuthPages.buildLoginBody({ phone: '13800000000', password: 'plain-password' }, (value) => 'md5-' + value),
{ phone: '13800000000', password: 'md5-plain-password' }
);
});
test('registration body carries the SMS code but never sends confirmation or captcha fields', () => {
assert.deepEqual(
AuthPages.buildRegisterBody({
@@ -40,11 +59,11 @@ test('password reset body carries only the reset DTO fields', () => {
);
});
test('authentication forms use the PC SMS scenes defined by the PC endpoint', () => {
assert.equal(AuthPages.getCaptchaScene('login'), 'PC_SMS_LOGIN');
assert.equal(AuthPages.getCaptchaScene('sms-login'), 'PC_SMS_LOGIN');
assert.equal(AuthPages.getCaptchaScene('register'), 'PC_REGISTER');
assert.equal(AuthPages.getCaptchaScene('password-reset'), 'PC_FORGOT_PASSWORD');
test('authentication forms use the PC verification operation codes defined by Apifox', () => {
assert.equal(AuthPages.getCaptchaOperation('login'), 'password-login');
assert.equal(AuthPages.getCaptchaOperation('sms-login'), 'sms-login');
assert.equal(AuthPages.getCaptchaOperation('register'), 'register');
assert.equal(AuthPages.getCaptchaOperation('password-reset'), 'forgot-password');
});
test('registration validates SMS code and password confirmation before submission', () => {
@@ -67,3 +86,9 @@ test('registration page exposes the documented SMS and confirmation inputs', ()
assert.match(source, /data-api-send-code/);
assert.match(source, /name="confirmPassword"/);
});
test('password login page retains a verification token field for the PC captcha strategy', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'login.html'), 'utf8');
assert.match(source, /id="login-password-form"[\s\S]*name="validToken"/);
});
+44
View File
@@ -0,0 +1,44 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const CaptchaPages = require('../public/js/captcha-pages.js');
test('captcha request bodies keep operationCode in the URL and never submit legacy client or scene fields', () => {
const api = { tenantId: '000000', clientId: 'web-client' };
const challenge = CaptchaPages.buildChallengeBody({
operationCode: 'sms-login',
subject: '13800000000'
}, api);
const verification = CaptchaPages.buildVerifyBody({
data: {
trackList: [{ x: 0, y: 0 }, { x: 120, y: 4 }]
}
}, {
operationCode: 'sms-login',
subject: '13800000000',
challengeId: 'challenge-1'
}, api);
assert.deepEqual(challenge, {
tenantId: '000000',
subject: '13800000000'
});
assert.deepEqual(verification, {
tenantId: '000000',
subject: '13800000000',
challengeId: 'challenge-1',
providerCode: 'tianai',
captchaType: 'SLIDER',
payload: {
track: {
trackList: [{ x: 0, y: 0 }, { x: 120, y: 4 }],
left: 120,
top: 4
}
}
});
assert.equal('clientId' in challenge, false);
assert.equal('sceneCode' in challenge, false);
assert.equal('clientId' in verification, false);
assert.equal('sceneCode' in verification, false);
});
+47
View File
@@ -0,0 +1,47 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const GenealogyEntryPages = require('../public/js/genealogy-entry-pages.js');
function read(relativePath) {
return fs.readFileSync(path.resolve(__dirname, '..', relativePath), 'utf8');
}
test('家谱入口只接受本地家谱业务页作为返回目标', () => {
assert.equal(GenealogyEntryPages.getTargetPage('?next=profile-feed.html'), 'profile-feed.html');
assert.equal(GenealogyEntryPages.getTargetPage('?next=https%3A%2F%2Fevil.example'), 'profile-family-home.html');
});
test('家谱入口只展示当前 PC 已定义的额度,不伪造家谱编号', () => {
const message = GenealogyEntryPages.buildStatus({ createRemaining: 1, joinRemaining: 2 }, 'profile-feed.html');
assert.match(message, /还可创建 1 部/);
assert.match(message, /还可加入 2 部/);
assert.match(message, /不能伪造家谱编号/);
});
test('负数额度不向用户展示为可用数量', () => {
assert.doesNotMatch(
GenealogyEntryPages.buildStatus({ createRemaining: -1, joinRemaining: -1 }, 'profile-feed.html'),
/-1/
);
});
test('个人中心和内容发布在缺少家谱上下文时统一进入家谱入口', () => {
const profile = read('profile.html');
const content = read('profile-content.html');
const familyHome = read('profile-family-home.html');
const familyEntry = read('profile-families.html');
const profileCommon = read('public/js/profile-common.js');
assert.match(profile, /进入我的家谱/);
assert.doesNotMatch(profile, /四川武胜汤氏族|四川达州刘氏族/);
assert.match(profile, /href="profile-family-admin\.html" data-genealogy-context-link/);
assert.match(content, /href="profile-feed\.html" data-feature-link="available" data-genealogy-context-link/);
assert.match(familyHome, /href="profile-family-admin\.html" data-genealogy-context-link/);
assert.match(familyEntry, /href="profile-family-admin\.html" data-genealogy-context-link/);
assert.match(profileCommon, /function getGenealogyEntryUrl\(href\)/);
assert.match(profileCommon, /genealogyId \? withGenealogyId\([^\n]+\) : getGenealogyEntryUrl\(/);
});
+51
View File
@@ -0,0 +1,51 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const GrowthPages = require('../public/js/growth-pages.js');
test('成长记录页只从 URL 读取真实家谱编号', () => {
assert.equal(GrowthPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(GrowthPages.getCurrentGenealogyId(''), '');
});
test('成长记录写入只构造 Apifox GrowthRecordBody 字段', () => {
assert.deepEqual(
GrowthPages.buildGrowthRecordBody({
lineagePersonId: '2060000000000000001',
recordType: 'birth',
recordTitle: ' 出生记录 ',
recordContent: ' 平安出生 ',
recordDate: '2026-07-24',
remindTime: '2026-07-24T09:00:00+08:00',
mediaOssIds: '101,102',
sortOrder: '3',
status: 'enabled',
content: '旧字段',
personId: '旧字段'
}),
{
lineagePersonId: '2060000000000000001',
recordType: 'birth',
recordTitle: '出生记录',
recordContent: '平安出生',
recordDate: '2026-07-24',
remindTime: '2026-07-24T09:00:00+08:00',
mediaOssIds: '101,102',
sortOrder: 3,
status: 'enabled'
}
);
});
test('成长记录校验标题、整数 ID、附件 OSS ID 和排序值', () => {
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '' }), '请填写记录标题');
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', lineagePersonId: 'A-1' }), '世系人物 ID 必须是整数');
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101, 102' }), '附件 OSS ID 请使用英文逗号分隔的正整数');
assert.equal(GrowthPages.validateGrowthRecordBody(GrowthPages.buildGrowthRecordBody({ recordTitle: '记录', sortOrder: '1.5' })), '排序值必须是安全整数');
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101,102', sortOrder: 1 }), '');
});
test('成长记录列表不猜测未展开的响应 DTO', () => {
assert.deepEqual(GrowthPages.normalizeGrowthList([{ property1: 'value' }]), [{ property1: 'value' }]);
assert.deepEqual(GrowthPages.normalizeGrowthList({ rows: [] }), []);
});
+45
View File
@@ -0,0 +1,45 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const MemoPages = require('../public/js/memo-pages.js');
test('备忘录页只从 URL 读取真实家谱编号', () => {
assert.equal(MemoPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(MemoPages.getCurrentGenealogyId(''), '');
});
test('备忘录写入只构造 Apifox 已核验请求体字段', () => {
assert.deepEqual(
MemoPages.buildMemoBody({
memoTitle: ' 祭祖提醒 ',
memoContent: ' 准备供品 ',
remindTime: '2026-07-24T09:00:00+08:00',
completed: ' 0 ',
mediaOssIds: '101,102',
sortOrder: '3',
status: ' enabled ',
relativeName: '旧字段'
}),
{
memoTitle: '祭祖提醒',
memoContent: '准备供品',
remindTime: '2026-07-24T09:00:00+08:00',
completed: '0',
mediaOssIds: '101,102',
sortOrder: 3,
status: 'enabled'
}
);
});
test('备忘录校验标题、附件 OSS ID 和排序值', () => {
assert.equal(MemoPages.validateMemoBody({ memoTitle: '' }), '请填写备忘标题');
assert.equal(MemoPages.validateMemoBody({ memoTitle: '提醒', mediaOssIds: '101, 102' }), '附件 OSS ID 请使用英文逗号分隔的正整数');
assert.equal(MemoPages.validateMemoBody(MemoPages.buildMemoBody({ memoTitle: '提醒', sortOrder: '1.5' })), '排序值必须是安全整数');
assert.equal(MemoPages.validateMemoBody({ memoTitle: '提醒', mediaOssIds: '101,102', sortOrder: 1 }), '');
});
test('备忘录列表不猜测未展开的响应 DTO', () => {
assert.deepEqual(MemoPages.normalizeMemoList([{ property1: 'value' }]), [{ property1: 'value' }]);
assert.deepEqual(MemoPages.normalizeMemoList({ rows: [] }), []);
});
+26
View File
@@ -0,0 +1,26 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const NotificationPages = require('../public/js/notification-pages.js');
test('notification page preserves only the unexpanded PC list records and unread count', () => {
const rows = [{ notificationId: '2060000000000000001', title: '系统通知' }];
assert.deepEqual(NotificationPages.normalizeNotifications(rows), rows);
assert.deepEqual(NotificationPages.normalizeNotifications({ rows }), []);
assert.equal(NotificationPages.getUnreadCount(3), 3);
assert.equal(NotificationPages.getUnreadCount(-1), 0);
assert.equal(NotificationPages.formatNotification(rows[0]), '{\n "notificationId": "2060000000000000001",\n "title": "系统通知"\n}');
});
test('message page loads the PC notification module instead of pending-page controls', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-messages.html'), 'utf8');
assert.doesNotMatch(source, /data-feature-status="pending"/);
assert.match(source, /src="public\/js\/notification-pages\.js"/);
assert.doesNotMatch(source, /src="public\/js\/pending-pages\.js"/);
assert.match(source, /data-notification-unread-count/);
assert.match(source, /data-notification-read-all/);
});
+4 -2
View File
@@ -30,9 +30,9 @@ test('PC 对接规划明确以 Apifox 目录而非导出快照为契约源', ()
test('pages do not load scripts for unavailable business APIs', () => {
const removedScripts = [
'album-pages.js', 'article-pages.js', 'ceremony-pages.js',
'feedback-pages.js', 'genealogy-pages.js', 'growth-pages.js',
'feedback-pages.js', 'genealogy-pages.js',
'help-pages.js', 'join-apply-pages.js', 'member-admin-pages.js',
'memo-pages.js', 'notification-pages.js', 'promotion-pages.js', 'vip-pages.js'
'promotion-pages.js', 'vip-pages.js'
];
const retainedPages = fs.readdirSync(root).filter((entry) => entry.endsWith('.html'));
@@ -48,4 +48,6 @@ test('pages do not load scripts for unavailable business APIs', () => {
});
assert.equal(read('profile-generation.html').includes('src="public/js/generation-pages.js"'), true, 'profile-generation.html must load generation-pages.js');
assert.equal(read('profile-tree.html').includes('src="public/js/lineage-pages.js"'), true, 'profile-tree.html must load lineage-pages.js');
assert.equal(read('profile-relative.html').includes('src="public/js/relative-pages.js"'), true, 'profile-relative.html must load relative-pages.js');
assert.equal(read('profile-memo.html').includes('src="public/js/memo-pages.js"'), true, 'profile-memo.html must load memo-pages.js');
});
+11 -6
View File
@@ -23,13 +23,8 @@ const contentPendingPages = [
'profile-video.html',
'profile-gift.html',
'profile-gift-edit.html',
'profile-growth.html',
'profile-growth-edit.html',
'profile-memo.html',
'profile-memo-edit.html',
'profile-merit.html',
'profile-merit-edit.html',
'profile-messages.html',
'profile-feedback.html',
'profile-admin-permissions.html',
'profile-data-reminders.html'
@@ -70,11 +65,18 @@ test('待开发页面显式加载状态脚本', () => {
});
test('已接入的家谱业务页面保持真实功能状态', () => {
['profile-feed.html', 'profile-feed-edit.html', 'profile-generation.html', 'profile-tree.html'].forEach((page) => {
['profile-feed.html', 'profile-feed-edit.html', 'profile-generation.html', 'profile-tree.html', 'profile-growth.html', 'profile-growth-edit.html', 'profile-relative.html', 'profile-relative-edit.html', 'profile-memo.html', 'profile-memo-edit.html', 'profile-messages.html'].forEach((page) => {
assert.doesNotMatch(read(page), /data-feature-status="pending"/, `${page} 不应标记为待开发`);
});
assert.match(read('profile-generation.html'), /src="public\/js\/generation-pages\.js"/);
assert.match(read('profile-tree.html'), /src="public\/js\/lineage-pages\.js"/);
assert.match(read('profile-growth.html'), /src="public\/js\/growth-pages\.js"/);
assert.match(read('profile-growth-edit.html'), /src="public\/js\/growth-pages\.js"/);
assert.match(read('profile-relative.html'), /src="public\/js\/relative-pages\.js"/);
assert.match(read('profile-relative-edit.html'), /src="public\/js\/relative-pages\.js"/);
assert.match(read('profile-memo.html'), /src="public\/js\/memo-pages\.js"/);
assert.match(read('profile-messages.html'), /src="public\/js\/notification-pages\.js"/);
assert.match(read('profile-memo-edit.html'), /src="public\/js\/memo-pages\.js"/);
});
test('待开发的家谱管理页明确放行已接入的字辈入口并透传上下文', () => {
@@ -94,6 +96,9 @@ test('待开发页面仅放行显式标记的可用功能入口', () => {
test('内容入口明确保留家族圈动态跳转', () => {
assert.match(read('profile-content.html'), /href="profile-feed\.html" data-feature-link="available"/);
assert.match(read('profile-content.html'), /href="profile-growth\.html" data-feature-link="available" data-genealogy-context-link/);
assert.match(read('profile-content.html'), /href="profile-relative\.html" data-feature-link="available" data-genealogy-context-link/);
assert.match(read('profile-content.html'), /href="profile-memo\.html" data-feature-link="available" data-genealogy-context-link/);
});
test('家谱主页明确保留家族圈动态跳转', () => {
+23
View File
@@ -0,0 +1,23 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const RelativePages = require('../public/js/relative-pages.js');
test('亲友往来页只从 URL 读取真实家谱编号', () => {
assert.equal(RelativePages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(RelativePages.getCurrentGenealogyId(''), '');
});
test('亲友往来写入只构造 Apifox RelativeRecordBody 字段', () => {
assert.deepEqual(RelativePages.buildRelativeRecordBody({
relativeName: ' 王叔 ', relationName: '长辈', eventName: '寿宴', eventTime: '2026-07-24T09:00:00+08:00',
giftAmount: '500.5', recordContent: ' 出席 ', mediaOssIds: '101,102', sortOrder: '3', status: 'enabled', memoTitle: '旧字段'
}), { relativeName: '王叔', relationName: '长辈', eventName: '寿宴', eventTime: '2026-07-24T09:00:00+08:00', giftAmount: 500.5, recordContent: '出席', mediaOssIds: '101,102', status: 'enabled', sortOrder: 3 });
});
test('亲友往来校验必填姓名、金额、附件和排序值', () => {
assert.equal(RelativePages.validateRelativeRecordBody({ relativeName: '' }), '请填写亲友姓名');
assert.equal(RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({ relativeName: '王叔', giftAmount: '一百' })), '礼金金额必须是数字');
assert.equal(RelativePages.validateRelativeRecordBody({ relativeName: '王叔', mediaOssIds: '101, 102' }), '附件 OSS ID 请使用英文逗号分隔的正整数');
assert.equal(RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({ relativeName: '王叔', sortOrder: '1.5' })), '排序值必须是安全整数');
});
+9 -7
View File
@@ -6,9 +6,9 @@ const test = require('node:test');
const SecurityPages = require('../public/js/security-pages.js');
const UploadPages = require('../public/js/upload-pages.js');
test('security forms use SMS captcha scenes and submit only documented business fields', () => {
assert.equal(SecurityPages.getCaptchaScene('phone'), 'PC_PHONE_CHANGE');
assert.equal(SecurityPages.getCaptchaScene('deactivate'), 'PC_ACCOUNT_DEACTIVATE');
test('security forms use PC verification operation codes and submit only documented business fields', () => {
assert.equal(SecurityPages.getCaptchaOperation('phone'), 'phone-change');
assert.equal(SecurityPages.getCaptchaOperation('deactivate'), 'account-deactivate');
assert.deepEqual(
SecurityPages.buildPasswordChangeBody({
oldPassword: 'old',
@@ -34,10 +34,12 @@ test('security forms use SMS captcha scenes and submit only documented business
assert.deepEqual(SecurityPages.buildDeactivateBody({ smsCode: '654321' }), { smsCode: '654321' });
});
test('upload helper exposes only single-file upload support', () => {
assert.equal('uploadResumable' in UploadPages, false);
assert.equal('buildResumableInitBody' in UploadPages, false);
assert.equal(UploadPages.getUploadMode(5 * 1024 * 1024), 'single');
test('upload helper keeps the avatar flow blocked until the current PC resumable response is defined', async () => {
assert.equal(UploadPages.getUploadMode(5 * 1024 * 1024), 'resumable');
await assert.rejects(
UploadPages.uploadFileForPage({ initResumableUpload() {} }, { name: 'avatar.png' }),
/当前 PC 文件上传响应未定义头像回填字段/
);
});
test('security page field names match phone-change and account-deactivation DTOs', () => {
+42
View File
@@ -0,0 +1,42 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..');
const editorPages = [
'profile-article-edit.html',
'profile-feed-edit.html',
'profile-gift-edit.html',
'profile-growth-edit.html',
'profile-merit-edit.html'
];
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
test('所有富文本页面统一加载 wangEditor v5', () => {
editorPages.forEach((page) => {
const source = read(page);
assert.match(source, /public\/js\/wangeditor5\/css\/style\.css/);
assert.match(source, /public\/js\/wangeditor5\/index\.js/);
assert.match(source, /class="js-rich-editor"/);
assert.doesNotMatch(source, /kindeditor|public\/js\/ke\//i);
});
});
test('公共富文本适配层只使用 wangEditor 并在提交前同步 textarea', () => {
const source = read('public/js/rich-editor.js');
assert.match(source, /root\.wangEditor/);
assert.match(source, /wangEditor\.createEditor/);
assert.match(source, /wangEditor\.createToolbar/);
assert.match(source, /documentRef\.addEventListener\('submit'/);
assert.doesNotMatch(source, /KindEditor/);
});
test('旧 KindEditor 资源已移除', () => {
assert.equal(fs.existsSync(path.join(root, 'public/js/ke/kindeditor.min.js')), false);
});
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>工单详情 - 代代相传</title>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/css/site-info.css" />
</head>
<body
data-feature-status="pending"
data-feature-message="在线工单详情服务正在开发中,请先通过帮助中心了解常见问题。"
>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html">
<img
class="brand-logo"
src="public/images/logo.png"
alt="我们的家谱,代代相传"
/>
</a>
<nav class="nav-links">
<a href="index.html">首页</a>
<a class="active" href="help.html">帮助中心</a>
<a href="news.html">新闻资讯</a>
</nav>
<div class="nav-actions"><a href="login.html">登录</a></div>
</div>
</header>
<main>
<section class="site-info-hero">
<div class="container">
<div>
<div class="site-info-kicker">Ticket Detail</div>
<h1>工单详情</h1>
<p>工单处理记录、客服回复与进度状态将在工单接口交付后接入。</p>
</div>
<div class="site-info-mark"></div>
</div>
</section>
<section class="site-info-section alt">
<div class="container">
<div class="site-info-status">
<h2>暂未开放在线详情查询</h2>
<p>当前页面不会读取或展示工单数据,也不会伪造处理进度。</p>
<div class="site-info-actions">
<a
class="btn ghost magnetic"
href="help.html"
data-feature-link="available"
>返回帮助中心</a
>
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer-legal">
<div>© 2026 代代相传. All rights reserved.</div>
<a href="privacy.html">隐私政策</a>
</div>
</div>
</footer>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
-17
View File
@@ -1,17 +0,0 @@
请继续 C:\Users\Administrator\Desktop\job\jiapu 的 PC 接口对接工作。
先完整阅读:
1. docs/交接文档.md
2. docs/PC接口对接规划.md
3. AGENTS.md
强制规则:
- Apifox 的 PC 目录是唯一接口依据。
- 不使用 APP 接口,不根据本地 OpenAPI 导出文件猜路径或字段。
- 不写死 genealogyId;没有 PC 家谱入口接口时保持阻止状态。
- 页面只能通过 utils/ApiClient.js 请求接口,不能页面里直接 Axios 或拼路径。
- 当前工作树有未提交修改,禁止 git reset --hard、git checkout -- 或删除未跟踪文件。
已完成认证、资料、家族圈、字辈谱、世系人物等 57 条已在 Apifox PC 核验的接口施工。
下一步从 Apifox 的 PC「族务记录」16 条接口开始:逐条核验 → ApiClient → 对应页面 → 聚焦测试。
每完成一组,同步更新 docs/交接文档.md、docs/PC接口对接规划.md 和测试。
+176 -38
View File
@@ -52,17 +52,6 @@
return data && (data.token || data.accessToken || data.access_token || data.tokenValue) || '';
}
function createFormData(fileOrFormData) {
var FormDataCtor = root.FormData;
if (FormDataCtor && fileOrFormData instanceof FormDataCtor) return fileOrFormData;
if (!FormDataCtor) throw new Error('当前环境不支持文件上传');
var formData = new FormDataCtor();
formData.append('file', fileOrFormData);
return formData;
}
function createChunkFormData(bodyOrFormData) {
var FormDataCtor = root.FormData;
var source = bodyOrFormData || {};
@@ -132,11 +121,15 @@
}
function withTenant(body) {
return Object.assign({ tenantId: tenantId, clientId: clientId }, body || {});
return Object.assign({ tenantId: tenantId }, body || {});
}
function withClient(body) {
return Object.assign({}, body || {}, { clientId: clientId });
function buildVerificationPath(operationCode, suffix) {
return '/genealogy/pc/auth/verification/' + toRequiredPathId(operationCode, 'PC 认证动作') + (suffix || '');
}
function buildSmsCodePath(operationCode) {
return '/genealogy/pc/auth/sms/' + toRequiredPathId(operationCode, 'PC 短信认证动作') + '/code';
}
function toRequiredPathId(value, label) {
@@ -182,6 +175,63 @@
);
}
function buildGrowthRecordPath(genealogyId, recordId) {
var path = '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') + '/growth-records';
return recordId === undefined || recordId === null || recordId === ''
? path
: path + '/' + toRequiredPathId(recordId, '成长记录编号');
}
function buildRelativeRecordPath(genealogyId, relativeId) {
var path = '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') + '/relative-records';
return relativeId === undefined || relativeId === null || relativeId === ''
? path
: path + '/' + toRequiredPathId(relativeId, '亲友记录编号');
}
function buildMemoPath(genealogyId, memoId) {
var path = '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') + '/memos';
return memoId === undefined || memoId === null || memoId === ''
? path
: path + '/' + toRequiredPathId(memoId, '备忘录编号');
}
function buildMeritRecordPath(genealogyId, meritId) {
return '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') +
'/merit-records/' + toRequiredPathId(meritId, '功德记录编号');
}
function buildArticlePath(genealogyId, articleId) {
return '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') +
'/articles/' + toRequiredPathId(articleId, '谱文编号');
}
function buildAlbumPath(genealogyId, albumId) {
return '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') +
'/albums/' + toRequiredPathId(albumId, '相册编号');
}
function buildAlbumPhotoPath(genealogyId, albumId, photoId) {
return buildAlbumPath(genealogyId, albumId) + '/photos/' + toRequiredPathId(photoId, '相册照片编号');
}
function buildVideoPath(genealogyId, videoId) {
return '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') +
'/videos/' + toRequiredPathId(videoId, '视频编号');
}
function buildCeremonyPath(genealogyId, ceremonyId) {
return '/genealogy/pc/genealogies/' + toRequiredPathId(genealogyId, '家谱编号') +
'/ceremonies/' + toRequiredPathId(ceremonyId, '祭祀活动编号');
}
function buildCeremonyGiftPath(genealogyId, ceremonyId, giftId) {
return buildCeremonyPath(genealogyId, ceremonyId) + '/gifts/' + toRequiredPathId(giftId, '祭品编号');
}
async function login(body) {
var data = await request('POST', '/genealogy/pc/auth/login', {
auth: false,
@@ -208,7 +258,7 @@
async function deactivateAccount(body) {
var data = await request('POST', '/genealogy/pc/auth/account/deactivate', {
body: withClient(body)
body: body
});
clearToken();
return data;
@@ -240,11 +290,11 @@
},
login: login,
loginBySms: loginBySms,
sendSmsCode: function (body) {
return request('POST', '/genealogy/pc/auth/sms/code', {
sendSmsCode: function (operationCode, body) {
return request('POST', buildSmsCodePath(operationCode), {
auth: false,
// 最新接口文档要求短信发送场景携带 grantType 与验证码票据
body: Object.assign({ grantType: 'sms' }, withTenant(body))
// PC 认证目录以路径 operationCode 绑定动作;clientid 只由请求头提供
body: Object.assign({ grantType: 'sms', tenantId: tenantId }, body || {})
});
},
currentProfile: function () {
@@ -263,13 +313,10 @@
});
},
changePhone: function (body) {
return request('PUT', '/genealogy/pc/auth/phone', { body: withClient(body) });
return request('PUT', '/genealogy/pc/auth/phone', { body: body });
},
deactivateAccount: deactivateAccount,
logout: logout,
uploadFile: function (fileOrFormData) {
return request('POST', '/genealogy/pc/files/upload', { body: createFormData(fileOrFormData) });
},
initResumableUpload: function (body) {
return request('POST', '/genealogy/pc/files/resumable/init', { body: body });
},
@@ -281,30 +328,28 @@
completeResumableUpload: function (body) {
return request('POST', '/genealogy/pc/files/resumable/complete', { body: body });
},
createFileReference: function (body) {
return request('POST', '/genealogy/pc/files/reference', { body: body });
},
deleteFileReference: function (query) {
return request('DELETE', '/genealogy/pc/files/reference', { query: query });
},
captchaRequirement: function (body) {
var source = body || {};
captchaRequirement: function (operationCode, query) {
var source = query || {};
return request('GET', '/captcha/require', {
return request('GET', buildVerificationPath(operationCode, '/require'), {
auth: false,
query: {
tenantId: source.tenantId || tenantId,
clientId: source.clientId || clientId,
sceneCode: source.sceneCode,
subject: source.subject
}
});
},
captchaChallenge: function (body) {
return request('POST', '/captcha/challenge', { auth: false, body: withTenant(body) });
captchaChallenge: function (operationCode, body) {
return request('POST', buildVerificationPath(operationCode, '/challenge'), { auth: false, body: body });
},
captchaVerify: function (body) {
return request('POST', '/captcha/verify', { auth: false, body: withTenant(body) });
captchaVerify: function (operationCode, body) {
return request('POST', buildVerificationPath(operationCode, '/verify'), { auth: false, body: body });
},
captchaChallengeUrl: function (operationCode) {
return buildApiUrl(baseUrl, buildVerificationPath(operationCode, '/challenge'));
},
captchaVerifyUrl: function (operationCode) {
return buildApiUrl(baseUrl, buildVerificationPath(operationCode, '/verify'));
},
regionChildren: function (parentCode) {
return request('GET', '/genealogy/region/children', { query: { parentCode: parentCode } });
@@ -318,6 +363,21 @@
regionDetail: function (regionCode) {
return request('GET', '/genealogy/region/' + encodeURIComponent(regionCode));
},
genealogyQuota: function () {
return request('GET', '/genealogy/pc/genealogies/quota');
},
notifications: function (query) {
return request('GET', '/genealogy/pc/notifications', { query: query });
},
unreadNotificationCount: function () {
return request('GET', '/genealogy/pc/notifications/unread-count');
},
markNotificationRead: function (notificationId) {
return request('POST', '/genealogy/pc/notifications/' + toRequiredPathId(notificationId, '通知编号') + '/read');
},
markAllNotificationsRead: function () {
return request('POST', '/genealogy/pc/notifications/read-all');
},
feeds: function (genealogyId) {
return request('GET', buildFeedPath(genealogyId));
},
@@ -413,6 +473,84 @@
},
createLineageSpouse: function (genealogyId, personId, body) {
return request('POST', buildLineagePersonPath(genealogyId, personId, '/spouses'), { body: body });
},
growthRecords: function (genealogyId) {
return request('GET', buildGrowthRecordPath(genealogyId));
},
createGrowthRecord: function (genealogyId, body) {
return request('POST', buildGrowthRecordPath(genealogyId), { body: body });
},
growthRecordDetail: function (genealogyId, recordId) {
return request('GET', buildGrowthRecordPath(genealogyId, recordId));
},
updateGrowthRecord: function (genealogyId, recordId, body) {
return request('PUT', buildGrowthRecordPath(genealogyId, recordId), { body: body });
},
deleteGrowthRecord: function (genealogyId, recordId) {
return request('DELETE', buildGrowthRecordPath(genealogyId, recordId));
},
relativeRecords: function (genealogyId) {
return request('GET', buildRelativeRecordPath(genealogyId));
},
createRelativeRecord: function (genealogyId, body) {
return request('POST', buildRelativeRecordPath(genealogyId), { body: body });
},
relativeRecordDetail: function (genealogyId, relativeId) {
return request('GET', buildRelativeRecordPath(genealogyId, relativeId));
},
updateRelativeRecord: function (genealogyId, relativeId, body) {
return request('PUT', buildRelativeRecordPath(genealogyId, relativeId), { body: body });
},
deleteRelativeRecord: function (genealogyId, relativeId) {
return request('DELETE', buildRelativeRecordPath(genealogyId, relativeId));
},
memos: function (genealogyId) {
return request('GET', buildMemoPath(genealogyId));
},
createMemo: function (genealogyId, body) {
return request('POST', buildMemoPath(genealogyId), { body: body });
},
memoDetail: function (genealogyId, memoId) {
return request('GET', buildMemoPath(genealogyId, memoId));
},
updateMemo: function (genealogyId, memoId, body) {
return request('PUT', buildMemoPath(genealogyId, memoId), { body: body });
},
deleteMemo: function (genealogyId, memoId) {
return request('DELETE', buildMemoPath(genealogyId, memoId));
},
deleteMeritRecord: function (genealogyId, meritId) {
return request('DELETE', buildMeritRecordPath(genealogyId, meritId));
},
deleteArticle: function (genealogyId, articleId) {
return request('DELETE', buildArticlePath(genealogyId, articleId));
},
deleteAlbum: function (genealogyId, albumId) {
return request('DELETE', buildAlbumPath(genealogyId, albumId));
},
deleteAlbumPhoto: function (genealogyId, albumId, photoId) {
return request('DELETE', buildAlbumPhotoPath(genealogyId, albumId, photoId));
},
deleteVideo: function (genealogyId, videoId) {
return request('DELETE', buildVideoPath(genealogyId, videoId));
},
replaceCeremonyInvitees: function (genealogyId, ceremonyId, body) {
return request('PUT', buildCeremonyPath(genealogyId, ceremonyId) + '/invitees', { body: body });
},
ceremonyInvitations: function (genealogyId, ceremonyId) {
return request('GET', buildCeremonyPath(genealogyId, ceremonyId) + '/invitations');
},
respondCeremonyInvitation: function (genealogyId, ceremonyId, body) {
return request('PUT', buildCeremonyPath(genealogyId, ceremonyId) + '/invitations/me', { body: body });
},
myCeremonyInvitations: function () {
return request('GET', '/genealogy/pc/genealogies/ceremony-invitations/mine');
},
deleteCeremony: function (genealogyId, ceremonyId) {
return request('DELETE', buildCeremonyPath(genealogyId, ceremonyId));
},
deleteCeremonyGift: function (genealogyId, ceremonyId, giftId) {
return request('DELETE', buildCeremonyGiftPath(genealogyId, ceremonyId, giftId));
}
};
}