# 家谱主页 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 ? '' : '
暂无世系树
'; } 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. 不创建或修改任何真实数据。