修改底层请求逻辑,封装请求方法

This commit is contained in:
rain
2026-07-10 14:11:10 +08:00
parent 6050508144
commit 6129a9221a
66 changed files with 5861 additions and 451 deletions
+5 -1
View File
@@ -143,7 +143,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/promotion-pages.js"></script> <script src="public/js/promotion-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -102,7 +102,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/article-pages.js"></script> <script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+21 -49
View File
@@ -1,5 +1,5 @@
(function (root, factory) { (function (root, factory) {
// 全局配置同时支持浏览器页面和 Node 测试 // 根配置只负责运行环境与接口基础地址
if (typeof module === 'object' && module.exports) { if (typeof module === 'object' && module.exports) {
module.exports = factory; module.exports = factory;
return; return;
@@ -9,69 +9,41 @@
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict'; 'use strict';
var DEFAULT_ENV = 'development'; var ENVIRONMENTS = {
var API_BASE_URLS = { development: {
development: 'http://test-genealogy-api.ddxcjp.cn', apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
production: '' },
}; production: {
var TEST_API_HTTP_URL = 'http://test-genealogy-api.ddxcjp.cn'; // 后端尚未提供正式生产域名,暂时保持用户指定的测试地址。
var TEST_API_HTTPS_URL = 'https://test-genealogy-api.ddxcjp.cn'; apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
var CLIENT_ID = 'ced7e5f0498645c6ec642dcf450b036f';
var TENANT_ID = '000000';
var TOKEN_KEY = 'genealogy_auth_token';
var TOKEN_HEADER_NAME = 'Authorization';
var ENV_KEY = 'genealogy_env';
var BASE_URL_KEY = 'genealogy_api_base_url';
function readStorage(key) {
try {
return root.localStorage && root.localStorage.getItem(key);
} catch (error) {
return null;
} }
} };
function getEnv() { function getEnvironment() {
return readStorage(ENV_KEY) || DEFAULT_ENV; var location = root.location || {};
} var hostname = location.hostname || '';
function normalizeApiBaseUrl(value) { if (hostname === 'localhost' || hostname === '127.0.0.1' || location.port) {
// 测试域名当前 HTTPS 握手失败,开发环境统一走可访问的 HTTP 地址。 return 'development';
var url = String(value || '').replace(/\/+$/, ''); }
if (url === TEST_API_HTTPS_URL) return TEST_API_HTTP_URL; return 'production';
return url;
} }
function getApiBaseUrl() { function getApiBaseUrl() {
return normalizeApiBaseUrl(readStorage(BASE_URL_KEY) || API_BASE_URLS[getEnv()] || API_BASE_URLS.development); return ENVIRONMENTS[getEnvironment()].apiBaseUrl;
} }
function getConfig() { function getConfig() {
return { return {
env: getEnv(), environment: getEnvironment(),
apiBaseUrl: getApiBaseUrl(), apiBaseUrl: getApiBaseUrl()
clientId: CLIENT_ID,
tenantId: TENANT_ID,
tokenKey: TOKEN_KEY,
tokenHeaderName: TOKEN_HEADER_NAME
}; };
} }
return { return {
getConfig: getConfig, getEnvironment: getEnvironment,
getApiBaseUrl: getApiBaseUrl, getApiBaseUrl: getApiBaseUrl,
getClientId: function () { getConfig: getConfig
return CLIENT_ID;
},
getTenantId: function () {
return TENANT_ID;
},
getTokenKey: function () {
return TOKEN_KEY;
},
getTokenHeaderName: function () {
return TOKEN_HEADER_NAME;
}
}; };
}); });
+3 -2
View File
@@ -127,8 +127,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/region-pages.js"></script> <script src="public/js/region-pages.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
@@ -57,18 +57,18 @@
| 编号 | 状态 | 页面范围 | 接口范围 | 计划与验证 | | 编号 | 状态 | 页面范围 | 接口范围 | 计划与验证 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| PC-000 | 已完成 | 全项目 | `genealogy-pc-openapi.yaml``https://test-genealogy-api.ddxcjp.cn` | 2026-07-09 15:14:58 +08:00 完成。确认旧 APP 接口依据错误,停止继续按 APP 路径推进;确认 PC YAML 当前只明确认证、文件地区接口。 | | PC-000 | 复核通过 | 全项目 | `genealogy-pc-openapi.yaml` | 历史记录保留;当前 PC YAML `paths` 再次核对通过,真实接口范围仍只有验证码、PC 认证、PC 文件地区。 |
| PC-001 | 已完成 | 全项目配置层 | `config.js``https://test-genealogy-api.ddxcjp.cn` | 2026-07-09 15:57:36 +08:00 修正完成。根目录 `config.js` 作为开发/生产环境切换的唯一入口;开发环境基础地址为 `https://test-genealogy-api.ddxcjp.cn`PC `clientid` 按用户最新截图改为 `ced7e5f0498645c6ec642dcf450b036f`,客户端 Key 为 `web_pc`。验证:`node tests\config.test.js` 输出 `config tests passed``node --check config.js` 通过。 | | PC-001 | 复核通过 | 根目录 `config.js` | 开发/生产环境、API 基础地址 | 当前 `config.js` 已收敛为环境和基础地址唯一入口;`node tests\config.test.js``node --check config.js` 通过。 |
| PC-002 | 已完成 | `utils` 公共工具层`public/js/api-client.js` | PC `clientid`、通用请求、token、表单、存储、PC YAML paths | 2026-07-09 15:51:34 +08:00 完成。新增 `utils/StorageUtil.js``utils/FormUtil.js``utils/RequestUtil.js`;重写 `public/js/api-client.js`,真实请求只保留 PC YAML 中的 `/captcha/*``/auth/code``/genealogy/pc/auth/*``/genealogy/pc/files/*``/genealogy/region/*`,PC YAML 未覆盖业务方法改为抛 `PC_API_NOT_AVAILABLE`,不再发旧 APP 请求。验证:`node tests\utils.test.js``node tests\api-client.test.js` 均通过;`Select-String -Path public\js\api-client.js -Pattern '/genealogy/app/'` 无输出;`node --check public\js\api-client.js` 通过。 | | PC-002 | 复核通过 | `utils` 公共工具层 | Axios、请求封装、PC 接口客户端 | 当前公共层为 `utils/axios.js``utils/AxiosRequestUtil.js``utils/ApiClient.js`;旧 fetch 工具和旧 public API 客户端已删除。 |
| PC-003 | 已完成 | `login.html``register.html``forgot-password.html``profile-security.html``profile-data.html``profile.html` | `/genealogy/pc/auth/*``/captcha/*` | 2026-07-09 16:00:22 +08:00 完成。`login.html` 使用 `WEB_H5_LOGIN``register.html` 使用 `WEB_H5_REGISTER``forgot-password.html` 使用 `WEB_H5_FORGOT_PASSWORD`认证/资料/安全页面均已补齐 `config.js``utils/*``api-client.js` 脚本顺序。资料提交字段按 PC YAML 使用 `provinceCode/cityCode/districtCode`。验证:`node tests\auth-pages.test.js``node tests\captcha-pages.test.js``node tests\profile-pages.test.js``node tests\security-pages.test.js` 均通过;相关 JS `node --check` 通过;旧 `PC_LOGIN/PC_REGISTER/PC_PASSWORD_RESET/APP_*` 场景扫描无输出。 | | PC-003 | 本地复核通过 | 认证、资料、安全页面 | `/genealogy/pc/auth/*``/captcha/*` | 认证资料安全测试和脚本顺序检查均通过;真实登录业务仍依赖后端账户数据。 |
| PC-004 | 已完成 | 文件上传相关页面 | `/genealogy/pc/files/*` | 2026-07-09 16:03:23 +08:00 完成。`api-client.js` 文件上传、分片上传、文件引用绑定均走 `/genealogy/pc/files/*``profile-album.html``profile-article-edit.html``profile-feed-edit.html``profile-growth-edit.html``profile-memo-edit.html``profile-gift-edit.html` 已补齐 `config.js``utils/*``api-client.js``upload-pages.js` 顺序。验证:`node tests\upload-pages.test.js``node tests\api-client.test.js` 通过;`Select-String -Path public\js\api-client.js,public\js\upload-pages.js -Pattern '/genealogy/app/files'` 无输出。 | | PC-004 | 本地复核通过 | 文件上传页面 | `/genealogy/pc/files/*` | `node tests\upload-pages.test.js``node tests\api-client.test.js` 通过;文件上传仍需真实文件和后端授权进行端到端验证。 |
| PC-005 | 已完成 | 使用地区选择页面 | `/genealogy/region/*` | 2026-07-09 16:05:12 +08:00 完成。`create-genealogy.html``profile-create-family.html``profile-data.html` 地区选择页面已补齐 `config.js``utils/*``api-client.js``region-pages.js` 顺序;地区请求只走 `/genealogy/region/children``/genealogy/region/path/{regionCode}``/genealogy/region/search``/genealogy/region/{regionCode}`。验证:`node tests\region-pages.test.js``node tests\profile-pages.test.js` 通过`node --check public\js\region-pages.js` 通过;地区旧 APP 路径扫描无输出。 | | PC-005 | 本地复核通过 | 地区选择页面 | `/genealogy/region/*` | `node tests\region-pages.test.js``node tests\profile-pages.test.js` 通过。 |
| PC-006 | 已修正 | 全项目页面覆盖矩阵 | PC YAML 现有接口覆盖范围与缺口 | 2026-07-09 15:34:26 +08:00 完成修正。该项只作为文档清单任务,不做业务代码。输出页面级覆盖矩阵:完整可闭环、接口存在但验证码待确认、只能保留公共能力、文件能力可切换但主业务待确认、PC YAML 未覆盖。避免把“页面里某个公共接口可用”误标为“整页已可对接”。 | | PC-006 | 复核通过 | 页面覆盖矩阵 | PC YAML 覆盖边界 | 保持“公共能力不等于整页业务已对接”的边界,未覆盖业务继续受控失败。 |
| PC-007 | 已完成 | 详细实施规划 | `genealogy-pc-openapi.yaml` `paths``docs/superpowers/plans/2026-07-09-pc-api-replan.md` | 2026-07-09 15:41:40 +08:00 完成修正。重写详细实施计划:只以 YAML `paths` 为真实接口来源;`api-client.js` 不能保留 APP 路径 fallback;PC YAML 未覆盖页面改为受控“接口待确认/PC_API_NOT_AVAILABLE”;补充资料字段 `provinceCode/cityCode/districtCode`、TAC 登录场景 `WEB_H5_LOGIN`、文件/地区局部能力边界。 | | PC-007 | 复核通过 | 实施规划 | `genealogy-pc-openapi.yaml` | 当前计划与 YAML `paths` 一致,不使用旧 APP 路径 fallback。 |
| PC-008 | 已完成 | PC YAML 未覆盖业务页面 | `PC_API_NOT_AVAILABLE`、旧 APP 网络路径清理 | 2026-07-09 16:06:35 +08:00 完成。`api-client.js` 中家谱主体、成员、字辈、世系、动态、文章、相册、祭祀、族务、VIP、通知、反馈、帮助、推广等 PC YAML 未覆盖业务方法保留方法名但统一抛 `PC_API_NOT_AVAILABLE`不再发 `/genealogy/app/` 请求。验证:`Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }` 全部通过;`Select-String -Path public\js\api-client.js -Pattern '/genealogy/app/'` 无输出。 | | PC-008 | 复核通过 | PC YAML 未覆盖业务页面 | `PC_API_NOT_AVAILABLE` | `utils/ApiClient.js` 统一抛 `PC_API_NOT_AVAILABLE` `/genealogy/app/` 请求路径扫描无结果。 |
| PC-009 | 已完成 | `login.html``register.html``forgot-password.html` | `/genealogy/pc/auth/login``/genealogy/pc/auth/login/sms``/genealogy/pc/auth/register``/genealogy/pc/auth/sms/code``/genealogy/pc/auth/password/reset``/captcha/*` | 2026-07-09 16:32:17 +08:00 开始,2026-07-09 16:40:02 +08:00 完成。专项计划见 `docs/superpowers/plans/2026-07-09-pc-auth-three-pages.md``login.html` 已补齐密码登录/短信登录两种模式,短信登录使用 `WEB_H5_LOGIN` 发送验证码并调用 `/genealogy/pc/auth/login/sms`;注册页继续使用 `WEB_H5_REGISTER``/genealogy/pc/auth/register`;忘记密码页继续使用 `WEB_H5_FORGOT_PASSWORD``/genealogy/pc/auth/sms/code``/genealogy/pc/auth/password/reset`。修改文件:`login.html``public/css/login.css``public/js/auth-pages.js``tests/auth-pages.test.js``tests/api-client.test.js`。验证:`node tests\auth-pages.test.js``node tests\api-client.test.js``node tests\captcha-pages.test.js` 通过;全量 `tests/*.test.js` 通过;`node --check public\js\auth-pages.js``node --check public\js\api-client.js``node --check public\js\captcha-pages.js` 通过;内联样式和旧验证码场景扫描无输出。 | | PC-009 | 本地复核通过 | 登录、注册、找回密码 | PC auth、`/captcha/*` | 三页场景编码、表单契约、Axios 请求客户端和相关测试均通过;真实账号流程待业务数据联调。 |
| PC-010 | 已完成 | `config.js``public/js/api-client.js` | 开发环境接口基础地址、`/captcha/require``/captcha/challenge` | 2026-07-09 16:45:35 +08:00 开始,2026-07-09 16:48:05 +08:00 完成。根因:浏览器和 `curl.exe` 复现 `https://test-genealogy-api.ddxcjp.cn` TLS 握手失败,错误为 `ERR_SSL_UNRECOGNIZED_NAME_ALERT`/`SEC_E_ILLEGAL_MESSAGE`;同一 `/captcha/require` 请求改用 `http://test-genealogy-api.ddxcjp.cn` 返回 `200 OK`。处理:开发环境基础地址改为 `http://test-genealogy-api.ddxcjp.cn``config.js` 对旧的 `https://test-genealogy-api.ddxcjp.cn` 本地覆盖值做归一,避免浏览器 localStorage 残留继续走坏地址;`api-client.js` fallback 默认地址同步改为 HTTP。修改文件:`config.js``public/js/api-client.js``tests/config.test.js``tests/api-client.test.js`。验证:`node tests\config.test.js``node tests\api-client.test.js``node --check config.js``node --check public\js\api-client.js` 通过。 | | 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-011 | 已完成 | `login.html``register.html``forgot-password.html``public/css/public.css``public/js/captcha-pages.js` | `public/tac` 弹窗挂载`/captcha/challenge``/captcha/verify` | 2026-07-09 16:56:22 +08:00 开始,2026-07-09 17:02:35 +08:00 完成。根因:`public/tac/css/tac.css``#tianai-captcha-parent` 是相对定位,会在绑定元素内渲染;此前把 `[data-captcha-box]` 放进表单内部,导致验证码块直接占据卡片内容区域。处理:三张认证页移除表单内部挂载点,新增页面级 `.auth-captcha-modal[data-captcha-box]``public/css/public.css` 增加固定遮罩、居中和移动端宽度约束;`captcha-pages.js` 优先使用页面级挂载点;未修改 `public/tac/js/tac.min.js`。修改文件:`login.html``register.html``forgot-password.html``public/css/public.css``public/js/captcha-pages.js``tests/auth-page-structure.test.js``tests/captcha-pages.test.js`。验证:`node tests\auth-page-structure.test.js``node tests\captcha-pages.test.js` 通过;全量 `tests/*.test.js` 通过;`node --check public\js\captcha-pages.js``node --check public\js\auth-pages.js` 通过;表单内部 `data-captcha-box` 扫描无输出。 | | PC-011 | 本地复核通过 | 三张认证页 TAC 弹窗 | `public/tac``/captcha/*` | 页面级挂载、移动端约束和结构测试通过;未修改 `tac.min.js`。 |
## PC 页面覆盖矩阵草案 ## PC 页面覆盖矩阵草案
@@ -109,7 +109,7 @@
- 2026-07-09 17:04:21 +08:00:复现用户反馈的 `503 Service Unavailable``curl.exe` 请求 `/captcha/require` 和域名根路径 `/` 均返回 `Server: SakuraFrp``503 Service Unavailable`,响应页提示检查 SakuraFrp 隧道节点、隧道配置、frpc 是否运行、隧道是否在线。结论:这是测试接口域名/内网穿透服务当前不可用,不是前端字段、TAC 样式或请求参数导致。 - 2026-07-09 17:04:21 +08:00:复现用户反馈的 `503 Service Unavailable``curl.exe` 请求 `/captcha/require` 和域名根路径 `/` 均返回 `Server: SakuraFrp``503 Service Unavailable`,响应页提示检查 SakuraFrp 隧道节点、隧道配置、frpc 是否运行、隧道是否在线。结论:这是测试接口域名/内网穿透服务当前不可用,不是前端字段、TAC 样式或请求参数导致。
## PC-012 认证页 layui 提示与 TAC 弹窗样式修正 ## PC-012 认证页 layui 提示与 TAC 弹窗样式修正
- 状态:已完成 - 状态:本地复核通过
- 计划开始时间:2026-07-09 17:14:08 +08:00 - 计划开始时间:2026-07-09 17:14:08 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-09-auth-layer-message.md` - 计划文件:`docs/superpowers/plans/2026-07-09-auth-layer-message.md`
- 页面范围:`login.html``register.html``forgot-password.html` - 页面范围:`login.html``register.html``forgot-password.html`
@@ -132,3 +132,112 @@
3. 已完成:`auth-pages.js``captcha-pages.js` 提示统一走 `MessageUtil`/`layui.layer.msg`,不再使用浏览器原生 `alert` 3. 已完成:`auth-pages.js``captcha-pages.js` 提示统一走 `MessageUtil`/`layui.layer.msg`,不再使用浏览器原生 `alert`
4. 已完成:`public/css/public.css` 增加认证页 layer 提示皮肤。 4. 已完成:`public/css/public.css` 增加认证页 layer 提示皮肤。
5. 已完成:焦点测试、相关测试、语法检查和全量 Node 测试通过。 5. 已完成:焦点测试、相关测试、语法检查和全量 Node 测试通过。
## PC-013 TAC 取消或请求失败后的遮罩清理
- 状态:本地复核通过
- 计划开始时间:2026-07-10 09:10:03 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-modal-cleanup.md`
- 页面范围:`login.html``register.html``forgot-password.html`
- 接口范围:`/captcha/require``/captcha/challenge``/captcha/verify`
- 根因记录:现有 `.auth-captcha-modal` 仅依赖 `:empty` 判断隐藏;TAC 在取消、CORS 失败或 503 失败后仍可能保留子节点,导致外层遮罩层不再为空从而不会消失。
- 处理计划:由 `captcha-pages.js` 显式管理 `is-active` 状态、`aria-hidden` 和残留 DOM;成功、取消、初始化异常、Promise 失败路径全部调用统一清理函数。
- 完成时间:2026-07-10 09:15:55 +08:00
- 修改文件:`public/js/captcha-pages.js``public/css/captcha-modal.css``login.html``register.html``forgot-password.html``tests/captcha-pages.test.js``tests/auth-page-structure.test.js``docs/superpowers/plans/2026-07-10-tac-modal-cleanup.md`
- 完成结果:`openCaptchaBox` 在每次初始化前清理旧 TAC DOM 并显示遮罩;`closeCaptchaBox` 在成功、取消、初始化异常、Promise 失败后清空 DOM、隐藏遮罩并更新 `aria-hidden`;三张认证页加载新的公共验证码弹窗 CSS。
- 验证结果:`node tests\\captcha-pages.test.js``node tests\\auth-page-structure.test.js``node --check public\\js\\captcha-pages.js``Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }` 均通过。
- 剩余风险:`/captcha/*` 的 503、CORS 或 TLS 错误属于测试网关/服务端可用性问题;本项只保证它们触发取消或异常后,前端不会继续遗留遮罩。
## PC-014 已完成记录全量复核
- 状态:已完成
- 开始时间:2026-07-10 09:20:38 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md`
- 复核范围:PC-000 至 PC-013 中所有标记为“已完成”或“已修正”的项目。
- 复核规则:以当前 `genealogy-pc-openapi.yaml``paths`、当前代码、当前 Node 测试、语法检查和静态扫描为准;历史记录只作为待核对内容,不作为证明。
- 状态规则:当前证据完整才保留“已完成”;代码或记录不一致改为“待修正”;仅受测试网关影响的项目标记“外部环境待验证”。
- 暂停时间:2026-07-10 09:27:30 +08:00
- 暂停原因:用户要求删除现有 fetch 请求层并整体切换为 Axios;重构前取得的复核结果不能作为重构后的最终证据。
- 完成时间:2026-07-10 10:03:18 +08:00
- 当前结论:上方 PC-000 至 PC-011 及下方 PC-012、PC-013 的复核状态为本次有效状态,历史文件名、历史命令和历史时间仅用于追溯。
- 验证证据:当前 YAML 路径扫描、Axios 公共层测试、认证/验证码/资料/安全/上传/地区测试、45 页脚本顺序测试、全量 Node 测试、语法检查均通过;受限环境外 `curl.exe -I http://test-genealogy-api.ddxcjp.cn/captcha/require` 返回 `HTTP 200 OK`
## PC-015 Axios 公共请求层重构
- 状态:已完成
- 开始时间:2026-07-10 09:27:30 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-axios-request-layer-refactor.md`
- 目录边界:`utils/axios.js` 存第三方 Axios 非压缩浏览器构建;`utils/AxiosRequestUtil.js` 存公共请求封装;`utils/ApiClient.js` 存跨页面 PC 接口客户端;`public/js` 只存页面业务脚本。
- 清理目标:删除 `utils/RequestUtil.js``public/js/api-client.js`,并清除 `fetch``fetchImpl``root.fetch` 和旧页面脚本引用;不提供兼容分支。
- 范围修正:2026-07-10 09:27:30 +08:00 初始清单只按旧 RequestUtil 引用列出 14 页;本轮静态扫描发现总共有 45 页加载接口客户端,已修正为自动扫描全部相关 HTML 页面,剩余 31 页必须迁移后才能完成 PC-015。
- 配置职责修正:2026-07-10 09:47:25 +08:00 参考 `C:\Users\Administrator\Desktop\job\3Dyjs\config.js` 后确认,根目录 `config.js` 只负责开发/生产环境与 API 基础地址;PC clientId、tenantId、token key 和 Authorization header 由 `utils/ApiClient.js` 负责。当前未提供生产地址,生产环境暂时使用用户指定的测试地址,待后端提供生产域名后单独替换。
- 配置单一来源复核:2026-07-10 10:10:56 +08:00 已移除 `utils/ApiClient.js``DEFAULT_BASE_URL`;客户端现在只能消费根目录 `config.js``apiBaseUrl` 或调用方显式传入的 `baseUrl`,缺失时立即抛出明确错误。
- 完成时间:2026-07-10 10:03:18 +08:00
- 完成结果:下载 Axios 1.7.9 非压缩浏览器构建至 `utils/axios.js`;创建 `utils/AxiosRequestUtil.js``utils/ApiClient.js`;删除 `utils/RequestUtil.js``public/js/api-client.js`45 页统一加载 `config.js``StorageUtil.js`、Axios、AxiosRequestUtil、ApiClient 后再加载页面业务脚本。
- 验证结果:`node tests\config.test.js``node tests\utils.test.js``node tests\api-client.test.js``node tests\api-script-order.test.js`、全量 Node 测试、`node --check` 和旧 fetch/旧脚本扫描均通过;`git diff --check` 无空白错误。2026-07-10 10:10:56 +08:00 再次回归通过,并确认客户端不再导出 `DEFAULT_BASE_URL`
## PC-016 TAC 原生样式调用清理
- 状态:已完成(由 PC-017 修正调用位置)
- 开始时间:2026-07-10 10:19:46 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-native-style-cleanup.md`
- 页面范围:`login.html``register.html``forgot-password.html`
- 约束:只调用 `public/tac` 自带的 CSS 和 JS;移除项目自定义验证码遮罩、尺寸约束与视觉配置,不改动 TAC 第三方文件。
- 完成时间:2026-07-10 10:32:20 +08:00
- 修改结果:删除 `public/css/captcha-modal.css`;删除 `public/css/public.css` 中 35 行自定义验证码规则;三页仅保留 `public/tac/css/tac.css`,并使用无 class 的 `[data-captcha-box]``captcha-pages.js` 只调用 `new TAC(config)`
- 验证结果:`node tests\auth-page-structure.test.js``node tests\captcha-pages.test.js`、全量 Node 测试、`node --check public\js\captcha-pages.js`、自定义验证码视觉标识扫描和 `git diff --check` 均通过。
- 外部边界:本项只恢复 TAC 原生视觉与调用边界;验证码背景尺寸校验失败仍需以后端挑战响应的原始图片尺寸为依据另行修复。
- 复核时间:2026-07-10 10:38:10 +08:00
- 复核结论:TAC 的 `init()` 只将组件追加到 `CaptchaConfig.bindEl`,不负责页面级定位;删除静态挂载点定位样式后,组件被追加到 `body` 末尾,因此渲染到页面底部。本项不能保留“已完成”。
## PC-017 TAC 默认弹层调用纠正
- 状态:已完成
- 开始时间:2026-07-10 10:38:10 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-layer-call-correction.md`
- 调用位置:`auth-pages.js` 表单提交前调用 `CaptchaPages.ensureToken`;验证码适配层使用 Layui 默认 `layer.open` 创建临时容器,容器传入 `CaptchaConfig.bindEl`,然后调用 `new TAC(config)`
- 功能边界:不限制后端下发的 `SLIDER``ROTATE``CONCAT``WORD_IMAGE_CLICK``DISABLED` 类型;刷新由 TAC `reloadCaptcha()` 处理;成功、关闭与异常路径由适配层关闭对应 Layui 层。
- 完成时间:2026-07-10 10:44:11 +08:00
- 修改结果:删除三页静态 `[data-captcha-box]``captcha-pages.js` 用 Layui 默认 `layer.open` 创建临时容器作为 `CaptchaConfig.bindEl`,并保持 `new TAC(config)` 原样调用;TAC 结束后关闭同一 Layui 层。
- 验证结果:`node tests\auth-page-structure.test.js``node tests\captcha-pages.test.js`、全量 Node 测试、`node --check public\js\captcha-pages.js``git diff --check` 均通过;静态扫描确认三页、公共 CSS 和验证码适配层没有 `data-captcha-box``auth-captcha-modal``captcha-modal.css``bgUrl``logoUrl`
- 浏览器边界:尚未在真实测试网关完成有效轨迹验证;需在浏览器发起一次注册、登录或找回密码操作,确认 Layui 默认层居中和后端 `validToken` 流程。
## PC-018 TAC Layui content 契约修复
- 状态:已完成(由 PC-019 修正真实挂载点)
- 开始时间:2026-07-10 10:50:06 +08:00
- 完成时间:2026-07-10 10:54:03 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-layui-content-contract-fix.md`
- 页面范围:`login.html``register.html``forgot-password.html`
- 接口范围:`/captcha/require``/captcha/challenge``/captcha/verify`
- 根因记录:浏览器中 `/captcha/require` 已返回 `200`,但点击提交后提示 `d.parents is not a function` 且 TAC 不显示。根因是 `captcha-pages.js` 将原生 DOM 节点直接传入 `layui.layer.open({ content })`;当前 Layui 2.11.5 弹层内部会对 `content` 调用 `.parents()`,原生 DOM 没有该方法,导致弹层创建中断。
- 修改文件:`public/js/captcha-pages.js``tests/captcha-pages.test.js``docs/superpowers/plans/2026-07-10-tac-layui-content-contract-fix.md``docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改结果:`createCaptchaLayer` 现在使用 `layui.$``$``jQuery` 包装临时 DOM 后传给 `layer.open`;返回给 TAC 的 `box` 仍是真实 DOM 节点,继续作为 `CaptchaConfig.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 行尾提示,没有空白错误。
## PC-020 TAC Layui 弹层居中修复
- 状态:已完成
- 开始时间:2026-07-10 11:11:05 +08:00
- 完成时间:2026-07-10 11:14:31 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-layer-center-fix.md`
- 页面范围:`login.html``register.html``forgot-password.html`
- 接口范围:`/captcha/require``/captcha/challenge``/captcha/verify`
- 根因记录:Layui 打开弹层时验证码挂载点仍是空节点,TAC 后续异步创建自己的 `#tianai-captcha-parent`,原生尺寸为 `318px * 318px`,导致 Layui 先按空内容定位后,最终验证码窗口偏离视觉中心。
- 修改文件:`public/js/captcha-pages.js``tests/captcha-pages.test.js``docs/superpowers/plans/2026-07-10-tac-layer-center-fix.md``docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改结果:`createCaptchaLayer` 使用 Layui 默认 `layer.open` 时增加 `area: ['318px', '318px']``offset: 'auto'`,尺寸来自 TAC 原生 `#tianai-captcha-parent`;未修改 `public/tac`,未新增验证码自定义 CSS,未传 TAC 视觉配置。
- 验证结果:先运行 `node tests\captcha-pages.test.js` 复现 `layerOptions.area` 缺失失败;修复后 `node tests\captcha-pages.test.js``node tests\auth-page-structure.test.js``node --check public\js\captcha-pages.js``git diff --check` 通过。`git diff --check` 仅输出既有 LF/CRLF 换行提示,没有空白错误。
- 复核结论:该项只解决 `d.parents is not a function`;用户 2026-07-10 10:59 左右浏览器复测显示 Layui 遮罩出现、`/captcha/challenge` 返回 200,但 TAC 本体未显示,因此真实挂载点问题进入 PC-019 修正。
## PC-019 TAC Layui 实际挂载点修复
- 状态:已完成
- 开始时间:2026-07-10 10:59:52 +08:00
- 完成时间:2026-07-10 11:04:18 +08:00
- 计划文件:`docs/superpowers/plans/2026-07-10-tac-layer-real-mount-fix.md`
- 页面范围:`login.html``register.html``forgot-password.html`
- 接口范围:`/captcha/require``/captcha/challenge``/captcha/verify`
- 根因记录:用户浏览器截图显示 Layui 遮罩已经出现,`/captcha/require``/captcha/challenge` 都返回 `200`,但弹层内没有 TAC 验证窗口。根因是 PC-018 仍返回打开前的临时 DOM 给 `CaptchaConfig.bindEl`,不能保证该节点就是 Layui 弹层内容区里的真实节点。
- 修改文件:`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 视觉配置。
- 验证结果:先运行 `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 行尾提示,没有空白错误。
@@ -0,0 +1,251 @@
# Axios 公共请求层重构 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 删除现有 `fetch` 请求实现,以本地 Axios 浏览器版和 `utils/AxiosRequestUtil.js` 替换为唯一公共请求层,页面业务 JS 不再依赖或实现底层网络请求。
**Architecture:** `utils/axios.js` 是固定版本的第三方浏览器依赖;`utils/AxiosRequestUtil.js` 负责 Axios 实例、请求头、token、参数、响应解包和错误转换;`utils/ApiClient.js` 负责所有 PC 接口契约与 token 生命周期。所有页面在页面业务脚本前加载三个 utils 文件,旧 `utils/RequestUtil.js``public/js/api-client.js` 被删除且不保留兼容入口。
**Tech Stack:** Axios 1.7.9 浏览器非压缩构建、原生 JavaScript、Node `assert` 测试、PowerShell 静态扫描。
## Global Constraints
- 计划创建时间:2026-07-10 09:27:30 +08:00。
- `utils` 只存通用依赖和公共请求封装;`public/js` 只存页面业务脚本,不存 API 客户端或底层请求实现。
- 不保留 `fetch``fetchImpl``root.fetch``RequestUtil``public/js/api-client.js` 或旧脚本引用;不做 Axios/fetch 双实现兼容。
- Axios 使用本地 `utils/axios.js`,页面运行时不依赖 CDN。
- 普通 JSON 请求传对象给 Axios;文件上传传 `FormData`;不手写 JSON 字符串。
- 每个请求都带 `clientid`;有 token 且 `auth !== false` 时带 `Authorization: Bearer <token>`
- PC-014 在本重构完成后重新执行,重构前的复核结论不作为最终证据。
---
### Task 0: 收敛环境配置职责
**Files:**
- Modify: `config.js`
- Modify: `tests/config.test.js`
- Modify: `utils/ApiClient.js`
**Interfaces:**
- Produces: `GenealogyConfig.getEnvironment()`,只返回 `development``production`
- Produces: `GenealogyConfig.getApiBaseUrl()`,只返回当前环境的接口基础地址。
- Produces: `GenealogyConfig.getConfig()`,只返回 `{ environment, apiBaseUrl }`
- Consumes: `ApiClient` 自己定义 PC `clientId``tenantId`、token key 和 token header。
- [x] **Step 1: 写配置职责失败测试**
```js
assert.deepStrictEqual(config.getConfig(), {
environment: 'development',
apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
});
assert.strictEqual(config.getClientId, undefined);
assert.strictEqual(config.getTenantId, undefined);
assert.strictEqual(config.getTokenKey, undefined);
```
- [x] **Step 2: 运行测试确认旧配置职责过宽**
Run: `node tests\\config.test.js`
Expected: FAIL,旧配置仍暴露 clientId、tenantId、token key 或 token header。
- [x] **Step 3: 重写 config.js 为环境和域名唯一入口**
```js
var environments = {
development: { apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn' },
production: { apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn' }
};
function getConfig() {
return {
environment: getEnvironment(),
apiBaseUrl: getApiBaseUrl()
};
}
```
- [x] **Step 4: 将 PC 身份和 token 常量保留在 ApiClient**
```js
var DEFAULT_CLIENT_ID = 'ced7e5f0498645c6ec642dcf450b036f';
var DEFAULT_TENANT_ID = '000000';
var DEFAULT_TOKEN_KEY = 'genealogy_auth_token';
var DEFAULT_TOKEN_HEADER_NAME = 'Authorization';
```
补充约束:`ApiClient` 不定义接口基础地址;必须由根目录 `config.js``apiBaseUrl` 或调用方显式 `baseUrl` 提供。
- [x] **Step 5: 运行配置与 API 客户端测试**
Run: `node tests\\config.test.js; node tests\\api-client.test.js; node --check config.js; node --check utils\\ApiClient.js`
Expected: 所有命令通过。
### Task 1: 锁定 Axios 请求契约的失败测试
**Files:**
- Modify: `tests/utils.test.js`
- Modify: `tests/api-client.test.js`
**Interfaces:**
- Produces: `AxiosRequestUtil.createRequester(options)`
- Consumes: `options.axiosInstance`,该对象实现 `request(config)`
- Produces: `request(method, path, { query, body, headers, auth })`,向 Axios 传入 `{ method, url, params, data, headers }`
- [x] **Step 1: 把工具测试改为 Axios 假实例**
```js
const axiosCalls = [];
const requester = AxiosRequestUtil.createRequester({
baseUrl: 'https://api.example.test',
clientId: 'pc-client',
getToken: () => 'token-x',
axiosInstance: {
request: async (config) => {
axiosCalls.push(config);
return { status: 200, data: { code: 200, data: { ok: true } } };
}
}
});
```
- [x] **Step 2: 运行测试确认因旧 fetch 契约失败**
Run: `node tests\\utils.test.js; node tests\\api-client.test.js`
Expected: FAIL,提示缺少 `AxiosRequestUtil` 或仍使用 `fetchImpl`
### Task 2: 创建 Axios 工具层并删除 fetch 工具层
**Files:**
- Create: `utils/axios.js`
- Create: `utils/AxiosRequestUtil.js`
- Create: `utils/ApiClient.js`
- Delete: `utils/RequestUtil.js`
- Delete: `public/js/api-client.js`
**Interfaces:**
- `AxiosRequestUtil.createRequester(options)` 只接受 Axios 实例,不接受 `fetchImpl`
- `GenealogyApi.createClient(options)``utils/ApiClient.js` 提供,只接受 `axiosInstance` 测试注入;生产环境读取 `window.axios`
- [x] **Step 1: 下载固定 Axios 非压缩浏览器构建**
Run: `curl.exe --fail --location https://cdn.jsdelivr.net/npm/axios@1.7.9/dist/axios.js --output utils\\axios.js`
Expected: `utils/axios.js` 存在且首行包含 Axios 版本信息。
- [x] **Step 2: 实现 AxiosRequestUtil 最小请求流程**
```js
var response = await axiosInstance.request({
method: method.toLowerCase(),
url: path,
params: requestOptions.query,
data: requestOptions.body,
headers: headers
});
return unwrapResponse(response.data);
```
- [x] **Step 3: 从零实现 utils/ApiClient.js 并只依赖 AxiosRequestUtil**
```js
var requestUtil = getAxiosRequestUtil();
var requester = requestUtil.createRequester({
baseUrl: baseUrl,
clientId: clientId,
getToken: getToken,
axiosInstance: settings.axiosInstance || root.axios
});
```
- [x] **Step 4: 删除旧 fetch 请求文件和旧公共 API 文件**
Run: `Remove-Item -LiteralPath utils\\RequestUtil.js; Remove-Item -LiteralPath public\\js\\api-client.js`
Expected: 两个文件不存在,后续扫描无 `RequestUtil``fetchImpl``root.fetch``fetch(``public/js/api-client.js` 匹配。
- [x] **Step 5: 运行工具和 API 客户端测试**
Run: `node tests\\utils.test.js; node tests\\api-client.test.js; node --check utils\\AxiosRequestUtil.js; node --check utils\\ApiClient.js`
Expected: 所有命令通过。
### Task 3: 更新页面公共脚本顺序
**Files:**
- Modify: 所有根目录中加载 `public/js/api-client.js``utils/ApiClient.js` 的 HTML 页面。
- Test: `tests/auth-page-structure.test.js`
- Test: `tests/api-script-order.test.js`
- [x] **Step 1: 以自动发现的脚本顺序测试锁定页面依赖**
```js
assert.strictEqual(pages.length, 45);
assert.ok(axiosIndex < axiosRequestUtilIndex);
assert.ok(axiosRequestUtilIndex < apiClientIndex);
assert.strictEqual(html.includes('utils/RequestUtil.js'), false);
```
- [x] **Step 2: 运行测试确认旧脚本顺序不满足新契约**
Run: `node tests\\auth-page-structure.test.js`
Expected: FAIL,仍有页面加载旧 API 客户端或未加载 Axios 和 AxiosRequestUtil。
- [x] **Step 3: 逐页替换公共脚本引用**
```html
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
```
- [x] **Step 4: 运行结构测试和静态扫描**
Run: `node tests\\auth-page-structure.test.js; rg -n "utils/RequestUtil\\.js|public/js/api-client\\.js|fetchImpl|root\\.fetch|fetch\\(" --glob "!public/tac/**" --glob "!utils/axios.js" .`
Expected: 测试通过,旧请求层标识无匹配。
### Task 4: 重跑 PC 复核并更新记录
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Modify: `docs/superpowers/plans/2026-07-10-axios-request-layer-refactor.md`
- Modify: `docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md`
- [x] **Step 1: 运行所有 Node 测试与语法检查**
Run: `Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }; node --check utils\\AxiosRequestUtil.js; node --check utils\\ApiClient.js; git diff --check`
Expected: 所有测试通过,语法检查和差异检查无错误。
- [x] **Step 2: 执行 PC-014 复核命令并记录外部网关状态**
Run: `curl.exe -I --max-time 15 http://test-genealogy-api.ddxcjp.cn/captcha/require`
Expected: 记录当次结果;仅本地验证通过的项标为“本地复核通过”,网关不可用时标注“外部环境待验证”。
- [x] **Step 3: 写入完成时间和清理结论**
Record: Axios 文件来源、删除的 `fetch` 文件、页面脚本替换范围、所有验证命令及结果。
## 自检
- 所有底层网络调用都从 `utils/AxiosRequestUtil.js` 发起。
- `public/js` 中不含 `fetch`、Axios 实例创建或请求头拼装。
- 所有页面在页面业务脚本前加载 `utils/axios.js``utils/AxiosRequestUtil.js``utils/ApiClient.js`
- `utils/RequestUtil.js``public/js/api-client.js``fetchImpl``root.fetch` 和旧页面脚本引用均不存在。
## 完成记录
- 完成时间:2026-07-10 10:10:56 +08:00。
- Axios 来源:`axios@1.7.9/dist/axios.js` 的本地非压缩浏览器构建,存放于 `utils/axios.js`
- 请求契约:所有 PC 请求均由 `utils/AxiosRequestUtil.js` 通过 `axios.request` 发起;`utils/ApiClient.js` 仅封装 PC 接口和 token 生命周期。
- 配置边界:根目录 `config.js` 是接口基础地址唯一来源;`ApiClient` 不再保存 `DEFAULT_BASE_URL`
- 页面范围:45 个加载接口客户端的 HTML 页面已统一脚本顺序;旧 `utils/RequestUtil.js``public/js/api-client.js` 已删除。
- 验证:全量 Node 测试、语法检查、45 页脚本顺序检查、旧 fetch/旧脚本扫描和 `git diff --check` 均通过。
@@ -0,0 +1,150 @@
# PC 接口对接记录全量复核 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 重新验证 `pc-api-page-integration-tracker-2026-07-09.md` 中 PC-000 至 PC-013 的完成状态,并以当前代码、当前 PC YAML 和本次命令结果修正记录。
**Architecture:** `genealogy-pc-openapi.yaml``paths` 是接口范围唯一来源;代码和 HTML 是当前实现来源;Node 测试、语法检查和静态扫描是可重复验证来源。无法由前端离线验证的测试网关可用性明确标记为“外部环境待验证”,不将其当作页面对接完成证据。
**Tech Stack:** OpenAPI YAML、原生 JavaScript、Node `assert` 测试、PowerShell 静态扫描、Git 差异检查。
## Global Constraints
- 计划创建时间:2026-07-10 09:20:38 +08:00。
- 范围仅限追踪表中状态为“已完成”或“已修正”的 PC-000 至 PC-013;不新增业务接口或页面功能。
- 复核时不采用旧 APP 路径,不以历史测试文字代替当前命令结果。
- 每个结论必须记录命令、结果、时间和证据边界;证据不足必须改为“复核失败/待修正”或“外部环境待验证”。
- 仅在记录与事实不一致时修改文档;不改无关业务代码。
---
### Task 1: 复核契约和配置项
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Read: `genealogy-pc-openapi.yaml`
- Read: `config.js`
- Read: `public/js/api-client.js`
- Test: `tests/config.test.js`
- Test: `tests/api-client.test.js`
**Covers:** PC-000、PC-001、PC-007、PC-010。
- [x] **Step 1: 核对 YAML 路径与客户端配置**
Run: `rg -n "^ /captcha/|^ /auth/code:|^ /genealogy/pc/auth/|^ /genealogy/pc/files/|^ /genealogy/region/" genealogy-pc-openapi.yaml; rg -n "test-genealogy-api|ced7e5f0498645c6ec642dcf450b036f|web_pc" config.js public/js/api-client.js`
Expected: 当前 YAML 只包含验证码、PC 认证、PC 文件、地区路径;代码使用当前 PC clientId 和开发环境 HTTP 基础地址。
- [x] **Step 2: 运行配置与 API 客户端测试**
Run: `node tests\\config.test.js; node tests\\api-client.test.js; node --check config.js; node --check public\\js\\api-client.js`
Expected: 四条命令均通过。
### Task 2: 复核通用工具与未覆盖业务降级
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Read: `utils/StorageUtil.js`
- Read: `utils/FormUtil.js`
- Read: `utils/RequestUtil.js`
- Read: `public/js/api-client.js`
- Test: `tests/utils.test.js`
- Test: `tests/api-client.test.js`
**Covers:** PC-002、PC-008。
- [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`
Expected: 两个测试通过,旧 `/genealogy/app/` 请求路径无匹配结果。
- [x] **Step 2: 核对未覆盖业务的错误边界**
Run: `rg -n "PC_API_NOT_AVAILABLE" public\\js\\api-client.js`
Expected: 未被 YAML `paths` 覆盖的业务方法统一受控失败,不生成旧 APP 网络请求。
### Task 3: 复核认证、验证码与资料页面
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Read: `login.html`
- Read: `register.html`
- Read: `forgot-password.html`
- Read: `public/js/auth-pages.js`
- Read: `public/js/captcha-pages.js`
- Read: `utils/MessageUtil.js`
- Test: `tests/auth-pages.test.js`
- Test: `tests/captcha-pages.test.js`
- Test: `tests/auth-page-structure.test.js`
- Test: `tests/auth-message.test.js`
- Test: `tests/profile-pages.test.js`
- Test: `tests/security-pages.test.js`
**Covers:** PC-003、PC-009、PC-011、PC-012、PC-013。
- [x] **Step 1: 检查认证场景、接口路径、TAC 挂载和 layui 提示**
Run: `rg -n "WEB_H5_LOGIN|WEB_H5_REGISTER|WEB_H5_FORGOT_PASSWORD|captcha-modal.css|auth-captcha-modal|MessageUtil|layer.msg" login.html register.html forgot-password.html public\\js\\auth-pages.js public\\js\\captcha-pages.js utils\\MessageUtil.js`
Expected: 三张认证页分别使用正确场景;验证码使用页面级挂载点;提示由 `MessageUtil`/layui 提供。
- [x] **Step 2: 运行认证和验证码测试**
Run: `node tests\\auth-pages.test.js; node tests\\captcha-pages.test.js; node tests\\auth-page-structure.test.js; node tests\\auth-message.test.js; node tests\\profile-pages.test.js; node tests\\security-pages.test.js; node --check public\\js\\auth-pages.js; node --check public\\js\\captcha-pages.js; node --check utils\\MessageUtil.js`
Expected: 所有测试和语法检查通过。
- [x] **Step 3: 记录网关外部依赖边界**
Run: `curl.exe -I --max-time 15 http://test-genealogy-api.ddxcjp.cn/captcha/require`
Expected: 仅记录当次 HTTP 结果;无论结果如何,不将测试网关可用性视为前端实现的通过或失败。
### Task 4: 复核文件与地区公共能力
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Read: `public/js/upload-pages.js`
- Read: `public/js/region-pages.js`
- Test: `tests/upload-pages.test.js`
- Test: `tests/region-pages.test.js`
**Covers:** PC-004、PC-005、PC-006。
- [x] **Step 1: 运行文件和地区测试**
Run: `node tests\\upload-pages.test.js; node tests\\region-pages.test.js; node --check public\\js\\upload-pages.js; node --check public\\js\\region-pages.js`
Expected: 所有测试和语法检查通过。
- [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`
Expected: 文件、地区旧 APP 路径无匹配;业务页面覆盖边界仍由 `PC_API_NOT_AVAILABLE` 保护。
### Task 5: 全量验证与追踪表修正
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Modify: `docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md`
- [x] **Step 1: 运行全量测试和差异检查**
Run: `Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }; git diff --check`
Expected: 所有测试通过,差异检查无空白错误。
- [x] **Step 2: 写入复核矩阵和最终状态**
Record: PC-000 至 PC-013 每项的本次复核时间、验证命令、结论和外部环境边界;任何不一致项从“已完成”改为“待修正”。
## 自检
- 覆盖范围:PC-000 至 PC-013 的所有“已完成/已修正”项均被归入一个复核任务。
- 来源一致性:接口范围只引用 `genealogy-pc-openapi.yaml``paths`,不引用旧 APP 文档。
- 状态规则:只有当前命令和当前代码同时支持的结论才能保留“已完成”。
@@ -0,0 +1,84 @@
# TAC 默认层调用纠正实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**目标:** 在不写任何验证码 CSS、也不修改 TAC 第三方文件的前提下,将 TAC 挂载到 Layui 默认弹层中,不再渲染到认证页底部。
**架构:** `auth-pages.js` 仍在表单提交时调用 `CaptchaPages.ensureToken``captcha-pages.js` 通过已加载的 `layui.layer.open` 创建默认容器,将该容器传给 `CaptchaConfig.bindEl`,然后原样调用 `new TAC(config)`。TAC 继续根据后端下发的 `captchaType` 自己选择滑块、旋转、拼图、文字点击或禁用态。
**技术栈:** Layui 内置 `layer``public/tac/css/tac.css``public/tac/js/tac.min.js`、原生 JavaScript、Node `assert` 测试。
## 全局约束
- 计划创建时间:2026-07-10 10:38:10 +08:00。
- 不修改 `public/tac/css/tac.css``public/tac/js/tac.min.js` 或 Layui 第三方文件。
- 不创建或注入任何验证码 CSS;不传 `bgUrl``logoUrl`、尺寸、皮肤等 TAC 视觉配置。
- 只使用 Layui 默认 `layer.open` 作为弹层容器,TAC 的内部外观完全由其自带文件负责。
- 只修改认证三页、验证码适配、对应测试和记录。
---
### 任务 1:锁定正确的弹层挂载契约
**文件:**
- 修改:`tests/auth-page-structure.test.js`
- 修改:`tests/captcha-pages.test.js`
- [x] **步骤 1:写页面结构失败测试。**
断言 `login.html``register.html``forgot-password.html` 不再保留 `[data-captcha-box]` 静态挂载点,且 `public/layui/layui.js``captcha-pages.js` 前加载。
- [x] **步骤 2:写验证码适配失败测试。**
断言适配层调用 `root.layui.layer.open`、在结束时调用 `root.layui.layer.close`,仍只调用 `new root.TAC(config)`,不存在自定义验证码视觉标识。
- [x] **步骤 3:运行测试并确认当前底部挂载实现失败。**
运行:`node tests\auth-page-structure.test.js; node tests\captcha-pages.test.js`
预期:因静态 `[data-captcha-box]` 与缺少 `layer.open` 而失败。
### 任务 2:使用 Layui 默认层承载 TAC
**文件:**
- 修改:`login.html`
- 修改:`register.html`
- 修改:`forgot-password.html`
- 修改:`public/js/captcha-pages.js`
- [x] **步骤 1:删除三页静态 `[data-captcha-box]`。**
- [x] **步骤 2:在 `captcha-pages.js` 创建无样式的 DOM 容器,并以 `layui.layer.open({ type: 1, content: mount })` 打开默认弹层。**
- [x] **步骤 3:将该 DOM 容器作为 `CaptchaConfig.bindEl`,并保持 `new TAC(config)` 原样调用。**
- [x] **步骤 4:在 TAC 成功、关闭、初始化异常和 Promise 异常时销毁 TAC 并关闭对应 Layui 层。**
- [x] **步骤 5:运行聚焦测试和语法检查。**
运行:`node tests\auth-page-structure.test.js; node tests\captcha-pages.test.js; node --check public\js\captcha-pages.js`
预期:全部通过。
### 任务 3:全量验证和记录
**文件:**
- 修改:`docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改:`docs/superpowers/plans/2026-07-10-tac-layer-call-correction.md`
- [x] **步骤 1:运行全量 Node 测试、静态扫描和差异检查。**
运行:`Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }; rg -n "data-captcha-box|auth-captcha-modal|captcha-modal.css|bgUrl|logoUrl" login.html register.html forgot-password.html public\css public\js; git diff --check`
预期:测试和差异检查通过;认证页中不再有静态验证码挂载点或自定义视觉标识。
- [x] **步骤 2:记录完成时间、TAC 功能边界和浏览器实测边界。**
## 完成记录
- 完成时间:2026-07-10 10:44:11 +08:00。
- 根因:TAC 的 `init()` 只向 `CaptchaConfig.bindEl` 追加组件;原先静态挂载点位于 `body` 末尾,移除定位样式后自然渲染在页面底部。
- 修改:删除三页静态 `[data-captcha-box]`;验证码适配层使用 Layui 默认 `layer.open` 创建临时容器并传给 `bindEl`;成功、TAC 关闭、初始化异常与 Promise 异常都会清空容器并关闭同一层。
- TAC 边界:保留原生 `new TAC(config)`、原生样式、原生刷新和后端类型分发;不传视觉配置,不写验证码 CSS,不改动任何 `public/tac` 文件。
- 验证:失败测试已复现底部挂载;聚焦测试、全量 Node 测试、`node --check public/js/captcha-pages.js``git diff --check` 均通过。
- 浏览器边界:未在真实测试网关完成一次有效轨迹验证;仍需在浏览器点击注册后确认 Layui 默认层居中显示及后端 `validToken` 返回。
@@ -0,0 +1,23 @@
# 2026-07-10 TAC 弹层居中修复计划
- 计划编号:PC-020
- 开始时间:2026-07-10 11:11:05 +08:00
- 页面范围:`login.html``register.html``forgot-password.html`
- 接口范围:`/captcha/require``/captcha/challenge``/captcha/verify`
- 约束:不修改 `public/tac`,不新增验证码自定义样式,不传 TAC 视觉配置,只调整 Layui 弹层调用参数。
## 根因
Layui 打开页面层时,`content` 里的验证码挂载点还是空节点;TAC 后续异步创建自己的 `#tianai-captcha-parent`,该原生外层尺寸是 `318px * 318px`。Layui 已经按空内容完成定位,所以最终验证码窗口会偏离预期位置。
## 执行清单
1. [x] 写失败测试:要求验证码弹层通过 Layui `area` 预留 `318px * 318px`,并保持默认居中。
2. [x] 修改 `captcha-pages.js`:只给 `layer.open` 增加 TAC 原生尺寸和默认居中参数。
3. [x] 运行焦点测试、结构测试、语法检查和空白检查。
4. [x] 将完成结果同步回 `pc-api-page-integration-tracker-2026-07-09.md`
## 验证记录
- 2026-07-10 11:11:05 +08:00`node tests\captcha-pages.test.js` 已按预期失败,失败点为 `layerOptions.area` 缺失。
- 2026-07-10 11:14:31 +08:00:修复后 `node tests\captcha-pages.test.js``node tests\auth-page-structure.test.js``node --check public\js\captcha-pages.js` 均通过;`git diff --check` 无空白错误,仅有既有 LF/CRLF 换行提示。
@@ -0,0 +1,72 @@
# TAC Layui 实际挂载点修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**目标:** 修复 Layui 遮罩已显示、验证码接口已返回 200,但 TAC 验证窗口仍不显示的问题。
**架构:** `CaptchaPages.createCaptchaLayer` 使用 Layui `layer.open` 的字符串 `content` 创建弹层内部真实挂载节点;弹层创建完成后再从 `document` 查询该实际节点,并把它交给 `CaptchaConfig.bindEl`。这样 Layui 负责弹层,TAC 挂到已经进入页面的真实 DOM。
**技术栈:** Layui 2.11.5、TAC 原生 CSS/JS、原生 JavaScript、Node `assert` 测试。
## 全局约束
- 计划创建时间:2026-07-10 10:59:52 +08:00。
- 不修改 `public/tac/css/tac.css``public/tac/js/tac.min.js` 或 Layui 第三方文件。
- 不新增验证码自定义 CSS,不注入样式,不传 TAC 视觉配置。
- 保留 `auth-pages.js -> CaptchaPages.ensureToken -> TAC` 调用链,只修正弹层挂载点。
---
### 任务 1:复现“遮罩有但实际挂载点不可见”
**文件:**
- 修改:`tests/captcha-pages.test.js`
- [x] **步骤 1:写失败测试**
断言 `layer.open``content` 是包含唯一 `data-captcha-layer-box` 的字符串;`createCaptchaLayer` 必须在弹层打开后从 `document.querySelector` 取到实际挂载节点并返回。
- [x] **步骤 2:运行失败测试**
运行:`node tests\captcha-pages.test.js`
预期:失败,当前实现仍返回打开前创建的原生 DOM。
### 任务 2:修正实际挂载点
**文件:**
- 修改:`public/js/captcha-pages.js`
- [x] **步骤 1:最小实现**
增加内部自增 ID,用字符串内容创建 `<div data-captcha-layer-box="..."></div>``layer.open` 后查询该节点并作为 `box` 返回。
- [x] **步骤 2:运行聚焦测试**
运行:`node tests\captcha-pages.test.js; node --check public\js\captcha-pages.js`
预期:全部通过。
### 任务 3:记录和回归
**文件:**
- 修改:`docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改:`docs/superpowers/plans/2026-07-10-tac-layer-real-mount-fix.md`
- [x] **步骤 1:补充 PC-019 记录**
记录用户浏览器复现现象、根因、修正方式、验证命令。
- [x] **步骤 2:最终验证**
运行:`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` 仅允许 Git 行尾提示,不允许空白错误。
## 完成记录
- 完成时间:2026-07-10 11:04:18 +08:00。
- 根因:PC-018 只让 `layer.open` 不再因 `.parents()` 报错,但仍把打开前创建的临时 DOM 返回给 TAC;真实浏览器里 Layui 遮罩已出现,`/captcha/challenge` 也返回 200,但 TAC 没有挂到可见弹层内容区。
- 修复:`layer.open` 使用字符串 `content` 创建弹层内部真实节点,随后通过 `document.querySelector('[data-captcha-layer-box="..."]')` 获取实际挂载点并交给 `CaptchaConfig.bindEl`
- 约束:未修改 `public/tac`,未新增验证码 CSS,未传 TAC 视觉配置。
- 验证:失败测试已先复现;修复后 `node tests\captcha-pages.test.js``node --check public\js\captcha-pages.js` 通过。最终回归记录见 `docs/pc-api-page-integration-tracker-2026-07-09.md` 的 PC-019。
@@ -0,0 +1,71 @@
# TAC Layui content 契约修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**目标:** 修复认证页点击后只出现 `d.parents is not a function` 提示、TAC 弹层不显示的问题。
**架构:** `CaptchaPages.createCaptchaLayer` 继续只负责创建 Layui 默认弹层容器,不写验证码样式、不改 TAC 文件。Layui `layer.open``content` 使用 Layui/jQuery 包装对象满足弹层内部 `.parents()` 契约,TAC `bindEl` 继续使用对应的真实 DOM 节点。
**技术栈:** Layui 2.11.5、TAC 原生 CSS/JS、原生 JavaScript、Node `assert` 测试。
## 全局约束
- 计划创建时间:2026-07-10 10:50:06 +08:00。
- 不修改 `public/tac/css/tac.css``public/tac/js/tac.min.js` 或 Layui 第三方文件。
- 不新增验证码自定义 CSS,不注入样式,不传 TAC 视觉配置。
- 只修正 Layui 弹层 content 契约、对应测试和记录文档。
---
### 任务 1:复现 Layui content 契约错误
**文件:**
- 修改:`tests/captcha-pages.test.js`
- [x] **步骤 1:写失败测试**
断言 `createCaptchaLayer` 传给 `layui.layer.open``content` 必须是包装对象,包装对象需要保留真实 DOM 节点,并具备 `.parents()` 方法。
- [x] **步骤 2:运行失败测试**
运行:`node tests\captcha-pages.test.js`
预期:失败,当前实现把原生 DOM 直接传给 `content`
### 任务 2:修正 createCaptchaLayer
**文件:**
- 修改:`public/js/captcha-pages.js`
- [x] **步骤 1:最小实现**
`createCaptchaLayer` 内用 `root.layui.$ || root.$ || root.jQuery` 包装真实 DOM 节点;`layer.open({ content })` 使用包装对象;返回值中的 `box` 仍为真实 DOM,供 TAC `bindEl` 使用。
- [x] **步骤 2:运行聚焦测试**
运行:`node tests\captcha-pages.test.js; node --check public\js\captcha-pages.js`
预期:全部通过。
### 任务 3:记录和回归
**文件:**
- 修改:`docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改:`docs/superpowers/plans/2026-07-10-tac-layui-content-contract-fix.md`
- [x] **步骤 1:补充 PC-018 完成记录**
记录根因、修改文件、验证命令和结果。
- [x] **步骤 2:运行最终验证**
运行:`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` 仅允许 Git 行尾提示,不允许空白错误。
## 完成记录
- 完成时间:2026-07-10 10:54:03 +08:00。
- 根因:Layui 2.11.5 的 `layer.open` 在处理 DOM 内容时会调用 `.parents()`;原实现将原生 DOM 节点直接传给 `content`,导致浏览器报 `d.parents is not a function`,弹层创建中断。
- 修复:`createCaptchaLayer` 使用 `layui.$``$``jQuery` 包装临时 DOM 后传给 `layer.open({ content })`,同时继续把真实 DOM 节点传给 `CaptchaConfig.bindEl`
- 验证:失败测试已先复现;修复后 `node tests\captcha-pages.test.js``node --check public\js\captcha-pages.js` 通过。最终回归记录见 `docs/pc-api-page-integration-tracker-2026-07-09.md` 的 PC-018。
@@ -0,0 +1,150 @@
# TAC 验证弹窗遮罩清理 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 TAC 验证被取消、初始化异常或接口失败时,主动关闭认证页遮罩并清空残留的验证码 DOM。
**Architecture:** `public/js/captcha-pages.js` 作为验证码弹窗生命周期的唯一拥有者,新增打开和关闭挂载容器的函数。由于原 `public.css` 不是 UTF-8,无法使用补丁安全编辑,`public/css/captcha-modal.css` 作为新增公共样式文件只根据 `is-active` 类决定遮罩显示;三张认证页继续复用同一个页面级挂载点。
**Tech Stack:** 原生 JavaScript、Node `assert` 测试、layui 提示、TAC `public/tac/js/tac.min.js`
## Global Constraints
- 计划创建时间:2026-07-10 09:10:03 +08:00。
- 仅修改 TAC 遮罩生命周期,不修改 `public/tac/js/tac.min.js`,不改变后端验证码类型选择。
- 新增或修改的 JS、CSS 注释使用中文;样式只写入 CSS 文件。
- 失败提示继续使用 `MessageUtil`/layui,禁止回退到浏览器原生 `alert`
- 503/CORS 是服务端或跨域环境可用性问题;本任务只保证前端失败后不遗留遮罩。
---
### Task 1: 用例锁定与生命周期工具
**Files:**
- Modify: `tests/captcha-pages.test.js`
- Modify: `public/js/captcha-pages.js`
**Interfaces:**
- Produces: `openCaptchaBox(box)`,清空旧内容、激活容器、设置 `aria-hidden="false"`
- Produces: `closeCaptchaBox(box)`,清空内容、取消激活、设置 `aria-hidden="true"`
- [x] **Step 1: 写失败测试**
```js
CaptchaPages.openCaptchaBox(box);
assert.strictEqual(box.classList.contains('is-active'), true);
assert.strictEqual(box.attributes['aria-hidden'], 'false');
CaptchaPages.closeCaptchaBox(box);
assert.strictEqual(box.classList.contains('is-active'), false);
assert.strictEqual(box.attributes['aria-hidden'], 'true');
assert.strictEqual(box.innerHTML, '');
```
- [x] **Step 2: 运行失败测试**
Run: `node tests\\captcha-pages.test.js`
Expected: FAIL,提示 `openCaptchaBox is not a function`
- [x] **Step 3: 实现最小生命周期函数**
```js
function openCaptchaBox(box) {
if (!box) return;
box.innerHTML = '';
box.classList.add('is-active');
box.setAttribute('aria-hidden', 'false');
}
function closeCaptchaBox(box) {
if (!box) return;
box.innerHTML = '';
box.classList.remove('is-active');
box.setAttribute('aria-hidden', 'true');
}
```
- [x] **Step 4: 将关闭函数接入 TAC 成功、取消、初始化异常和 Promise 捕获路径**
```js
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
closeCaptchaBox(box);
```
- [x] **Step 5: 运行单元测试与语法检查**
Run: `node tests\\captcha-pages.test.js; node --check public\\js\\captcha-pages.js`
Expected: 两条命令均通过。
### Task 2: CSS 显示状态收口
**Files:**
- Create: `public/css/captcha-modal.css`
- Modify: `login.html`
- Modify: `register.html`
- Modify: `forgot-password.html`
- Test: `tests/auth-page-structure.test.js`
**Interfaces:**
- Consumes: `.auth-captcha-modal.is-active`
- Produces: 默认隐藏、激活后显示的页面级遮罩。
- [x] **Step 1: 写失败结构测试**
```js
assert.ok(publicCss.includes('.auth-captcha-modal.is-active'));
assert.ok(publicCss.includes('display: none;'));
```
- [x] **Step 2: 运行失败测试**
Run: `node tests\\auth-page-structure.test.js`
Expected: FAIL,缺少 `.auth-captcha-modal.is-active` 规则。
- [x] **Step 3: 实现 CSS 状态规则**
```css
.auth-captcha-modal {
display: none;
}
.auth-captcha-modal.is-active {
display: grid;
}
```
- [x] **Step 4: 运行结构测试**
Run: `node tests\\auth-page-structure.test.js`
Expected: PASS。
### Task 3: 全量验证与记录
**Files:**
- Modify: `docs/pc-api-page-integration-tracker-2026-07-09.md`
- Modify: `docs/superpowers/plans/2026-07-10-tac-modal-cleanup.md`
- [x] **Step 1: 运行验证码相关和全量 Node 测试**
Run: `Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }`
Expected: 所有测试通过。
- [x] **Step 2: 记录完成时间、修改文件和验证结果**
```markdown
- [x] 完成时间:YYYY-MM-DD HH:mm:ss +08:00
- [x] 验证:相关测试、语法检查、全量 Node 测试均通过。
```
## 自检
- 需求覆盖:取消后遮罩清理、失败后遮罩清理、成功后遮罩清理、CSS 显示状态和回归测试均有对应任务。
- 占位扫描:无 `TODO``TBD` 或未定义接口。
- 接口一致性:JS 导出名、测试调用名和 CSS 类名统一为 `openCaptchaBox``closeCaptchaBox``is-active`
## 完成记录
- 完成时间:2026-07-10 09:15:55 +08:00。
- 修改文件:`public/js/captcha-pages.js``public/css/captcha-modal.css``login.html``register.html``forgot-password.html``tests/captcha-pages.test.js``tests/auth-page-structure.test.js`
- 验证:`node tests\\captcha-pages.test.js``node tests\\auth-page-structure.test.js``node --check public\\js\\captcha-pages.js` 和全量 `tests/*.test.js` 均通过。
- 已知边界:测试域名的 503、CORS 或 TLS 可用性问题仍需后端或网关处理;该前端修复保证取消或异常后不残留页面遮罩。
@@ -0,0 +1,85 @@
# TAC 原生样式调用清理实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**目标:** 登录、注册、找回密码页面只调用 `public/tac` 自带的验证码组件和样式,不再追加任何验证码视觉样式或视觉配置。
**架构:** 页面保留一个无 class 的 `[data-captcha-box]` 挂载点;`captcha-pages.js` 只负责验证码接口参数和成功、取消、异常后的 DOM 清理;TAC 的外观完全由 `public/tac/css/tac.css``tac.min.js` 决定。
**技术栈:** 原生 JavaScript、TAC 本地组件、Node `assert` 测试。
## 全局约束
- 计划创建时间:2026-07-10 10:19:46 +08:00。
- 不修改 `public/tac/css/tac.css``public/tac/js/tac.min.js`
- 不为验证码新增、覆盖或注入 CSS;不向 `new TAC` 传入 `bgUrl``logoUrl` 等视觉配置。
- 仅修改登录、注册、找回密码三页及其验证码适配、测试和记录。
---
### 任务 1:用测试锁定原生 TAC 调用边界
**文件:**
- 修改:`tests/auth-page-structure.test.js`
- 修改:`tests/captcha-pages.test.js`
- [x] **步骤 1:将页面结构测试改为禁止自定义验证码 CSS 与容器 class。**
断言三页加载 `public/tac/css/tac.css`,不加载 `public/css/captcha-modal.css`,且 `[data-captcha-box]` 不含 `auth-captcha-modal` class。
- [x] **步骤 2:将组件测试改为要求原生构造调用。**
断言 `captcha-pages.js` 中不存在 `bgUrl``logoUrl``.auth-captcha-modal` 视觉控制。
- [x] **步骤 3:运行测试并确认当前实现失败。**
运行:`node tests\auth-page-structure.test.js; node tests\captcha-pages.test.js`
预期:因当前自定义验证码 CSS、容器 class 和 TAC 视觉配置而失败。
### 任务 2:删除自定义验证码视觉层
**文件:**
- 修改:`login.html`
- 修改:`register.html`
- 修改:`forgot-password.html`
- 修改:`public/css/public.css`
- 删除:`public/css/captcha-modal.css`
- 修改:`public/js/captcha-pages.js`
- [x] **步骤 1:删除三页对 `captcha-modal.css` 的引用,并将挂载点改为无 class 的 `data-captcha-box`。**
- [x] **步骤 2:删除 `public.css` 中 `.auth-captcha-modal` 及其 TAC 尺寸限制规则。**
- [x] **步骤 3:删除 `captcha-modal.css`。**
- [x] **步骤 4:将 `new TAC(config, { ... })` 改为 `new TAC(config)`,并将挂载点查询、打开和关闭逻辑改为无视觉状态的 DOM 清理。**
- [x] **步骤 5:运行两项聚焦测试并确认通过。**
运行:`node tests\auth-page-structure.test.js; node tests\captcha-pages.test.js; node --check public\js\captcha-pages.js`
预期:全部通过。
### 任务 3:全量验证并记录
**文件:**
- 修改:`docs/pc-api-page-integration-tracker-2026-07-09.md`
- 修改:`docs/superpowers/plans/2026-07-10-tac-native-style-cleanup.md`
- [x] **步骤 1:运行全量 Node 测试、验证码样式静态扫描和差异检查。**
运行:`Get-ChildItem -Path tests -Filter *.test.js | Sort-Object Name | ForEach-Object { node $_.FullName }; rg -n "captcha-modal.css|auth-captcha-modal|bgUrl|logoUrl" login.html register.html forgot-password.html public\css public\js; git diff --check`
预期:测试与差异检查通过;扫描没有自定义验证码视觉标识。
- [x] **步骤 2:记录完成时间、修改范围、验证结果和外部验证码实测边界。**
## 完成记录
- 完成时间:2026-07-10 10:32:20 +08:00。
- 删除 `public/css/captcha-modal.css`,并从 `public/css/public.css` 删除 35 行 `.auth-captcha-box``.auth-captcha-modal` 和 TAC 尺寸限制规则。
- 三个认证页仅加载 `public/tac/css/tac.css`,无 class 的 `[data-captcha-box]` 只作为 TAC 挂载点。
- `captcha-pages.js` 现在只执行 `new TAC(config)`;不再传入 `bgUrl``logoUrl` 或切换任何视觉 class。成功、取消、初始化异常后仍清空 TAC 第三方 DOM。
- 验证:聚焦结构/验证码测试、全量 Node 测试、`node --check public/js/captcha-pages.js`、自定义样式标识扫描和 `git diff --check` 均通过。
- 外部边界:尚未用有效验证码轨迹完成真实服务端验证;`/captcha/verify` 的尺寸校验问题仍需以后端挑战响应原始尺寸为准单独处理。
+5 -1
View File
@@ -97,7 +97,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/promotion-pages.js"></script> <script src="public/js/promotion-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -131,7 +131,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -4
View File
@@ -59,15 +59,14 @@
</form> </form>
</section> </section>
</main> </main>
<!-- 滑动验证弹窗由 captcha-pages.js 按需挂载到页面级容器,避免挤占表单布局 -->
<div class="auth-captcha-modal" data-captcha-box aria-live="polite"></div>
<script src="public/js/md5.js"></script> <script src="public/js/md5.js"></script>
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/MessageUtil.js"></script> <script src="utils/MessageUtil.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/ApiClient.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/tac/js/tac.min.js"></script> <script src="public/tac/js/tac.min.js"></script>
<script src="public/js/captcha-pages.js"></script> <script src="public/js/captcha-pages.js"></script>
+5 -1
View File
@@ -119,7 +119,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/help-pages.js"></script> <script src="public/js/help-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -31,7 +31,11 @@
</form> </form>
</section> </section>
</main> </main>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
</body> </body>
</html> </html>
+3 -4
View File
@@ -89,15 +89,14 @@
</div> </div>
</section> </section>
</main> </main>
<!-- 滑动验证弹窗由 captcha-pages.js 按需挂载到页面级容器,避免挤占表单布局 -->
<div class="auth-captcha-modal" data-captcha-box aria-live="polite"></div>
<script src="public/js/md5.js"></script> <script src="public/js/md5.js"></script>
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/MessageUtil.js"></script> <script src="utils/MessageUtil.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/ApiClient.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/tac/js/tac.min.js"></script> <script src="public/tac/js/tac.min.js"></script>
<script src="public/js/captcha-pages.js"></script> <script src="public/js/captcha-pages.js"></script>
+5 -1
View File
@@ -107,7 +107,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/promotion-pages.js"></script> <script src="public/js/promotion-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -138,7 +138,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -78,7 +78,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/member-admin-pages.js"></script> <script src="public/js/member-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -133,8 +133,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/album-pages.js"></script> <script src="public/js/album-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+3 -2
View File
@@ -120,8 +120,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/article-pages.js"></script> <script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -87,7 +87,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/article-pages.js"></script> <script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -119,7 +119,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/article-pages.js"></script> <script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -127,8 +127,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/region-pages.js"></script> <script src="public/js/region-pages.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
+3 -2
View File
@@ -202,8 +202,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.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>
+5 -1
View File
@@ -102,7 +102,11 @@
<script src="public/js/jquery360.js"></script> <script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/genealogy-pages.js"></script> <script src="public/js/genealogy-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -112,7 +112,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/member-admin-pages.js"></script> <script src="public/js/member-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -107,7 +107,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/lineage-pages.js"></script> <script src="public/js/lineage-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -104,8 +104,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/feed-pages.js"></script> <script src="public/js/feed-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -101,7 +101,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/feed-pages.js"></script> <script src="public/js/feed-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -94,7 +94,11 @@
<script src="public/js/jquery360.js"></script> <script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/feedback-pages.js"></script> <script src="public/js/feedback-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -146,7 +146,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/generation-pages.js"></script> <script src="public/js/generation-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -120,8 +120,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/ceremony-pages.js"></script> <script src="public/js/ceremony-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -91,7 +91,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/ceremony-pages.js"></script> <script src="public/js/ceremony-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -121,8 +121,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/growth-pages.js"></script> <script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -79,7 +79,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/growth-pages.js"></script> <script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -67,7 +67,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/member-admin-pages.js"></script> <script src="public/js/member-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -70,7 +70,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/join-apply-pages.js"></script> <script src="public/js/join-apply-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -99,7 +99,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/join-apply-pages.js"></script> <script src="public/js/join-apply-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -109,8 +109,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script> <script src="public/js/upload-pages.js"></script>
<script src="public/js/memo-pages.js"></script> <script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -84,7 +84,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/memo-pages.js"></script> <script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -116,7 +116,11 @@
<script src="public/js/ke/kindeditor.min.js"></script> <script src="public/js/ke/kindeditor.min.js"></script>
<script src="public/js/rich-editor.js"></script> <script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/ceremony-pages.js"></script> <script src="public/js/ceremony-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -85,7 +85,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/ceremony-pages.js"></script> <script src="public/js/ceremony-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -110,7 +110,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/notification-pages.js"></script> <script src="public/js/notification-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -150,8 +150,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/security-pages.js"></script> <script src="public/js/security-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -159,7 +159,11 @@
<script src="public/js/jquery360.js"></script> <script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/vip-pages.js"></script> <script src="public/js/vip-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -82,7 +82,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/member-admin-pages.js"></script> <script src="public/js/member-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+5 -1
View File
@@ -162,7 +162,11 @@
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script> <script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/lineage-pages.js"></script> <script src="public/js/lineage-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+3 -2
View File
@@ -365,8 +365,9 @@
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/profile-pages.js"></script> <script src="public/js/profile-pages.js"></script>
<script src="public/js/profile-common.js"></script> <script src="public/js/profile-common.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
+5 -1
View File
@@ -93,7 +93,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/album-pages.js"></script> <script src="public/js/album-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
-35
View File
@@ -591,41 +591,6 @@ textarea {
transform: translateY(-1px); transform: translateY(-1px);
} }
.auth-captcha-box {
/* 认证页滑动验证挂载点,TAC 内部 DOM 由第三方库生成 */
display: flex;
justify-content: center;
}
.auth-captcha-box:empty {
display: none;
}
.auth-captcha-box #tianai-captcha-parent {
max-width: 100%;
}
.auth-captcha-modal {
/* 认证页滑动验证弹窗挂载点,TAC 内部 DOM 由第三方库生成 */
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
background: rgba(38, 48, 44, .32);
backdrop-filter: blur(4px);
}
.auth-captcha-modal:empty {
display: none;
}
.auth-captcha-modal #tianai-captcha-parent {
max-width: min(318px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
}
/* 验证码输入和按钮的通用双列布局,窄屏在页面 CSS 中改为单列 */ /* 验证码输入和按钮的通用双列布局,窄屏在页面 CSS 中改为单列 */
.code-row { .code-row {
display: grid; display: grid;
+62 -14
View File
@@ -10,6 +10,8 @@
'use strict'; 'use strict';
var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN'; var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN';
var CAPTCHA_LAYER_AREA = ['318px', '318px'];
var captchaLayerSeed = 0;
function getApi() { function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient; return root.GenealogyApi && root.GenealogyApi.defaultClient;
@@ -111,11 +113,44 @@
return query('input[name="validToken"]', form); return query('input[name="validToken"]', form);
} }
function getCaptchaBox(form) { function createCaptchaLayer() {
// 优先使用页面级弹窗挂载点,避免 TAC 验证码挤占表单布局 // Layui 只负责提供默认弹层容器,验证码外观完全由 TAC 自带文件决定
return query('.auth-captcha-modal[data-captcha-box]') if (!root.layui || !root.layui.layer || !root.layui.layer.open || !root.document) {
|| query('[data-captcha-box]', form) throw new Error('缺少 layui.layer 验证码弹层依赖');
|| query('[data-captcha-box]'); }
var layerId = 'captcha-layer-' + (++captchaLayerSeed);
var selector = '[data-captcha-layer-box="' + layerId + '"]';
var layerIndex = root.layui.layer.open({
type: 1,
title: false,
closeBtn: 0,
// 尺寸来自 TAC 原生 #tianai-captcha-parent,提前交给 Layui 计算居中位置。
area: CAPTCHA_LAYER_AREA,
offset: 'auto',
content: '<div data-captcha-layer-box="' + layerId + '"></div>'
});
var box = query(selector);
if (!box) {
root.layui.layer.close(layerIndex);
throw new Error('缺少 Layui 验证码实际挂载点');
}
return {
box: box,
layerIndex: layerIndex
};
}
function closeCaptchaLayer(captchaLayer) {
// 组件结束后清空第三方 DOM 并关闭本次创建的 Layui 层。
if (!captchaLayer) return;
if (captchaLayer.box) captchaLayer.box.innerHTML = '';
if (root.layui && root.layui.layer && root.layui.layer.close) {
root.layui.layer.close(captchaLayer.layerIndex);
}
} }
function writeValidToken(form, token) { function writeValidToken(form, token) {
@@ -132,7 +167,8 @@
function createTac(form, options, api, resolve, reject) { function createTac(form, options, api, resolve, reject) {
var settings = options || {}; var settings = options || {};
var box = getCaptchaBox(form); var captchaLayer;
var box;
var context = buildChallengeBody(settings, api); var context = buildChallengeBody(settings, api);
var lastChallenge = {}; var lastChallenge = {};
var captcha; var captcha;
@@ -143,8 +179,11 @@
return null; return null;
} }
if (!box) { try {
reject(new Error('缺少滑动验证容器')); captchaLayer = createCaptchaLayer();
box = captchaLayer.box;
} catch (error) {
reject(error);
return null; return null;
} }
@@ -159,12 +198,15 @@
var token = extractValidToken(response); var token = extractValidToken(response);
if (!token) { if (!token) {
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
reject(new Error('滑动验证未返回有效票据')); reject(new Error('滑动验证未返回有效票据'));
return; return;
} }
writeValidToken(form, token); writeValidToken(form, token);
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow(); if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
resolve(token); resolve(token);
}, },
validFail: function (response, captchaControl, captchaInstance) { validFail: function (response, captchaControl, captchaInstance) {
@@ -173,6 +215,7 @@
}, },
btnCloseFun: function (event, captchaInstance) { btnCloseFun: function (event, captchaInstance) {
if (captchaInstance) captchaInstance.destroyWindow(); if (captchaInstance) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
reject(new Error('已取消滑动验证')); reject(new Error('已取消滑动验证'));
} }
}); });
@@ -213,11 +256,15 @@
} }
}); });
captcha = new root.TAC(config, { try {
bgUrl: 'public/tac/images/dun.jpeg', captcha = new root.TAC(config);
logoUrl: null captcha.init();
}); } catch (error) {
captcha.init(); closeCaptchaLayer(captchaLayer);
reject(error);
return null;
}
return captcha; return captcha;
} }
@@ -252,7 +299,8 @@
buildChallengeBody: buildChallengeBody, buildChallengeBody: buildChallengeBody,
adaptChallengeResponse: adaptChallengeResponse, adaptChallengeResponse: adaptChallengeResponse,
buildVerifyBody: buildVerifyBody, buildVerifyBody: buildVerifyBody,
getCaptchaBox: getCaptchaBox, createCaptchaLayer: createCaptchaLayer,
closeCaptchaLayer: closeCaptchaLayer,
extractValidToken: extractValidToken, extractValidToken: extractValidToken,
normalizeVerifyResponse: normalizeVerifyResponse, normalizeVerifyResponse: normalizeVerifyResponse,
shouldRequireCaptcha: shouldRequireCaptcha, shouldRequireCaptcha: shouldRequireCaptcha,
+4 -5
View File
@@ -45,22 +45,21 @@
placeholder="设置密码" placeholder="设置密码"
/> />
<input type="hidden" name="validToken" /> <input type="hidden" name="validToken" />
<button class="btn primary" type="submit">注册并创建家谱</button> <button class="btn primary" type="submit">注册账号</button>
<div class="auth-links"> <div class="auth-links">
<a href="login.html">已有账号,去登录</a> <a href="login.html">已有账号,去登录</a>
</div> </div>
</form> </form>
</section> </section>
</main> </main>
<!-- 滑动验证弹窗由 captcha-pages.js 按需挂载到页面级容器,避免挤占表单布局 -->
<div class="auth-captcha-modal" data-captcha-box aria-live="polite"></div>
<script src="public/js/md5.js"></script> <script src="public/js/md5.js"></script>
<script src="config.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script> <script src="utils/StorageUtil.js"></script>
<script src="utils/FormUtil.js"></script> <script src="utils/FormUtil.js"></script>
<script src="utils/RequestUtil.js"></script> <script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/MessageUtil.js"></script> <script src="utils/MessageUtil.js"></script>
<script src="public/js/api-client.js"></script> <script src="utils/ApiClient.js"></script>
<script src="public/layui/layui.js"></script> <script src="public/layui/layui.js"></script>
<script src="public/tac/js/tac.min.js"></script> <script src="public/tac/js/tac.min.js"></script>
<script src="public/js/captcha-pages.js"></script> <script src="public/js/captcha-pages.js"></script>
+5 -1
View File
@@ -105,7 +105,11 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="public/js/api-client.js"></script> <script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/feedback-pages.js"></script> <script src="public/js/feedback-pages.js"></script>
<script src="public/js/page-effects.js"></script> <script src="public/js/page-effects.js"></script>
</body> </body>
+44 -35
View File
@@ -1,5 +1,5 @@
const assert = require('assert'); const assert = require('assert');
const GenealogyApi = require('../public/js/api-client.js'); const GenealogyApi = require('../utils/ApiClient.js');
function createStore(initial) { function createStore(initial) {
const values = Object.assign({}, initial); const values = Object.assign({}, initial);
@@ -24,7 +24,8 @@ async function run() {
'token-key': 'abc-token' 'token-key': 'abc-token'
}); });
assert.strictEqual(GenealogyApi.DEFAULT_BASE_URL, 'http://test-genealogy-api.ddxcjp.cn'); // 接口基础地址只能由根目录 config.js 负责,公共客户端不能重复声明。
assert.strictEqual(GenealogyApi.DEFAULT_BASE_URL, undefined);
const client = GenealogyApi.createClient({ const client = GenealogyApi.createClient({
baseUrl: 'https://test-genealogy-api.ddxcjp.cn', baseUrl: 'https://test-genealogy-api.ddxcjp.cn',
@@ -33,31 +34,32 @@ async function run() {
tokenKey: 'token-key', tokenKey: 'token-key',
tokenHeaderName: 'Authorization', tokenHeaderName: 'Authorization',
tokenStore: store, tokenStore: store,
fetchImpl: async (url, options) => { axiosInstance: {
calls.push({ url, options }); request: async (config) => {
calls.push(config);
return { return {
ok: true, status: 200,
status: 200, data: {
json: async () => ({
code: 200, code: 200,
msg: '操作成功', msg: '操作成功',
data: { token: 'server-token', url } data: { token: 'server-token', url: config.url }
}) }
}; };
}
} }
}); });
await client.login({ phone: '13800000000', password: 'md5' }); await client.login({ phone: '13800000000', password: 'md5' });
assert.strictEqual(calls[0].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/login'); assert.strictEqual(calls[0].url, '/genealogy/pc/auth/login');
assert.strictEqual(calls[0].options.headers.clientid, 'client-x'); assert.strictEqual(calls[0].headers.clientid, 'client-x');
assert.strictEqual(calls[0].options.headers.Authorization, undefined); assert.strictEqual(calls[0].headers.Authorization, undefined);
assert.strictEqual(JSON.parse(calls[0].options.body).tenantId, 'tenant-x'); assert.strictEqual(calls[0].data.tenantId, 'tenant-x');
assert.strictEqual(store.values['token-key'], 'server-token'); assert.strictEqual(store.values['token-key'], 'server-token');
await client.getProfile(); await client.getProfile();
assert.strictEqual(calls[1].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/profile'); assert.strictEqual(calls[1].url, '/genealogy/pc/auth/profile');
assert.strictEqual(calls[1].options.headers.Authorization, 'Bearer server-token'); assert.strictEqual(calls[1].headers.Authorization, 'Bearer server-token');
await client.updateProfile({ await client.updateProfile({
nickName: '张三', nickName: '张三',
@@ -65,12 +67,12 @@ async function run() {
cityCode: '510100', cityCode: '510100',
districtCode: '510104' districtCode: '510104'
}); });
assert.strictEqual(calls[2].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/profile'); assert.strictEqual(calls[2].url, '/genealogy/pc/auth/profile');
assert.strictEqual(calls[2].options.method, 'PUT'); assert.strictEqual(calls[2].method, 'put');
await client.uploadFile('file-content'); await client.uploadFile('file-content');
assert.strictEqual(calls[3].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/files/upload'); assert.strictEqual(calls[3].url, '/genealogy/pc/files/upload');
assert.strictEqual(calls[3].options.headers['Content-Type'], undefined); assert.strictEqual(calls[3].headers['Content-Type'], undefined);
await client.initResumableUpload({ await client.initResumableUpload({
fileName: 'cover.jpg', fileName: 'cover.jpg',
@@ -79,10 +81,11 @@ async function run() {
chunkSize: 4194304, chunkSize: 4194304,
totalChunks: 1 totalChunks: 1
}); });
assert.strictEqual(calls[4].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/files/resumable/init'); assert.strictEqual(calls[4].url, '/genealogy/pc/files/resumable/init');
await client.getRegionChildren('51'); await client.getRegionChildren('51');
assert.strictEqual(calls[5].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/region/children?parentCode=51'); assert.strictEqual(calls[5].url, '/genealogy/region/children');
assert.deepStrictEqual(calls[5].params, { parentCode: '51' });
await client.captchaRequire({ await client.captchaRequire({
sceneCode: 'WEB_H5_LOGIN', sceneCode: 'WEB_H5_LOGIN',
@@ -90,31 +93,37 @@ async function run() {
}); });
assert.strictEqual( assert.strictEqual(
calls[6].url, calls[6].url,
'https://test-genealogy-api.ddxcjp.cn/captcha/require?tenantId=tenant-x&clientId=client-x&sceneCode=WEB_H5_LOGIN&subject=13800000000' '/captcha/require'
); );
assert.strictEqual(calls[6].options.headers.Authorization, undefined); assert.deepStrictEqual(calls[6].params, {
tenantId: 'tenant-x',
clientId: 'client-x',
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000'
});
assert.strictEqual(calls[6].headers.Authorization, undefined);
await client.loginBySms({ await client.loginBySms({
phone: '13800000000', phone: '13800000000',
smsCode: '6666', smsCode: '6666',
validToken: 'sms-ticket' validToken: 'sms-ticket'
}); });
assert.strictEqual(calls[7].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/login/sms'); assert.strictEqual(calls[7].url, '/genealogy/pc/auth/login/sms');
assert.strictEqual(calls[7].options.headers.Authorization, undefined); assert.strictEqual(calls[7].headers.Authorization, undefined);
assert.strictEqual(JSON.parse(calls[7].options.body).grantType, 'sms'); assert.strictEqual(calls[7].data.grantType, 'sms');
assert.strictEqual(JSON.parse(calls[7].options.body).tenantId, 'tenant-x'); assert.strictEqual(calls[7].data.tenantId, 'tenant-x');
assert.strictEqual(JSON.parse(calls[7].options.body).clientId, 'client-x'); assert.strictEqual(calls[7].data.clientId, 'client-x');
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[8].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/sms/code'); assert.strictEqual(calls[8].url, '/genealogy/pc/auth/sms/code');
assert.strictEqual(calls[8].options.headers.Authorization, undefined); assert.strictEqual(calls[8].headers.Authorization, undefined);
assert.strictEqual(JSON.parse(calls[8].options.body).grantType, 'sms'); assert.strictEqual(calls[8].data.grantType, 'sms');
assert.strictEqual(JSON.parse(calls[8].options.body).sceneCode, 'WEB_H5_LOGIN'); assert.strictEqual(calls[8].data.sceneCode, 'WEB_H5_LOGIN');
assert.strictEqual(JSON.parse(calls[8].options.body).validToken, 'captcha-ticket'); assert.strictEqual(calls[8].data.validToken, 'captcha-ticket');
assert.throws( assert.throws(
() => client.listGenealogies(), () => client.listGenealogies(),
+45
View File
@@ -0,0 +1,45 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const rootDirectory = path.join(__dirname, '..');
function scriptIndex(html, source) {
return html.indexOf(`src="${source}"`);
}
function run() {
const pages = fs.readdirSync(rootDirectory).filter((fileName) => {
if (!fileName.endsWith('.html')) return false;
const html = fs.readFileSync(path.join(rootDirectory, fileName), 'utf8');
return html.includes('public/js/api-client.js') || html.includes('utils/ApiClient.js');
});
assert.strictEqual(pages.length, 45, '接口客户端加载页面数量应保持为 45 页');
pages.forEach((fileName) => {
const html = fs.readFileSync(path.join(rootDirectory, fileName), 'utf8');
const configIndex = scriptIndex(html, 'config.js');
const storageIndex = scriptIndex(html, 'utils/StorageUtil.js');
const axiosIndex = scriptIndex(html, 'utils/axios.js');
const requestIndex = scriptIndex(html, 'utils/AxiosRequestUtil.js');
const apiIndex = scriptIndex(html, 'utils/ApiClient.js');
assert.notStrictEqual(configIndex, -1, `${fileName} 应加载环境配置`);
assert.notStrictEqual(storageIndex, -1, `${fileName} 应加载 token 存储工具`);
assert.notStrictEqual(axiosIndex, -1, `${fileName} 应加载本地 Axios`);
assert.notStrictEqual(requestIndex, -1, `${fileName} 应加载 AxiosRequestUtil`);
assert.notStrictEqual(apiIndex, -1, `${fileName} 应加载 ApiClient`);
assert.ok(configIndex < storageIndex, `${fileName} 应先加载环境配置再加载存储工具`);
assert.ok(storageIndex < axiosIndex, `${fileName} 应先加载存储工具再加载 Axios`);
assert.ok(axiosIndex < requestIndex, `${fileName} 应先加载 Axios 再加载请求封装`);
assert.ok(requestIndex < apiIndex, `${fileName} 应先加载请求封装再加载接口客户端`);
assert.strictEqual(html.includes('utils/RequestUtil.js'), false, `${fileName} 不应继续加载旧 RequestUtil`);
assert.strictEqual(html.includes('public/js/api-client.js'), false, `${fileName} 不应继续加载旧 ApiClient`);
});
console.log('api-script-order tests passed');
}
run();
+6 -3
View File
@@ -16,15 +16,18 @@ function run() {
pages.forEach((fileName) => { pages.forEach((fileName) => {
const html = readPage(fileName); const html = readPage(fileName);
const formCaptchaBox = /<form[\s\S]*?data-captcha-box[\s\S]*?<\/form>/i.test(html); const formCaptchaBox = /<form[\s\S]*?data-captcha-box[\s\S]*?<\/form>/i.test(html);
const pageModalBox = /class="[^"]*\bauth-captcha-modal\b[^"]*"[^>]*data-captcha-box/.test(html) const pageCaptchaBox = /data-captcha-box/.test(html);
|| /data-captcha-box[^>]*class="[^"]*\bauth-captcha-modal\b[^"]*"/.test(html);
const hasLayuiCss = html.includes('href="public/layui/css/layui.css"'); const hasLayuiCss = html.includes('href="public/layui/css/layui.css"');
const hasTacCss = html.includes('href="public/tac/css/tac.css"');
const hasCaptchaModalCss = html.includes('href="public/css/captcha-modal.css"');
const layuiScriptIndex = html.indexOf('src="public/layui/layui.js"'); const layuiScriptIndex = html.indexOf('src="public/layui/layui.js"');
const authScriptIndex = html.indexOf('src="public/js/auth-pages.js"'); const authScriptIndex = html.indexOf('src="public/js/auth-pages.js"');
assert.strictEqual(formCaptchaBox, false, `${fileName} 不应把 TAC 挂载点放在表单内部`); assert.strictEqual(formCaptchaBox, false, `${fileName} 不应把 TAC 挂载点放在表单内部`);
assert.strictEqual(pageModalBox, true, `${fileName} 应提供页面级 TAC 弹窗挂载点`); assert.strictEqual(pageCaptchaBox, false, `${fileName} 不应保留会将 TAC 渲染到页面底部的静态挂载点`);
assert.strictEqual(hasLayuiCss, true, `${fileName} 应加载 layui.css 作为 layer 提示样式依赖`); assert.strictEqual(hasLayuiCss, true, `${fileName} 应加载 layui.css 作为 layer 提示样式依赖`);
assert.strictEqual(hasTacCss, true, `${fileName} 应加载 TAC 自带样式`);
assert.strictEqual(hasCaptchaModalCss, false, `${fileName} 不应加载自定义验证码样式`);
assert.notStrictEqual(layuiScriptIndex, -1, `${fileName} 应加载 layui.js 使用 layer.msg`); assert.notStrictEqual(layuiScriptIndex, -1, `${fileName} 应加载 layui.js 使用 layer.msg`);
assert.ok(layuiScriptIndex < authScriptIndex, `${fileName} 应在 auth-pages.js 之前加载 layui.js`); assert.ok(layuiScriptIndex < authScriptIndex, `${fileName} 应在 auth-pages.js 之前加载 layui.js`);
}); });
+44 -8
View File
@@ -1,4 +1,6 @@
const assert = require('assert'); const assert = require('assert');
const fs = require('fs');
const path = require('path');
const CaptchaPages = require('../public/js/captcha-pages.js'); const CaptchaPages = require('../public/js/captcha-pages.js');
const fakeApi = { const fakeApi = {
@@ -7,6 +9,14 @@ const fakeApi = {
}; };
function run() { function run() {
const captchaSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'captcha-pages.js'), 'utf8');
assert.strictEqual(captchaSource.includes('auth-captcha-modal'), false, '验证码适配层不应控制自定义弹窗样式');
assert.strictEqual(captchaSource.includes('bgUrl:'), false, '验证码适配层不应覆盖 TAC 背景样式');
assert.strictEqual(captchaSource.includes('logoUrl:'), false, '验证码适配层不应覆盖 TAC Logo 样式');
assert.strictEqual(captchaSource.includes('root.layui.layer.open'), true, '验证码适配层应使用 Layui 默认弹层承载 TAC');
assert.strictEqual(captchaSource.includes('root.layui.layer.close'), true, '验证码结束时应关闭对应 Layui 弹层');
assert.deepStrictEqual(CaptchaPages.buildChallengeBody({ assert.deepStrictEqual(CaptchaPages.buildChallengeBody({
sceneCode: 'WEB_H5_LOGIN', sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000' subject: '13800000000'
@@ -81,23 +91,49 @@ function run() {
required: true required: true
}), true); }), true);
const modalBox = { nodeName: 'modal' };
const formBox = { nodeName: 'form-box' };
const originalDocument = global.document; const originalDocument = global.document;
const originalLayui = global.layui;
const mountedBox = {
innerHTML: '<div class="tianai-captcha-parent"></div>'
};
let layerOptions;
let closedLayerIndex;
global.document = { global.document = {
querySelector(selector) { querySelector(selector) {
return selector === '.auth-captcha-modal[data-captcha-box]' ? modalBox : null; assert.strictEqual(selector, '[data-captcha-layer-box="captcha-layer-1"]');
return mountedBox;
}
};
global.layui = {
layer: {
open(options) {
layerOptions = options;
return 7;
},
close(index) {
closedLayerIndex = index;
}
} }
}; };
assert.strictEqual(CaptchaPages.getCaptchaBox({ const captchaLayer = CaptchaPages.createCaptchaLayer();
querySelector(selector) { assert.strictEqual(captchaLayer.box, mountedBox);
return selector === '[data-captcha-box]' ? formBox : null; assert.strictEqual(captchaLayer.layerIndex, 7);
} assert.strictEqual(layerOptions.type, 1);
}), modalBox); assert.strictEqual(layerOptions.title, false);
assert.strictEqual(layerOptions.closeBtn, 0);
assert.deepStrictEqual(layerOptions.area, ['318px', '318px']);
assert.strictEqual(layerOptions.offset, 'auto');
assert.strictEqual(typeof layerOptions.content, 'string');
assert.strictEqual(layerOptions.content.includes('data-captcha-layer-box="captcha-layer-1"'), true);
CaptchaPages.closeCaptchaLayer(captchaLayer);
assert.strictEqual(mountedBox.innerHTML, '');
assert.strictEqual(closedLayerIndex, 7);
global.document = originalDocument; global.document = originalDocument;
global.layui = originalLayui;
console.log('captcha-pages tests passed'); console.log('captcha-pages tests passed');
} }
+20 -16
View File
@@ -1,26 +1,30 @@
const assert = require('assert'); const assert = require('assert');
const configFactory = require('../config.js'); const configFactory = require('../config.js');
const store = {}; const developmentConfig = configFactory({
const config = configFactory({ location: {
localStorage: { hostname: '127.0.0.1',
getItem: (key) => Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null, port: '5501'
setItem: (key, value) => {
store[key] = String(value);
}
} }
}); });
assert.strictEqual(config.getApiBaseUrl(), 'http://test-genealogy-api.ddxcjp.cn'); assert.deepStrictEqual(developmentConfig.getConfig(), {
assert.strictEqual(config.getClientId(), 'ced7e5f0498645c6ec642dcf450b036f'); environment: 'development',
assert.strictEqual(config.getTenantId(), '000000'); apiBaseUrl: 'http://test-genealogy-api.ddxcjp.cn'
assert.strictEqual(config.getTokenKey(), 'genealogy_auth_token'); });
assert.strictEqual(config.getConfig().tokenHeaderName, 'Authorization'); assert.strictEqual(developmentConfig.getClientId, undefined);
assert.strictEqual(developmentConfig.getTenantId, undefined);
assert.strictEqual(developmentConfig.getTokenKey, undefined);
assert.strictEqual(developmentConfig.getTokenHeaderName, undefined);
store.genealogy_api_base_url = 'https://custom.example.test'; const productionConfig = configFactory({
assert.strictEqual(config.getApiBaseUrl(), 'https://custom.example.test'); location: {
hostname: 'genealogy.example.test',
port: ''
}
});
store.genealogy_api_base_url = 'https://test-genealogy-api.ddxcjp.cn'; assert.strictEqual(productionConfig.getEnvironment(), 'production');
assert.strictEqual(config.getApiBaseUrl(), 'http://test-genealogy-api.ddxcjp.cn'); assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://test-genealogy-api.ddxcjp.cn');
console.log('config tests passed'); console.log('config tests passed');
+15 -12
View File
@@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const StorageUtil = require('../utils/StorageUtil.js'); const StorageUtil = require('../utils/StorageUtil.js');
const FormUtil = require('../utils/FormUtil.js'); const FormUtil = require('../utils/FormUtil.js');
const RequestUtil = require('../utils/RequestUtil.js'); const AxiosRequestUtil = require('../utils/AxiosRequestUtil.js');
assert.strictEqual(FormUtil.trimOrUndefined(' 张三 '), '张三'); assert.strictEqual(FormUtil.trimOrUndefined(' 张三 '), '张三');
assert.strictEqual(FormUtil.trimOrUndefined(' '), undefined); assert.strictEqual(FormUtil.trimOrUndefined(' '), undefined);
@@ -27,19 +27,20 @@ StorageUtil.remove(storage, 'token');
assert.strictEqual(StorageUtil.read(storage, 'token'), null); assert.strictEqual(StorageUtil.read(storage, 'token'), null);
const calls = []; const calls = [];
const requester = RequestUtil.createRequester({ const requester = AxiosRequestUtil.createRequester({
baseUrl: 'https://test-genealogy-api.ddxcjp.cn', baseUrl: 'https://test-genealogy-api.ddxcjp.cn',
clientId: 'pc-client', clientId: 'pc-client',
tokenHeaderName: 'Authorization', tokenHeaderName: 'Authorization',
getToken: () => 'token-x', getToken: () => 'token-x',
fetchImpl: async (url, options) => { axiosInstance: {
calls.push({ url, options }); request: async (config) => {
calls.push(config);
return { return {
ok: true, status: 200,
status: 200, data: { code: 200, data: { ok: true } }
json: async () => ({ code: 200, data: { ok: true } }) };
}; }
} }
}); });
@@ -47,8 +48,10 @@ requester('GET', '/genealogy/pc/auth/profile', {
query: { keyword: '汤 氏' } query: { keyword: '汤 氏' }
}).then((data) => { }).then((data) => {
assert.deepStrictEqual(data, { ok: true }); assert.deepStrictEqual(data, { ok: true });
assert.strictEqual(calls[0].url, 'https://test-genealogy-api.ddxcjp.cn/genealogy/pc/auth/profile?keyword=%E6%B1%A4+%E6%B0%8F'); assert.strictEqual(calls[0].method, 'get');
assert.strictEqual(calls[0].options.headers.clientid, 'pc-client'); assert.strictEqual(calls[0].url, '/genealogy/pc/auth/profile');
assert.strictEqual(calls[0].options.headers.Authorization, 'Bearer token-x'); assert.deepStrictEqual(calls[0].params, { keyword: '汤 氏' });
assert.strictEqual(calls[0].headers.clientid, 'pc-client');
assert.strictEqual(calls[0].headers.Authorization, 'Bearer token-x');
console.log('utils tests passed'); console.log('utils tests passed');
}); });
+70 -149
View File
@@ -1,5 +1,5 @@
(function (root, factory) { (function (root, factory) {
// 同一份接口客户端同时支持浏览器页面和 Node 测试环境 // PC 接口客户端同时支持浏览器页面和 Node 单元测试加载
if (typeof module === 'object' && module.exports) { if (typeof module === 'object' && module.exports) {
module.exports = factory(root); module.exports = factory(root);
return; return;
@@ -9,7 +9,6 @@
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict'; 'use strict';
var DEFAULT_BASE_URL = 'http://test-genealogy-api.ddxcjp.cn';
var DEFAULT_CLIENT_ID = 'ced7e5f0498645c6ec642dcf450b036f'; var DEFAULT_CLIENT_ID = 'ced7e5f0498645c6ec642dcf450b036f';
var DEFAULT_TENANT_ID = '000000'; var DEFAULT_TENANT_ID = '000000';
var DEFAULT_TOKEN_KEY = 'genealogy_auth_token'; var DEFAULT_TOKEN_KEY = 'genealogy_auth_token';
@@ -26,20 +25,18 @@
} }
function getConfigApi() { function getConfigApi() {
var configFactory;
if (root.GenealogyConfig) return root.GenealogyConfig; if (root.GenealogyConfig) return root.GenealogyConfig;
configFactory = loadNodeModule('../../config.js'); var configFactory = loadNodeModule('../config.js');
return configFactory ? configFactory(root) : null; return configFactory ? configFactory(root) : null;
} }
function getStorageUtil() { function getStorageUtil() {
return root.StorageUtil || loadNodeModule('../../utils/StorageUtil.js'); return root.StorageUtil || loadNodeModule('./StorageUtil.js');
} }
function getRequestUtil() { function getAxiosRequestUtil() {
return root.RequestUtil || loadNodeModule('../../utils/RequestUtil.js'); return root.AxiosRequestUtil || loadNodeModule('./AxiosRequestUtil.js');
} }
function getStorage(store) { function getStorage(store) {
@@ -53,7 +50,7 @@
} }
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.tokenValue || '';
} }
@@ -79,12 +76,11 @@
function createFormData(fileOrFormData) { function createFormData(fileOrFormData) {
var FormDataCtor = root.FormData; var FormDataCtor = root.FormData;
var formData;
if (FormDataCtor && fileOrFormData instanceof FormDataCtor) return fileOrFormData; if (FormDataCtor && fileOrFormData instanceof FormDataCtor) return fileOrFormData;
if (!FormDataCtor) throw new ApiError('当前环境不支持文件上传'); if (!FormDataCtor) throw new ApiError('当前环境不支持文件上传');
formData = new FormDataCtor(); var formData = new FormDataCtor();
formData.append('file', fileOrFormData); formData.append('file', fileOrFormData);
return formData; return formData;
} }
@@ -92,12 +88,11 @@
function createChunkFormData(body) { function createChunkFormData(body) {
var FormDataCtor = root.FormData; var FormDataCtor = root.FormData;
var source = body || {}; var source = body || {};
var formData;
if (FormDataCtor && body instanceof FormDataCtor) return body; if (FormDataCtor && body instanceof FormDataCtor) return body;
if (!FormDataCtor) throw new ApiError('当前环境不支持文件上传'); if (!FormDataCtor) throw new ApiError('当前环境不支持文件上传');
formData = new FormDataCtor(); var formData = new FormDataCtor();
formData.append('uploadId', source.uploadId); formData.append('uploadId', source.uploadId);
formData.append('chunkIndex', source.chunkIndex); formData.append('chunkIndex', source.chunkIndex);
formData.append('chunkMd5', source.chunkMd5); formData.append('chunkMd5', source.chunkMd5);
@@ -105,22 +100,37 @@
return formData; return formData;
} }
function buildApiUrl(baseUrl, path, query) {
var url = String(baseUrl || '').replace(/\/+$/g, '') + '/' + String(path || '').replace(/^\/+/, '');
var params = new URLSearchParams();
Object.keys(query || {}).forEach(function (key) {
var value = query[key];
if (value !== undefined && value !== null && value !== '') params.append(key, value);
});
return params.toString() ? url + '?' + params.toString() : url;
}
function createClient(options) { function createClient(options) {
var settings = options || {}; var settings = options || {};
var configApi = getConfigApi(); var configApi = getConfigApi();
var config = configApi && configApi.getConfig ? configApi.getConfig() : {}; var config = configApi && configApi.getConfig ? configApi.getConfig() : {};
var storageUtil = getStorageUtil(); var storageUtil = getStorageUtil();
var requestUtil = getRequestUtil(); var axiosRequestUtil = getAxiosRequestUtil();
var store = getStorage(settings.tokenStore || settings.storage); var store = getStorage(settings.tokenStore || settings.storage);
var clientId = settings.clientId || config.clientId || DEFAULT_CLIENT_ID; var clientId = settings.clientId || config.clientId || DEFAULT_CLIENT_ID;
var tenantId = settings.tenantId || config.tenantId || DEFAULT_TENANT_ID; var tenantId = settings.tenantId || config.tenantId || DEFAULT_TENANT_ID;
var tokenKey = settings.tokenKey || config.tokenKey || DEFAULT_TOKEN_KEY; var tokenKey = settings.tokenKey || config.tokenKey || DEFAULT_TOKEN_KEY;
var tokenHeaderName = settings.tokenHeaderName || config.tokenHeaderName || DEFAULT_TOKEN_HEADER_NAME; var tokenHeaderName = settings.tokenHeaderName || config.tokenHeaderName || DEFAULT_TOKEN_HEADER_NAME;
var baseUrl = settings.baseUrl || config.apiBaseUrl || DEFAULT_BASE_URL; var baseUrl = settings.baseUrl || config.apiBaseUrl;
var requester;
if (!requestUtil || !requestUtil.createRequester) { if (!axiosRequestUtil || !axiosRequestUtil.createRequester) {
throw new ApiError('缺少 RequestUtil.createRequester 公共请求工具'); throw new ApiError('缺少 AxiosRequestUtil.createRequester 公共请求工具');
}
if (!baseUrl) {
throw new ApiError('缺少接口基础地址,请检查根目录 config.js');
} }
function getToken() { function getToken() {
@@ -135,12 +145,12 @@
if (storageUtil && storageUtil.remove) storageUtil.remove(store, tokenKey); if (storageUtil && storageUtil.remove) storageUtil.remove(store, tokenKey);
} }
requester = requestUtil.createRequester({ var requester = axiosRequestUtil.createRequester({
baseUrl: baseUrl, baseUrl: baseUrl,
clientId: clientId, clientId: clientId,
tokenHeaderName: tokenHeaderName, tokenHeaderName: tokenHeaderName,
getToken: getToken, getToken: getToken,
fetchImpl: settings.fetchImpl || root.fetch axiosInstance: settings.axiosInstance || root.axios
}); });
function request(method, path, requestOptions) { function request(method, path, requestOptions) {
@@ -157,11 +167,8 @@
async function login(body) { async function login(body) {
var data = await request('POST', '/genealogy/pc/auth/login', { var data = await request('POST', '/genealogy/pc/auth/login', {
auth: false, auth: false,
body: Object.assign({ body: Object.assign({ grantType: 'password' }, withTenant(body))
grantType: 'password'
}, withTenant(body))
}); });
setToken(getTokenFromResponse(data)); setToken(getTokenFromResponse(data));
return data; return data;
} }
@@ -169,12 +176,8 @@
async function register(body) { async function register(body) {
var data = await request('POST', '/genealogy/pc/auth/register', { var data = await request('POST', '/genealogy/pc/auth/register', {
auth: false, auth: false,
body: Object.assign({ body: Object.assign({ grantType: 'password', registerSource: 'PC' }, withTenant(body))
grantType: 'password',
registerSource: 'PC'
}, withTenant(body))
}); });
setToken(getTokenFromResponse(data)); setToken(getTokenFromResponse(data));
return data; return data;
} }
@@ -182,11 +185,8 @@
async function loginBySms(body) { async function loginBySms(body) {
var data = await request('POST', '/genealogy/pc/auth/login/sms', { var data = await request('POST', '/genealogy/pc/auth/login/sms', {
auth: false, auth: false,
body: Object.assign({ body: Object.assign({ grantType: 'sms' }, withTenant(body))
grantType: 'sms'
}, withTenant(body))
}); });
setToken(getTokenFromResponse(data)); setToken(getTokenFromResponse(data));
return data; return data;
} }
@@ -194,9 +194,7 @@
function sendSmsCode(body) { function sendSmsCode(body) {
return request('POST', '/genealogy/pc/auth/sms/code', { return request('POST', '/genealogy/pc/auth/sms/code', {
auth: false, auth: false,
body: Object.assign({ body: Object.assign({ grantType: 'sms' }, withTenant(body))
grantType: 'sms'
}, withTenant(body))
}); });
} }
@@ -214,14 +212,6 @@
}); });
} }
function buildApiUrl(path, query) {
if (requestUtil.joinUrl && requestUtil.appendQuery) {
return requestUtil.appendQuery(requestUtil.joinUrl(baseUrl, path), query);
}
return baseUrl + path;
}
var client = { var client = {
clientId: clientId, clientId: clientId,
tenantId: tenantId, tenantId: tenantId,
@@ -230,18 +220,20 @@
setToken: setToken, setToken: setToken,
clearToken: clearToken, clearToken: clearToken,
request: request, request: request,
buildApiUrl: buildApiUrl, buildApiUrl: function (path, query) {
return buildApiUrl(baseUrl, path, query);
},
get: function (path, query) { get: function (path, query) {
return request('GET', path, { query: query }); return request('GET', path, { query: query });
}, },
post: function (path, body, reqOptions) { post: function (path, body, requestOptions) {
return request('POST', path, Object.assign({}, reqOptions || {}, { body: body })); return request('POST', path, Object.assign({}, requestOptions || {}, { body: body }));
}, },
put: function (path, body, reqOptions) { put: function (path, body, requestOptions) {
return request('PUT', path, Object.assign({}, reqOptions || {}, { body: body })); return request('PUT', path, Object.assign({}, requestOptions || {}, { body: body }));
}, },
delete: function (path, reqOptions) { delete: function (path, requestOptions) {
return request('DELETE', path, reqOptions || {}); return request('DELETE', path, requestOptions || {});
}, },
login: login, login: login,
register: register, register: register,
@@ -263,9 +255,7 @@
resetPassword: function (body) { resetPassword: function (body) {
return request('PUT', '/genealogy/pc/auth/password/reset', { return request('PUT', '/genealogy/pc/auth/password/reset', {
auth: false, auth: false,
body: Object.assign({ body: Object.assign({ tenantId: tenantId }, body || {})
tenantId: tenantId
}, body || {})
}); });
}, },
changePhone: function (body) { changePhone: function (body) {
@@ -273,28 +263,22 @@
}, },
deactivateAccount: async function (body) { deactivateAccount: async function (body) {
var data = await request('POST', '/genealogy/pc/auth/account/deactivate', { body: body }); var data = await request('POST', '/genealogy/pc/auth/account/deactivate', { body: body });
clearToken(); clearToken();
return data; return data;
}, },
logout: async function () { logout: async function () {
var data = await request('DELETE', '/genealogy/pc/auth/logout'); var data = await request('DELETE', '/genealogy/pc/auth/logout');
clearToken(); clearToken();
return data; return data;
}, },
uploadFile: function (fileOrFormData) { uploadFile: function (fileOrFormData) {
return request('POST', '/genealogy/pc/files/upload', { return request('POST', '/genealogy/pc/files/upload', { body: createFormData(fileOrFormData) });
body: createFormData(fileOrFormData)
});
}, },
initResumableUpload: function (body) { initResumableUpload: function (body) {
return request('POST', '/genealogy/pc/files/resumable/init', { body: body }); return request('POST', '/genealogy/pc/files/resumable/init', { body: body });
}, },
uploadChunk: function (body) { uploadChunk: function (body) {
return request('POST', '/genealogy/pc/files/resumable/chunk', { return request('POST', '/genealogy/pc/files/resumable/chunk', { body: createChunkFormData(body) });
body: createChunkFormData(body)
});
}, },
uploadResumableChunk: function (body) { uploadResumableChunk: function (body) {
return this.uploadChunk(body); return this.uploadChunk(body);
@@ -309,11 +293,7 @@
return request('DELETE', '/genealogy/pc/files/reference', { query: query }); return request('DELETE', '/genealogy/pc/files/reference', { query: query });
}, },
getRegionChildren: function (parentCode) { getRegionChildren: function (parentCode) {
return request('GET', '/genealogy/region/children', { return request('GET', '/genealogy/region/children', { query: { parentCode: parentCode } });
query: {
parentCode: parentCode
}
});
}, },
regionChildren: function (parentCode) { regionChildren: function (parentCode) {
return this.getRegionChildren(parentCode); return this.getRegionChildren(parentCode);
@@ -349,87 +329,20 @@
} }
}; };
// PC YAML 未提供这些业务 paths,保留方法名只用于阻止旧移动端路径继续发请求。
[ [
'listGenealogies', 'listGenealogies', 'createGenealogy', 'publicGenealogies', 'myGenealogies', 'genealogyDetail', 'genealogyOverview',
'createGenealogy', 'applyJoinGenealogy', 'myJoinApplies', 'pendingJoinApplies', 'auditJoinApply', 'cancelJoinApply', 'genealogyMembers',
'publicGenealogies', 'genealogyMemberOptions', 'updateGenealogyMember', 'removeGenealogyMember', 'leaveGenealogy', 'transferGenealogyOwner',
'myGenealogies', 'articleCategories', 'articles', 'articleDetail', 'createArticle', 'updateArticle', 'feeds', 'feedsPage', 'feedDetail',
'genealogyDetail', 'createFeed', 'updateFeed', 'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
'genealogyOverview', 'deleteFeedComment', 'albums', 'createAlbum', 'updateAlbum', 'albumPhotos', 'createAlbumPhoto', 'ceremonies',
'applyJoinGenealogy', 'ceremonyDetail', 'createCeremony', 'updateCeremony', 'ceremonyGifts', 'createCeremonyGift', 'meritRecords',
'myJoinApplies', 'createMeritRecord', 'growthRecords', 'growthRecordDetail', 'createGrowthRecord', 'updateGrowthRecord', 'memos',
'pendingJoinApplies', 'memoDetail', 'createMemo', 'updateMemo', 'vipPackages', 'vipOrders', 'createVipOrder', 'generationPoems',
'auditJoinApply', 'createGenerationPoem', 'updateGenerationPoem', 'previewGenerationPoems', 'saveGenerationPoemsBatch', 'lineageTree',
'cancelJoinApply', 'lineagePersons', 'lineagePersonsPage', 'lineagePersonOptions', 'lineagePersonDetail', 'createLineagePerson',
'genealogyMembers', 'updateLineagePerson', 'deleteLineagePerson', 'addLineageRelation', 'notifications', 'markNotificationRead',
'genealogyMemberOptions', 'markAllNotificationsRead', 'helpArticles', 'helpArticleDetail', 'promotions', 'feedbackList', 'submitFeedback'
'updateGenealogyMember',
'removeGenealogyMember',
'leaveGenealogy',
'transferGenealogyOwner',
'articleCategories',
'articles',
'articleDetail',
'createArticle',
'updateArticle',
'feeds',
'feedsPage',
'feedDetail',
'createFeed',
'updateFeed',
'likeFeed',
'unlikeFeed',
'feedComments',
'feedCommentsPage',
'createFeedComment',
'deleteFeedComment',
'albums',
'createAlbum',
'updateAlbum',
'albumPhotos',
'createAlbumPhoto',
'ceremonies',
'ceremonyDetail',
'createCeremony',
'updateCeremony',
'ceremonyGifts',
'createCeremonyGift',
'meritRecords',
'createMeritRecord',
'growthRecords',
'growthRecordDetail',
'createGrowthRecord',
'updateGrowthRecord',
'memos',
'memoDetail',
'createMemo',
'updateMemo',
'vipPackages',
'vipOrders',
'createVipOrder',
'generationPoems',
'createGenerationPoem',
'updateGenerationPoem',
'previewGenerationPoems',
'saveGenerationPoemsBatch',
'lineageTree',
'lineagePersons',
'lineagePersonsPage',
'lineagePersonOptions',
'lineagePersonDetail',
'createLineagePerson',
'updateLineagePerson',
'deleteLineagePerson',
'addLineageRelation',
'notifications',
'markNotificationRead',
'markAllNotificationsRead',
'helpArticles',
'helpArticleDetail',
'promotions',
'feedbackList',
'submitFeedback'
].forEach(function (name) { ].forEach(function (name) {
client[name] = createUnavailableMethod(name); client[name] = createUnavailableMethod(name);
}); });
@@ -437,13 +350,21 @@
return client; return client;
} }
function createDefaultClient() {
// Node 测试环境可无全局 Axios,浏览器页面在加载顺序正确时会创建默认客户端。
try {
return createClient();
} catch (error) {
return null;
}
}
return { return {
DEFAULT_BASE_URL: DEFAULT_BASE_URL,
DEFAULT_CLIENT_ID: DEFAULT_CLIENT_ID, DEFAULT_CLIENT_ID: DEFAULT_CLIENT_ID,
DEFAULT_TENANT_ID: DEFAULT_TENANT_ID, DEFAULT_TENANT_ID: DEFAULT_TENANT_ID,
TOKEN_KEY: DEFAULT_TOKEN_KEY, TOKEN_KEY: DEFAULT_TOKEN_KEY,
ApiError: ApiError, ApiError: ApiError,
createClient: createClient, createClient: createClient,
defaultClient: createClient() defaultClient: createDefaultClient()
}; };
}); });
@@ -1,62 +1,32 @@
(function (root, factory) { (function (root, factory) {
// 请求工具封装 URL、Header 和响应解包,业务路径由 api-client 统一提供 // Axios 请求工具同时支持浏览器页面和 Node 单元测试加载
if (typeof module === 'object' && module.exports) { if (typeof module === 'object' && module.exports) {
module.exports = factory(root); module.exports = factory(root);
return; return;
} }
root.RequestUtil = factory(root); root.AxiosRequestUtil = factory(root);
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict'; 'use strict';
function trimSlashes(value) {
return String(value || '').replace(/^\/+|\/+$/g, '');
}
function joinUrl(baseUrl, path) {
var base = String(baseUrl || '').replace(/\/+$/g, '');
var cleanPath = '/' + trimSlashes(path);
return base + cleanPath;
}
function appendQuery(url, query) {
if (!query) return url;
var params = new URLSearchParams();
Object.keys(query).forEach(function (key) {
var value = query[key];
if (value === undefined || value === null || value === '') return;
params.append(key, value);
});
var queryText = params.toString();
return queryText ? url + '?' + queryText : url;
}
function isFormData(body) { function isFormData(body) {
return typeof FormData !== 'undefined' && body instanceof FormData; return typeof FormData !== 'undefined' && body instanceof FormData;
} }
function createHttpError(message, detail) { function createHttpError(message, detail) {
var error = new Error(message); var error = new Error(message);
Object.keys(detail || {}).forEach(function (key) { Object.keys(detail || {}).forEach(function (key) {
error[key] = detail[key]; error[key] = detail[key];
}); });
return error;
}
async function parseJson(response) { return error;
try {
return await response.json();
} catch (error) {
return null;
}
} }
function unwrapResponse(payload) { function unwrapResponse(payload) {
if (!payload || typeof payload !== 'object') return payload; if (!payload || typeof payload !== 'object') return payload;
if (payload.code !== undefined && payload.code !== 200) { if (payload.code !== undefined && Number(payload.code) !== 200) {
throw createHttpError(payload.msg || '接口请求失败', { throw createHttpError(payload.msg || '接口请求失败', {
code: payload.code, code: payload.code,
response: payload response: payload
@@ -74,47 +44,66 @@
return payload; return payload;
} }
function getAxiosInstance(options) {
var settings = options || {};
var axios = settings.axiosInstance || root.axios;
if (!axios || typeof axios.request !== 'function' && typeof axios.create !== 'function') {
throw new Error('缺少 Axios 公共依赖');
}
if (typeof axios.create === 'function') {
return axios.create({
baseURL: settings.baseUrl
});
}
return axios;
}
function createRequester(options) { function createRequester(options) {
var settings = options || {}; var settings = options || {};
var fetchImpl = settings.fetchImpl || root.fetch; var axiosInstance = getAxiosInstance(settings);
return async function request(method, path, requestOptions) { return async function request(method, path, requestOptions) {
var req = requestOptions || {}; var req = requestOptions || {};
var headers = Object.assign({}, req.headers || {}); var headers = Object.assign({}, req.headers || {});
var token = settings.getToken ? settings.getToken() : ''; var token = settings.getToken ? settings.getToken() : '';
var tokenHeaderName = settings.tokenHeaderName || 'Authorization'; var tokenHeaderName = settings.tokenHeaderName || 'Authorization';
var url = appendQuery(joinUrl(settings.baseUrl, path), req.query);
var body = req.body; var body = req.body;
var response;
headers.clientid = settings.clientId; headers.clientid = settings.clientId;
if (token && req.auth !== false) headers[tokenHeaderName] = 'Bearer ' + token; if (token && req.auth !== false) headers[tokenHeaderName] = 'Bearer ' + token;
if (body !== undefined && body !== null && !isFormData(body)) { if (body !== undefined && body !== null && !isFormData(body)) {
headers['Content-Type'] = headers['Content-Type'] || 'application/json'; headers['Content-Type'] = headers['Content-Type'] || 'application/json';
body = JSON.stringify(body);
} }
var response = await fetchImpl(url, { try {
method: method, response = await axiosInstance.request({
headers: headers, method: String(method || 'GET').toLowerCase(),
body: body url: path,
}); params: req.query,
var payload = await parseJson(response); data: body,
headers: headers
});
} catch (error) {
var responseData = error.response && error.response.data;
if (!response.ok) { throw createHttpError((responseData && responseData.msg) || error.message || '接口请求失败', {
throw createHttpError((payload && payload.msg) || '接口请求失败', { status: error.response && error.response.status,
status: response.status, response: responseData,
response: payload cause: error
}); });
} }
return unwrapResponse(payload); return unwrapResponse(response.data);
}; };
} }
return { return {
createRequester: createRequester, createRequester: createRequester,
joinUrl: joinUrl, unwrapResponse: unwrapResponse
appendQuery: appendQuery
}; };
}); });
+4288
View File
File diff suppressed because it is too large Load Diff