现在有的接口对接测试完成
This commit is contained in:
+112
-63
@@ -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
@@ -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` 和相关测试,并报告改动、验证和剩余阻塞。
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
autocomplete="current-password"
|
||||
placeholder="密码"
|
||||
/>
|
||||
<input type="hidden" name="validToken" />
|
||||
<button class="btn primary" type="submit">登录</button>
|
||||
</form>
|
||||
<form
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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('请填写动态内容');
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
Vendored
-1
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 |
@@ -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 |
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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.$));
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
||||
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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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\(/);
|
||||
});
|
||||
@@ -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: [] }), []);
|
||||
});
|
||||
@@ -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: [] }), []);
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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('家谱主页明确保留家族圈动态跳转', () => {
|
||||
|
||||
@@ -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' })), '排序值必须是安全整数');
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user