309598bfa6
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
377 lines
12 KiB
Markdown
377 lines
12 KiB
Markdown
# 家谱主页 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将 `profile-family-home.html` 从静态预览开放为真实 PC 家谱主页,展示当前家谱概览和世系树预览,并保留已接入业务页入口。
|
||
|
||
**Architecture:** `profile-common.js` 继续唯一拥有当前 `genealogyId`;页面只调用已有 `genealogyOverview(genealogyId)` 和 `lineageTree(genealogyId)`。`lineage-pages.js` 继续唯一拥有世系树节点规范化与 HTML 生成,新建 `family-home-pages.js` 只负责概览 DTO、权限显隐、并行读取和页面状态。
|
||
|
||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||
|
||
**Status:** 2026-07-29 已按 Task 1–4 实施;自动化与无家谱真实账号分支已验证,有家谱数据态等待具备真实家谱的账号复核。
|
||
|
||
## Global Constraints
|
||
|
||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||
- 只使用 PC 接口;不得调用 `/genealogy/dashboard/overview`、APP 或管理后台接口。
|
||
- `genealogyId` 只能来自 `ProfileUI.getGenealogyId()`,始终按字符串处理。
|
||
- `GET /genealogy/pc/genealogies/{genealogyId}/overview` 返回 `AppGenealogyVo`,它不是内容统计接口;不得制造文章、相册、视频或活动数量。
|
||
- 世系树只读调用 `GET /genealogy/pc/genealogies/{genealogyId}/lineage/tree`。
|
||
- 家谱管理入口仅在响应 `canManage=true` 时显示;内容页自身继续负责更细权限。
|
||
- PC 没有成员邀请创建/分享接口;主页不得开放“邀请家人”操作。
|
||
- 本批次没有写接口,不创建、修改或删除任何真实业务数据。
|
||
|
||
---
|
||
|
||
### Task 1: 家谱主页 DTO 与页面契约
|
||
|
||
**Files:**
|
||
- Create: `public/js/family-home-pages.js`
|
||
- Create: `tests/family-home-pages.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `ProfileUI.getGenealogyId()`、`GenealogyApi.defaultClient`
|
||
- Produces:
|
||
- `normalizeFamilyOverview(item, expectedGenealogyId)`
|
||
- `renderFamilyOverview(overview)`
|
||
- `shouldShowFamilyManagement(overview)`
|
||
- `shouldRedirectToLogin(api, error)`
|
||
|
||
- [ ] **Step 1: 写失败数据边界测试**
|
||
|
||
```js
|
||
const overview = FamilyHomePages.normalizeFamilyOverview({
|
||
genealogyId: '2062179707935264769',
|
||
genealogyNo: 'G20260729001',
|
||
genealogyName: '叶氏家谱',
|
||
surname: '叶',
|
||
ancestralHall: '南阳堂',
|
||
originPlace: '四川成都',
|
||
regionFullName: '四川省 成都市',
|
||
memberCount: 12,
|
||
personCount: 36,
|
||
status: '0',
|
||
canManage: true,
|
||
canEditContent: true,
|
||
ownerUserId: 'must-not-render',
|
||
coverOssId: 'must-not-render'
|
||
}, '2062179707935264769');
|
||
|
||
assert.deepEqual(overview, {
|
||
genealogyId: '2062179707935264769',
|
||
genealogyNo: 'G20260729001',
|
||
genealogyName: '叶氏家谱',
|
||
surname: '叶',
|
||
ancestralHall: '南阳堂',
|
||
originPlace: '四川成都',
|
||
regionFullName: '四川省 成都市',
|
||
memberCount: 12,
|
||
personCount: 36,
|
||
canManage: true,
|
||
canEditContent: true
|
||
});
|
||
assert.equal(
|
||
FamilyHomePages.normalizeFamilyOverview(
|
||
{ genealogyId: Number.MAX_SAFE_INTEGER + 1, genealogyName: '非法', status: '0' },
|
||
'2062179707935264769'
|
||
),
|
||
null
|
||
);
|
||
assert.equal(
|
||
FamilyHomePages.normalizeFamilyOverview(
|
||
{ genealogyId: '2', genealogyName: '串谱', status: '0' },
|
||
'2062179707935264769'
|
||
),
|
||
null
|
||
);
|
||
```
|
||
|
||
同时断言:
|
||
|
||
- `genealogyName` 为空、`status!='0'`、负数/非安全计数均拒绝;
|
||
- 渲染转义全部文本,不出现 `ownerUserId`、`coverOssId` 或原始 JSON;
|
||
- 只有布尔值 `canManage===true` 才开放管理入口;
|
||
- 401 清理登录态,403 不清理登录态。
|
||
|
||
- [ ] **Step 2: 运行 RED**
|
||
|
||
Run: `node --test tests/family-home-pages.test.js`
|
||
|
||
Expected: FAIL,`family-home-pages.js` 不存在。
|
||
|
||
- [ ] **Step 3: 最小实现概览规范化**
|
||
|
||
```js
|
||
function normalizeFamilyOverview(item, expectedGenealogyId) {
|
||
var source = item || {};
|
||
var genealogyId = normalizeId(source.genealogyId);
|
||
var expectedId = normalizeId(expectedGenealogyId);
|
||
var memberCount = normalizeCount(source.memberCount);
|
||
var personCount = normalizeCount(source.personCount);
|
||
|
||
if (!genealogyId || genealogyId !== expectedId ||
|
||
!text(source.genealogyName) || String(source.status) !== '0' ||
|
||
memberCount === null || personCount === null) return null;
|
||
return {
|
||
genealogyId: genealogyId,
|
||
genealogyNo: text(source.genealogyNo),
|
||
genealogyName: text(source.genealogyName),
|
||
surname: text(source.surname),
|
||
ancestralHall: text(source.ancestralHall),
|
||
originPlace: text(source.originPlace),
|
||
regionFullName: text(source.regionFullName),
|
||
memberCount: memberCount,
|
||
personCount: personCount,
|
||
canManage: source.canManage === true,
|
||
canEditContent: source.canEditContent === true
|
||
};
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行 GREEN**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
node --test tests/family-home-pages.test.js
|
||
node --check public/js/family-home-pages.js
|
||
```
|
||
|
||
Expected: PASS。
|
||
|
||
---
|
||
|
||
### Task 2: 复用世系树唯一渲染 owner
|
||
|
||
**Files:**
|
||
- Modify: `public/js/lineage-pages.js`
|
||
- Modify: `tests/lineage-pages.test.js`
|
||
- Modify: `tests/family-home-pages.test.js`
|
||
|
||
**Interfaces:**
|
||
- Produces: `LineagePages.renderLineageTreeHtml(data)`
|
||
- Consumes: 现有 `normalizeLineagePerson(item)`、`renderTreeNode(item, ancestry)`
|
||
|
||
- [ ] **Step 1: 写失败共享渲染测试**
|
||
|
||
```js
|
||
const tree = [{
|
||
personId: '2062179707935264770',
|
||
genealogyId: '2062179707935264769',
|
||
name: '<始祖>',
|
||
status: '0',
|
||
spouses: [],
|
||
children: []
|
||
}];
|
||
|
||
const html = LineagePages.renderLineageTreeHtml(tree);
|
||
assert.match(html, /<始祖>/);
|
||
assert.match(html, /data-lineage-person="2062179707935264770"/);
|
||
assert.doesNotMatch(html, /2062179707935264769/);
|
||
assert.match(LineagePages.renderLineageTreeHtml([]), /暂无世系树/);
|
||
```
|
||
|
||
- [ ] **Step 2: 运行 RED**
|
||
|
||
Run: `node --test tests/lineage-pages.test.js tests/family-home-pages.test.js`
|
||
|
||
Expected: FAIL,`renderLineageTreeHtml` 未导出。
|
||
|
||
- [ ] **Step 3: 从现有 `renderTree` 提取纯 HTML owner**
|
||
|
||
```js
|
||
function renderLineageTreeHtml(data) {
|
||
var nodes = normalizeList(data)
|
||
.map(function (item) { return renderTreeNode(item, {}); })
|
||
.filter(Boolean);
|
||
|
||
return nodes.length
|
||
? '<ul class="lineage-tree">' + nodes.join('') + '</ul>'
|
||
: '<div class="api-empty">暂无世系树</div>';
|
||
}
|
||
|
||
function renderTree(data) {
|
||
var container = query('[data-lineage-tree]');
|
||
if (container) container.innerHTML = renderLineageTreeHtml(data);
|
||
}
|
||
```
|
||
|
||
在 UMD 导出对象加入:
|
||
|
||
```js
|
||
renderLineageTreeHtml: renderLineageTreeHtml
|
||
```
|
||
|
||
- [ ] **Step 4: 运行 GREEN**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
node --test tests/lineage-pages.test.js tests/family-home-pages.test.js
|
||
node --check public/js/lineage-pages.js
|
||
```
|
||
|
||
Expected: PASS,现有世系管理页输出不变。
|
||
|
||
---
|
||
|
||
### Task 3: 开放真实家谱主页
|
||
|
||
**Files:**
|
||
- Modify: `profile-family-home.html`
|
||
- Modify: `public/js/family-home-pages.js`
|
||
- Modify: `tests/family-home-pages.test.js`
|
||
- Modify: `tests/pending-pages.test.js`
|
||
- Modify: `tests/stage6-navigation.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes:
|
||
- `api.genealogyOverview(genealogyId)`
|
||
- `api.lineageTree(genealogyId)`
|
||
- `LineagePages.renderLineageTreeHtml(data)`
|
||
- Produces: `initFamilyHomePage()`、`init()`
|
||
|
||
- [ ] **Step 1: 写失败页面测试**
|
||
|
||
断言:
|
||
|
||
- `profile-family-home.html` 不再包含 `data-feature-status="pending"` 或 `pending-pages.js`;
|
||
- 加载顺序为 `profile-common.js`、`lineage-pages.js`、`family-home-pages.js`;
|
||
- 标题、摘要、计数、管理入口、世系预览均有稳定 `data-*` hook;
|
||
- 硬编码“四川武胜汤氏族”被删除;
|
||
- “邀请家人”不再是可点击业务入口,并明确提示“PC 暂未开放邀请”;
|
||
- 谱文、相册、视频、功德、祭祀、世系、动态入口继续携带 `data-genealogy-context-link`;
|
||
- 初始化只并行调用:
|
||
|
||
```js
|
||
Promise.all([
|
||
api.genealogyOverview(genealogyId),
|
||
api.lineageTree(genealogyId)
|
||
])
|
||
```
|
||
|
||
- [ ] **Step 2: 运行 RED**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||
```
|
||
|
||
Expected: FAIL,主页仍为 pending 且包含硬编码家谱。
|
||
|
||
- [ ] **Step 3: 实现只读初始化流程**
|
||
|
||
```js
|
||
async function initFamilyHomePage() {
|
||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId();
|
||
var results;
|
||
var overview;
|
||
|
||
if (!genealogyId) {
|
||
root.location.replace('profile-families.html?next=profile-family-home.html');
|
||
return;
|
||
}
|
||
try {
|
||
results = await Promise.all([
|
||
api.genealogyOverview(genealogyId),
|
||
api.lineageTree(genealogyId)
|
||
]);
|
||
overview = normalizeFamilyOverview(results[0], genealogyId);
|
||
if (!overview) throw new Error('家谱概览响应无效');
|
||
renderFamilyOverview(overview);
|
||
renderManagementAccess(overview.canManage);
|
||
query('[data-lineage-home-tree]').innerHTML =
|
||
root.LineagePages.renderLineageTreeHtml(results[1]);
|
||
} catch (error) {
|
||
if (shouldRedirectToLogin(api, error)) return redirectToLogin(api);
|
||
renderFamilyHomeError(error);
|
||
}
|
||
}
|
||
```
|
||
|
||
页面只展示:
|
||
|
||
- 家谱名称、编号、姓氏、堂号、祖籍/地区;
|
||
- `memberCount`、`personCount`;
|
||
- 真实世系树及进入完整世系页的链接;
|
||
- 后端已经接入的内容模块入口。
|
||
|
||
- [ ] **Step 4: 运行 GREEN**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||
node --check public/js/family-home-pages.js
|
||
```
|
||
|
||
Expected: PASS。
|
||
|
||
---
|
||
|
||
### Task 4: 规划记录与完整验收
|
||
|
||
**Files:**
|
||
- Modify: `docs/PC接口对接规划.md`
|
||
- Modify: `docs/superpowers/plans/2026-07-29-family-home.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 1–3 的只读家谱主页闭环。
|
||
|
||
- [ ] **Step 1: 更新规划字段表**
|
||
|
||
新增家谱主页小节,逐字段记录:
|
||
|
||
| 字段 | 分类 | 页面用途 |
|
||
| --- | --- | --- |
|
||
| `genealogyId` | I/A | 当前上下文、两条请求 path、响应一致性校验 |
|
||
| `genealogyNo`、`genealogyName`、`surname` | R | 标题和基础信息 |
|
||
| `ancestralHall`、`originPlace`、`regionFullName` | R | 非空时展示 |
|
||
| `memberCount`、`personCount` | R | 非负只读计数 |
|
||
| `canManage`、`canEditContent` | I | 权限显隐,不作为用户输入 |
|
||
| `ownerUserId`、`coverOssId` | I | 当前主页不展示、不手填 |
|
||
| `LineagePersonTreeView.spouses/children` | R | 递归世系预览 |
|
||
|
||
同时记录:
|
||
|
||
- `/overview` 实际是详情别名,不包含内容聚合统计;
|
||
- `/genealogy/dashboard/overview` 是后台权限接口,不进入 PC 前端;
|
||
- 成员邀请没有 PC 接口,继续阻断。
|
||
|
||
- [ ] **Step 2: 聚焦验证**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
node --test tests/family-home-pages.test.js tests/lineage-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js tests/api-client-contract.test.js
|
||
node --check public/js/family-home-pages.js
|
||
node --check public/js/lineage-pages.js
|
||
```
|
||
|
||
Expected: 全部 PASS。
|
||
|
||
- [ ] **Step 3: 全量和差异验证**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
npm test
|
||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||
```
|
||
|
||
Expected: 0 failed,差异检查无错误。
|
||
|
||
- [ ] **Step 4: 浏览器真实只读验证**
|
||
|
||
使用真实登录态验证:
|
||
|
||
1. 无家谱上下文时只跳转选择页,不发家谱业务请求;
|
||
2. 有真实家谱时标题、概览计数和世系树来自 PC 响应;
|
||
3. 普通成员看不到管理按钮,管理者可见;
|
||
4. 所有入口透传同一 `genealogyId`;
|
||
5. 空世系、403、404、网络错误都有明确页面状态;
|
||
6. 控制台无错误;
|
||
7. 不创建或修改任何真实数据。
|