import { GENEALOGY_ACCESS_PRESET, getGenealogyAccessPresetLabel, } from '../utils/genealogy-contracts.js' export const currentUser = { id: 1, name: '汤文远', phone: '138****1024', role: '创建者' } export const genealogies = [ { id: '1001', surname: '汤', name: '汤氏家谱', hall: '敦睦堂', location: '河南·洛阳', memberCount: 158, updatedAt: '2024-05-12', activeCount: 8, accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY, membership: 'created', motto: '敦亲睦族,敬祖传家。', ancestorName: '汤文远', parentName: '汤氏中华总谱', branchName: '洛阳主支', source: '由洛阳汤氏族人整理并维护', publicDescription: '公开展示家谱身份、地区、堂号与支系信息。' }, { id: '1002', surname: '汤', name: '汤氏宗谱', hall: '承志堂', location: '山东·济宁', memberCount: 286, updatedAt: '2024-04-28', activeCount: 15, accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, membership: 'joined', motto: '继往开来,世守家风。', ancestorName: '汤正明', parentName: '汤氏鲁西总谱', branchName: '济宁主支', source: '由济宁汤氏族人整理并维护', publicDescription: '公开展示家谱身份、地区、堂号与支系信息。' } ] // G01、G05、G06 与 G08 共用这一份家谱展示夹具。成员关系只在 // genealogies 和本地创建预览中出现;公开搜索结果绝不能据此获得管理权限。 const toPublicProjection = ({ membership: _membership, ...genealogy }) => genealogy export const publicGenealogies = [ { id: '2001', surname: '汤', name: '汤氏南阳宗谱', hall: '敦睦堂', location: '河南·南阳', parentName: '汤氏中华总谱', branchName: '南阳主支', manager: '管理员 汤文礼', certification: '资料已认证', memberCount: 428, activeCount: 316, updatedAt: '2026-07-12', relation: 'available', source: '由南阳汤氏族人整理并维护', publicDescription: '公开展示家谱身份、地区、堂号与支系信息;成员资料和世系详情仅向已加入成员开放。', motto: '敦亲睦族,敬祖传家。', accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, ancestorName: '汤文远' }, { ...toPublicProjection(genealogies[1]), manager: '管理员 汤文远', certification: '资料已认证', relation: 'joined' }, { id: '2003', surname: '汤', name: '汤氏济宁宗谱', hall: '敬宗堂', location: '山东·济宁', parentName: '汤氏鲁西总谱', branchName: '济宁主支', manager: '管理员 汤正明', certification: '管理员已实名', memberCount: 286, updatedAt: '2026-07-08', relation: 'pending', accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, }, { id: '2004', surname: '汤', name: '汤氏清河家谱', hall: '思源堂', location: '山东·临清', parentName: '汤氏鲁西总谱', branchName: '清河支系', manager: '管理员 汤志成', certification: '资料已认证', memberCount: 96, updatedAt: '2026-07-05', relation: 'rejected', accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, }, { id: '2005', surname: '汤', name: '汤氏汝南支谱', hall: '崇本堂', location: '河南·驻马店', parentName: '汤氏中原总谱', branchName: '汝南三支', manager: '管理员 汤国安', certification: '管理员已实名', memberCount: 72, updatedAt: '2026-07-02', relation: 'removed', accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, }, { ...toPublicProjection(genealogies[0]), manager: '创建者 当前用户', certification: '资料待完善', relation: 'owned' } ] const localCreatedGenealogy = { id: 'local-created-genealogy', surname: '汤', name: '本地创建预览', hall: '堂号待补', location: '所在地待补', memberCount: 1, activeCount: 1, updatedAt: '尚未同步', accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY, localPreview: true, motto: '当前仅为本地流程预览,尚未提交服务器。', ancestorName: '待补', parentName: '无上级谱', branchName: '主支', source: '本地创建流程预览', publicDescription: '尚未同步到服务器。' } const localGenealogyPreviews = new Map([ [localCreatedGenealogy.id, localCreatedGenealogy] ]) let localPreviewSequence = 0 const buildLocalGenealogyPreview = (id, draft, previous = {}) => { const accessPreset = Object.values(GENEALOGY_ACCESS_PRESET).includes(draft.accessPreset) ? draft.accessPreset : GENEALOGY_ACCESS_PRESET.MEMBER_ONLY return { ...previous, id, surname: String(draft.surname || '').trim(), name: String(draft.name || '').trim(), hall: String(draft.hall || '').trim() || '堂号待补', location: String(draft.location || '').trim(), memberCount: 1, activeCount: 1, updatedAt: '尚未同步', accessPreset, localPreview: true, motto: '当前仅为本地流程预览,尚未提交服务器。', ancestorName: previous.ancestorName || '待补', parentName: '无上级谱', branchName: '主支', source: '本地创建流程预览', publicDescription: `尚未同步到服务器;访问规则为“${getGenealogyAccessPresetLabel(accessPreset)}”。` } } // G03 与 G05 共用这一份临时预览 owner。它只在当前运行实例中保存用户刚提交的 // 快照,不建立成员关系、不写持久存储,也不冒充后端创建结果。 export const createLocalGenealogyPreview = (draft) => { localPreviewSequence += 1 const id = `local-created-${Date.now().toString(36)}-${localPreviewSequence}` localGenealogyPreviews.set(id, buildLocalGenealogyPreview(id, draft)) return id } export const updateLocalGenealogyPreview = (genealogyId, draft) => { const normalizedId = String(genealogyId || '') const preview = localGenealogyPreviews.get(normalizedId) if (!preview) return null localGenealogyPreviews.set( normalizedId, buildLocalGenealogyPreview(normalizedId, draft, preview) ) return normalizedId } export const updateLocalGenealogyPreviewAncestor = (genealogyId, draft) => { const normalizedId = String(genealogyId || '') const preview = localGenealogyPreviews.get(normalizedId) if (!preview) return null const updated = { ...preview, ancestorName: String(draft.personName || '').trim() || '待补', ancestor: Object.freeze({ ...draft }) } localGenealogyPreviews.set(normalizedId, updated) return updated } export const removeLocalGenealogyPreview = (genealogyId) => { const normalizedId = String(genealogyId || '') if (!normalizedId || normalizedId === localCreatedGenealogy.id) return false return localGenealogyPreviews.delete(normalizedId) } export const findGenealogyFixture = (genealogyId) => { const normalizedId = String(genealogyId || '') return [...genealogies, ...publicGenealogies] .find((item) => String(item.id) === normalizedId) || localGenealogyPreviews.get(normalizedId) || null } // 这里只解析本地视觉夹具的显示角色,不代表后端授权。成员关系仍由 // genealogies[*].membership 唯一持有;localPreview 只能进入无业务入口的预览态。 export const getGenealogyFixtureAccess = (genealogyId) => { const normalizedId = String(genealogyId || '') const fixture = findGenealogyFixture(normalizedId) const memberFixture = genealogies.find((item) => item.id === normalizedId) const publicFixture = publicGenealogies.find((item) => item.id === normalizedId) const access = fixture?.localPreview ? 'preview' : memberFixture?.membership === 'created' ? 'owner' : memberFixture?.membership === 'joined' ? 'member' : 'public' const relation = fixture?.localPreview ? 'preview' : memberFixture?.membership === 'created' ? 'owned' : memberFixture?.membership === 'joined' ? 'joined' : publicFixture?.relation || 'unknown' return Object.freeze({ viewMode: access === 'public' || access === 'preview' ? access : 'member', accessRole: access === 'owner' || access === 'member' ? access : 'guest', relation, canView: Boolean(fixture?.localPreview) || access === 'owner' || access === 'member' || fixture?.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, canApply: fixture?.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY && ['available', 'rejected', 'removed'].includes(relation) }) } // 搜索结果的公开性与操作资格消费同一访问预设。成员可看到自己已经加入或 // 创建的私密家谱;陌生账号只能看到明确为 PUBLIC_APPLY 的投影。 export const isGenealogySearchVisible = (genealogyId) => { const fixture = findGenealogyFixture(genealogyId) if (!fixture) return false const access = getGenealogyFixtureAccess(genealogyId) return ( access.accessRole !== 'guest' || fixture.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY ) } // T01、T03—T08 与旧 mock API 共用这一份成员夹具。utils/api.js 是当前阶段 // 唯一允许写入裸数组的模块;页面必须通过下方查询函数取得深拷贝,避免页面 // 表单或关系投影反向污染世系树和其他页面。 export const treeMembers = [ { id: '101', appUserId: '2001', genealogyId: '1001', parentId: '', name: '汤文远', relation: '始祖', generation: 12, generationName: '文字辈', branch: '主支', years: '1940—2012', birthDate: '1940-03-01', deathDate: '2012-08-16', birthplace: '河南南阳', status: 'deceased', summary: '一生敦亲睦族,参与整理家族旧谱。', note: '始祖 · 档案完整', relatives: [ { personId: '102', relation: '长子' }, { personId: '103', relation: '次子' } ] }, { id: '102', appUserId: '2002', genealogyId: '1001', parentId: '101', name: '汤正国', relation: '长子', generation: 13, generationName: '正字辈', branch: '长房', years: '1965—', birthDate: '1965-05-12', deathDate: '', birthplace: '河南洛阳', status: 'privacy', summary: '负责长房资料核对。', note: '家谱管理员', relatives: [ { personId: '101', relation: '父亲' }, { personId: '104', relation: '长子' }, { personId: '105', relation: '女儿' } ] }, { id: '103', appUserId: '2003', genealogyId: '1001', parentId: '101', name: '汤正华', relation: '次子', generation: 13, generationName: '正字辈', branch: '二房', years: '资料受限', birthDate: '1968-09-03', deathDate: '', birthplace: '河南洛阳', status: 'forbidden', summary: '资料仍在补充。', note: '资料待补充', relatives: [ { personId: '101', relation: '父亲' }, { personId: '106', relation: '长子' } ] }, { id: '104', appUserId: '2004', genealogyId: '1001', parentId: '102', name: '汤凯', relation: '长孙', generation: 14, generationName: '凯字辈', branch: '长房', years: '1992—', birthDate: '1992-06-01', deathDate: '', birthplace: '河南洛阳', status: 'privacy', summary: '协助整理年轻一代成员资料。', note: '档案已核对', relatives: [{ personId: '102', relation: '父亲' }] }, { id: '105', appUserId: '2005', genealogyId: '1001', parentId: '102', name: '汤悦', relation: '长孙女', generation: 14, generationName: '凯字辈', branch: '长房', years: '1995—', birthDate: '1995-04-18', deathDate: '', birthplace: '河南洛阳', status: 'privacy', summary: '参与家族影像与口述资料整理。', note: '档案已核对', relatives: [{ personId: '102', relation: '父亲' }] }, { id: '106', appUserId: '2006', genealogyId: '1001', parentId: '103', name: '汤晨', relation: '次孙', generation: 14, generationName: '凯字辈', branch: '二房', years: '1998—', birthDate: '1998-11-09', deathDate: '', birthplace: '河南洛阳', status: 'privacy', summary: '二房成员资料已完成初步核对。', note: '档案已核对', relatives: [{ personId: '103', relation: '父亲' }] } ] const cloneTreeMemberFixture = (member) => ({ ...member, relatives: member.relatives.map((relative) => ({ ...relative })) }) export const listTreeMemberFixtures = (genealogyId) => { const normalizedGenealogyId = String(genealogyId || '') if (!normalizedGenealogyId) return [] return treeMembers .filter((member) => member.genealogyId === normalizedGenealogyId) .map(cloneTreeMemberFixture) } export const findTreeMemberFixture = (genealogyId, personId) => { const normalizedGenealogyId = String(genealogyId || '') const normalizedPersonId = String(personId || '') if (!normalizedGenealogyId || !normalizedPersonId) return null const member = treeMembers.find( (item) => item.genealogyId === normalizedGenealogyId && item.id === normalizedPersonId ) return member ? cloneTreeMemberFixture(member) : null } // 页面展示只能消费这个受控投影。privacy/forbidden 成员在 owner 边界即删除 // 生平、居住地、支系、亲属等字段,避免组件先取得完整对象再依赖模板隐藏。 const projectTreeMemberPresentation = (member) => { if (!['privacy', 'forbidden'].includes(member.status)) { return cloneTreeMemberFixture(member) } return { id: member.id, appUserId: member.appUserId, genealogyId: member.genealogyId, name: member.name, relation: member.relation, generation: member.generation, status: member.status } } export const listTreeMemberPresentationFixtures = (genealogyId) => { const normalizedGenealogyId = String(genealogyId || '') if (!normalizedGenealogyId) return [] return treeMembers .filter((member) => member.genealogyId === normalizedGenealogyId) .map(projectTreeMemberPresentation) } export const findTreeMemberPresentationFixture = (genealogyId, personId) => { const normalizedGenealogyId = String(genealogyId || '') const normalizedPersonId = String(personId || '') if (!normalizedGenealogyId || !normalizedPersonId) return null const member = treeMembers.find( (item) => item.genealogyId === normalizedGenealogyId && item.id === normalizedPersonId ) return member ? projectTreeMemberPresentation(member) : null } // F01—F09 共用这一组只读内容夹具。家谱 ID 与实体 ID 共同构成身份;页面和 // mock API 只能通过下方 list/find 查询取得深拷贝,不能把页面草稿写回正式列表。 const familyFeeds = [ { id: '1', genealogyId: '1001', tag: '团圆记忆', time: '今天 10:24', title: '端午家宴', content: '今年端午全家相聚,长辈讲起祖居旧事,孩子们也为大家拍下了新的全家福。饭后我们把照片和口述片段整理进家族档案,让这份热闹成为往后仍能翻看的共同记忆。', author: '汤正国', comments: [ { id: '1', author: '汤淑华', time: '今天 10:42', content: '一家人能常常相聚,就是最珍贵的福气。' }, { id: '2', author: '汤文清', time: '今天 11:08', content: '照片已经整理好了,晚些时候放进春节团圆相册。' } ] }, { id: '2', genealogyId: '1001', tag: '家族通知', time: '昨天 18:02', title: '修谱资料征集', content: '请家人补充老照片中的人物姓名、拍摄时间和地点。无法确认的信息也可以先写下线索,由熟悉往事的长辈共同核对。', author: '谱主', comments: [] } ] const familyArticles = [ { id: '101', genealogyId: '1001', category: '家风家训', title: '孝友传家的日常', summary: '从敬老、睦亲与守信的小事里,看见家风如何代代相传。', author: '汤文正', updatedAt: '2024 年 5 月 12 日', paragraphs: [ '孝友传家,不只在族谱序言里,也在一家人每日的言行中。长辈以宽厚待晚辈,晚辈以耐心照料长辈,亲友之间守信互助,便是最朴素也最长久的家风。', '勤俭并非一味节省,而是珍惜所得、量入为出,也愿意在家人需要时伸出援手。家中每一代人都可以用自己的方式,把这份分寸与担当继续传下去。', '敬祖睦宗,最终是为了让今天的家人彼此认识、彼此关心。记录姓名与世代之外,也应留下真实的生活、共同经历和温暖记忆。' ] }, { id: '102', genealogyId: '1001', category: '家族往事', title: '祖居门前的那棵桂花树', summary: '长辈口述的旧居记忆,以及每年中秋一家人相聚的故事。', author: '汤淑华', updatedAt: '2024 年 5 月 10 日', paragraphs: [ '祖居门前曾有一棵桂花树。每到中秋,院里都是清甜的香气,远道回来的家人也总能循着那股味道找到家门。', '后来房屋几经修缮,桂花树仍被大家小心保留下来。它见过孩子长大,也见过长辈把往事一遍遍讲给后来人。' ] }, { id: '103', genealogyId: '1001', category: '族谱序言', title: '续修族谱序', summary: '说明本次续修的缘起、资料来源与共同参与的家人。', author: '谱主', updatedAt: '2024 年 5 月 8 日', paragraphs: [ '本次续修以旧谱、碑记、户籍资料和长辈口述为基础,由家人共同核对补充。凡暂不能确认之处,均保留来源和疑问,留待后续查证。', '愿这份记录不仅理清世系,也能保存家风、人物与共同记忆。' ] } ] const familyAlbums = [ { id: '201', genealogyId: '1001', name: '2024 春节团圆', updatedAt: '今天更新', description: '三代家人的团圆饭与院前合影', cover: '/static/assets/modules/family/f08/f08-reunion-hero.png', photos: [ { id: '20101', src: '/static/assets/modules/family/f08/f08-reunion-hero.png', alt: '春节团圆时三代家人的合影', caption: '除夕团圆 · 2024' }, { id: '20102', src: '/static/assets/modules/family/f08/f08-family-portrait.png', alt: '家人在院落前的春节合影', caption: '院前合影 · 2024' }, { id: '20103', src: '/static/assets/modules/family/f08/f08-reunion-table.png', alt: '家人围坐吃年夜饭', caption: '围桌守岁 · 2024' }, { id: '20104', src: '/static/assets/modules/family/f08/f08-ancestral-home.png', alt: '祖居院落的复古旧照', caption: '祖居旧影 · 1968' }, { id: '20105', src: '/static/assets/modules/family/f08/f08-ancestral-portrait.png', alt: '老一辈家人在祖居门前的合影', caption: '门前合影 · 1972' } ] }, { id: '202', genealogyId: '1001', name: '祖居旧影', updatedAt: '5 月 10 日更新', description: '祖居、旧物与长辈珍藏的老照片', cover: '/static/assets/modules/family/f08/f08-ancestral-home.png', photos: [ { id: '20201', src: '/static/assets/modules/family/f08/f08-ancestral-home.png', alt: '祖居院落的复古旧照', caption: '祖居旧影 · 1968' }, { id: '20202', src: '/static/assets/modules/family/f08/f08-ancestral-portrait.png', alt: '老一辈家人在祖居门前的合影', caption: '门前合影 · 1972' } ] }, { id: '203', genealogyId: '1001', name: '儿童成长', updatedAt: '持续更新', description: '记录孩子们每一个值得珍藏的瞬间', cover: '/static/assets/modules/family/f08/f08-family-portrait.png', photos: [ { id: '20301', src: '/static/assets/modules/family/f08/f08-family-portrait.png', alt: '家人在院落前的春节合影', caption: '院前合影 · 2024' } ] } ] const cloneFamilyFeedFixture = (feed) => ({ ...feed, comments: feed.comments.map((comment) => ({ ...comment })) }) const cloneFamilyArticleFixture = (article) => ({ ...article, paragraphs: [...article.paragraphs] }) const cloneFamilyAlbumFixture = (album) => ({ ...album, photoCount: album.photos.length, photos: album.photos.map((photo) => ({ ...photo })) }) const listScopedFamilyFixtures = (items, clone, genealogyId) => { const normalizedGenealogyId = String(genealogyId || '') if (!normalizedGenealogyId) return [] return items .filter((item) => item.genealogyId === normalizedGenealogyId) .map(clone) } const findScopedFamilyFixture = (items, clone, genealogyId, entityId) => { const normalizedGenealogyId = String(genealogyId || '') const normalizedEntityId = String(entityId || '') if (!normalizedGenealogyId || !normalizedEntityId) return null const item = items.find( (candidate) => candidate.genealogyId === normalizedGenealogyId && candidate.id === normalizedEntityId ) return item ? clone(item) : null } export const listFamilyFeedFixtures = (genealogyId) => listScopedFamilyFixtures(familyFeeds, cloneFamilyFeedFixture, genealogyId) export const findFamilyFeedFixture = (genealogyId, feedId) => findScopedFamilyFixture(familyFeeds, cloneFamilyFeedFixture, genealogyId, feedId) export const listFamilyArticleFixtures = (genealogyId) => listScopedFamilyFixtures(familyArticles, cloneFamilyArticleFixture, genealogyId) export const findFamilyArticleFixture = (genealogyId, articleId) => findScopedFamilyFixture(familyArticles, cloneFamilyArticleFixture, genealogyId, articleId) export const listFamilyAlbumFixtures = (genealogyId) => listScopedFamilyFixtures(familyAlbums, cloneFamilyAlbumFixture, genealogyId) export const findFamilyAlbumFixture = (genealogyId, albumId) => findScopedFamilyFixture(familyAlbums, cloneFamilyAlbumFixture, genealogyId, albumId) // R03—R11 共用这一组只读记录夹具。它们只描述本地视觉预览,不冒充 // OpenAPI 响应;每个实体都显式携带 genealogyId,详情必须由家谱与实体 ID // 共同定位。页面只能通过下方 list/find 选择器取得深拷贝,不能把草稿、完成 // 状态或新增记录写回这里。 const relativeRecordFixtures = [ { relativeId: '301', genealogyId: '1001', relativeName: '汤文正一家', relationName: '族亲', eventName: '新春贺礼', eventTime: '2024-02-10', giftAmount: 600, recordContent: '新春团拜时赠予长辈的心意' }, { relativeId: '302', genealogyId: '1001', relativeName: '汤淑华', relationName: '家族长辈', eventName: '寿宴礼单', eventTime: '2024-04-18', giftAmount: 1000, recordContent: '汤老先生八十寿辰' } ] const ceremonyFixtures = [ { ceremonyId: '501', genealogyId: '1001', ceremonyType: '祭祖', ceremonyTitle: '清明祭祖', ceremonyTime: '2025-04-04', location: '汤氏宗祠', ceremonyDesc: '缅怀先祖,整理祭扫礼序,并由长辈讲述家族往事。', invitees: [ { inviteeUserId: '2001', inviteStatus: '1' }, { inviteeUserId: '2002', inviteStatus: '0' } ] }, { ceremonyId: '502', genealogyId: '1001', ceremonyType: '家宴', ceremonyTitle: '中秋家宴', ceremonyTime: '2025-09-17', location: '祖居院落', ceremonyDesc: '家人团聚,共叙近况并整理年度家族影像。', invitees: [{ inviteeUserId: '2003', inviteStatus: '1' }] }, { ceremonyId: '503', genealogyId: '1001', ceremonyType: '团拜', ceremonyTitle: '新春团拜', ceremonyTime: '2025-01-29', location: '家族礼堂', ceremonyDesc: '新春相聚,向长辈问安并记录家族近况。', invitees: [] } ] const growthRecordFixtures = [ { recordId: '701', genealogyId: '1001', lineagePersonId: '101', recordTitle: '整理第一册旧谱', recordDate: '1988-03', recordContent: '第一次独立整理家中保存的旧谱与口述线索。' }, { recordId: '702', genealogyId: '1001', lineagePersonId: '104', recordTitle: '第一次参与修谱', recordDate: '2024-06', recordContent: '协助长辈核对照片人物与出生年份。' } ] const memoFixtures = [ { memoId: '801', genealogyId: '1001', memoTitle: '修谱资料整理', remindTime: '本月底前', memoContent: '补充老照片中的人物姓名和拍摄时间。', completedLabel: '待办理' }, { memoId: '802', genealogyId: '1001', memoTitle: '重阳敬老活动', remindTime: '10 月 11 日上午', memoContent: '在祠堂集合,并确认接送长辈的车辆。', completedLabel: '已完成' } ] const meritRecordFixtures = [ { meritId: '901', genealogyId: '1001', meritTypeLabel: '共同修缮', meritTitle: '修缮祠堂', donorName: '汤氏家人共同参与', meritTime: '2024 年春', amount: null, meritContent: '协助整理院落、修补门窗并登记旧物。' }, { meritId: '902', genealogyId: '1001', meritTypeLabel: '奖学助学', meritTitle: '支持后辈勤学', donorName: '家族教育小组', meritTime: '2024 年夏', amount: null, meritContent: '为家族中努力求学的孩子提供书籍与经验分享。' } ] const cloneRecordFixture = (record) => ({ ...record, ...(Array.isArray(record.invitees) ? { invitees: record.invitees.map((invitee) => ({ ...invitee })) } : {}) }) const listScopedRecordFixtures = (records, genealogyId) => { const normalizedGenealogyId = String(genealogyId || '') if (!normalizedGenealogyId) return [] return records .filter((record) => record.genealogyId === normalizedGenealogyId) .map(cloneRecordFixture) } const findScopedRecordFixture = (records, idField, genealogyId, entityId) => { const normalizedGenealogyId = String(genealogyId || '') const normalizedEntityId = String(entityId || '') if (!normalizedGenealogyId || !normalizedEntityId) return null const record = records.find( (item) => item.genealogyId === normalizedGenealogyId && item[idField] === normalizedEntityId ) return record ? cloneRecordFixture(record) : null } export const listRelativeRecordFixtures = (genealogyId) => listScopedRecordFixtures(relativeRecordFixtures, genealogyId) export const findRelativeRecordFixture = (genealogyId, relativeId) => findScopedRecordFixture(relativeRecordFixtures, 'relativeId', genealogyId, relativeId) export const listCeremonyFixtures = (genealogyId) => listScopedRecordFixtures(ceremonyFixtures, genealogyId) export const findCeremonyFixture = (genealogyId, ceremonyId) => findScopedRecordFixture(ceremonyFixtures, 'ceremonyId', genealogyId, ceremonyId) export const listGrowthRecordFixtures = (genealogyId, lineagePersonId = '') => { const records = listScopedRecordFixtures(growthRecordFixtures, genealogyId) const normalizedPersonId = String(lineagePersonId || '') return normalizedPersonId ? records.filter((record) => record.lineagePersonId === normalizedPersonId) : records } export const findGrowthRecordFixture = (genealogyId, recordId) => findScopedRecordFixture(growthRecordFixtures, 'recordId', genealogyId, recordId) export const listMemoFixtures = (genealogyId) => listScopedRecordFixtures(memoFixtures, genealogyId) export const findMemoFixture = (genealogyId, memoId) => findScopedRecordFixture(memoFixtures, 'memoId', genealogyId, memoId) export const listMeritRecordFixtures = (genealogyId) => listScopedRecordFixtures(meritRecordFixtures, genealogyId) export const findMeritRecordFixture = (genealogyId, meritId) => findScopedRecordFixture(meritRecordFixtures, 'meritId', genealogyId, meritId) export const notifications = [ { id: 'review-1', title: '申请待审核', content: '汤志成申请加入汤氏家谱,请核实亲属关系。', body: '汤志成申请加入汤氏家谱,请核实申请人的亲属关系与世代信息后完成审核。', time: '今天 10:28', source: '汝南汤氏家谱', unread: true, targetType: 'GENEALOGY_REVIEW', targetParams: { genealogyId: '1001' }, targetLabel: '前往入谱审核' }, { id: 'approved', title: '入谱申请已通过', content: '你申请加入汝南汤氏家谱的请求已通过。', body: '你申请加入汝南汤氏家谱的请求已通过,现在可以查看家谱与家族动态。', time: '昨天 18:10', source: '汝南汤氏家谱', unread: false, targetType: 'GENEALOGY_HOME', targetParams: { genealogyId: '1001' }, targetLabel: '查看我的家谱' } ] const cloneNotificationFixture = (notification) => ({ ...notification, targetParams: { ...notification.targetParams } }) export const listNotificationFixtures = () => notifications.map(cloneNotificationFixture) export const findNotificationFixture = (notificationId) => { const normalizedId = String(notificationId || '') if (!normalizedId) return null const notification = notifications.find((item) => item.id === normalizedId) return notification ? cloneNotificationFixture(notification) : null } export const joinApplications = [ { id: '1', name: '汤志成', phone: '139****6421', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' }, { id: '2', name: '汤雨薇', phone: '136****2798', relation: '自述为汤正国之女', appliedAt: '昨天 18:02', status: 'PENDING' } ]