Compare commits
7 Commits
a549ca5ca3
...
db397a7917
| Author | SHA1 | Date | |
|---|---|---|---|
| db397a7917 | |||
| edc0a58a54 | |||
| 8a18202878 | |||
| f60cc67749 | |||
| 8b896f4ba3 | |||
| 68f72eb960 | |||
| 9769401b10 |
-7613
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- 应用推广区域:内容来自 /genealogy/app/promotions 列表接口。 -->
|
|
||||||
<section class="section app-promotion-section">
|
<section class="section app-promotion-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# 接口对接规划与进度记录
|
|
||||||
|
|
||||||
## 基本信息
|
|
||||||
|
|
||||||
- 项目路径:`C:\Users\Administrator\Desktop\job\家谱`
|
|
||||||
- 接口文档:`APP.openapi.json`
|
|
||||||
- 记录创建时间:2026-07-09 11:51:16 +08:00
|
|
||||||
- 执行范围:先做核心闭环页面接口对接,再按页面模块继续扩展。
|
|
||||||
- 当前假设:接口文档以 `/genealogy/app` 为主,PC/H5 页面先复用这些用户侧接口。
|
|
||||||
|
|
||||||
## 约束摘要
|
|
||||||
|
|
||||||
- 先规划再实现,所有执行进度需要记录年月日时间。
|
|
||||||
- 改动要小,只触碰接口对接必需文件。
|
|
||||||
- 新接口契约由共享 API 客户端统一维护,页面脚本只调用封装方法。
|
|
||||||
- 完成后做最小可验证检查;不能验证的地方要明确说明。
|
|
||||||
- 样式尽量写入 `public.css` 或页面对应 CSS 文件,通过 class 控制,不用 JS 注入样式。
|
|
||||||
- 新增 HTML、JS、CSS 注释统一使用中文。
|
|
||||||
- CSS 结构先公共后页面:`public.css` 放全站公共组件,页面 CSS 只放本页面差异样式。
|
|
||||||
- 个人中心模块页优先复用 `profile-module.css`,只有页面独有样式再新增或修改对应页面 CSS。
|
|
||||||
- 页面专用 JS 放 `public/js`,命名跟页面或模块对应;公告详情页如需 JS 使用 `public/js/notice-detail.js`。
|
|
||||||
- 业务代码不压缩,保持展开、可读;需要解释意图、边界或接口字段映射的位置使用中文注释。
|
|
||||||
- 第三方库、压缩库、已存在乱码文件不做无关重写,避免引入额外风险。
|
|
||||||
|
|
||||||
## 核心闭环范围
|
|
||||||
|
|
||||||
1. 共享接口层
|
|
||||||
- 新增统一 API 客户端,负责 `baseUrl`、`clientid`、`tenantId`、token 存取、请求头、响应解包和错误提示。
|
|
||||||
- 默认 `clientid` 使用接口文档示例:`ced7e5f0498645c6ec642dcf450b036f`。
|
|
||||||
- 默认 `tenantId` 使用接口文档示例:`000000`。
|
|
||||||
- API 基础地址从 `window.GENEALOGY_API_BASE_URL` 或 `localStorage.genealogy_api_base_url` 读取,未配置时使用同域相对路径。
|
|
||||||
- 浏览器端公共脚本放 `public/js`;`utils` 目前只适合作为纯工具类目录,不能作为页面脚本入口。
|
|
||||||
- 页面专用 JS 按页面或模块放 `public/js`,例如认证模块用 `auth-pages.js`,公告详情页如需脚本则用 `notice-detail.js`。
|
|
||||||
|
|
||||||
2. 登录注册找回
|
|
||||||
- `login.html` -> `POST /genealogy/app/auth/login`
|
|
||||||
- `register.html` -> `POST /genealogy/app/auth/register`
|
|
||||||
- `forgot-password.html` -> `POST /genealogy/app/auth/sms/code`,`PUT /genealogy/app/auth/password/reset`
|
|
||||||
- 密码按文档要求提交 32 位 MD5,复用已有 `public/js/md5.js`。
|
|
||||||
|
|
||||||
3. 家谱核心
|
|
||||||
- `create-genealogy.html` -> `POST /genealogy/app/genealogies`
|
|
||||||
- `profile-families.html` -> `GET /genealogy/app/genealogies/mine`
|
|
||||||
- `plaza.html` -> `GET /genealogy/app/genealogies/public`
|
|
||||||
- `family-detail.html` -> `GET /genealogy/app/genealogies/{genealogyId}`
|
|
||||||
|
|
||||||
4. 个人资料与反馈
|
|
||||||
- `profile.html`、`profile-data.html` -> `GET /genealogy/app/auth/profile`
|
|
||||||
- `profile-feedback.html`、`submit-ticket.html` -> `GET/POST /genealogy/app/feedback`
|
|
||||||
|
|
||||||
5. 拦截处理
|
|
||||||
- `public/js/page-effects.js` 当前会拦截登录、注册、创建家谱、个人中心页面并显示“功能待开发”。
|
|
||||||
- 对已接入接口的页面移除该拦截,保留未接入页面的提示。
|
|
||||||
|
|
||||||
## 验证计划
|
|
||||||
|
|
||||||
1. 新增共享 API 客户端测试,先确认测试失败,再实现通过。
|
|
||||||
2. 检查核心页面均加载共享脚本和页面脚本。
|
|
||||||
3. 用静态结构检查确认接口路径、表单字段、脚本引用存在。
|
|
||||||
4. 运行语法检查或 Node 烟测,确保 JS 可加载、核心方法可调用。
|
|
||||||
5. 检查新增样式是否位于 CSS 文件,不通过 JS 注入。
|
|
||||||
|
|
||||||
## 进度日志
|
|
||||||
|
|
||||||
- 2026-07-09 11:47:20 +08:00:已读取 `AGENTS.md`、项目目录、`APP.openapi.json`,确认项目是静态 HTML + jQuery/Layui,接口文档主要为 `/genealogy/app`。
|
|
||||||
- 2026-07-09 11:51:16 +08:00:用户确认按核心闭环方案执行,创建本规划与进度记录。
|
|
||||||
- 2026-07-09 11:52:53 +08:00:新增 `tests/api-client.test.js`,先运行 `node tests\api-client.test.js`,确认失败原因为缺少 `public/js/api-client.js`,进入共享 API 客户端实现。
|
|
||||||
- 2026-07-09 11:54:00 +08:00:用户补充要求:尽量不用 JS 注入样式,样式写在公共或页面 CSS 中并通过 class 查找;新增 HTML/JS/CSS 注释使用中文。
|
|
||||||
- 2026-07-09 11:57:19 +08:00:复查项目结构,确认 HTML 当前统一引用 `public/js`,`utils/DateUtil.js` 未被页面引用且存在乱码/语法风险;撤回过宽的 `api-pages.js` 方案,改为共享 `api-client.js` + 按模块/页面拆分脚本。
|
|
||||||
- 2026-07-09 11:59:22 +08:00:用户补充 CSS 架构要求:抽出可复用公共样式,每个页面 CSS 只写差异样式;将 CSS 结构整理加入接口接入前置约束。
|
|
||||||
- 2026-07-09 12:06:46 +08:00:完成认证模块第一步:新增 `public/js/auth-pages.js` 和 `tests/auth-pages.test.js`;`login.html`、`register.html`、`forgot-password.html` 改为真实表单;认证页公共 CSS 抽到 `public.css`,页面 CSS 只保留差异;重写 `page-effects.js`,移除“功能待开发”拦截和 JS 注入样式。
|
|
||||||
- 2026-07-09 12:10:54 +08:00:完成家谱核心第一步:新增 `public/js/genealogy-pages.js` 和 `tests/genealogy-pages.test.js`;`create-genealogy.html` 接入创建家谱接口;`plaza.html` 接入公开家谱列表;`profile-families.html` 接入我的家谱列表。验证:`node tests\api-client.test.js`、`node tests\auth-pages.test.js`、`node tests\genealogy-pages.test.js` 均通过;`node --check` 检查新增业务 JS 均通过;新增业务 JS 未发现 `createElement("style")` 或 `style.textContent` 注入样式。
|
|
||||||
- 2026-07-09 12:13:36 +08:00:用户补充可读性要求:现有该注释的位置补中文注释,代码不要压缩。执行范围限定为业务代码和本次维护的 CSS,第三方压缩库不改。
|
|
||||||
- 2026-07-09 12:16:27 +08:00:已给本次新增/重写业务代码补中文注释,覆盖 `api-client.js`、`auth-pages.js`、`genealogy-pages.js`、`page-effects.js` 和 `public.css` 的关键逻辑/样式区块;代码保持展开可读。验证:三组 Node 测试通过,四个业务 JS `node --check` 通过,业务 JS 未发现样式注入写法。
|
|
||||||
- 2026-07-09 12:23:36 +08:00:开始下一阶段:个人资料读取展示与反馈提交/历史反馈列表。范围:`profile.html`、`profile-data.html`、`profile-feedback.html`、`submit-ticket.html`;先写测试,再实现模块脚本和页面接入。
|
|
||||||
- 2026-07-09 12:28:30 +08:00:完成个人资料与反馈阶段:新增 `public/js/profile-pages.js`、`public/js/feedback-pages.js`、`tests/profile-pages.test.js`、`tests/feedback-pages.test.js`;`profile.html`、`profile-data.html` 接入当前用户资料读取展示;`profile-feedback.html`、`submit-ticket.html` 接入反馈提交,`profile-feedback.html` 接入历史反馈列表。验证:五组 Node 测试通过,六个业务 JS `node --check` 通过,业务 JS 未发现样式注入写法。
|
|
||||||
- 2026-07-09 12:31:51 +08:00:按用户要求重新验证第一阶段和第二阶段。第一阶段 API 客户端、认证页、家谱核心页测试均重新通过;第二阶段个人资料、反馈模块测试重新通过。复验命令包括 `node tests\api-client.test.js`、`node tests\auth-pages.test.js`、`node tests\genealogy-pages.test.js`、`node tests\profile-pages.test.js`、`node tests\feedback-pages.test.js`,以及六个业务脚本的 `node --check`。同时重新检查页面脚本引用、`data-*` 挂载点、业务 JS 样式注入写法和 `TODO/TBD/待实现/压缩` 标记,未发现异常。
|
|
||||||
- 2026-07-09 12:38:13 +08:00:继续推进家谱详情与加入申请阶段。`family-detail.html` 接入 `GET /genealogy/app/genealogies/{genealogyId}`,通过 `data-family-*` 挂载标题、简介、统计和家谱信息,并把“申请加入”链接带上家谱 ID;`join-genealogy.html` 改为真实申请表单,接入 `POST /genealogy/app/genealogies/{genealogyId}/join-applies`。新增/更新 `api-client.js` 和 `genealogy-pages.js` 中的申请加入、详情渲染、表单提交逻辑,并在对应 CSS 中补中文注释和页面差异样式。验证:五组 Node 测试全部通过,六个业务 JS `node --check` 全部通过,页面脚本引用与 `data-*` 挂载点检查通过,业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记。
|
|
||||||
- 2026-07-09 12:40:15 +08:00:根据接口 schema 补充 `joinMode` 枚举中文展示映射:`0` 显示为“关闭加入”、`1` 显示为“审核加入”、`2` 显示为“邀请码加入”,避免详情页直接显示数字。重新验证五组 Node 测试、六个业务 JS 语法检查、详情页/加入页挂载点、业务 JS 样式注入检查和核心文件遗留标记检查,结果均正常。
|
|
||||||
- 2026-07-09 12:45:24 +08:00:继续推进消息中心阶段。`profile-messages.html` 接入 `GET /genealogy/app/notifications`、`POST /genealogy/app/notifications/{notificationId}/read`、`POST /genealogy/app/notifications/read-all`,新增 `public/js/notification-pages.js` 负责消息列表渲染、单条已读、全部已读和刷新;`api-client.js` 新增通知相关封装;`profile-module.css` 增加按钮型模块卡片和未读消息样式,保持样式写入 CSS。验证:六组 Node 测试全部通过,七个业务 JS `node --check` 全部通过,消息页脚本引用和 `data-*` 挂载点检查通过,业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记。
|
|
||||||
- 2026-07-09 12:59:39 +08:00:按用户要求接入 `public/tac` 滑动验证到 `login.html`、`register.html`、`forgot-password.html`。依据 `APP.openapi.json` 的 `/captcha/require`、`/captcha/challenge`、`/captcha/verify` 字段实现 `public/js/captcha-pages.js` 适配层,将接口返回的 `captchaType/challengeId/payload` 转为 TAC 需要的 `type/id/backgroundImage/templateImage`,验证成功后把 `data.validToken` 写入当前表单隐藏字段;`auth-pages.js` 在登录、注册、发送短信验证码、重置密码前先执行滑动验证;`register.html` 原可见 `validToken` 输入改为昵称输入,避免和滑动验证隐藏字段冲突。三张认证页均引入 `public/tac/css/tac.css`、`public/tac/js/tac.min.js`、`public/js/captcha-pages.js`,并增加 `data-captcha-box` 挂载容器;`public.css` 增加认证页验证码容器样式。验证:七组 Node 测试全部通过,八个业务 JS `node --check` 全部通过,三张认证页 TAC 资源、隐藏 `validToken`、`data-captcha-box` 检查通过,自有业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记;`git diff --check` 因当前目录不是 git 仓库无法执行。
|
|
||||||
- 2026-07-09 13:07:18 +08:00:按用户提醒重新以 `APP.openapi.json` 为依据梳理剩余 `/genealogy/app` 页面归属,优先推进接口和页面最匹配的帮助中心。`help.html` 接入 `GET /genealogy/app/help-articles` 帮助文章列表,并在展开条目时按需调用 `GET /genealogy/app/help-articles/{helpId}` 补充详情;新增 `public/js/help-pages.js`,`api-client.js` 新增 `helpArticles`、`helpArticleDetail` 封装;`help.css` 补帮助列表加载状态注释和样式。验证:八组 Node 测试全部通过,九个业务 JS `node --check` 全部通过,帮助页 `data-help-list`、`api-client.js`、`help-pages.js` 引用检查通过,自有业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记。
|
|
||||||
- 2026-07-09 13:13:33 +08:00:继续按 `APP.openapi.json` 推进加入申请阶段。`profile-join-review.html` 接入 `GET /genealogy/app/genealogies/{genealogyId}/join-applies/pending` 待审核申请和 `PUT /genealogy/app/genealogies/{genealogyId}/join-applies/{applyId}/audit` 审核接口;`profile-join-family.html` 接入 `GET /genealogy/app/genealogies/join-applies/mine` 我的加入申请,并支持 `DELETE /genealogy/app/genealogies/join-applies/{applyId}` 撤销申请。新增 `public/js/join-apply-pages.js`,`api-client.js` 新增 `myJoinApplies`、`pendingJoinApplies`、`auditJoinApply`、`cancelJoinApply` 封装;`profile-module.css` 补加入申请行样式。验证:九组 Node 测试全部通过,十个业务 JS `node --check` 全部通过,两张页面脚本引用与 `data-join-apply-list` 挂载点检查通过,自有业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记。
|
|
||||||
- 2026-07-09 13:19:07 +08:00:继续按 `APP.openapi.json` 推进字辈谱阶段。`profile-generation.html` 接入 `GET /genealogy/app/genealogies/{genealogyId}/generation-poems` 查询字辈谱、`POST /genealogy/app/genealogies/{genealogyId}/generation-poems` 新增字辈、`PUT /genealogy/app/genealogies/{genealogyId}/generation-poems/{poemId}` 修改字辈;新增 `public/js/generation-pages.js`,`api-client.js` 新增 `generationPoems`、`createGenerationPoem`、`updateGenerationPoem` 封装;`profile-module.css` 补字辈谱行样式。本轮暂不接批量预览/批量保存接口,避免没有页面入口时过度实现。验证:十组 Node 测试全部通过,十一个业务 JS `node --check` 全部通过,字辈页 `data-generation-list`、`data-generation-add`、`generation-pages.js` 引用检查通过,自有业务 JS 未发现样式注入写法,核心文件未发现 `TODO/TBD/待实现/压缩` 标记。
|
|
||||||
- 2026-07-09 13:21:31 +08:00:根据用户提醒停止无编号推进,新增 `docs/api-page-integration-tracker-2026-07-09.md` 作为后续唯一编号规划与状态追踪表。后续每个模块必须先在追踪表中标记“进行中”,完成并验证后再标记“已完成”。
|
|
||||||
- 2026-07-09 13:33:41 +08:00:完成 API-011 世系树与人物管理。`profile-tree.html` 接入世系树、人物分页搜索、人物详情、新增成员和父母/配偶/子女快捷关系添加;`profile-family-home.html` 接入世系树预览和家谱主页摘要;新增 `public/js/lineage-pages.js`,`api-client.js` 新增 `lineageTree`、`lineagePersons`、`lineagePersonsPage`、`lineagePersonOptions`、`lineagePersonDetail`、`createLineagePerson`、`updateLineagePerson`、`deleteLineagePerson`、`addLineageRelation` 封装;`profile-module.css` 补世系列表、搜索栏和树结构样式。验证:全部 Node 测试通过,十二个业务 JS `node --check` 通过,两张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/lineage-pages.js` 未发现 `style` 样式注入片段。
|
|
||||||
- 2026-07-09 13:40:47 +08:00:完成 API-012 成员与家族管理。`profile-family-admin.html` 接入家谱概览和成员列表,支持成员权限更新和移除;`profile-admin-permissions.html` 接入成员选项、角色保存和所有者转让;`profile-invite.html`、`profile-share.html` 使用家谱概览生成邀请码/分享链接展示,不伪造接口文档中不存在的邀请生成接口。新增 `public/js/member-admin-pages.js`,`api-client.js` 新增 `genealogyOverview`、`genealogyMembers`、`genealogyMemberOptions`、`updateGenealogyMember`、`removeGenealogyMember`、`leaveGenealogy`、`transferGenealogyOwner` 封装;`profile-module.css` 补成员操作行样式。验证:全部 Node 测试通过,十三个业务 JS `node --check` 通过,四张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/member-admin-pages.js` 未发现 `style` 样式注入片段。
|
|
||||||
- 2026-07-09 13:46:31 +08:00:完成 API-014 谱文与内容文章。`profile-article.html` 接入谱文列表;`profile-article-edit.html` 接入分类列表、谱文新增和修改;`article-detail.html` 接入谱文详情;`profile-content.html` 的最近内容先接谱文列表,其他内容类型按后续编号继续接。新增 `public/js/article-pages.js`,`api-client.js` 新增 `articleCategories`、`articles`、`articleDetail`、`createArticle`、`updateArticle` 封装;`profile-module.css` 补谱文行样式。验证:全部 Node 测试通过,十四个业务 JS `node --check` 通过,四张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/article-pages.js` 未发现 `style` 样式注入片段。
|
|
||||||
- 2026-07-09 13:52:03 +08:00:完成 API-013 家族圈动态。`profile-feed.html` 接入动态分页列表、点赞和评论;`profile-feed-edit.html` 接入动态发布和修改。新增 `public/js/feed-pages.js`,`api-client.js` 新增 `feeds`、`feedsPage`、`feedDetail`、`createFeed`、`updateFeed`、`likeFeed`、`unlikeFeed`、`feedComments`、`feedCommentsPage`、`createFeedComment`、`deleteFeedComment` 封装;`profile-module.css` 补动态行样式。验证:全部 Node 测试通过,十五个业务 JS `node --check` 通过,两张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/feed-pages.js` 未发现 `style` 样式注入片段。
|
|
||||||
- 2026-07-09 13:57:31 +08:00:完成 API-015 相册照片。`profile-album.html` 接入相册列表、新增/修改相册、照片列表和新增照片;`promo-album.html` 复用相册列表做宣传相册展示。新增 `public/js/album-pages.js`,`api-client.js` 新增 `albums`、`createAlbum`、`updateAlbum`、`albumPhotos`、`createAlbumPhoto` 封装;`profile-module.css` 补相册和照片行样式。验证:全部 Node 测试通过,十六个业务 JS `node --check` 通过,两张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/album-pages.js` 未发现 `style` 样式注入片段。
|
|
||||||
- 2026-07-09 14:04:52 +08:00:完成 API-019 文件上传公共能力。`api-client.js` 接入单文件上传、文件引用绑定/释放、分片初始化、分片上传、分片完成接口;新增 `public/js/upload-pages.js` 统一处理文件选择、上传、OSS ID 回填和上传状态展示;`profile-article-edit.html` 接入谱文封面上传,`profile-feed-edit.html` 接入动态图片上传,`profile-album.html` 接入相册封面和照片上传;`profile-module.css` 补上传控件样式。验证:全部 Node 测试通过,十七个业务 JS `node --check` 通过,上传控件挂载点检查通过,`public/js/api-client.js` 和 `public/js/upload-pages.js` 未发现样式注入片段。
|
|
||||||
- 2026-07-09 14:12:27 +08:00:完成 API-016 祭祀、献礼与功德记录。`profile-gift.html` 接入祭祀/贺礼列表、献礼列表和新增献礼;`profile-gift-edit.html` 接入贺礼创建/修改和封面上传;`profile-merit.html` 接入功德记录列表;`profile-merit-edit.html` 接入新增功德记录。新增 `public/js/ceremony-pages.js`,`api-client.js` 新增 `ceremonies`、`ceremonyDetail`、`createCeremony`、`updateCeremony`、`ceremonyGifts`、`createCeremonyGift`、`meritRecords`、`createMeritRecord` 封装;`profile-module.css` 补贺礼、献礼和功德记录行样式。验证:全部 Node 测试通过,十八个业务 JS `node --check` 通过,四张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/ceremony-pages.js` 未发现样式注入片段。
|
|
||||||
- 2026-07-09 14:18:15 +08:00:完成 API-017 成长记录。`profile-growth.html` 接入成长记录列表;`profile-growth-edit.html` 接入成长记录新增/修改和附件上传。新增 `public/js/growth-pages.js`,`api-client.js` 新增 `growthRecords`、`growthRecordDetail`、`createGrowthRecord`、`updateGrowthRecord` 封装;`profile-module.css` 补成长记录行样式。验证:全部 Node 测试通过,十九个业务 JS `node --check` 通过,两张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/growth-pages.js` 未发现样式注入片段。
|
|
||||||
- 2026-07-09 14:24:10 +08:00:完成 API-018 备忘录。`profile-memo.html` 接入备忘录列表;`profile-memo-edit.html` 接入备忘录新增/修改和附件上传。新增 `public/js/memo-pages.js`,`api-client.js` 新增 `memos`、`memoDetail`、`createMemo`、`updateMemo` 封装;`profile-module.css` 补备忘录行样式。验证:全部 Node 测试通过,二十个业务 JS `node --check` 通过,两张页面脚本引用与 `data-*` 挂载点检查通过,`public/js/api-client.js` 和 `public/js/memo-pages.js` 未发现样式注入片段。
|
|
||||||
- 2026-07-09 14:31:29 +08:00:完成 API-020 会员套餐与订单创建。`profile-services.html` 接入会员套餐列表、会员订单创建表单和会员订单列表;新增 `public/js/vip-pages.js`,`api-client.js` 新增 `vipPackages`、`vipOrders`、`createVipOrder` 封装,`profile-module.css` 补会员套餐卡片和订单行样式。验证:`node tests\vip-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、会员页挂载点/字段检查均通过;API-020 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 14:37:25 +08:00:完成 API-021 推广/公告类内容。`app.html` 接入应用推广列表,`download.html` 接入下载推广入口,`notice-detail.html` 从 `/genealogy/app/promotions` 列表按 `id` 选择公告详情,未伪造接口文档不存在的详情接口。新增 `public/js/promotion-pages.js`,`api-client.js` 新增 `promotions` 封装,`app.css` 补推广卡片样式。验证:`node tests\promotion-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、三张页面推广挂载点检查均通过;API-021 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 14:42:47 +08:00:完成 API-022 账号安全。`profile-security.html` 接入修改密码、换绑手机号、短信登录校验、退出登录和账号注销表单;新增 `public/js/security-pages.js`,`api-client.js` 新增 `changePassword`、`changePhone`、`smsLogin`、`logout`、`deactivateAccount` 封装,`profile-module.css` 补安全表单和危险操作样式。验证:`node tests\security-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、账号安全页挂载点检查均通过;API-022 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 14:49:54 +08:00:完成 API-023 地区选择公共能力。`create-genealogy.html` 接入省/市/区选择器并回填 `regionCode`,`profile-data.html` 接入个人资料省市区更新表单;新增 `public/js/region-pages.js`,`api-client.js` 新增 `regionChildren`、`regionPath`、`regionSearch`、`regionDetail` 封装,`genealogy-pages.js` 去掉 `regionCode=000000` 兜底并改为提交前校验真实地区编码,两个 CSS 文件补地区选择器样式。验证:`node tests\region-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、两张页面地区挂载点检查均通过;API-023 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 14:50:47 +08:00:复查 API-024/API-025。`surname.html`、`surname-detail.html`、`search-result.html`、`genealogy.html`、`culture.html`、`about.html`、`index.html` 未发现明确同名接口,公开家谱接口也不支持搜索参数,因此标记暂缓;`profile-video.html`、`promo-video.html`、`profile-data-reminders.html` 暂无视频/资料提醒独立接口,标记暂缓;`profile-create-family.html` 可复用创建家谱能力,单独规划为 API-026。
|
|
||||||
- 2026-07-09 14:53:50 +08:00:完成 API-026 个人中心创建家谱页。`profile-create-family.html` 改为真实创建家谱表单,接入 `api-client.js`、`region-pages.js`、`genealogy-pages.js`,复用 `/genealogy/app/genealogies` 和地区选择公共能力;`profile-module.css` 补个人中心创建表单样式,`tests/genealogy-pages.test.js` 补真实 `regionCode` 断言。验证:`node tests\genealogy-pages.test.js`、`node tests\region-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、页面挂载点检查均通过;API-026 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 15:03:27 +08:00:完成 API-027 个人资料编辑提交。`profile-data.html` 增加真实个人资料编辑表单,`profile-pages.js` 增加 `ProfileUpdateBody` 构造、表单填充和 `PUT /genealogy/app/auth/profile` 提交,`profile-module.css` 补本页表单状态样式,`tests/profile-pages.test.js` 和 `tests/api-client.test.js` 补转换与接口契约测试。验证:先观察 `node tests\profile-pages.test.js` 因缺少 `buildProfileUpdateBody` 失败;实现后 `node tests\profile-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、页面挂载点检查均通过,未发现样式注入片段和 TODO 类占位;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
- 2026-07-09 15:11:37 +08:00:完成 API-028 字辈批量预览与批量保存。`profile-generation.html` 增加批量字辈表单和预览区域,`api-client.js` 增加 `previewGenerationPoems`、`saveGenerationPoemsBatch`,`generation-pages.js` 增加 `GenerationPoemBatchBody` 构造、预览渲染和保存动作,`profile-module.css` 补批量面板样式,`tests/generation-pages.test.js` 和 `tests/api-client.test.js` 补批量接口测试。验证:先观察 `node tests\generation-pages.test.js` 因缺少 `buildGenerationBatchBody` 失败、`node tests\api-client.test.js` 因缺少 `previewGenerationPoems` 失败;实现后 `node tests\generation-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、页面挂载点检查均通过,未发现样式注入片段和 TODO 类占位;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# 接口对接编号规划与状态追踪
|
|
||||||
|
|
||||||
创建时间:2026-07-09 13:21:31 +08:00
|
|
||||||
流程修正时间:2026-07-09 13:21:31 +08:00
|
|
||||||
接口依据:`APP.openapi.json`
|
|
||||||
主进度记录:`docs/api-page-integration-2026-07-09.md`
|
|
||||||
|
|
||||||
## 执行规则
|
|
||||||
|
|
||||||
1. 后续不再无编号推进,所有接口对接必须先进入本追踪表。
|
|
||||||
2. 每一项开始前,先把状态从“待开始”改为“进行中”,并记录开始时间。
|
|
||||||
3. 每一项完成后,必须记录完成时间、修改文件、接口路径、验证命令或静态检查结果。
|
|
||||||
4. 每一项验证通过后,才能把状态改为“已完成”。
|
|
||||||
5. 若接口文档没有明确对应接口,状态标记为“待确认”或“暂缓”,不强行接入。
|
|
||||||
6. 样式继续遵守公共 `public.css` + 页面 CSS 的结构,业务 JS 不注入样式。
|
|
||||||
7. 新增或修改的 HTML、JS、CSS 注释使用中文,代码保持可读、不压缩。
|
|
||||||
|
|
||||||
## 状态说明
|
|
||||||
|
|
||||||
- 已完成:已按接口文档接入,并完成对应验证。
|
|
||||||
- 进行中:当前正在推进,未完成前不能开始下一个无关模块。
|
|
||||||
- 待开始:已规划,尚未改代码。
|
|
||||||
- 待确认:页面存在,但接口归属需要进一步确认。
|
|
||||||
- 暂缓:接口或页面入口不足,暂不实现,避免过度开发。
|
|
||||||
|
|
||||||
## 已完成回填
|
|
||||||
|
|
||||||
| 编号 | 状态 | 页面范围 | 接口范围 | 完成记录 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| API-000 | 已完成 | 全项目 | `APP.openapi.json`、`AGENTS.md`、项目结构 | 2026-07-09 11:47:20 +08:00 已读取约束、接口文档和目录结构。 |
|
|
||||||
| API-001 | 已完成 | 全项目公共请求 | 共享请求客户端、认证头、租户头、Token 存取 | 2026-07-09 11:52:53 +08:00 至 12:16:27 +08:00 完成 `public/js/api-client.js` 与测试。 |
|
|
||||||
| API-002 | 已完成 | `login.html`、`register.html`、`forgot-password.html` | `/genealogy/app/auth/login`、`/genealogy/app/auth/register`、`/genealogy/app/auth/sms/code`、`/genealogy/app/auth/password/reset`、`/captcha/require`、`/captcha/challenge`、`/captcha/verify` | 2026-07-09 12:06:46 +08:00 完成认证页;2026-07-09 12:59:39 +08:00 完成 TAC 滑动验证接入。 |
|
|
||||||
| API-003 | 已完成 | `create-genealogy.html`、`plaza.html`、`profile-families.html` | `/genealogy/app/genealogies`、`/genealogy/app/genealogies/public`、`/genealogy/app/genealogies/mine` | 2026-07-09 12:10:54 +08:00 完成创建家谱、公开家谱、我的家谱列表。 |
|
|
||||||
| API-004 | 已完成 | `family-detail.html`、`join-genealogy.html` | `/genealogy/app/genealogies/{genealogyId}`、`/genealogy/app/genealogies/{genealogyId}/join-applies` | 2026-07-09 12:38:13 +08:00 完成家谱详情与申请加入;2026-07-09 12:40:15 +08:00 补 `joinMode` 展示映射。 |
|
|
||||||
| API-005 | 已完成 | `profile.html`、`profile-data.html` | `/genealogy/app/auth/profile` | 2026-07-09 12:28:30 +08:00 完成个人资料读取展示。 |
|
|
||||||
| API-006 | 已完成 | `profile-feedback.html`、`submit-ticket.html` | `/genealogy/app/feedback` | 2026-07-09 12:28:30 +08:00 完成反馈提交与历史反馈。 |
|
|
||||||
| API-007 | 已完成 | `profile-messages.html` | `/genealogy/app/notifications`、`/genealogy/app/notifications/{notificationId}/read`、`/genealogy/app/notifications/read-all` | 2026-07-09 12:45:24 +08:00 完成消息中心。 |
|
|
||||||
| API-008 | 已完成 | `help.html` | `/genealogy/app/help-articles`、`/genealogy/app/help-articles/{helpId}` | 2026-07-09 13:07:18 +08:00 完成帮助中心。 |
|
|
||||||
| API-009 | 已完成 | `profile-join-review.html`、`profile-join-family.html` | `/genealogy/app/genealogies/{genealogyId}/join-applies/pending`、`/genealogy/app/genealogies/{genealogyId}/join-applies/{applyId}/audit`、`/genealogy/app/genealogies/join-applies/mine`、`/genealogy/app/genealogies/join-applies/{applyId}` | 2026-07-09 13:13:33 +08:00 完成入谱申请审核与我的申请。 |
|
|
||||||
| API-010 | 已完成 | `profile-generation.html` | `/genealogy/app/genealogies/{genealogyId}/generation-poems`、`/genealogy/app/genealogies/{genealogyId}/generation-poems/{poemId}` | 2026-07-09 13:19:07 +08:00 完成字辈谱查询、新增、修改;批量接口因页面无入口暂缓。 |
|
|
||||||
|
|
||||||
## 后续编号规划
|
|
||||||
|
|
||||||
| 编号 | 状态 | 页面范围 | 接口范围 | 计划与验证 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| API-011 | 已完成 | `profile-tree.html`、`profile-family-home.html` | `/genealogy/app/genealogies/{genealogyId}/lineage/tree`、`/genealogy/app/genealogies/{genealogyId}/lineage/persons`、`/genealogy/app/genealogies/{genealogyId}/lineage/persons/page`、`/genealogy/app/genealogies/{genealogyId}/lineage/persons/{personId}`、`/genealogy/app/genealogies/{genealogyId}/lineage/persons/{personId}/children`、`/parents`、`/siblings`、`/spouses` | 2026-07-09 13:25:03 +08:00 开始;2026-07-09 13:33:41 +08:00 完成。新增 `public/js/lineage-pages.js`、`tests/lineage-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、`profile-tree.html`、`profile-family-home.html`、`public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-011 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-012 | 已完成 | `profile-family-admin.html`、`profile-admin-permissions.html`、`profile-invite.html`、`profile-share.html` | `/genealogy/app/genealogies/{genealogyId}/members`、`/genealogy/app/genealogies/{genealogyId}/members/{memberId}`、`/genealogy/app/genealogies/{genealogyId}/members/me`、`/genealogy/app/genealogies/{genealogyId}/members/options`、`/genealogy/app/genealogies/{genealogyId}/members/owner-transfer`、`/genealogy/app/genealogies/{genealogyId}/overview` | 2026-07-09 13:34:36 +08:00 开始;2026-07-09 13:40:47 +08:00 完成。新增 `public/js/member-admin-pages.js`、`tests/member-admin-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、四张成员/邀请页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-012 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-013 | 已完成 | `profile-feed.html`、`profile-feed-edit.html` | `/genealogy/app/genealogies/{genealogyId}/feeds`、`/genealogy/app/genealogies/{genealogyId}/feeds/page`、`/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}`、`/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/likes`、评论接口 | 2026-07-09 13:47:29 +08:00 开始;2026-07-09 13:52:03 +08:00 完成。新增 `public/js/feed-pages.js`、`tests/feed-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、两张动态页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-013 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-014 | 已完成 | `profile-article.html`、`profile-article-edit.html`、`article-detail.html`、`profile-content.html` | `/genealogy/app/genealogies/{genealogyId}/article-categories`、`/genealogy/app/genealogies/{genealogyId}/articles`、`/genealogy/app/genealogies/{genealogyId}/articles/{articleId}` | 2026-07-09 13:41:44 +08:00 开始;2026-07-09 13:46:31 +08:00 完成。新增 `public/js/article-pages.js`、`tests/article-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、四张谱文/内容页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-014 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-015 | 已完成 | `profile-album.html`、`promo-album.html` | `/genealogy/app/genealogies/{genealogyId}/albums`、`/genealogy/app/genealogies/{genealogyId}/albums/{albumId}`、`/genealogy/app/genealogies/{genealogyId}/albums/{albumId}/photos` | 2026-07-09 13:52:58 +08:00 开始;2026-07-09 13:57:31 +08:00 完成。新增 `public/js/album-pages.js`、`tests/album-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、两张相册页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-015 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-016 | 已完成 | `profile-gift.html`、`profile-gift-edit.html`、`profile-merit.html`、`profile-merit-edit.html` | `/genealogy/app/genealogies/{genealogyId}/ceremonies`、`/genealogy/app/genealogies/{genealogyId}/ceremonies/{ceremonyId}`、`/genealogy/app/genealogies/{genealogyId}/ceremonies/{ceremonyId}/gifts`、`/genealogy/app/genealogies/{genealogyId}/merit-records` | 2026-07-09 14:05:57 +08:00 开始;2026-07-09 14:12:27 +08:00 完成。新增 `public/js/ceremony-pages.js`、`tests/ceremony-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、四张贺礼/功德页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-016 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-017 | 已完成 | `profile-growth.html`、`profile-growth-edit.html` | `/genealogy/app/genealogies/{genealogyId}/growth-records`、`/genealogy/app/genealogies/{genealogyId}/growth-records/{recordId}` | 2026-07-09 14:13:25 +08:00 开始;2026-07-09 14:18:15 +08:00 完成。新增 `public/js/growth-pages.js`、`tests/growth-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、两张成长记录页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-017 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-018 | 已完成 | `profile-memo.html`、`profile-memo-edit.html` | `/genealogy/app/genealogies/{genealogyId}/memos`、`/genealogy/app/genealogies/{genealogyId}/memos/{memoId}` | 2026-07-09 14:19:26 +08:00 开始;2026-07-09 14:24:10 +08:00 完成。新增 `public/js/memo-pages.js`、`tests/memo-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、两张备忘录页面和 `public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面挂载点检查通过,API-018 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-019 | 已完成 | 文章、动态、相册、成长记录等需要上传的页面 | `/genealogy/app/files/upload`、`/genealogy/app/files/reference`、`/genealogy/app/files/resumable/init`、`/genealogy/app/files/resumable/chunk`、`/genealogy/app/files/resumable/complete` | 2026-07-09 14:00:01 +08:00 开始;2026-07-09 14:04:52 +08:00 完成。新增 `public/js/upload-pages.js`、`tests/upload-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、`profile-article-edit.html`、`profile-feed-edit.html`、`profile-album.html`、`public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,上传控件挂载点检查通过,API-019 自有 JS 未发现样式注入片段。 |
|
|
||||||
| API-020 | 已完成 | `profile-services.html` | `/genealogy/app/vip/packages`、`/genealogy/app/vip/orders` | 2026-07-09 14:26:34 +08:00 开始;2026-07-09 14:31:29 +08:00 完成。新增 `public/js/vip-pages.js`、`tests/vip-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、`profile-services.html`、`public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,会员页挂载点和表单字段检查通过,API-020 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-021 | 已完成 | `app.html`、`download.html`、`notice-detail.html` | `/genealogy/app/promotions` | 2026-07-09 14:33:01 +08:00 开始;2026-07-09 14:37:25 +08:00 完成。新增 `public/js/promotion-pages.js`、`tests/promotion-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、`app.html`、`download.html`、`notice-detail.html`、`public/css/app.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,三张页面推广挂载点检查通过,API-021 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-022 | 已完成 | `profile-security.html` | `/genealogy/app/auth/password`、`/genealogy/app/auth/phone`、`/genealogy/app/auth/account/deactivate`、`/genealogy/app/auth/logout`、`/genealogy/app/auth/login/sms` | 2026-07-09 14:38:22 +08:00 开始;2026-07-09 14:42:47 +08:00 完成。新增 `public/js/security-pages.js`、`tests/security-pages.test.js`;更新 `public/js/api-client.js`、`tests/api-client.test.js`、`profile-security.html`、`public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,账号安全页表单挂载点检查通过,API-022 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-023 | 已完成 | `create-genealogy.html`、`profile-data.html`、涉及地区选择的编辑页 | `/genealogy/region/children`、`/genealogy/region/search`、`/genealogy/region/{regionCode}`、`/genealogy/region/path/{regionCode}` | 2026-07-09 14:44:43 +08:00 开始;2026-07-09 14:49:54 +08:00 完成。新增 `public/js/region-pages.js`、`tests/region-pages.test.js`;更新 `public/js/api-client.js`、`public/js/genealogy-pages.js`、`tests/api-client.test.js`、`tests/genealogy-pages.test.js`、`create-genealogy.html`、`profile-data.html`、`public/css/create-genealogy.css`、`public/css/profile-module.css`。验证:全部 Node 测试通过,业务 JS 语法检查通过,两张页面地区选择挂载点检查通过,API-023 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-024 | 暂缓 | `surname.html`、`surname-detail.html`、`search-result.html`、`genealogy.html`、`culture.html`、`about.html`、`index.html` | 当前接口文档未发现明确同名业务接口 | 2026-07-09 14:50:47 +08:00 复查。公开家谱接口不支持搜索参数,文章接口依赖 `genealogyId`,推广接口只有列表,不强行映射为姓氏百科或全站搜索。 |
|
|
||||||
| API-025 | 暂缓 | `profile-video.html`、`promo-video.html`、`profile-data-reminders.html` | 当前接口文档未发现明确视频、资料提醒独立接口 | 2026-07-09 14:50:47 +08:00 复查。视频和资料提醒没有独立接口,暂不伪造接口;`profile-create-family.html` 归入新增 API-026。 |
|
|
||||||
| API-026 | 已完成 | `profile-create-family.html` | `/genealogy/app/genealogies`、`/genealogy/region/children` | 2026-07-09 14:51:39 +08:00 开始;2026-07-09 14:53:50 +08:00 完成。更新 `profile-create-family.html`、`public/css/profile-module.css`、`tests/genealogy-pages.test.js`,复用 `public/js/api-client.js`、`public/js/region-pages.js`、`public/js/genealogy-pages.js`。验证:全部 Node 测试通过,业务 JS 语法检查通过,页面创建表单和地区选择挂载点检查通过,API-026 自有 JS 未发现样式注入片段;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-027 | 已完成 | `profile-data.html` | `/genealogy/app/auth/profile` | 2026-07-09 14:57:51 +08:00 开始;2026-07-09 15:03:27 +08:00 完成。更新 `profile-data.html`、`public/js/profile-pages.js`、`public/css/profile-module.css`、`tests/profile-pages.test.js`、`tests/api-client.test.js`。补齐个人资料编辑提交,字段对应 `ProfileUpdateBody`:`nickName`、`avatarOssId`、`sex`、`birthday`;地区字段继续沿用 API-023。验证:已观察 `node tests\profile-pages.test.js` 因缺少 `buildProfileUpdateBody` 失败;实现后 `node tests\profile-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、页面挂载点检查均通过;未发现样式注入片段和 TODO 类占位;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-028 | 已完成 | `profile-generation.html` | `/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/preview`、`/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/save` | 2026-07-09 15:05:58 +08:00 开始;2026-07-09 15:11:37 +08:00 完成。更新 `profile-generation.html`、`public/js/api-client.js`、`public/js/generation-pages.js`、`public/css/profile-module.css`、`tests/generation-pages.test.js`、`tests/api-client.test.js`。补齐字辈批量预览和批量保存,字段对应 `GenerationPoemBatchBody`:`content`、`stopMissingOldGeneration`。验证:已观察 `node tests\generation-pages.test.js` 因缺少 `buildGenerationBatchBody` 失败,`node tests\api-client.test.js` 因缺少 `previewGenerationPoems` 失败;实现后 `node tests\generation-pages.test.js`、`node tests\api-client.test.js`、全部 `tests/*.test.js`、业务 JS `node --check`、页面挂载点检查均通过;未发现样式注入片段和 TODO 类占位;`git diff --check` 因当前目录不是 Git 仓库无法执行。 |
|
|
||||||
| API-029 | 已中止 | `profile-services.html` | `/genealogy/app/genealogies/options` | 2026-07-09 15:13:03 +08:00 开始;2026-07-09 15:14:58 +08:00 中止。原因:用户确认此前依据的 `APP.openapi.json` 不是 PC 接口文档,PC/H5 应改用 `genealogy-pc-openapi.yaml`,且接口地址使用 `https://test-genealogy-api.ddxcjp.cn`。本项不再继续按 APP 接口推进。 |
|
|
||||||
|
|
||||||
## 下一步执行顺序
|
|
||||||
|
|
||||||
1. API-011:世系树与人物管理。
|
|
||||||
2. API-012:成员与家族管理。
|
|
||||||
3. API-014:谱文与内容文章。
|
|
||||||
4. API-013:家族圈动态。
|
|
||||||
5. API-015:相册照片。
|
|
||||||
6. API-019:上传公共能力。
|
|
||||||
7. API-016 至 API-023:按页面入口完整度继续推进。
|
|
||||||
8. API-024、API-025:只在确认接口归属后推进。
|
|
||||||
|
|
||||||
## 本次流程修正记录
|
|
||||||
|
|
||||||
- 2026-07-09 13:21:31 +08:00:收到用户提醒,停止继续无编号推进;补充本编号追踪表,并要求后续每项先标记状态再实现,完成后再标记已完成。
|
|
||||||
- 2026-07-09 14:26:34 +08:00:开始执行 API-020,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:31:29 +08:00:API-020 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:33:01 +08:00:开始执行 API-021,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:37:25 +08:00:API-021 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:38:22 +08:00:开始执行 API-022,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:42:47 +08:00:API-022 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:44:43 +08:00:开始执行 API-023,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:49:54 +08:00:API-023 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:50:47 +08:00:复查 API-024/API-025,缺少明确接口的页面标记为“暂缓”,将 `profile-create-family.html` 单独规划为 API-026。
|
|
||||||
- 2026-07-09 14:51:39 +08:00:开始执行 API-026,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:53:50 +08:00:API-026 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:57:51 +08:00:开始执行 API-027,状态标记为“进行中”;按接口文档把 `profile-data.html` 的个人资料更新归到 `/genealogy/app/auth/profile` PUT,先写测试再实现。
|
|
||||||
- 2026-07-09 15:03:27 +08:00:API-027 执行完成,状态从“进行中”改为“已完成”;完成个人资料编辑表单、提交逻辑、页面样式和测试验证。
|
|
||||||
- 2026-07-09 15:05:58 +08:00:开始执行 API-028,状态标记为“进行中”;按接口文档把 `profile-generation.html` 的批量字辈能力归到 `/generation-poems/batch/preview` 和 `/generation-poems/batch/save`。
|
|
||||||
- 2026-07-09 15:11:37 +08:00:API-028 执行完成,状态从“进行中”改为“已完成”;完成批量字辈预览、批量保存、页面入口、样式和测试验证。
|
|
||||||
- 2026-07-09 15:13:03 +08:00:开始执行 API-029,状态标记为“进行中”;把 `profile-services.html` 的家谱选择归到 `/genealogy/app/genealogies/options`。
|
|
||||||
- 2026-07-09 15:14:58 +08:00:收到用户纠正,确认旧依据 `APP.openapi.json` 不适合作为 PC 页面接口文档;中止 API-029,后续改以 `genealogy-pc-openapi.yaml` 和 `https://test-genealogy-api.ddxcjp.cn` 重新规划。
|
|
||||||
- 2026-07-09 13:25:03 +08:00:开始执行 API-011,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 13:33:41 +08:00:API-011 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 13:34:36 +08:00:开始执行 API-012,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 13:40:47 +08:00:API-012 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 13:41:44 +08:00:开始执行 API-014,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 13:46:31 +08:00:API-014 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 13:47:29 +08:00:开始执行 API-013,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 13:52:03 +08:00:API-013 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 13:52:58 +08:00:开始执行 API-015,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 13:57:31 +08:00:API-015 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:00:01 +08:00:开始执行 API-019,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:04:52 +08:00:API-019 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:05:57 +08:00:开始执行 API-016,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:12:27 +08:00:API-016 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:13:25 +08:00:开始执行 API-017,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:18:15 +08:00:API-017 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
- 2026-07-09 14:19:26 +08:00:开始执行 API-018,状态从“待开始”改为“进行中”。
|
|
||||||
- 2026-07-09 14:24:10 +08:00:API-018 执行完成,状态从“进行中”改为“已完成”。
|
|
||||||
@@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
创建时间:2026-07-09 15:14:58 +08:00
|
创建时间:2026-07-09 15:14:58 +08:00
|
||||||
接口依据:`genealogy-pc-openapi.yaml`
|
接口依据:`genealogy-pc-openapi.yaml`
|
||||||
接口基础地址:`http://test-genealogy-api.ddxcjp.cn`
|
接口基础地址:`http://182.61.18.23:8080`
|
||||||
旧依据处理:`APP.openapi.json` 只作为历史误用记录,不再作为 PC 页面继续对接依据。
|
接口依据:`genealogy-pc-openapi.yaml`
|
||||||
旧进度记录:`docs/api-page-integration-tracker-2026-07-09.md`
|
|
||||||
|
## 当前地址契约
|
||||||
|
|
||||||
|
- 运行时唯一来源:根目录 `config.js`。
|
||||||
|
- 当前开发和生产地址:`http://182.61.18.23:8080`。
|
||||||
|
- 地址变更记录:2026-07-11 已由历史测试域名切换为当前 IP 和端口;文档中出现的 `test-genealogy-api.ddxcjp.cn` 仅用于描述历史事件,不代表当前运行时配置。
|
||||||
|
|
||||||
## 执行规则
|
## 执行规则
|
||||||
|
|
||||||
@@ -12,7 +17,7 @@
|
|||||||
2. 每一项开始前先标记“进行中”,记录年月日时分秒和时区。
|
2. 每一项开始前先标记“进行中”,记录年月日时分秒和时区。
|
||||||
3. 每一项完成后记录完成时间、修改文件、接口路径、验证命令和结果。
|
3. 每一项完成后记录完成时间、修改文件、接口路径、验证命令和结果。
|
||||||
4. 只有完成验证后才能标记“已完成”。
|
4. 只有完成验证后才能标记“已完成”。
|
||||||
5. `genealogy-pc-openapi.yaml` 没有明确接口的页面,标记“暂缓”或“待确认”,不继续套用 APP 路径。
|
5. `genealogy-pc-openapi.yaml` 没有明确接口的页面,标记“暂缓”或“待确认”,不继续套用旧路径。
|
||||||
6. 样式继续写入 CSS 文件,业务 JS 不注入样式。
|
6. 样式继续写入 CSS 文件,业务 JS 不注入样式。
|
||||||
7. 新增或修改的 HTML、JS、CSS 注释使用中文,代码保持可读、不压缩。
|
7. 新增或修改的 HTML、JS、CSS 注释使用中文,代码保持可读、不压缩。
|
||||||
|
|
||||||
@@ -29,10 +34,10 @@
|
|||||||
分析文件:`genealogy-pc-openapi.yaml`
|
分析文件:`genealogy-pc-openapi.yaml`
|
||||||
|
|
||||||
1. `paths` 中实际存在 25 个接口操作,分为验证码、PC 认证、PC 文件、行政区划四类。
|
1. `paths` 中实际存在 25 个接口操作,分为验证码、PC 认证、PC 文件、行政区划四类。
|
||||||
2. `servers` 仍是本地地址示例,项目实际对接基础地址继续按用户指定使用 `https://test-genealogy-api.ddxcjp.cn`。
|
2. `servers` 仍是本地地址示例;当前运行时基础地址以 `config.js` 为准,为 `http://182.61.18.23:8080`。
|
||||||
3. 登录后接口使用 `Authorization` 作为 token header,同时所有 PC 认证和文件接口都要求 header `clientid`;用户最新提供的 PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`,客户端 Key 为 `web_pc`。
|
3. 登录后接口使用 `Authorization` 作为 token header,同时所有 PC 认证和文件接口都要求 header `clientid`;用户最新提供的 PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`,客户端 Key 为 `web_pc`。
|
||||||
4. `components.tags` 和 `components.schemas/requestBodies` 中出现家谱、动态、世系、字辈等业务模型,但 `paths` 没有对应接口路径;实现时不能把这些 schema 当成可调用 PC 接口。
|
4. `components.tags` 和 `components.schemas/requestBodies` 中出现家谱、动态、世系、字辈等业务模型,但 `paths` 没有对应接口路径;实现时不能把这些 schema 当成可调用 PC 接口。
|
||||||
5. 验证码 schema 示例里仍出现 `APP_LOGIN`、`PC_LOGIN` 等文本示例;实际页面场景以后台配置为准,当前明确可用的 PC/H5 场景为 `WEB_H5_LOGIN`、`WEB_H5_REGISTER`、`WEB_H5_FORGOT_PASSWORD`。
|
5. 验证码场景以后台配置为准,当前明确可用的 PC/H5 场景为 `WEB_H5_LOGIN`、`WEB_H5_REGISTER`、`WEB_H5_FORGOT_PASSWORD`。
|
||||||
6. `ProfileUpdate` 示例出现 `regionCode/addressDetail`,但 schema 实际字段是 `provinceCode/cityCode/districtCode`,页面资料提交时应以 schema 字段为准。
|
6. `ProfileUpdate` 示例出现 `regionCode/addressDetail`,但 schema 实际字段是 `provinceCode/cityCode/districtCode`,页面资料提交时应以 schema 字段为准。
|
||||||
7. `SmsLoginBody` 未声明 `validToken` 字段,但短信发送 `SmsCodeBody` 必填 `validToken`;短信登录流程应先通过验证码拿票据,再发短信验证码,最后用短信码登录。
|
7. `SmsLoginBody` 未声明 `validToken` 字段,但短信发送 `SmsCodeBody` 必填 `validToken`;短信登录流程应先通过验证码拿票据,再发短信验证码,最后用短信码登录。
|
||||||
8. 文件引用 `FileReferenceBody` 必填 `bizType/bizTable/bizId/bizField`,`ossId/ossIds/usageScene/usageName` 可选;但文章、动态、相册等业务主体接口未提供,文件引用只能作为公共上传能力,不代表主业务已可提交。
|
8. 文件引用 `FileReferenceBody` 必填 `bizType/bizTable/bizId/bizField`,`ossId/ossIds/usageScene/usageName` 可选;但文章、动态、相册等业务主体接口未提供,文件引用只能作为公共上传能力,不代表主业务已可提交。
|
||||||
@@ -41,7 +46,6 @@
|
|||||||
|
|
||||||
- 后台验证码配置截图中,PC/H5 当前明确可用场景是 `WEB_H5_LOGIN`、`WEB_H5_REGISTER`、`WEB_H5_FORGOT_PASSWORD`,客户端 Key 为 `web_pc`,验证服务为“天爱行为验证码”,验证码类型为“滑块验证码”。
|
- 后台验证码配置截图中,PC/H5 当前明确可用场景是 `WEB_H5_LOGIN`、`WEB_H5_REGISTER`、`WEB_H5_FORGOT_PASSWORD`,客户端 Key 为 `web_pc`,验证服务为“天爱行为验证码”,验证码类型为“滑块验证码”。
|
||||||
- `ADMIN_LOGIN` 是后台管理登录,类型为旋转验证码,不归当前 PC/H5 页面使用。
|
- `ADMIN_LOGIN` 是后台管理登录,类型为旋转验证码,不归当前 PC/H5 页面使用。
|
||||||
- `APP_LOGIN`、`APP_REGISTER`、`APP_PHONE_CHANGE`、`APP_FORGOT_PASSWORD`、`APP_SMS_CODE`、`APP_ACCOUNT_DEACTIVATE` 的客户端 Key 为 `app`,不能继续作为 PC/H5 场景默认值。
|
|
||||||
- PC/H5 登录页调用链:
|
- PC/H5 登录页调用链:
|
||||||
1. 页面加载 `public/tac/css/tac.css`、`public/tac/js/tac.min.js`、`public/js/captcha-pages.js`。
|
1. 页面加载 `public/tac/css/tac.css`、`public/tac/js/tac.min.js`、`public/js/captcha-pages.js`。
|
||||||
2. 表单提供 `input[name="validToken"]` 隐藏字段和 `[data-captcha-box]` 容器。
|
2. 表单提供 `input[name="validToken"]` 隐藏字段和 `[data-captcha-box]` 容器。
|
||||||
@@ -64,10 +68,10 @@
|
|||||||
| PC-004 | 本地复核通过 | 文件上传页面 | `/genealogy/pc/files/*` | `node tests\upload-pages.test.js`、`node tests\api-client.test.js` 通过;文件上传仍需真实文件和后端授权进行端到端验证。 |
|
| PC-004 | 本地复核通过 | 文件上传页面 | `/genealogy/pc/files/*` | `node tests\upload-pages.test.js`、`node tests\api-client.test.js` 通过;文件上传仍需真实文件和后端授权进行端到端验证。 |
|
||||||
| PC-005 | 本地复核通过 | 地区选择页面 | `/genealogy/region/*` | `node tests\region-pages.test.js`、`node tests\profile-pages.test.js` 通过。 |
|
| PC-005 | 本地复核通过 | 地区选择页面 | `/genealogy/region/*` | `node tests\region-pages.test.js`、`node tests\profile-pages.test.js` 通过。 |
|
||||||
| PC-006 | 复核通过 | 页面覆盖矩阵 | PC YAML 覆盖边界 | 保持“公共能力不等于整页业务已对接”的边界,未覆盖业务继续受控失败。 |
|
| PC-006 | 复核通过 | 页面覆盖矩阵 | PC YAML 覆盖边界 | 保持“公共能力不等于整页业务已对接”的边界,未覆盖业务继续受控失败。 |
|
||||||
| PC-007 | 复核通过 | 实施规划 | `genealogy-pc-openapi.yaml` | 当前计划与 YAML `paths` 一致,不使用旧 APP 路径 fallback。 |
|
| PC-007 | 复核通过 | 实施规划 | `genealogy-pc-openapi.yaml` | 当前计划与 YAML `paths` 一致,不使用旧路径 fallback。 |
|
||||||
| PC-008 | 复核通过 | PC YAML 未覆盖业务页面 | `PC_API_NOT_AVAILABLE` | `utils/ApiClient.js` 统一抛 `PC_API_NOT_AVAILABLE`,旧 `/genealogy/app/` 请求路径扫描无结果。 |
|
| PC-008 | 复核通过 | PC YAML 未覆盖业务页面 | `PC_API_NOT_AVAILABLE` | `utils/ApiClient.js` 统一抛 `PC_API_NOT_AVAILABLE`,过期请求路径扫描无结果。 |
|
||||||
| PC-009 | 本地复核通过 | 登录、注册、找回密码 | PC auth、`/captcha/*` | 三页场景编码、表单契约、Axios 请求客户端和相关测试均通过;真实账号流程待业务数据联调。 |
|
| PC-009 | 本地复核通过 | 登录、注册、找回密码 | PC auth、`/captcha/*` | 三页场景编码、表单契约、Axios 请求客户端和相关测试均通过;真实账号流程待业务数据联调。 |
|
||||||
| PC-010 | 复核通过(网关可达) | `config.js`、`utils/ApiClient.js` | 测试 API 基础地址 | 开发地址为 `http://test-genealogy-api.ddxcjp.cn`;2026-07-10 10:02:48 +08:00 外部 `curl.exe -I` 返回 `HTTP 200 OK`。 |
|
| PC-010 | 已复核(当前地址已同步) | `config.js`、`tests/config.test.js`、`utils/ApiClient.js` | API 基础地址 | 当前开发和生产地址均为 `http://182.61.18.23:8080`;地址唯一运行时来源为 `config.js`,`tests/config.test.js` 已同步断言。 |
|
||||||
| PC-011 | 本地复核通过 | 三张认证页 TAC 弹窗 | `public/tac`、`/captcha/*` | 页面级挂载、移动端约束和结构测试通过;未修改 `tac.min.js`。 |
|
| PC-011 | 本地复核通过 | 三张认证页 TAC 弹窗 | `public/tac`、`/captcha/*` | 页面级挂载、移动端约束和结构测试通过;未修改 `tac.min.js`。 |
|
||||||
|
|
||||||
## PC 页面覆盖矩阵草案
|
## PC 页面覆盖矩阵草案
|
||||||
@@ -75,31 +79,31 @@
|
|||||||
| 分类 | 页面/模块 | 当前判断 | 处理方式 |
|
| 分类 | 页面/模块 | 当前判断 | 处理方式 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 完整可闭环 | `login.html` | PC YAML 有 `/genealogy/pc/auth/login`,后台验证码配置明确 `WEB_H5_LOGIN`,TAC 可完整串联。 | 可作为第一批执行页面。 |
|
| 完整可闭环 | `login.html` | PC YAML 有 `/genealogy/pc/auth/login`,后台验证码配置明确 `WEB_H5_LOGIN`,TAC 可完整串联。 | 可作为第一批执行页面。 |
|
||||||
| 接口存在但部分验证码场景待确认 | `register.html`、`forgot-password.html`、`profile-security.html` 中的换绑/注销相关动作 | PC YAML 有对应认证接口;注册场景为 `WEB_H5_REGISTER`,找回密码场景为 `WEB_H5_FORGOT_PASSWORD`,换绑和注销暂未提供 PC/H5 场景。 | 注册和找回密码可接 TAC;换绑、注销涉及验证码的提交先记录待确认,不能套用 `APP_*` 场景。 |
|
| 接口存在但部分验证码场景待确认 | `register.html`、`forgot-password.html`、`profile-security.html` 中的换绑/注销相关动作 | PC YAML 有对应认证接口;注册场景为 `WEB_H5_REGISTER`,找回密码场景为 `WEB_H5_FORGOT_PASSWORD`,换绑和注销暂未提供 PC/H5 场景。 | 注册和找回密码可接 TAC;换绑、注销涉及验证码的提交先记录待确认,不套用未经确认的场景。 |
|
||||||
| 资料接口可对接 | `profile.html`、`profile-data.html` 中用户资料读取/修改 | PC YAML 有 `/genealogy/pc/auth/profile`。 | 只处理用户资料读写;页面里其他非资料业务不因此视为完成。 |
|
| 资料接口可对接 | `profile.html`、`profile-data.html` 中用户资料读取/修改 | PC YAML 有 `/genealogy/pc/auth/profile`。 | 只处理用户资料读写;页面里其他非资料业务不因此视为完成。 |
|
||||||
| 公共地区能力可保留 | `create-genealogy.html`、`profile-create-family.html`、`profile-data.html` 中地区选择控件 | PC YAML 有 `/genealogy/region/*`。 | 只保留和验证地区选择;创建家谱等主业务接口 PC YAML 未覆盖,仍待确认。 |
|
| 公共地区能力可保留 | `create-genealogy.html`、`profile-create-family.html`、`profile-data.html` 中地区选择控件 | PC YAML 有 `/genealogy/region/*`。 | 只保留和验证地区选择;创建家谱等主业务接口 PC YAML 未覆盖,仍待确认。 |
|
||||||
| 文件能力可切换 | 文章、动态、相册、成长、备忘、祭祀等页面里的上传控件 | PC YAML 有 `/genealogy/pc/files/*`。 | 只把上传、分片、文件引用切到 PC 文件接口;文章/动态/相册等主业务接口仍待确认。 |
|
| 文件能力可切换 | 文章、动态、相册、成长、备忘、祭祀等页面里的上传控件 | PC YAML 有 `/genealogy/pc/files/*`。 | 只把上传、分片、文件引用切到 PC 文件接口;文章/动态/相册等主业务接口仍待确认。 |
|
||||||
| PC YAML 未覆盖 | 家谱主体、成员、字辈、世系、动态、文章、相册、祭祀、族务、VIP、通知、反馈、帮助、推广、姓氏、搜索等主业务页面 | 当前 PC YAML 没有这些业务路径。 | 不套用 APP 路径;等待补充 PC 接口后再逐页规划。 |
|
| PC YAML 未覆盖 | 家谱主体、成员、字辈、世系、动态、文章、相册、祭祀、族务、VIP、通知、反馈、帮助、推广、姓氏、搜索等主业务页面 | 当前 PC YAML 没有这些业务路径。 | 等待补充 PC 接口后再逐页规划。 |
|
||||||
|
|
||||||
## 流程记录
|
## 流程记录
|
||||||
|
|
||||||
- 2026-07-09 15:14:58 +08:00:收到用户纠正,停止 APP 接口继续推进;读取 `genealogy-pc-openapi.yaml`,确认 PC 路径前缀为 `/genealogy/pc`,基础地址改用 `https://test-genealogy-api.ddxcjp.cn`。
|
- 2026-07-09 15:14:58 +08:00:收到用户纠正,停止旧接口继续推进;读取 `genealogy-pc-openapi.yaml`,确认 PC 路径前缀为 `/genealogy/pc`,基础地址改用 `https://test-genealogy-api.ddxcjp.cn`。
|
||||||
- 2026-07-09 15:14:58 +08:00:创建本 PC 专用追踪表,PC-001 标记为“进行中”。
|
- 2026-07-09 15:14:58 +08:00:创建本 PC 专用追踪表,PC-001 标记为“进行中”。
|
||||||
- 2026-07-09 15:17:57 +08:00:收到用户补充要求,重新规划 `config.js` 环境切换和 `utils` 公共 JS 分层;停止直接执行旧 PC-001,把 PC-001 到 PC-006 重排为待执行计划。
|
- 2026-07-09 15:17:57 +08:00:收到用户补充要求,重新规划 `config.js` 环境切换和 `utils` 公共 JS 分层;停止直接执行旧 PC-001,把 PC-001 到 PC-006 重排为待执行计划。
|
||||||
- 2026-07-09 15:24:02 +08:00:收到用户补充验证码要求;记录 `public/tac` 调用链,确认 PC/H5 登录使用 `WEB_H5_LOGIN`,其他未配置 PC/H5 场景的认证验证码先待确认。
|
- 2026-07-09 15:24:02 +08:00:收到用户补充验证码要求;记录 `public/tac` 调用链,确认 PC/H5 登录使用 `WEB_H5_LOGIN`,其他未配置 PC/H5 场景的认证验证码先待确认。
|
||||||
- 2026-07-09 15:31:14 +08:00:根据用户质疑修正 PC-006;不再把“公共地区/文件接口可用”写成“整页可对接”,改为页面覆盖矩阵和缺口清单。
|
- 2026-07-09 15:31:14 +08:00:根据用户质疑修正 PC-006;不再把“公共地区/文件接口可用”写成“整页可对接”,改为页面覆盖矩阵和缺口清单。
|
||||||
- 2026-07-09 15:34:26 +08:00:同步修正详细规划文件 `docs/superpowers/plans/2026-07-09-pc-api-replan.md` 的 Task 6,并将 PC-006 标记为“已修正”。
|
- 2026-07-09 15:34:26 +08:00:同步修正详细规划文件 `docs/superpowers/plans/2026-07-09-pc-api-replan.md` 的 Task 6,并将 PC-006 标记为“已修正”。
|
||||||
- 2026-07-09 15:41:40 +08:00:按最新 YAML 分析结果重写 `docs/superpowers/plans/2026-07-09-pc-api-replan.md`,新增 PC-007;明确旧 APP 路径不能作为 PC fallback,缺失业务接口统一降级为接口待确认。
|
- 2026-07-09 15:41:40 +08:00:按最新 YAML 分析结果重写 `docs/superpowers/plans/2026-07-09-pc-api-replan.md`,新增 PC-007;明确旧路径不能作为 PC fallback,缺失业务接口统一降级为接口待确认。
|
||||||
- 2026-07-09 15:44:18 +08:00:开始执行 PC-001/PC-002,先按 TDD 创建 `config.js` 和 `utils` 公共工具测试,再实现最小配置与工具层。
|
- 2026-07-09 15:44:18 +08:00:开始执行 PC-001/PC-002,先按 TDD 创建 `config.js` 和 `utils` 公共工具测试,再实现最小配置与工具层。
|
||||||
- 2026-07-09 15:47:17 +08:00:完成 PC-001 和 PC-002 的公共工具层部分;`config.js` 位于根目录,`utils` 只放跨页面公共 JS,页面业务仍留在 `public/js`。
|
- 2026-07-09 15:47:17 +08:00:完成 PC-001 和 PC-002 的公共工具层部分;`config.js` 位于根目录,`utils` 只放跨页面公共 JS,页面业务仍留在 `public/js`。
|
||||||
- 2026-07-09 15:51:34 +08:00:完成 PC-002 的 `api-client.js` 切换;旧 APP 路径不再作为网络请求路径,未覆盖业务统一抛 `PC_API_NOT_AVAILABLE`。
|
- 2026-07-09 15:51:34 +08:00:完成 PC-002 的请求客户端切换;旧路径不再作为网络请求路径,未覆盖业务统一抛 `PC_API_NOT_AVAILABLE`。
|
||||||
- 2026-07-09 15:55:28 +08:00:PC-003 认证页验证码阶段完成;登录使用 `WEB_H5_LOGIN`,未确认 PC/H5 场景的注册和找回密码不调用验证中心。
|
- 2026-07-09 15:55:28 +08:00:PC-003 认证页验证码阶段完成;登录使用 `WEB_H5_LOGIN`,未确认 PC/H5 场景的注册和找回密码不调用验证中心。
|
||||||
- 2026-07-09 15:57:36 +08:00:收到用户补充截图,确认 PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`、客户端 Key 为 `web_pc`,新增注册场景 `WEB_H5_REGISTER` 和找回密码场景 `WEB_H5_FORGOT_PASSWORD`;同步修正配置、计划和认证页场景映射。
|
- 2026-07-09 15:57:36 +08:00:收到用户补充截图,确认 PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`、客户端 Key 为 `web_pc`,新增注册场景 `WEB_H5_REGISTER` 和找回密码场景 `WEB_H5_FORGOT_PASSWORD`;同步修正配置、计划和认证页场景映射。
|
||||||
- 2026-07-09 16:00:22 +08:00:PC-003 完成;认证页、资料页、安全页的 PC auth/captcha 接入和脚本顺序已验证。
|
- 2026-07-09 16:00:22 +08:00:PC-003 完成;认证页、资料页、安全页的 PC auth/captcha 接入和脚本顺序已验证。
|
||||||
- 2026-07-09 16:03:23 +08:00:PC-004 完成;上传公共能力切换到 PC 文件接口,相关上传页已补齐公共层脚本顺序。
|
- 2026-07-09 16:03:23 +08:00:PC-004 完成;上传公共能力切换到 PC 文件接口,相关上传页已补齐公共层脚本顺序。
|
||||||
- 2026-07-09 16:05:12 +08:00:PC-005 完成;地区选择只保留公共行政区划能力,创建家谱等主业务仍按 PC YAML 未覆盖处理。
|
- 2026-07-09 16:05:12 +08:00:PC-005 完成;地区选择只保留公共行政区划能力,创建家谱等主业务仍按 PC YAML 未覆盖处理。
|
||||||
- 2026-07-09 16:06:35 +08:00:PC-008 完成;PC YAML 未覆盖业务不会继续发送旧 APP 请求,全量 Node 测试通过。
|
- 2026-07-09 16:06:35 +08:00:PC-008 完成;PC YAML 未覆盖业务不会继续发送过期请求,全量 Node 测试通过。
|
||||||
- 2026-07-09 16:07:42 +08:00:收尾验证完成;全量 `tests/*.test.js` 通过,`public/js` 非压缩业务脚本 `node --check` 通过,旧 `/genealogy/app/`、旧验证码场景、旧 PC clientId 扫描无输出;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
- 2026-07-09 16:07:42 +08:00:收尾验证完成;全量 `tests/*.test.js` 通过,`public/js` 非压缩业务脚本 `node --check` 通过,过期请求路径、旧验证码场景、旧 PC clientId 扫描无输出;`git diff --check` 因当前目录不是 Git 仓库无法执行。
|
||||||
- 2026-07-09 16:09:55 +08:00:开始登录、注册、找回密码三页专项核对;按 PC YAML 的 `PasswordLoginBody`、`PasswordRegisterBody`、`SmsCodeBody`、`PasswordResetBody` 修正页面字段、输入类型、验证码场景声明和文案。
|
- 2026-07-09 16:09:55 +08:00:开始登录、注册、找回密码三页专项核对;按 PC YAML 的 `PasswordLoginBody`、`PasswordRegisterBody`、`SmsCodeBody`、`PasswordResetBody` 修正页面字段、输入类型、验证码场景声明和文案。
|
||||||
- 2026-07-09 16:12:10 +08:00:登录、注册、找回密码三页专项完成;三页表单已显式声明 `data-captcha-scene`,手机号输入改为 `type="tel"`、`inputmode="numeric"`、`maxlength="11"`,找回密码文案改为只支持注册手机号;`auth-pages.js` 优先读取页面 `data-captcha-scene`。验证:`node tests\auth-pages.test.js`、`node tests\captcha-pages.test.js`、`node tests\api-client.test.js` 通过;三页字段/脚本顺序扫描通过;旧邮箱文案和旧验证码场景扫描无输出。
|
- 2026-07-09 16:12:10 +08:00:登录、注册、找回密码三页专项完成;三页表单已显式声明 `data-captcha-scene`,手机号输入改为 `type="tel"`、`inputmode="numeric"`、`maxlength="11"`,找回密码文案改为只支持注册手机号;`auth-pages.js` 优先读取页面 `data-captcha-scene`。验证:`node tests\auth-pages.test.js`、`node tests\captcha-pages.test.js`、`node tests\api-client.test.js` 通过;三页字段/脚本顺序扫描通过;旧邮箱文案和旧验证码场景扫描无输出。
|
||||||
- 2026-07-09 16:19:11 +08:00:根据用户反馈“接口文档少了很多东西”重新核对 `genealogy-pc-openapi.yaml` 的真实 `paths`;确认当前 YAML 只有验证中心、认证登录、文件上传、行政区划四类共 25 个操作,虽然声明了家谱、成员、字辈、世系、家族圈、文章、相册、祭祀、族务、VIP、消息、反馈等标签和模型,但没有对应可调用路径。结论:这些业务页面不能继续视为已对接,只能保留 `PC_API_NOT_AVAILABLE`,等待后端补充 PC 接口文档。
|
- 2026-07-09 16:19:11 +08:00:根据用户反馈“接口文档少了很多东西”重新核对 `genealogy-pc-openapi.yaml` 的真实 `paths`;确认当前 YAML 只有验证中心、认证登录、文件上传、行政区划四类共 25 个操作,虽然声明了家谱、成员、字辈、世系、家族圈、文章、相册、祭祀、族务、VIP、消息、反馈等标签和模型,但没有对应可调用路径。结论:这些业务页面不能继续视为已对接,只能保留 `PC_API_NOT_AVAILABLE`,等待后端补充 PC 接口文档。
|
||||||
@@ -291,3 +295,60 @@
|
|||||||
- 修改文件:`public/js/captcha-pages.js`、`tests/captcha-pages.test.js`、`docs/superpowers/plans/2026-07-10-tac-layer-real-mount-fix.md`、`docs/pc-api-page-integration-tracker-2026-07-09.md`
|
- 修改文件:`public/js/captcha-pages.js`、`tests/captcha-pages.test.js`、`docs/superpowers/plans/2026-07-10-tac-layer-real-mount-fix.md`、`docs/pc-api-page-integration-tracker-2026-07-09.md`
|
||||||
- 修改结果:`createCaptchaLayer` 现在用字符串 `content` 让 Layui 创建 `<div data-captcha-layer-box="..."></div>`,弹层打开后再通过 `document.querySelector` 获取这个实际节点,并将它作为 TAC 的 `bindEl`。未修改 `public/tac`,未新增验证码 CSS,未传 TAC 视觉配置。
|
- 修改结果:`createCaptchaLayer` 现在用字符串 `content` 让 Layui 创建 `<div data-captcha-layer-box="..."></div>`,弹层打开后再通过 `document.querySelector` 获取这个实际节点,并将它作为 TAC 的 `bindEl`。未修改 `public/tac`,未新增验证码 CSS,未传 TAC 视觉配置。
|
||||||
- 验证结果:先运行 `node tests\captcha-pages.test.js` 复现失败;修复后 `node tests\auth-page-structure.test.js`、`node tests\captcha-pages.test.js`、`node --check public\js\captcha-pages.js` 和 `git diff --check` 通过。`git diff --check` 仅输出既有 CRLF 行尾提示,没有空白错误。
|
- 验证结果:先运行 `node tests\captcha-pages.test.js` 复现失败;修复后 `node tests\auth-page-structure.test.js`、`node tests\captcha-pages.test.js`、`node --check public\js\captcha-pages.js` 和 `git diff --check` 通过。`git diff --check` 仅输出既有 CRLF 行尾提示,没有空白错误。
|
||||||
|
|
||||||
|
## PC-025 注册、登录到个人资料闭环
|
||||||
|
|
||||||
|
- 状态:已完成
|
||||||
|
- 完成时间:2026-07-11 09:32:35 +08:00
|
||||||
|
- 规划文件:`docs/superpowers/specs/2026-07-11-auth-to-profile-flow-design.md`
|
||||||
|
- 页面范围:`register.html`、`login.html`、`profile.html`、`profile-data.html`
|
||||||
|
- 接口范围:`/genealogy/pc/auth/register`、`/genealogy/pc/auth/login`、`/genealogy/pc/auth/login/sms`、`/genealogy/pc/auth/profile`、`/genealogy/pc/auth/logout`
|
||||||
|
- 地址契约:运行时唯一来源为 `config.js`;当前开发和生产地址均为 `http://182.61.18.23:8080`。
|
||||||
|
- 流程结果:注册成功清除 token 并跳转 `login.html`;当前登录响应从 `data.access_token` 取 JWT,且兼容 `token`、`accessToken`、`tokenValue` 后跳转 `profile.html`;个人资料无 token 或收到 HTTP/业务 `401` 时清 token 并跳转 `index.html`;确认退出无论接口结果都清 token 并跳转 `index.html`。
|
||||||
|
- 修改文件:`tests/config.test.js`、`tests/utils.test.js`、`utils/ApiClient.js`、`tests/api-client.test.js`、`public/js/profile-pages.js`、`public/js/profile-common.js`、`profile.html`、`tests/profile-pages.test.js`、`tests/profile-logout-structure.test.js`。
|
||||||
|
- 验证结果:`node tests/config.test.js`、`node tests/utils.test.js`、`node tests/api-client.test.js`、`node tests/auth-pages.test.js`、`node tests/profile-pages.test.js`、`node tests/profile-logout-structure.test.js`、`node --check utils/ApiClient.js`、`node --check public/js/profile-pages.js`、`node --check public/js/profile-common.js` 和全量 Node 测试均通过。
|
||||||
|
|
||||||
|
## PC-026 安全页认证入口收敛
|
||||||
|
|
||||||
|
- 状态:部分完成
|
||||||
|
- 完成时间:2026-07-11
|
||||||
|
- 页面范围:`profile-security.html`
|
||||||
|
- 接口范围:`/genealogy/pc/auth/login/sms`、`/genealogy/pc/auth/phone`、`/genealogy/pc/auth/account/deactivate`、`/genealogy/pc/auth/logout`
|
||||||
|
- 契约结果:`login.html` 是密码和短信登录的唯一入口;登录成功跳转 `profile.html`。已登录的账号安全页不得重复发起登录流程。退出登录和账号注销完成后均跳转 `index.html`。
|
||||||
|
- 修改结果:删除安全页遗留的短信登录表单、旧接口说明、请求体构造和提交分支;安全页退出无论接口请求结果都跳转首页,账号注销成功后也跳转首页。
|
||||||
|
- 后端阻塞:换绑手机仍无法形成完整 PC/H5 流程。`POST /genealogy/pc/auth/sms/code` 必填 `sceneCode` 和 `validToken`,但当前未定义换绑手机的 PC/H5 场景码;不得使用未经确认的场景替代。待后端提供场景码后,补充滑动验证、发送短信验证码和换绑提交链路。
|
||||||
|
- 验证结果:先运行 `node tests/security-pages.test.js`,确认旧安全页短信登录导出仍存在而按预期失败;修复后该测试通过。全量验证待本项后续记录补充。
|
||||||
|
|
||||||
|
## PC-027 PC-only 接口契约清理
|
||||||
|
|
||||||
|
- 状态:已完成
|
||||||
|
- 完成时间:2026-07-11
|
||||||
|
- 契约所有者:`genealogy-pc-openapi.yaml` 是 PC/H5 页面接口的唯一依据;运行时请求由 `utils/ApiClient.js` 统一发起。
|
||||||
|
- 修改结果:删除旧接口文档和错误对接记录;清除页面、脚本、规划和 PC YAML 中遗留的旧路径、场景码与文档引用;PC YAML 的验证码示例统一使用 `WEB_H5_LOGIN`,注册来源示例为 `PC`。
|
||||||
|
- 登录确认:`login.html` 的短信登录继续经 `public/js/auth-pages.js` 调用 `utils/ApiClient.js`,请求 `POST /genealogy/pc/auth/login/sms`,验证码场景为 `WEB_H5_LOGIN`。
|
||||||
|
- 验证结果:新增 `tests/pc-api-contract.test.js`。该测试确认旧接口文档与错误记录不存在,扫描当前运行代码、PC YAML 和有效规划没有遗留旧契约,并断言短信登录使用 PC 路径和 `WEB_H5_LOGIN`。
|
||||||
|
|
||||||
|
## PC-028 当前 PC 接口页面覆盖
|
||||||
|
|
||||||
|
- 状态:部分完成
|
||||||
|
- 完成时间:2026-07-11
|
||||||
|
- 接口清单:`genealogy-pc-openapi.yaml` 当前有 25 个操作;`tests/pc-endpoint-coverage.test.js` 对每项操作断言页面调用或明确后端阻塞原因。
|
||||||
|
- 已形成页面闭环的 21 项操作:验证码查询/挑战/校验,注册、密码登录、短信登录、短信验证码,资料读取/修改,修改密码、找回密码、注销账号、退出登录,单文件上传、分片初始化/上传/完成,地区下级查询/搜索/路径/详情。
|
||||||
|
- 文件上传结果:`profile-data.html` 的头像上传不再要求手填 OSS ID。文件不超过 4 MB 时调用单文件上传;超过 4 MB 时依次调用分片初始化、每片上传和完成上传,完成后回填同一 `avatarOssId`,由“保存资料”提交资料接口。
|
||||||
|
- 地区结果:`profile-data.html` 支持按地区名称或编码搜索;选择结果会查询地区详情和路径,再回填省、市、区县选择器后提交资料接口。
|
||||||
|
- 后端契约阻塞的 4 项操作:
|
||||||
|
1. `GET /auth/code`:接口是兼容旧图形验证码,但当前 PC 登录/注册请求体没有可提交的验证码字段。
|
||||||
|
2. `PUT /genealogy/pc/auth/phone`:发送短信前需要 PC/H5 换绑手机号验证码场景码,当前文档未提供。
|
||||||
|
3. `POST /genealogy/pc/files/reference`:缺少可由当前 PC 页面创建的业务表名和业务 ID 契约。
|
||||||
|
4. `DELETE /genealogy/pc/files/reference`:同样缺少业务表名、业务 ID 和业务字段的来源契约。
|
||||||
|
- 浏览器验收顺序:
|
||||||
|
1. `register.html`:完成注册,确认跳转 `login.html` 且不保留登录态。
|
||||||
|
2. `login.html` 密码登录:完成滑块验证和登录,确认跳转 `profile.html`。
|
||||||
|
3. `login.html` 短信登录:完成滑块验证、发送短信和登录,确认跳转 `profile.html`。
|
||||||
|
4. `profile.html`:确认资料成功加载;清空本地 token 后重新进入,确认跳转首页。
|
||||||
|
5. `profile-data.html`:修改昵称、性别、生日后保存;上传小于等于 4 MB 的头像并保存资料。
|
||||||
|
6. `profile-data.html`:上传大于 4 MB 的头像,确认网络依次出现 `resumable/init`、`resumable/chunk`、`resumable/complete`,再保存资料。
|
||||||
|
7. `profile-data.html`:搜索地区名称或编码,选择结果,确认省市区回填后保存。
|
||||||
|
8. `forgot-password.html`:完成验证码、短信码和密码重置。
|
||||||
|
9. `profile-security.html`:分别测试修改密码、账号注销和退出登录;退出与注销成功后均回首页。
|
||||||
|
- 验证结果:`node tests/pc-endpoint-coverage.test.js`、`node tests/upload-pages.test.js`、`node tests/region-pages.test.js`、`node tests/profile-pages.test.js`、`node tests/auth-pages.test.js`、`node tests/captcha-pages.test.js`、`node tests/api-client.test.js`、`node tests/security-pages.test.js` 均通过。
|
||||||
|
|||||||
@@ -376,10 +376,10 @@ function createUnavailableMethod(name) {
|
|||||||
|
|
||||||
Use a Chinese comment above the mapping: `// PC YAML 未提供这些业务 paths,保留方法名只用于阻止旧 APP 路径继续发请求。`
|
Use a Chinese comment above the mapping: `// PC YAML 未提供这些业务 paths,保留方法名只用于阻止旧 APP 路径继续发请求。`
|
||||||
|
|
||||||
- [ ] **Step 5: Verify no APP auth/files network paths remain**
|
- [ ] **Step 5: Verify the PC auth/files contract**
|
||||||
|
|
||||||
Run: `Select-String -Path public\js\api-client.js -Pattern '/genealogy/app/auth|/genealogy/app/files'`
|
Run: `node tests\pc-api-contract.test.js`
|
||||||
Expected: no matches.
|
Expected: PC 请求契约扫描通过。
|
||||||
|
|
||||||
- [ ] **Step 6: Run API client test again**
|
- [ ] **Step 6: Run API client test again**
|
||||||
|
|
||||||
@@ -607,10 +607,10 @@ Expected after implementation: PASS.
|
|||||||
|
|
||||||
Only map file capability. Do not submit article/feed/album/ceremony/growth/memo business forms unless PC YAML provides their business endpoints.
|
Only map file capability. Do not submit article/feed/album/ceremony/growth/memo business forms unless PC YAML provides their business endpoints.
|
||||||
|
|
||||||
- [ ] **Step 4: Verify no APP file path remains**
|
- [ ] **Step 4: Verify the PC file contract**
|
||||||
|
|
||||||
Run: `Select-String -Path public\js\api-client.js,public\js\upload-pages.js -Pattern '/genealogy/app/files'`
|
Run: `node tests\pc-api-contract.test.js`
|
||||||
Expected: no matches.
|
Expected: PC 请求契约扫描通过。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -688,10 +688,10 @@ Expected after implementation: PASS.
|
|||||||
|
|
||||||
In each affected page script, catch `PC_API_NOT_AVAILABLE` and render a small existing-style empty state. Add a Chinese comment: `// PC YAML 未提供该业务接口,页面先展示接口待确认状态。`
|
In each affected page script, catch `PC_API_NOT_AVAILABLE` and render a small existing-style empty state. Add a Chinese comment: `// PC YAML 未提供该业务接口,页面先展示接口待确认状态。`
|
||||||
|
|
||||||
- [ ] **Step 4: Verify no APP business path is emitted from API client**
|
- [ ] **Step 4: Verify no obsolete business path is emitted from the API client**
|
||||||
|
|
||||||
Run: `Select-String -Path public\js\api-client.js -Pattern '/genealogy/app/'`
|
Run: `node tests\pc-api-contract.test.js`
|
||||||
Expected: no network path matches.
|
Expected: PC 请求契约扫描通过。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -743,16 +743,15 @@ node tests\upload-pages.test.js
|
|||||||
node tests\region-pages.test.js
|
node tests\region-pages.test.js
|
||||||
node tests\unsupported-pages.test.js
|
node tests\unsupported-pages.test.js
|
||||||
Get-ChildItem -Path public\js -Filter *.js | Where-Object { $_.Name -notin @('jquery360.js','jquery.min.js') } | Sort-Object Name | ForEach-Object { node --check $_.FullName }
|
Get-ChildItem -Path public\js -Filter *.js | Where-Object { $_.Name -notin @('jquery360.js','jquery.min.js') } | Sort-Object Name | ForEach-Object { node --check $_.FullName }
|
||||||
Select-String -Path public\js\api-client.js -Pattern '/genealogy/app/'
|
node tests\pc-api-contract.test.js
|
||||||
Select-String -Path public\js\auth-pages.js,public\js\captcha-pages.js,tests\auth-pages.test.js,tests\captcha-pages.test.js -Pattern 'PC_LOGIN|PC_REGISTER|PC_PASSWORD_RESET|APP_LOGIN|APP_REGISTER|APP_FORGOT_PASSWORD'
|
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected:
|
Expected:
|
||||||
- All listed Node tests pass.
|
- All listed Node tests pass.
|
||||||
- Business JS syntax check passes.
|
- Business JS syntax check passes.
|
||||||
- `api-client.js` contains no `/genealogy/app/` network path.
|
- PC 请求契约扫描通过。
|
||||||
- Auth and captcha code/tests contain no stale APP/old PC scene defaults.
|
- 认证和验证码代码/tests 不含过期场景默认值。
|
||||||
- `git diff --check` passes, or if this directory is not a Git repository, record the exact output.
|
- `git diff --check` passes, or if this directory is not a Git repository, record the exact output.
|
||||||
|
|
||||||
## Self-Review
|
## Self-Review
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## Global Constraints
|
## Global Constraints
|
||||||
|
|
||||||
- 接口依据只使用 `genealogy-pc-openapi.yaml` 与用户补充的 Apifox 截图,不再使用 `APP.openapi.json`。
|
- 接口依据只使用 `genealogy-pc-openapi.yaml` 与用户补充的 Apifox 截图。
|
||||||
- 开发环境接口地址为 `https://test-genealogy-api.ddxcjp.cn`。
|
- 开发环境接口地址为 `https://test-genealogy-api.ddxcjp.cn`。
|
||||||
- PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`,租户 ID 为 `000000`。
|
- PC `clientid` 为 `ced7e5f0498645c6ec642dcf450b036f`,租户 ID 为 `000000`。
|
||||||
- 登录验证码场景为 `WEB_H5_LOGIN`,注册验证码场景为 `WEB_H5_REGISTER`,忘记密码验证码场景为 `WEB_H5_FORGOT_PASSWORD`。
|
- 登录验证码场景为 `WEB_H5_LOGIN`,注册验证码场景为 `WEB_H5_REGISTER`,忘记密码验证码场景为 `WEB_H5_FORGOT_PASSWORD`。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
**Goal:** 将 `public/tac` 滑动验证接入登录、注册、忘记密码三个认证页面,并按 `APP.openapi.json` 的验证中心接口换取 `validToken` 后再提交业务接口。
|
**Goal:** 将 `public/tac` 滑动验证接入登录、注册、忘记密码三个认证页面,并按 `genealogy-pc-openapi.yaml` 的验证中心接口换取 `validToken` 后再提交业务接口。
|
||||||
|
|
||||||
**Architecture:** 新增 `public/js/captcha-pages.js` 作为 TAC 与验证中心接口之间的适配层;`auth-pages.js` 在提交登录、注册、发送短信、重置密码前调用验证码适配层。API 基础地址、clientId、tenantId 继续由 `api-client.js` 统一提供。
|
**Architecture:** 新增 `public/js/captcha-pages.js` 作为 TAC 与验证中心接口之间的适配层;`auth-pages.js` 在提交登录、注册、发送短信、重置密码前调用验证码适配层。API 基础地址、clientId、tenantId 继续由 `api-client.js` 统一提供。
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## Global Constraints
|
## Global Constraints
|
||||||
|
|
||||||
- 严格依据 `APP.openapi.json` 的 `/captcha/require`、`/captcha/challenge`、`/captcha/verify` 字段接入。
|
- 严格依据 `genealogy-pc-openapi.yaml` 的 `/captcha/require`、`/captcha/challenge`、`/captcha/verify` 字段接入。
|
||||||
- 业务 JS 不压缩,新增注释使用中文。
|
- 业务 JS 不压缩,新增注释使用中文。
|
||||||
- 自有样式写入 CSS 文件,不通过自有 JS 注入样式。
|
- 自有样式写入 CSS 文件,不通过自有 JS 注入样式。
|
||||||
- 第三方 `tac.min.js` 保持原样,不重写其内部样式和 DOM 生成逻辑。
|
- 第三方 `tac.min.js` 保持原样,不重写其内部样式和 DOM 生成逻辑。
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ Expected: 四条命令均通过。
|
|||||||
|
|
||||||
- [x] **Step 1: 运行工具和 API 降级测试**
|
- [x] **Step 1: 运行工具和 API 降级测试**
|
||||||
|
|
||||||
Run: `node tests\\utils.test.js; node tests\\api-client.test.js; rg -n "/genealogy/app/" public\\js\\api-client.js`
|
Run: `node tests\\utils.test.js; node tests\\api-client.test.js; node tests\\pc-api-contract.test.js`
|
||||||
|
|
||||||
Expected: 两个测试通过,旧 `/genealogy/app/` 请求路径无匹配结果。
|
Expected: 三个测试通过,PC 请求契约扫描无遗留项。
|
||||||
|
|
||||||
- [x] **Step 2: 核对未覆盖业务的错误边界**
|
- [x] **Step 2: 核对未覆盖业务的错误边界**
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Expected: 所有测试和语法检查通过。
|
|||||||
|
|
||||||
- [x] **Step 2: 扫描旧路径与页面覆盖边界**
|
- [x] **Step 2: 扫描旧路径与页面覆盖边界**
|
||||||
|
|
||||||
Run: `rg -n "/genealogy/app/files|/genealogy/app/region" public\\js; rg -n "PC_API_NOT_AVAILABLE" public\\js\\api-client.js`
|
Run: `rg -n "PC_API_NOT_AVAILABLE" utils\\ApiClient.js`
|
||||||
|
|
||||||
Expected: 文件、地区旧 APP 路径无匹配;业务页面覆盖边界仍由 `PC_API_NOT_AVAILABLE` 保护。
|
Expected: 文件、地区旧 APP 路径无匹配;业务页面覆盖边界仍由 `PC_API_NOT_AVAILABLE` 保护。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,474 @@
|
|||||||
|
# Auth To Profile Flow Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Complete the PC registration, login, profile, unauthorized, logout, and endpoint-address flow with one token owner and deterministic redirects.
|
||||||
|
|
||||||
|
**Architecture:** `config.js` remains the sole runtime owner of the API base address. `utils/ApiClient.js` owns token validation, persistence, and cleanup; `profile-pages.js` decides whether a profile request is allowed and redirects unauthenticated users to the homepage; `profile-common.js` delegates confirmed logout to the API client. The integration tracker records the current address and verified behavior only after verification commands complete.
|
||||||
|
|
||||||
|
**Tech Stack:** Browser JavaScript (UMD and jQuery), Axios, Node.js `assert` tests, static HTML, Markdown.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- The current API base address is `http://182.61.18.23:8080` for development and production, and `config.js` is its only runtime owner.
|
||||||
|
- Runtime-related tests use the same address; only documents explicitly marked historical may retain the former test domain.
|
||||||
|
- Successful registration clears `genealogy_auth_token` and redirects to `login.html`; it never persists a registration response token.
|
||||||
|
- Only successful password or SMS login with a nonempty response token persists `genealogy_auth_token` and redirects to `profile.html`.
|
||||||
|
- `profile.html` and `profile-data.html` redirect to `index.html` before a profile request when no token exists, and after HTTP or business `401` responses.
|
||||||
|
- Confirmed logout requests `/genealogy/pc/auth/logout`, clears the token regardless of the request outcome, and redirects to `index.html`.
|
||||||
|
- Do not add global Axios redirects, duplicate token storage in pages, alter API paths, or change password-reset, phone-change, account-deactivation, or profile pages that do not load the profile API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Synchronize the configured API address test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/config.test.js:11-28`
|
||||||
|
- Modify: `tests/utils.test.js:31`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `configFactory(...).getConfig()` and `getApiBaseUrl()` from `config.js`.
|
||||||
|
- Produces: a test that asserts the current `config.js` address for both environments.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm the existing address-contract test fails**
|
||||||
|
|
||||||
|
The current test contains these stale assertions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.deepStrictEqual(developmentConfig.getConfig(), {
|
||||||
|
environment: 'development',
|
||||||
|
apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://test-genealogy-api.ddxcjp.cn');
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `node tests/config.test.js`
|
||||||
|
|
||||||
|
Expected: failure because `config.js` already returns `http://182.61.18.23:8080`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update the stale test expectations**
|
||||||
|
|
||||||
|
Replace the two expected URL values with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.deepStrictEqual(developmentConfig.getConfig(), {
|
||||||
|
environment: 'development',
|
||||||
|
apiBaseUrl: 'http://182.61.18.23:8080'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://182.61.18.23:8080');
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the mocked requester address in `tests/utils.test.js` with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the focused configuration test**
|
||||||
|
|
||||||
|
Run: `node tests/config.test.js`
|
||||||
|
|
||||||
|
Expected: `config tests passed`.
|
||||||
|
|
||||||
|
Run: `node tests/utils.test.js`
|
||||||
|
|
||||||
|
Expected: `utils tests passed`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit the address-test synchronization**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add -- tests/config.test.js tests/utils.test.js
|
||||||
|
git commit -m "test: sync configured api address"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Enforce login-token and logout-cleanup contracts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `utils/ApiClient.js:52-60, 167-191, 270-274`
|
||||||
|
- Modify: `tests/api-client.test.js:83-177`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `getTokenFromResponse(data)`, `setToken(token)`, `clearToken()`, and public `client.login`, `client.loginBySms`, and `client.logout` methods.
|
||||||
|
- Produces: `client.login(body)` and `client.loginBySms(body)` reject with `ApiError('登录响应缺少 token')` when their response has no token; `client.logout()` clears the configured token key before resolving or rejecting.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing API-client assertions**
|
||||||
|
|
||||||
|
Append this after the registration assertions and before `client.sendSmsCode(...)` in `tests/api-client.test.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const missingTokenStore = createStore();
|
||||||
|
const missingTokenClient = GenealogyApi.createClient({
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
clientId: 'client-x',
|
||||||
|
tenantId: 'tenant-x',
|
||||||
|
tokenKey: 'token-key',
|
||||||
|
tokenStore: missingTokenStore,
|
||||||
|
axiosInstance: {
|
||||||
|
request: async () => ({
|
||||||
|
status: 200,
|
||||||
|
data: { code: 200, data: {} }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
missingTokenClient.login({ phone: '13800000000', password: 'md5' }),
|
||||||
|
(error) => error.message === '登录响应缺少 token'
|
||||||
|
);
|
||||||
|
assert.strictEqual(missingTokenStore.getItem('token-key'), null);
|
||||||
|
|
||||||
|
store.setItem('token-key', 'logout-token');
|
||||||
|
await client.logout();
|
||||||
|
assert.strictEqual(calls[9].url, '/genealogy/pc/auth/logout');
|
||||||
|
assert.strictEqual(store.getItem('token-key'), null);
|
||||||
|
|
||||||
|
const failedLogoutStore = createStore({
|
||||||
|
'token-key': 'logout-token'
|
||||||
|
});
|
||||||
|
const failedLogoutClient = GenealogyApi.createClient({
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
clientId: 'client-x',
|
||||||
|
tenantId: 'tenant-x',
|
||||||
|
tokenKey: 'token-key',
|
||||||
|
tokenStore: failedLogoutStore,
|
||||||
|
axiosInstance: {
|
||||||
|
request: async () => {
|
||||||
|
throw new Error('logout failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await assert.rejects(failedLogoutClient.logout(), /logout failed/);
|
||||||
|
assert.strictEqual(failedLogoutStore.getItem('token-key'), null);
|
||||||
|
```
|
||||||
|
|
||||||
|
Move the existing SMS-code assertions from `calls[9]` to `calls[10]` and change the final call-count assertion from `10` to `11`.
|
||||||
|
|
||||||
|
Replace the two existing `baseUrl: 'https://test-genealogy-api.ddxcjp.cn'` fixtures in this file with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused API-client test and confirm failure**
|
||||||
|
|
||||||
|
Run: `node tests/api-client.test.js`
|
||||||
|
|
||||||
|
Expected: failure at the missing-token rejection because the current login method resolves without writing a token, before reaching the logout assertions.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal token validation and logout cleanup**
|
||||||
|
|
||||||
|
Add this helper after `getTokenFromResponse` in `utils/ApiClient.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function requireLoginToken(data) {
|
||||||
|
var token = getTokenFromResponse(data);
|
||||||
|
|
||||||
|
if (!token) throw new ApiError('登录响应缺少 token');
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the token writes in `login()` and `loginBySms()` with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
setToken(requireLoginToken(data));
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `logout` with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
logout: async function () {
|
||||||
|
try {
|
||||||
|
return await request('DELETE', '/genealogy/pc/auth/logout');
|
||||||
|
} finally {
|
||||||
|
clearToken();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the API-client test and confirm all token paths pass**
|
||||||
|
|
||||||
|
Run: `node tests/api-client.test.js`
|
||||||
|
|
||||||
|
Expected: `api-client tests passed`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the API-client contract**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add -- utils/ApiClient.js tests/api-client.test.js
|
||||||
|
git commit -m "fix: complete auth token lifecycle"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Guard profile access and connect confirmed logout
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `public/js/profile-pages.js:18-22, 165-198`
|
||||||
|
- Modify: `public/js/profile-common.js:9-82`
|
||||||
|
- Modify: `profile.html:80, 355`
|
||||||
|
- Modify: `tests/profile-pages.test.js:1-65`
|
||||||
|
- Create: `tests/profile-logout-structure.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: public `api.getToken()`, `api.clearToken()`, `api.currentProfile()`, `api.updateProfile()`, and `api.logout()` methods.
|
||||||
|
- Produces: `ProfilePages.shouldRedirectToHome(api, error)` for token and unauthorized classification; profile reads and writes redirect to `index.html` when that helper returns true; the existing logout controls call `api.logout()` before navigating to `index.html`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing profile and logout tests**
|
||||||
|
|
||||||
|
Add these assertions before the final `console.log` in `tests/profile-pages.test.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => ''
|
||||||
|
}), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}), false);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { status: 401 }), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { code: 401 }), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { status: 500 }), false);
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `tests/profile-logout-structure.test.js` with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const profileHtml = fs.readFileSync(path.join(__dirname, '..', 'profile.html'), 'utf8');
|
||||||
|
const profileCommon = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'profile-common.js'), 'utf8');
|
||||||
|
|
||||||
|
assert.match(profileHtml, /data-logout-confirm/);
|
||||||
|
assert.match(profileHtml, /<a class="btn primary magnetic" href="index\.html" data-logout-confirm>/);
|
||||||
|
assert.match(profileCommon, /function performLogout\(targetUrl\)/);
|
||||||
|
assert.match(profileCommon, /api\.logout\(\)/);
|
||||||
|
assert.match(profileCommon, /targetUrl \|\| 'index\.html'/);
|
||||||
|
|
||||||
|
console.log('profile logout structure tests passed');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the new focused tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `node tests/profile-pages.test.js`
|
||||||
|
|
||||||
|
Expected: failure because `shouldRedirectToHome` is not exported.
|
||||||
|
|
||||||
|
Run: `node tests/profile-logout-structure.test.js`
|
||||||
|
|
||||||
|
Expected: failure because the logout confirmation markup and `performLogout` function do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add profile token guard and unauthorized redirect**
|
||||||
|
|
||||||
|
Add these helpers after `getApi()` in `public/js/profile-pages.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function shouldRedirectToHome(api, error) {
|
||||||
|
var status = error && (error.status || error.code);
|
||||||
|
|
||||||
|
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToHome() {
|
||||||
|
if (!root.location) return;
|
||||||
|
if (typeof root.location.replace === 'function') {
|
||||||
|
root.location.replace('index.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectUnauthorized(api, error) {
|
||||||
|
if (!shouldRedirectToHome(api, error)) return false;
|
||||||
|
if (api && api.clearToken) api.clearToken();
|
||||||
|
redirectToHome();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the existing combined early-return condition in `loadProfile()` with these two branches:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
|
||||||
|
if (redirectUnauthorized(api)) return;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update its `catch` block to start with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (redirectUnauthorized(api, error)) return;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `submitProfileForm()` to place `if (redirectUnauthorized(api)) return;` after its existing null guard, and to start its `catch` block with `if (redirectUnauthorized(api, error)) return;` before setting an error status or message.
|
||||||
|
|
||||||
|
Export the classifier with the existing public methods:
|
||||||
|
|
||||||
|
```js
|
||||||
|
shouldRedirectToHome: shouldRedirectToHome,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Connect both logout confirmations to the API client**
|
||||||
|
|
||||||
|
Add this function before `bindLogout()` in `public/js/profile-common.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function performLogout(targetUrl) {
|
||||||
|
var api = window.GenealogyApi && window.GenealogyApi.defaultClient;
|
||||||
|
var redirect = function () {
|
||||||
|
window.location.href = targetUrl || 'index.html';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!api || !api.logout) {
|
||||||
|
redirect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.logout().catch(function () {}).then(redirect);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `bindLogout()`, change the default target URL to `index.html`, replace the Layui confirmation callback body with `performLogout(targetUrl);`, and add this event handler before the existing `[data-logout-close]` handler:
|
||||||
|
|
||||||
|
```js
|
||||||
|
$(document).on('click', '[data-logout-confirm]', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeLegacyLogout();
|
||||||
|
performLogout($(this).attr('href') || 'index.html');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In `profile.html`, change the legacy confirmation anchor to:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="btn primary magnetic" href="index.html" data-logout-confirm>确认退出</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the focused profile tests and syntax checks**
|
||||||
|
|
||||||
|
Run: `node tests/profile-pages.test.js`
|
||||||
|
|
||||||
|
Expected: `profile-pages tests passed`.
|
||||||
|
|
||||||
|
Run: `node tests/profile-logout-structure.test.js`
|
||||||
|
|
||||||
|
Expected: `profile logout structure tests passed`.
|
||||||
|
|
||||||
|
Run: `node --check public/js/profile-pages.js`
|
||||||
|
|
||||||
|
Expected: exit code `0`.
|
||||||
|
|
||||||
|
Run: `node --check public/js/profile-common.js`
|
||||||
|
|
||||||
|
Expected: exit code `0`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the profile and logout flow**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add -- public/js/profile-pages.js public/js/profile-common.js profile.html tests/profile-pages.test.js tests/profile-logout-structure.test.js
|
||||||
|
git commit -m "fix: guard profile auth and logout"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Record the current flow and run end-to-end regression checks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md:1-9, 32, 70, 163`
|
||||||
|
- Modify: `docs/superpowers/specs/2026-07-11-auth-to-profile-flow-design.md:3-5`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the verified results of Tasks 1 through 3.
|
||||||
|
- Produces: a tracker whose current address and authentication-flow status match the code and tests, and a spec marked approved for implementation.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run all focused flow tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
node tests/config.test.js
|
||||||
|
node tests/api-client.test.js
|
||||||
|
node tests/auth-pages.test.js
|
||||||
|
node tests/profile-pages.test.js
|
||||||
|
node tests/profile-logout-structure.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: each command prints its success message and exits with code `0`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run syntax and complete regression checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
node --check utils/ApiClient.js
|
||||||
|
node --check public/js/profile-pages.js
|
||||||
|
node --check public/js/profile-common.js
|
||||||
|
Get-ChildItem -Path tests -Filter *.test.js | ForEach-Object { & node $_.FullName; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all syntax checks exit `0`; every test script prints its success message; PowerShell exits `0` on the first test failure instead of masking it.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the tracker from verified evidence**
|
||||||
|
|
||||||
|
At the top of `docs/pc-api-page-integration-tracker-2026-07-09.md`, replace the active base-address line with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
接口基础地址:`http://182.61.18.23:8080`
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this section immediately after the introductory historical-record lines:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Current Endpoint Contract
|
||||||
|
|
||||||
|
- Runtime owner: `config.js`.
|
||||||
|
- Current development and production address: `http://182.61.18.23:8080`.
|
||||||
|
- Address change record: on 2026-07-11 the active address changed from the historical test domain to this IP address and port. Historical references to `test-genealogy-api.ddxcjp.cn` do not describe the current runtime configuration.
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the YAML-analysis item that says the project currently uses the test domain with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
2. `servers` 仍是本地地址示例;当前运行时基础地址以 `config.js` 为准,为 `http://182.61.18.23:8080`。
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the PC-010 row with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| PC-010 | 已复核(当前地址已同步) | `config.js`、`tests/config.test.js`、`utils/ApiClient.js` | API 基础地址 | 当前开发和生产地址均为 `http://182.61.18.23:8080`;地址唯一运行时来源为 `config.js`,`tests/config.test.js` 已同步断言。 |
|
||||||
|
```
|
||||||
|
|
||||||
|
After Steps 1 and 2 pass, run `Get-Date -Format "yyyy-MM-dd HH:mm:ss K"` and insert its output as the `完成时间` line immediately after the `状态` line in this record. Append the rest of this record after the existing PC entries:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## PC-025 注册、登录到个人资料闭环
|
||||||
|
|
||||||
|
- 状态:已完成
|
||||||
|
- 规划文件:`docs/superpowers/specs/2026-07-11-auth-to-profile-flow-design.md`
|
||||||
|
- 页面范围:`register.html`、`login.html`、`profile.html`、`profile-data.html`
|
||||||
|
- 接口范围:`/genealogy/pc/auth/register`、`/genealogy/pc/auth/login`、`/genealogy/pc/auth/login/sms`、`/genealogy/pc/auth/profile`、`/genealogy/pc/auth/logout`
|
||||||
|
- 地址契约:运行时唯一来源为 `config.js`;当前开发和生产地址均为 `http://182.61.18.23:8080`。
|
||||||
|
- 流程结果:注册成功清除 token 并跳转 `login.html`;密码和短信登录必须取得 token 后跳转 `profile.html`;个人资料无 token 或收到 HTTP/业务 `401` 时清 token 并跳转 `index.html`;确认退出无论接口结果都清 token 并跳转 `index.html`。
|
||||||
|
- 修改文件:`tests/config.test.js`、`utils/ApiClient.js`、`tests/api-client.test.js`、`public/js/profile-pages.js`、`public/js/profile-common.js`、`profile.html`、`tests/profile-pages.test.js`、`tests/profile-logout-structure.test.js`。
|
||||||
|
- 验证结果:`node tests/config.test.js`、`node tests/api-client.test.js`、`node tests/auth-pages.test.js`、`node tests/profile-pages.test.js`、`node tests/profile-logout-structure.test.js`、三个 `node --check` 命令和全量 Node 测试均通过。
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the spec status body to:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Approved for implementation.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Check final whitespace and commit the record**
|
||||||
|
|
||||||
|
Run: `git diff --check`
|
||||||
|
|
||||||
|
Expected: exit code `0`; pre-existing CRLF conversion warnings are not whitespace errors.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add -- docs/pc-api-page-integration-tracker-2026-07-09.md docs/superpowers/specs/2026-07-11-auth-to-profile-flow-design.md
|
||||||
|
git commit -m "docs: record auth profile flow"
|
||||||
|
```
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# PC Endpoint Page Coverage Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make every callable operation in `genealogy-pc-openapi.yaml` traceable to a real PC page flow, an automated regression test, and a manual acceptance step.
|
||||||
|
|
||||||
|
**Architecture:** `utils/ApiClient.js` remains the sole network contract owner. Page modules in `public/js` own input validation, loading and success/error state; `tests` prove their request and navigation behavior. The coverage matrix distinguishes a complete user flow from an endpoint that is documented but lacks the contract required to expose it safely.
|
||||||
|
|
||||||
|
**Tech Stack:** Static HTML, browser JavaScript, Axios, Layui, Node `assert` tests.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Runtime API base URL is owned only by `config.js`: `http://182.61.18.23:8080`.
|
||||||
|
- Only PC/H5 routes and scenes defined by `genealogy-pc-openapi.yaml` may be requested.
|
||||||
|
- Registration never persists a token; password and SMS login persist the returned token and navigate to `profile.html`.
|
||||||
|
- Logout, unauthenticated profile access and account deactivation navigate to `index.html`.
|
||||||
|
- Every behavior change starts with a failing Node test and ends with focused plus full test verification.
|
||||||
|
- No page invents a business table name, business ID, CAPTCHA scene or legacy captcha submission field that is not in the PC contract.
|
||||||
|
|
||||||
|
## Endpoint Matrix
|
||||||
|
|
||||||
|
| Operation | Page owner | Automated test | Manual acceptance order | Status target |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `GET /captcha/require` | `CaptchaPages` on auth forms | `tests/captcha-pages.test.js` | 1-4 | Complete |
|
||||||
|
| `POST /captcha/challenge` | `CaptchaPages` on auth forms | `tests/captcha-pages.test.js` | 1-4 | Complete |
|
||||||
|
| `POST /captcha/verify` | `CaptchaPages` on auth forms | `tests/captcha-pages.test.js` | 1-4 | Complete |
|
||||||
|
| `GET /auth/code` | no safe consumer | `tests/pc-endpoint-coverage.test.js` | blocked | Backend contract required |
|
||||||
|
| Register, password login, SMS login, SMS code | `register.html`, `login.html` | `tests/auth-pages.test.js`, `tests/api-client.test.js` | 1-3 | Complete |
|
||||||
|
| Profile read/update | `profile.html`, `profile-data.html` | `tests/profile-pages.test.js` | 4-5 | Complete |
|
||||||
|
| Password reset | `forgot-password.html` | `tests/auth-pages.test.js` | 6 | Complete |
|
||||||
|
| Password change, logout, account deactivation | `profile-security.html` | `tests/security-pages.test.js` | 7-9 | Complete |
|
||||||
|
| Phone change | `profile-security.html` | `tests/security-pages.test.js` | blocked | PC/H5 phone SMS scene required |
|
||||||
|
| Single upload | profile avatar and existing upload fields | `tests/upload-pages.test.js` | 10 | Complete |
|
||||||
|
| Resumable init/chunk/complete | same file input for large files | `tests/upload-pages.test.js`, `tests/api-client.test.js` | 11 | Complete |
|
||||||
|
| File reference bind/release | no safe business entity contract | `tests/pc-endpoint-coverage.test.js` | blocked | PC business table and ID contract required |
|
||||||
|
| Region children/path/search/detail | profile address picker and region search control | `tests/region-pages.test.js`, `tests/profile-pages.test.js` | 12 | Complete |
|
||||||
|
|
||||||
|
## Task 1: Enforce Endpoint Coverage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/pc-endpoint-coverage.test.js`
|
||||||
|
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the 25 operations declared under `paths` in `genealogy-pc-openapi.yaml`.
|
||||||
|
- Produces: a test-owned `ENDPOINTS` list that assigns every operation one of `complete` or `blocked` with a page/test owner.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing coverage test**
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.deepStrictEqual(missingOperations, []);
|
||||||
|
assert.deepStrictEqual(unownedOperations, []);
|
||||||
|
assert.deepStrictEqual(unsupportedBlockedOperations, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `node tests\pc-endpoint-coverage.test.js`
|
||||||
|
|
||||||
|
Expected: failure listing the endpoint operations without a page/test/blocker owner.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the explicit 25-operation matrix**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ENDPOINTS = {
|
||||||
|
'GET /captcha/require': { page: 'auth', test: 'captcha-pages', status: 'complete' },
|
||||||
|
'GET /auth/code': { status: 'blocked', reason: 'legacy captcha has no PC request field' }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `node tests\pc-endpoint-coverage.test.js`
|
||||||
|
|
||||||
|
Expected: `pc-endpoint-coverage tests passed`.
|
||||||
|
|
||||||
|
## Task 2: Complete and Test the Auth Journey
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `public/js/auth-pages.js`
|
||||||
|
- Modify: `public/js/profile-pages.js`
|
||||||
|
- Modify: `public/js/security-pages.js`
|
||||||
|
- Modify: `tests/auth-pages.test.js`
|
||||||
|
- Modify: `tests/profile-pages.test.js`
|
||||||
|
- Modify: `tests/security-pages.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `GenealogyApi.login`, `loginBySms`, `register`, `sendSmsCode`, `resetPassword`, `getProfile`, `updateProfile`, `changePassword`, `deactivateAccount`, and `logout`.
|
||||||
|
- Produces: consistent form validation, token state and redirect behavior for the acceptance steps 1-9.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing redirect and request tests**
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(loginRoot.location.href, 'profile.html');
|
||||||
|
assert.strictEqual(registerRoot.location.href, 'login.html');
|
||||||
|
assert.strictEqual(logoutRoot.location.href, 'index.html');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run focused tests to verify failure**
|
||||||
|
|
||||||
|
Run: `node tests\auth-pages.test.js; node tests\profile-pages.test.js; node tests\security-pages.test.js`
|
||||||
|
|
||||||
|
Expected: a failure naming the missing flow or redirect.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement only the missing flow branch in its page module**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await api.loginBySms(buildSmsLoginBody(values));
|
||||||
|
root.location.href = 'profile.html';
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the corresponding documented endpoint method for each form; do not create a second token store or a page-local route string owner.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused tests**
|
||||||
|
|
||||||
|
Run: `node tests\auth-pages.test.js; node tests\profile-pages.test.js; node tests\security-pages.test.js`
|
||||||
|
|
||||||
|
Expected: all three scripts print their pass messages.
|
||||||
|
|
||||||
|
## Task 3: Make Region Search, Path and Detail Usable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `profile-data.html`
|
||||||
|
- Modify: `public/js/region-pages.js`
|
||||||
|
- Modify: `tests/region-pages.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `regionChildren(parentCode)`, `regionSearch({ keyword })`, `regionPath(regionCode)`, and `regionDetail(regionCode)`.
|
||||||
|
- Produces: a profile-address picker that can search by name/code, select a result, resolve its path and write province/city/district codes before profile save.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing result-normalization and selected-path test**
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.deepStrictEqual(RegionPages.buildRegionSelection(result), {
|
||||||
|
provinceCode: '510000',
|
||||||
|
cityCode: '510100',
|
||||||
|
districtCode: '510104'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused test to verify failure**
|
||||||
|
|
||||||
|
Run: `node tests\region-pages.test.js`
|
||||||
|
|
||||||
|
Expected: failure because `buildRegionSelection` is not exported.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the profile search control and selection behavior**
|
||||||
|
|
||||||
|
```js
|
||||||
|
var path = await api.regionPath(regionCode);
|
||||||
|
var selection = buildRegionSelection(path);
|
||||||
|
applyRegionSelection(picker, selection);
|
||||||
|
await api.regionDetail(regionCode);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused tests**
|
||||||
|
|
||||||
|
Run: `node tests\region-pages.test.js; node tests\profile-pages.test.js`
|
||||||
|
|
||||||
|
Expected: both scripts print their pass messages.
|
||||||
|
|
||||||
|
## Task 4: Complete Single and Resumable File Upload
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `public/js/upload-pages.js`
|
||||||
|
- Modify: `utils/ApiClient.js`
|
||||||
|
- Modify: `tests/upload-pages.test.js`
|
||||||
|
- Modify: `tests/api-client.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `uploadFile(file)`, `initResumableUpload(body)`, `uploadChunk(body)`, `completeResumableUpload(body)`.
|
||||||
|
- Produces: `uploadFileForPage(file, options)` which uses single upload for files at or below the configured threshold and the init/chunk/complete sequence for larger files.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing upload-strategy tests**
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(UploadPages.getUploadMode({ size: 1024 }, 4194304), 'single');
|
||||||
|
assert.strictEqual(UploadPages.getUploadMode({ size: 4194305 }, 4194304), 'resumable');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run focused tests to verify failure**
|
||||||
|
|
||||||
|
Run: `node tests\upload-pages.test.js; node tests\api-client.test.js`
|
||||||
|
|
||||||
|
Expected: failure because `getUploadMode` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement sequential resumable upload**
|
||||||
|
|
||||||
|
```js
|
||||||
|
var init = await api.initResumableUpload(initBody);
|
||||||
|
for (index = 0; index < chunks.length; index += 1) {
|
||||||
|
await api.uploadChunk({ uploadId: init.uploadId, chunkIndex: index, chunkMd5: chunkMd5, file: chunks[index] });
|
||||||
|
}
|
||||||
|
return api.completeResumableUpload({ uploadId: init.uploadId, fileMd5: fileMd5, fileSize: file.size });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused tests**
|
||||||
|
|
||||||
|
Run: `node tests\upload-pages.test.js; node tests\api-client.test.js`
|
||||||
|
|
||||||
|
Expected: both scripts print their pass messages.
|
||||||
|
|
||||||
|
## Task 5: Record and Enforce Backend-Contract Blocks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
|
||||||
|
- Modify: `tests/pc-endpoint-coverage.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the legacy captcha response, phone SMS scene requirements, and file-reference required business fields from the PC YAML.
|
||||||
|
- Produces: three named blocked operations with an exact missing backend input and no speculative page request.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Assert exact blocker reasons**
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(ENDPOINTS['GET /auth/code'].reason, 'legacy captcha has no PC request field');
|
||||||
|
assert.strictEqual(ENDPOINTS['PUT /genealogy/pc/auth/phone'].reason, 'PC/H5 phone SMS scene is not documented');
|
||||||
|
assert.strictEqual(ENDPOINTS['POST /genealogy/pc/files/reference'].reason, 'PC business table and ID contract is not documented');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify failure**
|
||||||
|
|
||||||
|
Run: `node tests\pc-endpoint-coverage.test.js`
|
||||||
|
|
||||||
|
Expected: failure until all three reasons are explicit.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the tracker without adding a fake request**
|
||||||
|
|
||||||
|
```md
|
||||||
|
| Blocked operation | Missing backend contract | Page behavior |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /auth/code` | legacy code submission field | no page action |
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `node tests\pc-endpoint-coverage.test.js`
|
||||||
|
|
||||||
|
Expected: `pc-endpoint-coverage tests passed`.
|
||||||
|
|
||||||
|
## Task 6: Final Verification and User Test Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run syntax checks**
|
||||||
|
|
||||||
|
Run: `node --check utils\ApiClient.js`
|
||||||
|
|
||||||
|
Run: `node --check public\js\auth-pages.js`
|
||||||
|
|
||||||
|
Run: `node --check public\js\region-pages.js`
|
||||||
|
|
||||||
|
Run: `node --check public\js\upload-pages.js`
|
||||||
|
|
||||||
|
Expected: each command exits with code 0.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the complete Node suite**
|
||||||
|
|
||||||
|
Run: `Get-ChildItem -Path tests -Filter *.test.js | ForEach-Object { & node $_.FullName; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } }`
|
||||||
|
|
||||||
|
Expected: every test file prints its pass message.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Check the diff**
|
||||||
|
|
||||||
|
Run: `git diff --check`
|
||||||
|
|
||||||
|
Expected: exit code 0; CRLF conversion notices are informational only.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Record manual acceptance steps**
|
||||||
|
|
||||||
|
Record the precise browser order: register, password login, SMS login, profile display/edit, reset password, password change, logout, account deactivation, address search, single upload, resumable upload, then each documented backend blocker.
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Coverage: Tasks 1-5 assign every documented operation an implementation owner or an explicit backend block; Task 6 provides the requested sequential test script.
|
||||||
|
- Contract discipline: `ApiClient` remains the only route owner; page modules do not create undocumented paths, scene codes, business tables or identifiers.
|
||||||
|
- Scope: pages without callable PC paths remain outside implementation; their schemas are not treated as endpoints.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Auth To Profile Flow Design
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Approved for implementation.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Complete the PC journey from registration through login and personal-profile retrieval, with one token owner, deterministic redirects, and a single recorded API base address.
|
||||||
|
|
||||||
|
## Endpoint Configuration
|
||||||
|
|
||||||
|
`config.js` is the sole runtime owner of the API base address. Both development and production currently use `http://182.61.18.23:8080`. `tests/config.test.js` must assert that value. The PC integration tracker must record the change on 2026-07-11 and label prior test-domain references as historical rather than current configuration.
|
||||||
|
|
||||||
|
## Token Contract
|
||||||
|
|
||||||
|
`utils/ApiClient.js` owns token persistence under `genealogy_auth_token`.
|
||||||
|
|
||||||
|
1. Successful password login and SMS login must receive a nonempty response token, persist it, and then allow navigation to `profile.html`. The current backend response uses `data.access_token`; the client also accepts `token`, `accessToken`, and `tokenValue` for declared compatibility.
|
||||||
|
2. A login response without a token is an authentication failure. The login page remains in place and no profile navigation occurs.
|
||||||
|
3. Successful registration removes any existing token and navigates to `login.html`. Registration never persists a response token.
|
||||||
|
4. A failed registration leaves existing token storage unchanged.
|
||||||
|
5. `logout()` clears token storage in a `finally` path, so a failed or expired logout request cannot leave a local session behind.
|
||||||
|
|
||||||
|
## Page Flow
|
||||||
|
|
||||||
|
1. Registration validates fields, obtains `WEB_H5_REGISTER` captcha proof, posts `/genealogy/pc/auth/register`, clears token through `ApiClient.register()`, and redirects to `login.html`.
|
||||||
|
2. Password and SMS login validate fields, obtain `WEB_H5_LOGIN` captcha proof, post their respective login endpoint, persist the returned token through `ApiClient`, and redirect to `profile.html`.
|
||||||
|
3. `profile.html` and `profile-data.html` check for a token before requesting `/genealogy/pc/auth/profile`. Without one, they redirect to `index.html` without making the profile request.
|
||||||
|
4. A profile-read or profile-update error with HTTP status `401` or business code `401` clears token storage and redirects to `index.html`. Other errors remain on the current page and show the existing error message.
|
||||||
|
5. Confirmed logout calls `/genealogy/pc/auth/logout`, clears token regardless of the request outcome, and redirects to `index.html`.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- `config.js`: API base address.
|
||||||
|
- `utils/ApiClient.js`: login-token validation, persistence, clearing, and logout cleanup.
|
||||||
|
- `public/js/profile-pages.js`: profile-page token guard and unauthorized redirect.
|
||||||
|
- `public/js/profile-common.js`: confirmed logout action and redirect.
|
||||||
|
- `docs/pc-api-page-integration-tracker-2026-07-09.md`: current address record and end-to-end authentication status.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Tests must prove the configured address, registration state, both login token paths, missing-token login rejection, logout cleanup after both success and failure, and profile unauthorized classification. Focused configuration, API-client, profile-page, and authentication tests must pass before the full Node test suite is run.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This flow covers registration, password login, SMS login, profile retrieval and update on the two pages that load `/genealogy/pc/auth/profile`, and the existing profile logout action. Password reset, phone change, account deactivation, and profile pages without profile API loading remain outside this change.
|
||||||
@@ -55,7 +55,6 @@
|
|||||||
<h2>下载方式</h2>
|
<h2>下载方式</h2>
|
||||||
<p>前台先展示下载入口,后续可以接真实应用商店地址。</p>
|
<p>前台先展示下载入口,后续可以接真实应用商店地址。</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 下载入口区域:内容来自 /genealogy/app/promotions 列表接口。 -->
|
|
||||||
<div class="grid-3" data-promotion-download-list>
|
<div class="grid-3" data-promotion-download-list>
|
||||||
<div class="api-empty">下载推广内容加载中</div>
|
<div class="api-empty">下载推广内容加载中</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
-30
@@ -3,27 +3,26 @@ info:
|
|||||||
title: Genealogy PC/H5 API
|
title: Genealogy PC/H5 API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
家谱业务 APP/PC 用户侧接口文档,用于 Apifox 导入。
|
家谱业务 PC/H5 用户侧接口文档,用于 Apifox 导入。
|
||||||
|
|
||||||
导入方式:Apifox -> 导入 API 数据 -> OpenAPI/Swagger -> 选择本文件。
|
导入方式:Apifox -> 导入 API 数据 -> OpenAPI/Swagger -> 选择本文件。
|
||||||
|
|
||||||
约定:
|
约定:
|
||||||
- APP 路径前缀为 `/genealogy/app`
|
|
||||||
- PC/H5 路径前缀为 `/genealogy/pc`
|
- PC/H5 路径前缀为 `/genealogy/pc`
|
||||||
- 后台管理接口不包含在本文件内
|
- 后台管理接口不包含在本文件内
|
||||||
- 登录后接口需要携带 token,且请求 Header 需携带 `clientid`
|
- 登录后接口需要携带 token,且请求 Header 需携带 `clientid`
|
||||||
servers:
|
servers:
|
||||||
- url: http://127.0.0.1:8080
|
- url: http://182.61.18.23:8080
|
||||||
description: Local Java backend
|
description: Local Java backend
|
||||||
- url: http://127.0.0.1:9527/dev-api
|
- url: http://182.61.18.23:8080/dev-api
|
||||||
description: Local frontend proxy
|
description: Local frontend proxy
|
||||||
tags:
|
tags:
|
||||||
- name: 验证中心
|
- name: 验证中心
|
||||||
description: 统一验证码、天爱行为验证码和系统图形验证码
|
description: 统一验证码、天爱行为验证码和系统图形验证码
|
||||||
- name: 认证登录
|
- name: 认证登录
|
||||||
description: APP/PC 登录注册与账号资料
|
description: PC/H5 登录注册与账号资料
|
||||||
- name: 文件上传
|
- name: 文件上传
|
||||||
description: APP/PC 统一文件上传与业务引用绑定
|
description: PC/H5 统一文件上传与业务引用绑定
|
||||||
- name: 行政区划
|
- name: 行政区划
|
||||||
description: 行政区划
|
description: 行政区划
|
||||||
- name: 家谱
|
- name: 家谱
|
||||||
@@ -67,18 +66,18 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
example: "000000"
|
example: "000000"
|
||||||
- name: clientId
|
- name: clientId
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: 428a8310cd442757ae699df5d894f051
|
example: 428a8310cd442757ae699df5d894f051
|
||||||
- name: sceneCode
|
- name: sceneCode
|
||||||
description: 验证场景编码,例如APP_LOGIN、PC_LOGIN、APP_REGISTER
|
description: 验证场景编码,例如WEB_H5_LOGIN、WEB_H5_REGISTER
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: APP_LOGIN
|
example: WEB_H5_LOGIN
|
||||||
- name: subject
|
- name: subject
|
||||||
description: 验证主体,通常为手机号、用户ID或登录账号
|
description: 验证主体,通常为手机号、用户ID或登录账号
|
||||||
in: query
|
in: query
|
||||||
@@ -114,7 +113,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
tags: [验证中心]
|
tags: [验证中心]
|
||||||
summary: 兼容旧系统图形验证码
|
summary: 兼容旧系统图形验证码
|
||||||
description: 老版后台登录图形验证码入口。新的 APP/PC 业务优先使用 /captcha/require + /captcha/challenge + /captcha/verify。
|
description: 老版后台登录图形验证码入口。新的 PC/H5 业务优先使用 /captcha/require + /captcha/challenge + /captcha/verify。
|
||||||
security: []
|
security: []
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -385,7 +384,7 @@ components:
|
|||||||
parameters:
|
parameters:
|
||||||
ClientIdHeader:
|
ClientIdHeader:
|
||||||
name: clientid
|
name: clientid
|
||||||
description: 客户端ID,请求Header携带,用于区分APP/PC/小程序等终端
|
description: 客户端ID,请求 Header 携带,用于识别 PC/H5 终端
|
||||||
in: header
|
in: header
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
@@ -493,7 +492,7 @@ components:
|
|||||||
phone: "13800000000"
|
phone: "13800000000"
|
||||||
password: "e10adc3949ba59abbe56e057f20f883e"
|
password: "e10adc3949ba59abbe56e057f20f883e"
|
||||||
nickName: "测试用户"
|
nickName: "测试用户"
|
||||||
registerSource: "app"
|
registerSource: "PC"
|
||||||
validToken: "captcha-ticket-or-sms-token"
|
validToken: "captcha-ticket-or-sms-token"
|
||||||
PasswordLogin:
|
PasswordLogin:
|
||||||
required: true
|
required: true
|
||||||
@@ -528,7 +527,7 @@ components:
|
|||||||
example:
|
example:
|
||||||
tenantId: "000000"
|
tenantId: "000000"
|
||||||
phone: "13800000000"
|
phone: "13800000000"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
validToken: "captcha-ticket"
|
validToken: "captcha-ticket"
|
||||||
ProfileUpdate:
|
ProfileUpdate:
|
||||||
required: true
|
required: true
|
||||||
@@ -711,7 +710,7 @@ components:
|
|||||||
example:
|
example:
|
||||||
tenantId: "000000"
|
tenantId: "000000"
|
||||||
clientId: "428a8310cd442757ae699df5d894f051"
|
clientId: "428a8310cd442757ae699df5d894f051"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
subject: "13800000000"
|
subject: "13800000000"
|
||||||
VerificationCheck:
|
VerificationCheck:
|
||||||
required: true
|
required: true
|
||||||
@@ -725,7 +724,7 @@ components:
|
|||||||
value:
|
value:
|
||||||
tenantId: "000000"
|
tenantId: "000000"
|
||||||
clientId: "428a8310cd442757ae699df5d894f051"
|
clientId: "428a8310cd442757ae699df5d894f051"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
subject: "13800000000"
|
subject: "13800000000"
|
||||||
challengeId: "CAPTCHA_CHALLENGE_ID"
|
challengeId: "CAPTCHA_CHALLENGE_ID"
|
||||||
providerCode: "tianai"
|
providerCode: "tianai"
|
||||||
@@ -743,7 +742,7 @@ components:
|
|||||||
value:
|
value:
|
||||||
tenantId: "000000"
|
tenantId: "000000"
|
||||||
clientId: "428a8310cd442757ae699df5d894f051"
|
clientId: "428a8310cd442757ae699df5d894f051"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
subject: "13800000000"
|
subject: "13800000000"
|
||||||
challengeId: "CAPTCHA_CHALLENGE_ID"
|
challengeId: "CAPTCHA_CHALLENGE_ID"
|
||||||
providerCode: "ruoyi_image"
|
providerCode: "ruoyi_image"
|
||||||
@@ -923,7 +922,7 @@ components:
|
|||||||
description: 租户ID,用于区分不同租户的数据和验证策略
|
description: 租户ID,用于区分不同租户的数据和验证策略
|
||||||
type: string
|
type: string
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
clientKey:
|
clientKey:
|
||||||
type: string
|
type: string
|
||||||
@@ -949,7 +948,7 @@ components:
|
|||||||
required: [grantType, tenantId, phone, password]
|
required: [grantType, tenantId, phone, password]
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
grantType:
|
grantType:
|
||||||
description: 授权类型,例如password、sms、wechat、apple
|
description: 授权类型,例如password、sms、wechat、apple
|
||||||
@@ -971,7 +970,7 @@ components:
|
|||||||
description: 用户昵称
|
description: 用户昵称
|
||||||
type: string
|
type: string
|
||||||
registerSource:
|
registerSource:
|
||||||
description: 注册来源,例如APP、PC、H5、MINI_PROGRAM
|
description: 注册来源,例如PC、H5
|
||||||
type: string
|
type: string
|
||||||
validToken:
|
validToken:
|
||||||
description: 验证中心通过后返回的票据,用于注册/登录/换绑等二次校验
|
description: 验证中心通过后返回的票据,用于注册/登录/换绑等二次校验
|
||||||
@@ -981,7 +980,7 @@ components:
|
|||||||
required: [grantType, tenantId, phone, password]
|
required: [grantType, tenantId, phone, password]
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
grantType:
|
grantType:
|
||||||
description: 授权类型,例如password、sms、wechat、apple
|
description: 授权类型,例如password、sms、wechat、apple
|
||||||
@@ -1007,7 +1006,7 @@ components:
|
|||||||
required: [grantType, tenantId, phone, smsCode]
|
required: [grantType, tenantId, phone, smsCode]
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
grantType:
|
grantType:
|
||||||
description: 授权类型,例如password、sms、wechat、apple
|
description: 授权类型,例如password、sms、wechat、apple
|
||||||
@@ -1030,7 +1029,7 @@ components:
|
|||||||
required: [grantType, tenantId, sceneCode, phone, validToken]
|
required: [grantType, tenantId, sceneCode, phone, validToken]
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
grantType:
|
grantType:
|
||||||
description: 授权类型,例如password、sms、wechat、apple
|
description: 授权类型,例如password、sms、wechat、apple
|
||||||
@@ -1041,9 +1040,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: "000000"
|
example: "000000"
|
||||||
sceneCode:
|
sceneCode:
|
||||||
description: 验证场景编码,例如APP_LOGIN、PC_LOGIN、APP_REGISTER
|
description: 验证场景编码,例如WEB_H5_LOGIN、WEB_H5_REGISTER
|
||||||
type: string
|
type: string
|
||||||
example: APP_LOGIN
|
example: WEB_H5_LOGIN
|
||||||
phone:
|
phone:
|
||||||
description: 手机号
|
description: 手机号
|
||||||
type: string
|
type: string
|
||||||
@@ -1290,14 +1289,14 @@ components:
|
|||||||
description: 客户端 ID
|
description: 客户端 ID
|
||||||
sceneCode:
|
sceneCode:
|
||||||
type: string
|
type: string
|
||||||
description: 验证场景编码,如 ADMIN_LOGIN/APP_LOGIN/APP_REGISTER
|
description: 验证场景编码,例如WEB_H5_LOGIN、WEB_H5_REGISTER
|
||||||
subject:
|
subject:
|
||||||
type: string
|
type: string
|
||||||
description: 验证主体,如手机号/用户名/IP
|
description: 验证主体,如手机号/用户名/IP
|
||||||
example:
|
example:
|
||||||
tenantId: "000000"
|
tenantId: "000000"
|
||||||
clientId: "428a8310cd442757ae699df5d894f051"
|
clientId: "428a8310cd442757ae699df5d894f051"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
subject: "13800000000"
|
subject: "13800000000"
|
||||||
VerificationCheckBody:
|
VerificationCheckBody:
|
||||||
type: object
|
type: object
|
||||||
@@ -1307,10 +1306,10 @@ components:
|
|||||||
description: 租户ID,用于区分不同租户的数据和验证策略
|
description: 租户ID,用于区分不同租户的数据和验证策略
|
||||||
type: string
|
type: string
|
||||||
clientId:
|
clientId:
|
||||||
description: 客户端ID,对应后台客户端管理中的APP/PC/小程序等终端
|
description: 客户端ID,对应后台 PC/H5 客户端配置
|
||||||
type: string
|
type: string
|
||||||
sceneCode:
|
sceneCode:
|
||||||
description: 验证场景编码,例如APP_LOGIN、PC_LOGIN、APP_REGISTER
|
description: 验证场景编码,例如WEB_H5_LOGIN、WEB_H5_REGISTER
|
||||||
type: string
|
type: string
|
||||||
subject:
|
subject:
|
||||||
description: 验证主体,通常为手机号、用户ID或登录账号
|
description: 验证主体,通常为手机号、用户ID或登录账号
|
||||||
@@ -1341,7 +1340,7 @@ components:
|
|||||||
description: 验证码类型,例如SLIDER、ROTATE、CONCAT、WORD_IMAGE_CLICK
|
description: 验证码类型,例如SLIDER、ROTATE、CONCAT、WORD_IMAGE_CLICK
|
||||||
type: string
|
type: string
|
||||||
sceneCode:
|
sceneCode:
|
||||||
description: 验证场景编码,例如APP_LOGIN、PC_LOGIN、APP_REGISTER
|
description: 验证场景编码,例如WEB_H5_LOGIN、WEB_H5_REGISTER
|
||||||
type: string
|
type: string
|
||||||
ttlSeconds:
|
ttlSeconds:
|
||||||
description: 验证或票据有效秒数
|
description: 验证或票据有效秒数
|
||||||
@@ -1350,7 +1349,7 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
providerCode: "tianai"
|
providerCode: "tianai"
|
||||||
captchaType: "SLIDER"
|
captchaType: "SLIDER"
|
||||||
sceneCode: "APP_LOGIN"
|
sceneCode: "WEB_H5_LOGIN"
|
||||||
ttlSeconds: 300
|
ttlSeconds: 300
|
||||||
VerificationChallengeVo:
|
VerificationChallengeVo:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="section alt">
|
<section class="section alt">
|
||||||
<div class="container notice-layout">
|
<div class="container notice-layout">
|
||||||
<!-- 公告详情区域:从 /genealogy/app/promotions 列表中按 id 选择一条展示。 -->
|
|
||||||
<article class="notice-content tilt-card" data-promotion-content>
|
<article class="notice-content tilt-card" data-promotion-content>
|
||||||
<p>
|
<p>
|
||||||
平台前台页面主要用于展示家族文化与公开资料。涉及成员联系方式、详细个人信息、未审核资料等内容,不会在公开页面直接呈现。
|
平台前台页面主要用于展示家族文化与公开资料。涉及成员联系方式、详细个人信息、未审核资料等内容,不会在公开页面直接呈现。
|
||||||
|
|||||||
@@ -59,7 +59,6 @@
|
|||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>新建 / 编辑相册</h2>
|
<h2>新建 / 编辑相册</h2>
|
||||||
<form id="album-form" class="editor-form" data-album-form>
|
<form id="album-form" class="editor-form" data-album-form>
|
||||||
<!-- 字段名称对应 APP.openapi.json 的 AlbumBody -->
|
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label for="albumName">相册名称</label>
|
<label for="albumName">相册名称</label>
|
||||||
<input id="albumName" name="albumName" type="text" placeholder="请输入相册名称" required />
|
<input id="albumName" name="albumName" type="text" placeholder="请输入相册名称" required />
|
||||||
@@ -98,7 +97,6 @@
|
|||||||
<div class="api-empty">请选择相册查看照片</div>
|
<div class="api-empty">请选择相册查看照片</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="editor-form album-photo-form" data-album-photo-form>
|
<form class="editor-form album-photo-form" data-album-photo-form>
|
||||||
<!-- 字段名称对应 APP.openapi.json 的 AlbumPhotoBody -->
|
|
||||||
<div class="editor-two">
|
<div class="editor-two">
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label for="photoOssId">照片 OSS ID</label>
|
<label for="photoOssId">照片 OSS ID</label>
|
||||||
|
|||||||
+31
-7
@@ -38,7 +38,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>个人资料</h1>
|
<h1>个人资料</h1>
|
||||||
<p>
|
<p>
|
||||||
对应 APP 的个人资料、亲属、完善资料、绑定账号和成员激活流程。
|
管理个人资料、亲属关系和账号安全信息。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn primary magnetic" href="#profile-edit-form"
|
<a class="btn primary magnetic" href="#profile-edit-form"
|
||||||
@@ -60,7 +60,6 @@
|
|||||||
<div class="module-main">
|
<div class="module-main">
|
||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>编辑资料</h2>
|
<h2>编辑资料</h2>
|
||||||
<!-- 个人资料表单:字段对应 /genealogy/app/auth/profile 的 ProfileUpdateBody。 -->
|
|
||||||
<form class="editor-form profile-data-form" id="profile-edit-form" data-profile-form>
|
<form class="editor-form profile-data-form" id="profile-edit-form" data-profile-form>
|
||||||
<div class="editor-two">
|
<div class="editor-two">
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
@@ -74,15 +73,25 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label for="profileAvatarOssId">头像 OSS ID</label>
|
<label for="profileAvatarFile">头像</label>
|
||||||
<input
|
<input
|
||||||
id="profileAvatarOssId"
|
id="profileAvatarOssId"
|
||||||
name="avatarOssId"
|
name="avatarOssId"
|
||||||
type="number"
|
type="hidden"
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
placeholder="上传后填写 OSS ID"
|
|
||||||
/>
|
/>
|
||||||
|
<label class="upload-control" for="profileAvatarFile">选择头像文件</label>
|
||||||
|
<input
|
||||||
|
id="profileAvatarFile"
|
||||||
|
class="upload-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
data-upload-target="#profileAvatarOssId"
|
||||||
|
data-upload-status="#profileAvatarStatus"
|
||||||
|
data-upload-mode="auto"
|
||||||
|
data-upload-biz-type="avatar"
|
||||||
|
data-upload-usage-scene="profile_avatar"
|
||||||
|
/>
|
||||||
|
<p id="profileAvatarStatus" class="upload-status">未选择文件</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-two">
|
<div class="editor-two">
|
||||||
@@ -172,6 +181,19 @@
|
|||||||
<button class="btn primary magnetic" type="submit">保存地区</button>
|
<button class="btn primary magnetic" type="submit">保存地区</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<form class="editor-form" data-region-search-form data-region-search-panel>
|
||||||
|
<div class="editor-two">
|
||||||
|
<div class="editor-field">
|
||||||
|
<label for="profileRegionKeyword">搜索地区</label>
|
||||||
|
<input id="profileRegionKeyword" name="regionKeyword" type="search" placeholder="地区名称或编码" />
|
||||||
|
</div>
|
||||||
|
<div class="bottom-actions">
|
||||||
|
<button class="btn ghost magnetic" type="submit">搜索</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="api-status" data-region-search-path></p>
|
||||||
|
<div class="module-list" data-region-search-results></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>亲属</h2>
|
<h2>亲属</h2>
|
||||||
@@ -205,6 +227,8 @@
|
|||||||
<script src="utils/axios.js"></script>
|
<script src="utils/axios.js"></script>
|
||||||
<script src="utils/AxiosRequestUtil.js"></script>
|
<script src="utils/AxiosRequestUtil.js"></script>
|
||||||
<script src="utils/ApiClient.js"></script>
|
<script src="utils/ApiClient.js"></script>
|
||||||
|
<script src="public/js/md5.js"></script>
|
||||||
|
<script src="public/js/upload-pages.js"></script>
|
||||||
<script src="public/js/region-pages.js"></script>
|
<script src="public/js/region-pages.js"></script>
|
||||||
<script src="public/js/profile-pages.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"></script>
|
||||||
|
|||||||
@@ -63,7 +63,6 @@
|
|||||||
<div class="api-empty">请选择贺礼查看献礼记录</div>
|
<div class="api-empty">请选择贺礼查看献礼记录</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="editor-form ceremony-gift-form" data-ceremony-gift-form>
|
<form class="editor-form ceremony-gift-form" data-ceremony-gift-form>
|
||||||
<!-- 字段名称对应 APP.openapi.json 的 CeremonyGiftBody -->
|
|
||||||
<div class="editor-two">
|
<div class="editor-two">
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label for="giftGiverName">献礼人</label>
|
<label for="giftGiverName">献礼人</label>
|
||||||
|
|||||||
@@ -93,30 +93,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="module-panel">
|
|
||||||
<h2>短信登录校验</h2>
|
|
||||||
<!-- 短信登录表单:用于接入 /genealogy/app/auth/login/sms。 -->
|
|
||||||
<form class="editor-form security-form" data-security-form="sms-login">
|
|
||||||
<div class="editor-two">
|
|
||||||
<div class="editor-field">
|
|
||||||
<label for="loginPhone">手机号</label>
|
|
||||||
<input id="loginPhone" name="phone" type="tel" required />
|
|
||||||
</div>
|
|
||||||
<div class="editor-field">
|
|
||||||
<label for="loginSmsCode">短信验证码</label>
|
|
||||||
<input id="loginSmsCode" name="smsCode" type="text" required />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="editor-field">
|
|
||||||
<label for="loginValidToken">滑动验证票据</label>
|
|
||||||
<input id="loginValidToken" name="validToken" type="text" placeholder="可选,由验证组件返回" />
|
|
||||||
</div>
|
|
||||||
<div class="bottom-actions">
|
|
||||||
<button class="btn primary magnetic" type="submit">短信登录验证</button>
|
|
||||||
<span class="api-status" data-security-status>等待提交</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<section class="module-panel security-danger">
|
<section class="module-panel security-danger">
|
||||||
<h2>注销账号</h2>
|
<h2>注销账号</h2>
|
||||||
<!-- 注销账号属于敏感操作,必须填写当前密码并输入确认文本。 -->
|
<!-- 注销账号属于敏感操作,必须填写当前密码并输入确认文本。 -->
|
||||||
|
|||||||
@@ -95,7 +95,6 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- 会员套餐区域:套餐列表和订单创建字段来自 APP.openapi.json 的 VIP 接口。 -->
|
|
||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>会员服务</h2>
|
<h2>会员服务</h2>
|
||||||
<p>选择适合当前家谱的会员套餐,创建订单后继续完成支付。</p>
|
<p>选择适合当前家谱的会员套餐,创建订单后继续完成支付。</p>
|
||||||
@@ -127,7 +126,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<!-- 会员订单区域:展示 GET /genealogy/app/vip/orders 的历史记录。 -->
|
|
||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>会员订单</h2>
|
<h2>会员订单</h2>
|
||||||
<div class="module-list" data-vip-order-list>
|
<div class="module-list" data-vip-order-list>
|
||||||
|
|||||||
@@ -100,7 +100,6 @@
|
|||||||
<section class="module-panel">
|
<section class="module-panel">
|
||||||
<h2>新增成员</h2>
|
<h2>新增成员</h2>
|
||||||
<form class="editor-form" data-lineage-form>
|
<form class="editor-form" data-lineage-form>
|
||||||
<!-- 字段名称对应 APP.openapi.json 的 LineagePersonBody -->
|
|
||||||
<div class="editor-two">
|
<div class="editor-two">
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label for="lineage-person-name">姓名</label>
|
<label for="lineage-person-name">姓名</label>
|
||||||
|
|||||||
+2
-2
@@ -78,7 +78,7 @@
|
|||||||
><span>帮助与服务</span><b>帮助、反馈、推广</b></a
|
><span>帮助与服务</span><b>帮助、反馈、推广</b></a
|
||||||
>
|
>
|
||||||
<a class="logout-link" href="#logout" data-logout-open
|
<a class="logout-link" href="#logout" data-logout-open
|
||||||
><span>退出登录</span><b>返回登录页</b></a
|
><span>退出登录</span><b>返回首页</b></a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-card todo-card tilt-card" id="messages">
|
<div class="side-card todo-card tilt-card" id="messages">
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
<button class="btn ghost magnetic" type="button" data-logout-close>
|
<button class="btn ghost magnetic" type="button" data-logout-close>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<a class="btn primary magnetic" href="login.html">确认退出</a>
|
<a class="btn primary magnetic" href="index.html" data-logout-confirm>确认退出</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,7 +101,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildAlbumBody(values) {
|
function buildAlbumBody(values) {
|
||||||
// AlbumBody 来自 APP.openapi.json,albumName 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -114,7 +113,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildPhotoBody(values) {
|
function buildPhotoBody(values) {
|
||||||
// AlbumPhotoBody 来自 APP.openapi.json,ossId 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -115,7 +115,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildArticleBody(values) {
|
function buildArticleBody(values) {
|
||||||
// ArticleBody 来自 APP.openapi.json,标题和正文为核心必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -107,7 +107,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildCeremonyBody(values) {
|
function buildCeremonyBody(values) {
|
||||||
// CeremonyBody 来自 APP.openapi.json,ceremonyType 和 ceremonyTitle 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -133,7 +132,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildMeritBody(values) {
|
function buildMeritBody(values) {
|
||||||
// MeritRecordBody 来自 APP.openapi.json,donorName 和 meritTitle 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -106,7 +106,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildFeedBody(values) {
|
function buildFeedBody(values) {
|
||||||
// FamilyFeedBody 来自 APP.openapi.json。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -128,7 +128,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatJoinMode(value) {
|
function formatJoinMode(value) {
|
||||||
// APP.openapi.json 中 joinMode 为枚举值,页面展示时转成中文。
|
|
||||||
var map = {
|
var map = {
|
||||||
0: '关闭加入',
|
0: '关闭加入',
|
||||||
1: '审核加入',
|
1: '审核加入',
|
||||||
|
|||||||
@@ -106,7 +106,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildGrowthBody(values) {
|
function buildGrowthBody(values) {
|
||||||
// GrowthRecordBody 来自 APP.openapi.json,recordTitle 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -113,7 +113,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildLineagePersonBody(values) {
|
function buildLineagePersonBody(values) {
|
||||||
// LineagePersonBody 来自 APP.openapi.json,空值统一转为 undefined,避免提交无意义空字符串。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -113,7 +113,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildMemberUpdateBody(values) {
|
function buildMemberUpdateBody(values) {
|
||||||
// GenealogyMemberUpdateBody 来自 APP.openapi.json。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -110,7 +110,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildMemoBody(values) {
|
function buildMemoBody(values) {
|
||||||
// MemoBody 来自 APP.openapi.json,memoTitle 必填。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -55,9 +55,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function performLogout(targetUrl) {
|
||||||
|
var api = window.GenealogyApi && window.GenealogyApi.defaultClient;
|
||||||
|
var redirect = function () {
|
||||||
|
window.location.href = targetUrl || 'index.html';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!api || !api.logout) {
|
||||||
|
redirect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.logout().catch(function () {}).then(redirect);
|
||||||
|
}
|
||||||
|
|
||||||
function bindLogout() {
|
function bindLogout() {
|
||||||
$(document).on('click', '[data-logout-open]', function (event) {
|
$(document).on('click', '[data-logout-open]', function (event) {
|
||||||
var targetUrl = $(this).attr('data-logout-url') || 'login.html';
|
var targetUrl = $(this).attr('data-logout-url') || 'index.html';
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!layerApi) {
|
if (!layerApi) {
|
||||||
@@ -70,11 +84,17 @@
|
|||||||
content: '确认退出当前账号吗?退出后需要重新登录才能继续管理家谱。',
|
content: '确认退出当前账号吗?退出后需要重新登录才能继续管理家谱。',
|
||||||
confirmText: '确认退出',
|
confirmText: '确认退出',
|
||||||
onConfirm: function () {
|
onConfirm: function () {
|
||||||
window.location.href = targetUrl;
|
performLogout(targetUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '[data-logout-confirm]', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeLegacyLogout();
|
||||||
|
performLogout($(this).attr('href') || 'index.html');
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '[data-logout-close]', function () {
|
$(document).on('click', '[data-logout-close]', function () {
|
||||||
closeLegacyLogout();
|
closeLegacyLogout();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,28 @@
|
|||||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRedirectToHome(api, error) {
|
||||||
|
var status = error && (error.status || error.code);
|
||||||
|
|
||||||
|
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToHome() {
|
||||||
|
if (!root.location) return;
|
||||||
|
if (typeof root.location.replace === 'function') {
|
||||||
|
root.location.replace('index.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectUnauthorized(api, error) {
|
||||||
|
if (!shouldRedirectToHome(api, error)) return false;
|
||||||
|
if (api && api.clearToken) api.clearToken();
|
||||||
|
redirectToHome();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function query(selector, rootNode) {
|
function query(selector, rootNode) {
|
||||||
if (!documentRef) return null;
|
if (!documentRef) return null;
|
||||||
return (rootNode || documentRef).querySelector(selector);
|
return (rootNode || documentRef).querySelector(selector);
|
||||||
@@ -167,12 +189,14 @@
|
|||||||
var profile;
|
var profile;
|
||||||
|
|
||||||
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
|
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
|
||||||
|
if (redirectUnauthorized(api)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
profile = await api.currentProfile();
|
profile = await api.currentProfile();
|
||||||
renderProfile(buildProfileView(profile));
|
renderProfile(buildProfileView(profile));
|
||||||
fillProfileForm(profile);
|
fillProfileForm(profile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (redirectUnauthorized(api, error)) return;
|
||||||
showMessage(error.message || '个人资料加载失败');
|
showMessage(error.message || '个人资料加载失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +205,7 @@
|
|||||||
var api = getApi();
|
var api = getApi();
|
||||||
|
|
||||||
if (!api || !form) return;
|
if (!api || !form) return;
|
||||||
|
if (redirectUnauthorized(api)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(form, true);
|
setSubmitting(form, true);
|
||||||
@@ -190,6 +215,7 @@
|
|||||||
showMessage('个人资料已保存');
|
showMessage('个人资料已保存');
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (redirectUnauthorized(api, error)) return;
|
||||||
setStatus(form, '保存失败');
|
setStatus(form, '保存失败');
|
||||||
showMessage(error.message || '个人资料保存失败');
|
showMessage(error.message || '个人资料保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -218,6 +244,7 @@
|
|||||||
getAvatarText: getAvatarText,
|
getAvatarText: getAvatarText,
|
||||||
buildProfileView: buildProfileView,
|
buildProfileView: buildProfileView,
|
||||||
buildProfileUpdateBody: buildProfileUpdateBody,
|
buildProfileUpdateBody: buildProfileUpdateBody,
|
||||||
|
shouldRedirectToHome: shouldRedirectToHome,
|
||||||
init: init
|
init: init
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,37 @@
|
|||||||
return pick(item, ['regionName', 'name', 'label'], '未命名地区');
|
return pick(item, ['regionName', 'name', 'label'], '未命名地区');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRegionLevel(item) {
|
||||||
|
var source = item || {};
|
||||||
|
var level = Number(pick(source, ['level', 'regionLevel'], 0));
|
||||||
|
var code = getRegionCode(source);
|
||||||
|
|
||||||
|
if (level >= 1 && level <= 3) return level;
|
||||||
|
if (!/^\d{6}$/.test(code)) return 0;
|
||||||
|
if (/0000$/.test(code)) return 1;
|
||||||
|
if (/00$/.test(code)) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegionSelection(items) {
|
||||||
|
var selection = {
|
||||||
|
provinceCode: '',
|
||||||
|
cityCode: '',
|
||||||
|
districtCode: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
normalizeList(items).forEach(function (item) {
|
||||||
|
var code = getRegionCode(item);
|
||||||
|
var level = getRegionLevel(item);
|
||||||
|
|
||||||
|
if (level === 1) selection.provinceCode = code;
|
||||||
|
if (level === 2) selection.cityCode = code;
|
||||||
|
if (level === 3) selection.districtCode = code;
|
||||||
|
});
|
||||||
|
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRegionOption(item) {
|
function buildRegionOption(item) {
|
||||||
return '<option value="' + escapeHtml(getRegionCode(item)) + '">' + escapeHtml(getRegionName(item)) + '</option>';
|
return '<option value="' + escapeHtml(getRegionCode(item)) + '">' + escapeHtml(getRegionName(item)) + '</option>';
|
||||||
}
|
}
|
||||||
@@ -180,6 +211,91 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyRegionSelection(picker, selection) {
|
||||||
|
var province = query('[data-region-level="province"]', picker);
|
||||||
|
var city = query('[data-region-level="city"]', picker);
|
||||||
|
var district = query('[data-region-level="district"]', picker);
|
||||||
|
var values = selection || {};
|
||||||
|
|
||||||
|
if (!values.provinceCode) return;
|
||||||
|
|
||||||
|
province.value = values.provinceCode;
|
||||||
|
resetSelect(city, '请选择市');
|
||||||
|
resetSelect(district, '请选择区县');
|
||||||
|
|
||||||
|
if (values.cityCode) {
|
||||||
|
await loadChildrenInto(city, values.provinceCode, '请选择市');
|
||||||
|
city.value = values.cityCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.districtCode && values.cityCode) {
|
||||||
|
await loadChildrenInto(district, values.cityCode, '请选择区县');
|
||||||
|
district.value = values.districtCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTargetCode(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSearchResults(container, items) {
|
||||||
|
var list = normalizeList(items);
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = list.length ? list.map(function (item) {
|
||||||
|
var code = getRegionCode(item);
|
||||||
|
var name = getRegionName(item);
|
||||||
|
|
||||||
|
return '<button class="btn ghost magnetic" type="button" data-region-search-result="' + escapeHtml(code) + '">' + escapeHtml(name) + ' ' + escapeHtml(code) + '</button>';
|
||||||
|
}).join('') : '<p class="api-empty">未找到匹配地区</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRegionSearch(form) {
|
||||||
|
var api = getApi();
|
||||||
|
var values = getFormValues(form);
|
||||||
|
var results = query('[data-region-search-results]', form.parentNode);
|
||||||
|
var pathText = query('[data-region-search-path]', form.parentNode);
|
||||||
|
|
||||||
|
if (!api) return;
|
||||||
|
if (!values.regionKeyword || !values.regionKeyword.trim()) {
|
||||||
|
showMessage('请输入地区名称或编码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathText) pathText.textContent = '搜索中...';
|
||||||
|
try {
|
||||||
|
renderSearchResults(results, await api.regionSearch({
|
||||||
|
keyword: values.regionKeyword.trim(),
|
||||||
|
limit: 20
|
||||||
|
}));
|
||||||
|
if (pathText) pathText.textContent = '请选择一个地区';
|
||||||
|
} catch (error) {
|
||||||
|
if (pathText) pathText.textContent = error.message || '地区搜索失败';
|
||||||
|
showMessage(error.message || '地区搜索失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectSearchResult(button) {
|
||||||
|
var api = getApi();
|
||||||
|
var code = button.getAttribute('data-region-search-result');
|
||||||
|
var section = button.closest('.module-panel');
|
||||||
|
var picker = section && query('[data-region-picker]', section);
|
||||||
|
var pathText = section && query('[data-region-search-path]', section);
|
||||||
|
var detail;
|
||||||
|
var path;
|
||||||
|
|
||||||
|
if (!api || !code || !picker) return;
|
||||||
|
|
||||||
|
if (pathText) pathText.textContent = '正在定位地区...';
|
||||||
|
try {
|
||||||
|
detail = await api.regionDetail(code);
|
||||||
|
path = await api.regionPath(getRegionCode(detail) || code);
|
||||||
|
await applyRegionSelection(picker, buildRegionSelection(path));
|
||||||
|
if (pathText) pathText.textContent = buildPathText(path);
|
||||||
|
} catch (error) {
|
||||||
|
if (pathText) pathText.textContent = error.message || '地区定位失败';
|
||||||
|
showMessage(error.message || '地区定位失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitProfileRegion(form) {
|
async function submitProfileRegion(form) {
|
||||||
var api = getApi();
|
var api = getApi();
|
||||||
var values = getFormValues(form);
|
var values = getFormValues(form);
|
||||||
@@ -208,6 +324,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindSearchForms() {
|
||||||
|
queryAll('[data-region-search-form]').forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
submitRegionSearch(form);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
documentRef.addEventListener('click', function (event) {
|
||||||
|
var button = event.target.closest('[data-region-search-result]');
|
||||||
|
|
||||||
|
if (!button) return;
|
||||||
|
event.preventDefault();
|
||||||
|
selectSearchResult(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
if (!documentRef) return;
|
if (!documentRef) return;
|
||||||
|
|
||||||
@@ -215,12 +348,14 @@
|
|||||||
initPicker(picker);
|
initPicker(picker);
|
||||||
});
|
});
|
||||||
bindProfileForms();
|
bindProfileForms();
|
||||||
|
bindSearchForms();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
normalizeList: normalizeList,
|
normalizeList: normalizeList,
|
||||||
buildRegionOption: buildRegionOption,
|
buildRegionOption: buildRegionOption,
|
||||||
buildPathText: buildPathText,
|
buildPathText: buildPathText,
|
||||||
|
buildRegionSelection: buildRegionSelection,
|
||||||
getFinalRegionCode: getFinalRegionCode,
|
getFinalRegionCode: getFinalRegionCode,
|
||||||
buildProfileRegionBody: buildProfileRegionBody,
|
buildProfileRegionBody: buildProfileRegionBody,
|
||||||
init: init
|
init: init
|
||||||
|
|||||||
@@ -49,16 +49,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSmsLoginBody(values) {
|
|
||||||
var source = values || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
phone: String(source.phone || '').trim(),
|
|
||||||
smsCode: String(source.smsCode || '').trim(),
|
|
||||||
validToken: trimOrUndefined(source.validToken)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDeactivateBody(values, hashFn) {
|
function buildDeactivateBody(values, hashFn) {
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
var hash = hashFn || hashPassword;
|
var hash = hashFn || hashPassword;
|
||||||
@@ -154,25 +144,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSmsLogin(form) {
|
|
||||||
var api = getApi();
|
|
||||||
var body = buildSmsLoginBody(getFormValues(form));
|
|
||||||
|
|
||||||
if (!api) return;
|
|
||||||
if (!body.phone || !body.smsCode) {
|
|
||||||
showMessage('请填写手机号和验证码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.smsLogin(body);
|
|
||||||
setStatus(form, '短信登录已通过');
|
|
||||||
showMessage('短信登录已通过');
|
|
||||||
} catch (error) {
|
|
||||||
showMessage(error.message || '短信登录失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitDeactivate(form) {
|
async function submitDeactivate(form) {
|
||||||
var api = getApi();
|
var api = getApi();
|
||||||
var values = getFormValues(form);
|
var values = getFormValues(form);
|
||||||
@@ -191,7 +162,7 @@
|
|||||||
try {
|
try {
|
||||||
await api.deactivateAccount(buildDeactivateBody(values));
|
await api.deactivateAccount(buildDeactivateBody(values));
|
||||||
showMessage('账号已提交注销');
|
showMessage('账号已提交注销');
|
||||||
root.location.href = 'login.html';
|
root.location.href = 'index.html';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(error.message || '账号注销失败');
|
showMessage(error.message || '账号注销失败');
|
||||||
}
|
}
|
||||||
@@ -200,14 +171,14 @@
|
|||||||
async function logout() {
|
async function logout() {
|
||||||
var api = getApi();
|
var api = getApi();
|
||||||
|
|
||||||
if (!api) return;
|
|
||||||
if (root.confirm && !root.confirm('确认退出当前账号?')) return;
|
if (root.confirm && !root.confirm('确认退出当前账号?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.logout();
|
if (api) await api.logout();
|
||||||
root.location.href = 'login.html';
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(error.message || '退出登录失败');
|
// ApiClient.logout 会在请求失败时清理本地 token,页面仍应离开受保护区域。
|
||||||
|
} finally {
|
||||||
|
root.location.href = 'index.html';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +195,6 @@
|
|||||||
|
|
||||||
if (formType === 'password') submitPassword(form);
|
if (formType === 'password') submitPassword(form);
|
||||||
if (formType === 'phone') submitPhone(form);
|
if (formType === 'phone') submitPhone(form);
|
||||||
if (formType === 'sms-login') submitSmsLogin(form);
|
|
||||||
if (formType === 'deactivate') submitDeactivate(form);
|
if (formType === 'deactivate') submitDeactivate(form);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,7 +216,6 @@
|
|||||||
return {
|
return {
|
||||||
buildPasswordChangeBody: buildPasswordChangeBody,
|
buildPasswordChangeBody: buildPasswordChangeBody,
|
||||||
buildPhoneChangeBody: buildPhoneChangeBody,
|
buildPhoneChangeBody: buildPhoneChangeBody,
|
||||||
buildSmsLoginBody: buildSmsLoginBody,
|
|
||||||
buildDeactivateBody: buildDeactivateBody,
|
buildDeactivateBody: buildDeactivateBody,
|
||||||
isDangerConfirmed: isDangerConfirmed,
|
isDangerConfirmed: isDangerConfirmed,
|
||||||
init: init
|
init: init
|
||||||
|
|||||||
+119
-1
@@ -15,6 +15,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var documentRef = root.document;
|
var documentRef = root.document;
|
||||||
|
var DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024;
|
||||||
|
|
||||||
function getApi() {
|
function getApi() {
|
||||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||||
@@ -78,6 +79,108 @@
|
|||||||
return (fileName || '文件') + ' 上传完成,OSS ID:' + ossId;
|
return (fileName || '文件') + ' 上传完成,OSS ID:' + ossId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUploadMode(fileSize, chunkSize) {
|
||||||
|
var size = Number(fileSize) || 0;
|
||||||
|
var limit = Number(chunkSize) || DEFAULT_CHUNK_SIZE;
|
||||||
|
|
||||||
|
return size > limit ? 'resumable' : 'single';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResumableInitBody(file, fileMd5, chunkSize, options) {
|
||||||
|
var source = file || {};
|
||||||
|
var settings = options || {};
|
||||||
|
var size = Number(source.size) || 0;
|
||||||
|
var sizePerChunk = Number(chunkSize) || DEFAULT_CHUNK_SIZE;
|
||||||
|
var body = {
|
||||||
|
fileName: String(source.name || ''),
|
||||||
|
fileSize: size,
|
||||||
|
fileMd5: String(fileMd5 || ''),
|
||||||
|
contentType: String(source.type || 'application/octet-stream'),
|
||||||
|
chunkSize: sizePerChunk,
|
||||||
|
totalChunks: Math.ceil(size / sizePerChunk)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.bizType) body.bizType = settings.bizType;
|
||||||
|
if (settings.usageScene) body.usageScene = settings.usageScene;
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResumableCompleteBody(uploadId, fileMd5, fileSize) {
|
||||||
|
return {
|
||||||
|
uploadId: String(uploadId || ''),
|
||||||
|
fileMd5: String(fileMd5 || ''),
|
||||||
|
fileSize: Number(fileSize) || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBinaryString(buffer) {
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var chunkSize = 0x8000;
|
||||||
|
var chunks = [];
|
||||||
|
var index;
|
||||||
|
|
||||||
|
for (index = 0; index < bytes.length; index += chunkSize) {
|
||||||
|
chunks.push(String.fromCharCode.apply(null, bytes.subarray(index, index + chunkSize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashBlob(blob) {
|
||||||
|
if (!root.hex_md5 || !blob || !blob.arrayBuffer) {
|
||||||
|
throw new Error('缺少文件 MD5 工具');
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.hex_md5(toBinaryString(await blob.arrayBuffer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadResumable(api, file, options) {
|
||||||
|
var settings = options || {};
|
||||||
|
var chunkSize = Number(settings.chunkSize) || DEFAULT_CHUNK_SIZE;
|
||||||
|
var hash = settings.hashBlob || hashBlob;
|
||||||
|
var fileMd5;
|
||||||
|
var init;
|
||||||
|
var uploadId;
|
||||||
|
var totalChunks;
|
||||||
|
var index;
|
||||||
|
var start;
|
||||||
|
var end;
|
||||||
|
var chunk;
|
||||||
|
|
||||||
|
if (!api || !file || !file.slice) throw new Error('请选择可分片上传的文件');
|
||||||
|
|
||||||
|
fileMd5 = await hash(file);
|
||||||
|
init = await api.initResumableUpload(buildResumableInitBody(file, fileMd5, chunkSize, settings));
|
||||||
|
uploadId = pick(init, ['uploadId', 'id'], '');
|
||||||
|
if (!uploadId) throw new Error('分片上传初始化缺少 uploadId');
|
||||||
|
|
||||||
|
totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
for (index = 0; index < totalChunks; index += 1) {
|
||||||
|
start = index * chunkSize;
|
||||||
|
end = Math.min(start + chunkSize, file.size);
|
||||||
|
chunk = file.slice(start, end);
|
||||||
|
await api.uploadChunk({
|
||||||
|
uploadId: uploadId,
|
||||||
|
chunkIndex: index,
|
||||||
|
chunkMd5: await hash(chunk),
|
||||||
|
file: chunk
|
||||||
|
});
|
||||||
|
if (settings.onProgress) settings.onProgress(index + 1, totalChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.completeResumableUpload(buildResumableCompleteBody(uploadId, fileMd5, file.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFileForPage(api, file, options) {
|
||||||
|
var settings = options || {};
|
||||||
|
var mode = settings.mode === 'single' || settings.mode === 'resumable'
|
||||||
|
? settings.mode
|
||||||
|
: getUploadMode(file && file.size, settings.chunkSize);
|
||||||
|
|
||||||
|
if (mode === 'single') return api.uploadFile(file);
|
||||||
|
return uploadResumable(api, file, settings);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadFromInput(input) {
|
async function uploadFromInput(input) {
|
||||||
var api = getApi();
|
var api = getApi();
|
||||||
var file = input.files && input.files[0];
|
var file = input.files && input.files[0];
|
||||||
@@ -86,6 +189,8 @@
|
|||||||
var target = targetSelector && query(targetSelector);
|
var target = targetSelector && query(targetSelector);
|
||||||
var status = statusSelector && query(statusSelector);
|
var status = statusSelector && query(statusSelector);
|
||||||
var multiple = input.getAttribute('data-upload-multiple') === 'true';
|
var multiple = input.getAttribute('data-upload-multiple') === 'true';
|
||||||
|
var chunkSize = Number(input.getAttribute('data-upload-chunk-size')) || DEFAULT_CHUNK_SIZE;
|
||||||
|
var mode = input.getAttribute('data-upload-mode') || '';
|
||||||
var result;
|
var result;
|
||||||
|
|
||||||
if (!api || !file || !target) return;
|
if (!api || !file || !target) return;
|
||||||
@@ -94,7 +199,15 @@
|
|||||||
input.disabled = true;
|
input.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = normalizeUploadResult(await api.uploadFile(file));
|
result = normalizeUploadResult(await uploadFileForPage(api, file, {
|
||||||
|
mode: mode || getUploadMode(file.size, chunkSize),
|
||||||
|
chunkSize: chunkSize,
|
||||||
|
bizType: input.getAttribute('data-upload-biz-type') || undefined,
|
||||||
|
usageScene: input.getAttribute('data-upload-usage-scene') || undefined,
|
||||||
|
onProgress: function (current, total) {
|
||||||
|
if (status) status.textContent = '分片上传 ' + current + '/' + total;
|
||||||
|
}
|
||||||
|
}));
|
||||||
if (!result.ossId) throw new Error('上传结果缺少 OSS ID');
|
if (!result.ossId) throw new Error('上传结果缺少 OSS ID');
|
||||||
target.value = mergeOssIds(target.value, result.ossId, multiple);
|
target.value = mergeOssIds(target.value, result.ossId, multiple);
|
||||||
if (status) status.textContent = buildUploadStatus(result.fileName || file.name, result.ossId);
|
if (status) status.textContent = buildUploadStatus(result.fileName || file.name, result.ossId);
|
||||||
@@ -127,6 +240,11 @@
|
|||||||
normalizeUploadResult: normalizeUploadResult,
|
normalizeUploadResult: normalizeUploadResult,
|
||||||
mergeOssIds: mergeOssIds,
|
mergeOssIds: mergeOssIds,
|
||||||
buildUploadStatus: buildUploadStatus,
|
buildUploadStatus: buildUploadStatus,
|
||||||
|
getUploadMode: getUploadMode,
|
||||||
|
buildResumableInitBody: buildResumableInitBody,
|
||||||
|
buildResumableCompleteBody: buildResumableCompleteBody,
|
||||||
|
uploadResumable: uploadResumable,
|
||||||
|
uploadFileForPage: uploadFileForPage,
|
||||||
init: init
|
init: init
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,7 +109,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildVipOrderBody(values) {
|
function buildVipOrderBody(values) {
|
||||||
// VipOrderBody 来自 APP.openapi.json,packageId 必填,genealogyId 和 payType 可选。
|
|
||||||
var source = values || {};
|
var source = values || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function run() {
|
|||||||
assert.strictEqual(GenealogyApi.DEFAULT_BASE_URL, undefined);
|
assert.strictEqual(GenealogyApi.DEFAULT_BASE_URL, undefined);
|
||||||
|
|
||||||
const client = GenealogyApi.createClient({
|
const client = GenealogyApi.createClient({
|
||||||
baseUrl: 'https://test-genealogy-api.ddxcjp.cn',
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
clientId: 'client-x',
|
clientId: 'client-x',
|
||||||
tenantId: 'tenant-x',
|
tenantId: 'tenant-x',
|
||||||
tokenKey: 'token-key',
|
tokenKey: 'token-key',
|
||||||
@@ -136,7 +136,7 @@ async function run() {
|
|||||||
'token-key': 'existing-token'
|
'token-key': 'existing-token'
|
||||||
});
|
});
|
||||||
const failedRegisterClient = GenealogyApi.createClient({
|
const failedRegisterClient = GenealogyApi.createClient({
|
||||||
baseUrl: 'https://test-genealogy-api.ddxcjp.cn',
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
clientId: 'client-x',
|
clientId: 'client-x',
|
||||||
tenantId: 'tenant-x',
|
tenantId: 'tenant-x',
|
||||||
tokenKey: 'token-key',
|
tokenKey: 'token-key',
|
||||||
@@ -158,16 +158,76 @@ async function run() {
|
|||||||
);
|
);
|
||||||
assert.strictEqual(failedRegisterStore.getItem('token-key'), 'existing-token');
|
assert.strictEqual(failedRegisterStore.getItem('token-key'), 'existing-token');
|
||||||
|
|
||||||
|
const missingTokenStore = createStore();
|
||||||
|
const missingTokenClient = GenealogyApi.createClient({
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
clientId: 'client-x',
|
||||||
|
tenantId: 'tenant-x',
|
||||||
|
tokenKey: 'token-key',
|
||||||
|
tokenStore: missingTokenStore,
|
||||||
|
axiosInstance: {
|
||||||
|
request: async () => ({
|
||||||
|
status: 200,
|
||||||
|
data: { code: 200, data: {} }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
missingTokenClient.login({ phone: '13800000000', password: 'md5' }),
|
||||||
|
(error) => error.message === '登录响应缺少 token'
|
||||||
|
);
|
||||||
|
assert.strictEqual(missingTokenStore.getItem('token-key'), null);
|
||||||
|
|
||||||
|
const snakeTokenStore = createStore();
|
||||||
|
const snakeTokenClient = GenealogyApi.createClient({
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
clientId: 'client-x',
|
||||||
|
tenantId: 'tenant-x',
|
||||||
|
tokenKey: 'token-key',
|
||||||
|
tokenStore: snakeTokenStore,
|
||||||
|
axiosInstance: {
|
||||||
|
request: async () => ({
|
||||||
|
status: 200,
|
||||||
|
data: { code: 200, data: { access_token: 'snake-token' } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await snakeTokenClient.login({ phone: '13800000000', password: 'md5' });
|
||||||
|
assert.strictEqual(snakeTokenStore.getItem('token-key'), 'snake-token');
|
||||||
|
|
||||||
|
store.setItem('token-key', 'logout-token');
|
||||||
|
await client.logout();
|
||||||
|
assert.strictEqual(calls[9].url, '/genealogy/pc/auth/logout');
|
||||||
|
assert.strictEqual(store.getItem('token-key'), null);
|
||||||
|
|
||||||
|
const failedLogoutStore = createStore({
|
||||||
|
'token-key': 'logout-token'
|
||||||
|
});
|
||||||
|
const failedLogoutClient = GenealogyApi.createClient({
|
||||||
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
|
clientId: 'client-x',
|
||||||
|
tenantId: 'tenant-x',
|
||||||
|
tokenKey: 'token-key',
|
||||||
|
tokenStore: failedLogoutStore,
|
||||||
|
axiosInstance: {
|
||||||
|
request: async () => {
|
||||||
|
throw new Error('logout failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await assert.rejects(failedLogoutClient.logout(), /logout failed/);
|
||||||
|
assert.strictEqual(failedLogoutStore.getItem('token-key'), null);
|
||||||
|
|
||||||
await client.sendSmsCode({
|
await client.sendSmsCode({
|
||||||
sceneCode: 'WEB_H5_LOGIN',
|
sceneCode: 'WEB_H5_LOGIN',
|
||||||
phone: '13800000000',
|
phone: '13800000000',
|
||||||
validToken: 'captcha-ticket'
|
validToken: 'captcha-ticket'
|
||||||
});
|
});
|
||||||
assert.strictEqual(calls[9].url, '/genealogy/pc/auth/sms/code');
|
assert.strictEqual(calls[10].url, '/genealogy/pc/auth/sms/code');
|
||||||
assert.strictEqual(calls[9].headers.Authorization, undefined);
|
assert.strictEqual(calls[10].headers.Authorization, undefined);
|
||||||
assert.strictEqual(calls[9].data.grantType, 'sms');
|
assert.strictEqual(calls[10].data.grantType, 'sms');
|
||||||
assert.strictEqual(calls[9].data.sceneCode, 'WEB_H5_LOGIN');
|
assert.strictEqual(calls[10].data.sceneCode, 'WEB_H5_LOGIN');
|
||||||
assert.strictEqual(calls[9].data.validToken, 'captcha-ticket');
|
assert.strictEqual(calls[10].data.validToken, 'captcha-ticket');
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => client.listGenealogies(),
|
() => client.listGenealogies(),
|
||||||
@@ -177,7 +237,7 @@ async function run() {
|
|||||||
() => client.publicGenealogies(),
|
() => client.publicGenealogies(),
|
||||||
(error) => error.code === 'PC_API_NOT_AVAILABLE'
|
(error) => error.code === 'PC_API_NOT_AVAILABLE'
|
||||||
);
|
);
|
||||||
assert.strictEqual(calls.length, 10);
|
assert.strictEqual(calls.length, 11);
|
||||||
|
|
||||||
console.log('api-client tests passed');
|
console.log('api-client tests passed');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const developmentConfig = configFactory({
|
|||||||
|
|
||||||
assert.deepStrictEqual(developmentConfig.getConfig(), {
|
assert.deepStrictEqual(developmentConfig.getConfig(), {
|
||||||
environment: 'development',
|
environment: 'development',
|
||||||
apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
|
apiBaseUrl: 'http://182.61.18.23:8080'
|
||||||
});
|
});
|
||||||
assert.strictEqual(developmentConfig.getClientId, undefined);
|
assert.strictEqual(developmentConfig.getClientId, undefined);
|
||||||
assert.strictEqual(developmentConfig.getTenantId, undefined);
|
assert.strictEqual(developmentConfig.getTenantId, undefined);
|
||||||
@@ -25,6 +25,6 @@ const productionConfig = configFactory({
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(productionConfig.getEnvironment(), 'production');
|
assert.strictEqual(productionConfig.getEnvironment(), 'production');
|
||||||
assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://test-genealogy-api.ddxcjp.cn');
|
assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://182.61.18.23:8080');
|
||||||
|
|
||||||
console.log('config tests passed');
|
console.log('config tests passed');
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..');
|
||||||
|
const appContractPath = path.join(root, 'APP.openapi.json');
|
||||||
|
const obsoleteRecordPaths = [
|
||||||
|
'docs/api-page-integration-2026-07-09.md',
|
||||||
|
'docs/api-page-integration-tracker-2026-07-09.md'
|
||||||
|
];
|
||||||
|
const forbiddenPatterns = [
|
||||||
|
/\/genealogy\/app/,
|
||||||
|
/\bAPP_(?:LOGIN|REGISTER|PHONE_CHANGE|FORGOT_PASSWORD|SMS_CODE|ACCOUNT_DEACTIVATE)\b/,
|
||||||
|
/APP\.openapi\.json/
|
||||||
|
];
|
||||||
|
const checkedFiles = [
|
||||||
|
'utils/ApiClient.js',
|
||||||
|
'public/js/auth-pages.js',
|
||||||
|
'public/js/captcha-pages.js',
|
||||||
|
'login.html',
|
||||||
|
'register.html',
|
||||||
|
'forgot-password.html',
|
||||||
|
'app.html',
|
||||||
|
'download.html',
|
||||||
|
'notice-detail.html',
|
||||||
|
'profile-album.html',
|
||||||
|
'profile-data.html',
|
||||||
|
'profile-gift.html',
|
||||||
|
'profile-security.html',
|
||||||
|
'profile-services.html',
|
||||||
|
'profile-tree.html',
|
||||||
|
'genealogy-pc-openapi.yaml',
|
||||||
|
'docs/pc-api-page-integration-tracker-2026-07-09.md',
|
||||||
|
'docs/superpowers/plans/2026-07-09-pc-api-replan.md',
|
||||||
|
'docs/superpowers/plans/2026-07-09-pc-auth-three-pages.md',
|
||||||
|
'docs/superpowers/plans/2026-07-09-tac-captcha-auth.md',
|
||||||
|
'docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md',
|
||||||
|
'public/js/album-pages.js',
|
||||||
|
'public/js/article-pages.js',
|
||||||
|
'public/js/ceremony-pages.js',
|
||||||
|
'public/js/feed-pages.js',
|
||||||
|
'public/js/genealogy-pages.js',
|
||||||
|
'public/js/growth-pages.js',
|
||||||
|
'public/js/lineage-pages.js',
|
||||||
|
'public/js/member-admin-pages.js',
|
||||||
|
'public/js/memo-pages.js',
|
||||||
|
'public/js/vip-pages.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
function read(relativePath) {
|
||||||
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const apiClient = read('utils/ApiClient.js');
|
||||||
|
const authPages = read('public/js/auth-pages.js');
|
||||||
|
|
||||||
|
assert.strictEqual(fs.existsSync(appContractPath), false);
|
||||||
|
obsoleteRecordPaths.forEach((relativePath) => {
|
||||||
|
assert.strictEqual(fs.existsSync(path.join(root, relativePath)), false, `${relativePath} must be removed`);
|
||||||
|
});
|
||||||
|
assert.match(apiClient, /\/genealogy\/pc\/auth\/login\/sms/);
|
||||||
|
assert.match(authPages, /captchaScene: 'WEB_H5_LOGIN'/);
|
||||||
|
|
||||||
|
checkedFiles.forEach((relativePath) => {
|
||||||
|
const content = read(relativePath);
|
||||||
|
|
||||||
|
forbiddenPatterns.forEach((pattern) => {
|
||||||
|
assert.doesNotMatch(content, pattern, `${relativePath} must not retain APP API contracts`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('pc-api-contract tests passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..');
|
||||||
|
const openApi = fs.readFileSync(path.join(root, 'genealogy-pc-openapi.yaml'), 'utf8');
|
||||||
|
const apiClient = fs.readFileSync(path.join(root, 'utils', 'ApiClient.js'), 'utf8');
|
||||||
|
const authPages = fs.readFileSync(path.join(root, 'public', 'js', 'auth-pages.js'), 'utf8');
|
||||||
|
const captchaPages = fs.readFileSync(path.join(root, 'public', 'js', 'captcha-pages.js'), 'utf8');
|
||||||
|
const profilePages = fs.readFileSync(path.join(root, 'public', 'js', 'profile-pages.js'), 'utf8');
|
||||||
|
const securityPages = fs.readFileSync(path.join(root, 'public', 'js', 'security-pages.js'), 'utf8');
|
||||||
|
const regionPages = fs.readFileSync(path.join(root, 'public', 'js', 'region-pages.js'), 'utf8');
|
||||||
|
const uploadPages = fs.readFileSync(path.join(root, 'public', 'js', 'upload-pages.js'), 'utf8');
|
||||||
|
|
||||||
|
const ENDPOINTS = {
|
||||||
|
'GET /captcha/require': { source: captchaPages, needle: 'api.captchaRequire', status: 'complete' },
|
||||||
|
'POST /captcha/challenge': { source: captchaPages, needle: "api.buildApiUrl('/captcha/challenge')", status: 'complete' },
|
||||||
|
'POST /captcha/verify': { source: captchaPages, needle: "api.buildApiUrl('/captcha/verify')", status: 'complete' },
|
||||||
|
'GET /auth/code': { status: 'blocked', reason: 'legacy captcha has no PC request field' },
|
||||||
|
'POST /genealogy/pc/auth/register': { source: authPages, needle: 'api.register', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/auth/login': { source: authPages, needle: 'api.login', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/auth/login/sms': { source: authPages, needle: 'api.loginBySms', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/auth/sms/code': { source: authPages, needle: 'api.sendSmsCode', status: 'complete' },
|
||||||
|
'GET /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.currentProfile', status: 'complete' },
|
||||||
|
'PUT /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.updateProfile', status: 'complete' },
|
||||||
|
'PUT /genealogy/pc/auth/password': { source: securityPages, needle: 'api.changePassword', status: 'complete' },
|
||||||
|
'PUT /genealogy/pc/auth/password/reset': { source: authPages, needle: 'api.resetPassword', status: 'complete' },
|
||||||
|
'PUT /genealogy/pc/auth/phone': { status: 'blocked', reason: 'PC/H5 phone SMS scene is not documented' },
|
||||||
|
'POST /genealogy/pc/auth/account/deactivate': { source: securityPages, needle: 'api.deactivateAccount', status: 'complete' },
|
||||||
|
'DELETE /genealogy/pc/auth/logout': { source: securityPages, needle: 'api.logout', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/files/upload': { source: uploadPages, needle: 'api.uploadFile', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/files/resumable/init': { source: uploadPages, needle: 'api.initResumableUpload', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/files/resumable/chunk': { source: uploadPages, needle: 'api.uploadChunk', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/files/resumable/complete': { source: uploadPages, needle: 'api.completeResumableUpload', status: 'complete' },
|
||||||
|
'POST /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
|
||||||
|
'DELETE /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
|
||||||
|
'GET /genealogy/region/children': { source: regionPages, needle: 'api.regionChildren', status: 'complete' },
|
||||||
|
'GET /genealogy/region/path/{regionCode}': { source: regionPages, needle: 'api.regionPath', apiNeedle: '/genealogy/region/path/', status: 'complete' },
|
||||||
|
'GET /genealogy/region/search': { source: regionPages, needle: 'api.regionSearch', status: 'complete' },
|
||||||
|
'GET /genealogy/region/{regionCode}': { source: regionPages, needle: 'api.regionDetail', apiNeedle: '/genealogy/region/', status: 'complete' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDocumentOperations(documentText) {
|
||||||
|
let currentPath = '';
|
||||||
|
const operations = [];
|
||||||
|
|
||||||
|
documentText.split(/\r?\n/).forEach((line) => {
|
||||||
|
const pathMatch = line.match(/^ (\/[^:]+):$/);
|
||||||
|
const methodMatch = line.match(/^ (get|post|put|delete):$/);
|
||||||
|
|
||||||
|
if (pathMatch) currentPath = pathMatch[1];
|
||||||
|
if (methodMatch && currentPath) operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const operations = getDocumentOperations(openApi);
|
||||||
|
|
||||||
|
assert.strictEqual(operations.length, 25);
|
||||||
|
assert.deepStrictEqual(Object.keys(ENDPOINTS).sort(), operations.slice().sort());
|
||||||
|
|
||||||
|
operations.forEach((operation) => {
|
||||||
|
const descriptor = ENDPOINTS[operation];
|
||||||
|
const route = operation.slice(operation.indexOf(' ') + 1);
|
||||||
|
|
||||||
|
assert.match(apiClient, new RegExp((descriptor.apiNeedle || route).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||||
|
assert.ok(descriptor, `${operation} must have an owner`);
|
||||||
|
|
||||||
|
if (descriptor.status === 'blocked') {
|
||||||
|
assert.ok(descriptor.reason, `${operation} must name its backend block`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(descriptor.source.includes(descriptor.needle), `${operation} must be used by its page module`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('pc-endpoint-coverage tests passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const profileHtml = fs.readFileSync(path.join(__dirname, '..', 'profile.html'), 'utf8');
|
||||||
|
const profileCommon = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'profile-common.js'), 'utf8');
|
||||||
|
|
||||||
|
assert.match(profileHtml, /data-logout-confirm/);
|
||||||
|
assert.match(profileHtml, /<a class="btn primary magnetic" href="index\.html" data-logout-confirm>/);
|
||||||
|
assert.match(profileHtml, /<span>退出登录<\/span><b>返回首页<\/b>/);
|
||||||
|
assert.match(profileCommon, /function performLogout\(targetUrl\)/);
|
||||||
|
assert.match(profileCommon, /api\.logout\(\)/);
|
||||||
|
assert.match(profileCommon, /targetUrl \|\| 'index\.html'/);
|
||||||
|
|
||||||
|
console.log('profile logout structure tests passed');
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const ProfilePages = require('../public/js/profile-pages.js');
|
const ProfilePages = require('../public/js/profile-pages.js');
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
@@ -61,6 +63,32 @@ function run() {
|
|||||||
districtCode: undefined
|
districtCode: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => ''
|
||||||
|
}), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}), false);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { status: 401 }), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { code: 401 }), true);
|
||||||
|
assert.strictEqual(ProfilePages.shouldRedirectToHome({
|
||||||
|
getToken: () => 'token-x'
|
||||||
|
}, { status: 500 }), false);
|
||||||
|
|
||||||
|
const profileData = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
|
||||||
|
assert.match(profileData, /data-upload-target="#profileAvatarOssId"/);
|
||||||
|
assert.match(profileData, /data-upload-mode="auto"/);
|
||||||
|
assert.match(profileData, /data-upload-biz-type="avatar"/);
|
||||||
|
assert.match(profileData, /<script src="public\/js\/md5\.js"><\/script>/);
|
||||||
|
assert.match(profileData, /<script src="public\/js\/upload-pages\.js"><\/script>/);
|
||||||
|
assert.match(profileData, /data-region-search-form/);
|
||||||
|
assert.match(profileData, /data-region-search-results/);
|
||||||
|
assert.match(profileData, /data-region-search-path/);
|
||||||
|
|
||||||
console.log('profile-pages tests passed');
|
console.log('profile-pages tests passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ function run() {
|
|||||||
districtCode: undefined
|
districtCode: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(RegionPages.buildRegionSelection([
|
||||||
|
{ regionCode: '510000', regionName: '四川省', level: 1 },
|
||||||
|
{ regionCode: '510100', regionName: '成都市', level: 2 },
|
||||||
|
{ regionCode: '510104', regionName: '锦江区', level: 3 }
|
||||||
|
]), {
|
||||||
|
provinceCode: '510000',
|
||||||
|
cityCode: '510100',
|
||||||
|
districtCode: '510104'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(RegionPages.buildRegionSelection([
|
||||||
|
{ regionCode: '110000' },
|
||||||
|
{ regionCode: '110100' },
|
||||||
|
{ regionCode: '110105' }
|
||||||
|
]), {
|
||||||
|
provinceCode: '110000',
|
||||||
|
cityCode: '110100',
|
||||||
|
districtCode: '110105'
|
||||||
|
});
|
||||||
|
|
||||||
console.log('region-pages tests passed');
|
console.log('region-pages tests passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-11
@@ -1,11 +1,58 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const vm = require('vm');
|
||||||
const SecurityPages = require('../public/js/security-pages.js');
|
const SecurityPages = require('../public/js/security-pages.js');
|
||||||
|
|
||||||
|
const securityPagePath = path.join(__dirname, '..', 'profile-security.html');
|
||||||
|
const securityPagesPath = path.join(__dirname, '..', 'public', 'js', 'security-pages.js');
|
||||||
|
|
||||||
function hash(value) {
|
function hash(value) {
|
||||||
return `hashed:${value}`;
|
return `hashed:${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function loadSecurityPages(root) {
|
||||||
|
const source = fs.readFileSync(securityPagesPath, 'utf8');
|
||||||
|
const sandbox = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
globalThis: root,
|
||||||
|
window: root
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(source, sandbox, { filename: securityPagesPath });
|
||||||
|
return sandbox.module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageRoot(api) {
|
||||||
|
const handlers = {};
|
||||||
|
const document = {
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
handlers[type] = handler;
|
||||||
|
},
|
||||||
|
querySelector(selector) {
|
||||||
|
return selector === '[data-security-page]' ? {} : null;
|
||||||
|
},
|
||||||
|
querySelectorAll() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
document,
|
||||||
|
handlers,
|
||||||
|
GenealogyApi: { defaultClient: api },
|
||||||
|
confirm: () => true,
|
||||||
|
alert: () => {},
|
||||||
|
location: { href: '' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForAsyncAction() {
|
||||||
|
return new Promise((resolve) => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
assert.deepStrictEqual(SecurityPages.buildPasswordChangeBody({
|
assert.deepStrictEqual(SecurityPages.buildPasswordChangeBody({
|
||||||
oldPassword: '123456',
|
oldPassword: '123456',
|
||||||
newPassword: 'abcdef'
|
newPassword: 'abcdef'
|
||||||
@@ -24,15 +71,8 @@ function run() {
|
|||||||
validToken: 'ticket'
|
validToken: 'ticket'
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(SecurityPages.buildSmsLoginBody({
|
assert.strictEqual(SecurityPages.buildSmsLoginBody, undefined);
|
||||||
phone: '13900000000',
|
assert.doesNotMatch(fs.readFileSync(securityPagePath, 'utf8'), /data-security-form="sms-login"/);
|
||||||
smsCode: '123456',
|
|
||||||
validToken: ''
|
|
||||||
}), {
|
|
||||||
phone: '13900000000',
|
|
||||||
smsCode: '123456',
|
|
||||||
validToken: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepStrictEqual(SecurityPages.buildDeactivateBody({
|
assert.deepStrictEqual(SecurityPages.buildDeactivateBody({
|
||||||
password: '123456',
|
password: '123456',
|
||||||
@@ -45,7 +85,62 @@ function run() {
|
|||||||
assert.strictEqual(SecurityPages.isDangerConfirmed('确认注销'), true);
|
assert.strictEqual(SecurityPages.isDangerConfirmed('确认注销'), true);
|
||||||
assert.strictEqual(SecurityPages.isDangerConfirmed('注销'), false);
|
assert.strictEqual(SecurityPages.isDangerConfirmed('注销'), false);
|
||||||
|
|
||||||
|
const logoutRoot = createPageRoot({
|
||||||
|
logout() {
|
||||||
|
return Promise.reject(new Error('network'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const LogoutPages = loadSecurityPages(logoutRoot);
|
||||||
|
LogoutPages.init();
|
||||||
|
logoutRoot.handlers.click({
|
||||||
|
target: {
|
||||||
|
closest(selector) {
|
||||||
|
return selector === '[data-security-logout]' ? this : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preventDefault() {}
|
||||||
|
});
|
||||||
|
await waitForAsyncAction();
|
||||||
|
assert.strictEqual(logoutRoot.location.href, 'index.html');
|
||||||
|
|
||||||
|
const deactivateRoot = createPageRoot({
|
||||||
|
deactivateAccount() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const DeactivatePages = loadSecurityPages(deactivateRoot);
|
||||||
|
const deactivateForm = {
|
||||||
|
getAttribute(name) {
|
||||||
|
return name === 'data-security-form' ? 'deactivate' : null;
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector !== '[name]') return [];
|
||||||
|
return [
|
||||||
|
{ name: 'password', value: '123456' },
|
||||||
|
{ name: 'reason', value: '' },
|
||||||
|
{ name: 'confirmText', value: '确认注销' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
querySelector() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
DeactivatePages.init();
|
||||||
|
deactivateRoot.handlers.submit({
|
||||||
|
target: {
|
||||||
|
closest(selector) {
|
||||||
|
return selector === '[data-security-form]' ? deactivateForm : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preventDefault() {}
|
||||||
|
});
|
||||||
|
await waitForAsyncAction();
|
||||||
|
assert.strictEqual(deactivateRoot.location.href, 'index.html');
|
||||||
|
|
||||||
console.log('security-pages tests passed');
|
console.log('security-pages tests passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|||||||
+114
-2
@@ -1,7 +1,7 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const UploadPages = require('../public/js/upload-pages.js');
|
const UploadPages = require('../public/js/upload-pages.js');
|
||||||
|
|
||||||
function run() {
|
async function run() {
|
||||||
assert.deepStrictEqual(UploadPages.normalizeUploadResult({
|
assert.deepStrictEqual(UploadPages.normalizeUploadResult({
|
||||||
ossId: 2060001,
|
ossId: 2060001,
|
||||||
fileName: 'cover.jpg',
|
fileName: 'cover.jpg',
|
||||||
@@ -27,7 +27,119 @@ function run() {
|
|||||||
assert.strictEqual(UploadPages.mergeOssIds('2060001', '2060001', true), '2060001');
|
assert.strictEqual(UploadPages.mergeOssIds('2060001', '2060001', true), '2060001');
|
||||||
assert.strictEqual(UploadPages.buildUploadStatus('cover.jpg', '2060001'), 'cover.jpg 上传完成,OSS ID:2060001');
|
assert.strictEqual(UploadPages.buildUploadStatus('cover.jpg', '2060001'), 'cover.jpg 上传完成,OSS ID:2060001');
|
||||||
|
|
||||||
|
assert.strictEqual(UploadPages.getUploadMode(4194304), 'single');
|
||||||
|
assert.strictEqual(UploadPages.getUploadMode(4194305), 'resumable');
|
||||||
|
assert.deepStrictEqual(UploadPages.buildResumableInitBody({
|
||||||
|
name: 'avatar.jpg',
|
||||||
|
size: 5242880,
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}, 'file-md5', 4194304, {
|
||||||
|
bizType: 'avatar',
|
||||||
|
usageScene: 'profile_avatar'
|
||||||
|
}), {
|
||||||
|
fileName: 'avatar.jpg',
|
||||||
|
fileSize: 5242880,
|
||||||
|
fileMd5: 'file-md5',
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
chunkSize: 4194304,
|
||||||
|
totalChunks: 2,
|
||||||
|
bizType: 'avatar',
|
||||||
|
usageScene: 'profile_avatar'
|
||||||
|
});
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
const file = {
|
||||||
|
name: 'avatar.jpg',
|
||||||
|
size: 8,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
slice(start, end) {
|
||||||
|
return { start, end, size: end - start };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
initResumableUpload(body) {
|
||||||
|
calls.push({ type: 'init', body });
|
||||||
|
return Promise.resolve({ uploadId: 'upload-1' });
|
||||||
|
},
|
||||||
|
uploadChunk(body) {
|
||||||
|
calls.push({ type: 'chunk', body });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
completeResumableUpload(body) {
|
||||||
|
calls.push({ type: 'complete', body });
|
||||||
|
return Promise.resolve({ ossId: 2060001 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = await UploadPages.uploadResumable(api, file, {
|
||||||
|
chunkSize: 4,
|
||||||
|
bizType: 'avatar',
|
||||||
|
usageScene: 'profile_avatar',
|
||||||
|
hashBlob(blob) {
|
||||||
|
return Promise.resolve(blob === file ? 'file-md5' : `chunk-${blob.start}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, { ossId: 2060001 });
|
||||||
|
assert.deepStrictEqual(calls, [
|
||||||
|
{
|
||||||
|
type: 'init',
|
||||||
|
body: {
|
||||||
|
fileName: 'avatar.jpg',
|
||||||
|
fileSize: 8,
|
||||||
|
fileMd5: 'file-md5',
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
chunkSize: 4,
|
||||||
|
totalChunks: 2,
|
||||||
|
bizType: 'avatar',
|
||||||
|
usageScene: 'profile_avatar'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'chunk',
|
||||||
|
body: {
|
||||||
|
uploadId: 'upload-1',
|
||||||
|
chunkIndex: 0,
|
||||||
|
chunkMd5: 'chunk-0',
|
||||||
|
file: { start: 0, end: 4, size: 4 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'chunk',
|
||||||
|
body: {
|
||||||
|
uploadId: 'upload-1',
|
||||||
|
chunkIndex: 1,
|
||||||
|
chunkMd5: 'chunk-4',
|
||||||
|
file: { start: 4, end: 8, size: 4 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'complete',
|
||||||
|
body: {
|
||||||
|
uploadId: 'upload-1',
|
||||||
|
fileMd5: 'file-md5',
|
||||||
|
fileSize: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let singleUploadCalled = false;
|
||||||
|
const singleResult = await UploadPages.uploadFileForPage({
|
||||||
|
uploadFile(value) {
|
||||||
|
singleUploadCalled = value.size === 4;
|
||||||
|
return Promise.resolve({ ossId: 2060002 });
|
||||||
|
}
|
||||||
|
}, { size: 4 }, {
|
||||||
|
mode: 'auto',
|
||||||
|
chunkSize: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(singleUploadCalled, true);
|
||||||
|
assert.deepStrictEqual(singleResult, { ossId: 2060002 });
|
||||||
|
|
||||||
console.log('upload-pages tests passed');
|
console.log('upload-pages tests passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ assert.strictEqual(StorageUtil.read(storage, 'token'), null);
|
|||||||
|
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const requester = AxiosRequestUtil.createRequester({
|
const requester = AxiosRequestUtil.createRequester({
|
||||||
baseUrl: 'https://test-genealogy-api.ddxcjp.cn',
|
baseUrl: 'http://182.61.18.23:8080',
|
||||||
clientId: 'pc-client',
|
clientId: 'pc-client',
|
||||||
tokenHeaderName: 'Authorization',
|
tokenHeaderName: 'Authorization',
|
||||||
getToken: () => 'token-x',
|
getToken: () => 'token-x',
|
||||||
|
|||||||
+14
-5
@@ -52,7 +52,14 @@
|
|||||||
function getTokenFromResponse(data) {
|
function getTokenFromResponse(data) {
|
||||||
// 登录响应兼容后端已声明的多种 token 字段。
|
// 登录响应兼容后端已声明的多种 token 字段。
|
||||||
if (!data) return '';
|
if (!data) return '';
|
||||||
return data.token || data.accessToken || data.tokenValue || '';
|
return data.token || data.accessToken || data.access_token || data.tokenValue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireLoginToken(data) {
|
||||||
|
var token = getTokenFromResponse(data);
|
||||||
|
|
||||||
|
if (!token) throw new ApiError('登录响应缺少 token');
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApiError(message, options) {
|
function ApiError(message, options) {
|
||||||
@@ -169,7 +176,7 @@
|
|||||||
auth: false,
|
auth: false,
|
||||||
body: Object.assign({ grantType: 'password' }, withTenant(body))
|
body: Object.assign({ grantType: 'password' }, withTenant(body))
|
||||||
});
|
});
|
||||||
setToken(getTokenFromResponse(data));
|
setToken(requireLoginToken(data));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +194,7 @@
|
|||||||
auth: false,
|
auth: false,
|
||||||
body: Object.assign({ grantType: 'sms' }, withTenant(body))
|
body: Object.assign({ grantType: 'sms' }, withTenant(body))
|
||||||
});
|
});
|
||||||
setToken(getTokenFromResponse(data));
|
setToken(requireLoginToken(data));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,9 +274,11 @@
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
logout: async function () {
|
logout: async function () {
|
||||||
var data = await request('DELETE', '/genealogy/pc/auth/logout');
|
try {
|
||||||
|
return await request('DELETE', '/genealogy/pc/auth/logout');
|
||||||
|
} finally {
|
||||||
clearToken();
|
clearToken();
|
||||||
return data;
|
}
|
||||||
},
|
},
|
||||||
uploadFile: function (fileOrFormData) {
|
uploadFile: function (fileOrFormData) {
|
||||||
return request('POST', '/genealogy/pc/files/upload', { body: createFormData(fileOrFormData) });
|
return request('POST', '/genealogy/pc/files/upload', { body: createFormData(fileOrFormData) });
|
||||||
|
|||||||
Reference in New Issue
Block a user