完成40%

This commit is contained in:
rain
2026-07-23 17:21:27 +08:00
parent f1edc6b533
commit bb6431b319
114 changed files with 10931 additions and 877 deletions
+319 -11
View File
@@ -14,7 +14,7 @@ import {
} from '@/data/mock.js'
import { hasRemoteConfig, resolveRuntimeMode, runtimeConfig } from '@/utils/config.js'
import { AUTH_TAC_SCENE, assertSmsCode } from '@/utils/auth-verification.js'
import { GENEALOGY_ACCESS_PRESET } from '@/utils/genealogy-contracts.js'
import { GENEALOGY_ACCESS_PRESET, fromApiGenealogyAccess } from '@/utils/genealogy-contracts.js'
import { session } from '@/utils/session.js'
const successCodes = [0, 200]
@@ -251,6 +251,100 @@ const normalizeFeedbackPayload = (payload) => {
return normalized
}
const normalizeMyGenealogyId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError('我的家谱响应包含无效标识', 'GENEALOGY_RESPONSE_INVALID')
}
const normalizeGenealogyPathId = (value) => {
const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value
if (typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) {
throw createRequestError('家谱标识无效', 'GENEALOGY_ID_INVALID')
}
return id
}
const normalizeOptionalGenealogyText = (value, label) => {
if (value === undefined || value === null) return ''
if (typeof value !== 'string') {
throw createRequestError(`我的家谱响应字段 ${label} 无效`, 'GENEALOGY_RESPONSE_INVALID')
}
return value.trim()
}
const normalizeAppGenealogy = (item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('我的家谱响应包含无效条目', 'GENEALOGY_RESPONSE_INVALID')
}
const name = normalizeOptionalGenealogyText(item.genealogyName, 'genealogyName')
if (!name) {
throw createRequestError('我的家谱响应缺少家谱名称', 'GENEALOGY_RESPONSE_INVALID')
}
for (const field of ['canManage', 'canEditContent']) {
if (typeof item[field] !== 'boolean') {
throw createRequestError(`我的家谱响应字段 ${field} 无效`, 'GENEALOGY_RESPONSE_INVALID')
}
}
if (item.canView === false) {
throw createRequestError('当前账号无权查看该家谱', 'GENEALOGY_FORBIDDEN')
}
if (!Number.isSafeInteger(item.memberCount) || item.memberCount < 0) {
throw createRequestError('我的家谱响应成员数量无效', 'GENEALOGY_RESPONSE_INVALID')
}
const personCount = item.personCount ?? null
if (personCount !== null && (!Number.isSafeInteger(personCount) || personCount < 0)) {
throw createRequestError('我的家谱响应世系人数无效', 'GENEALOGY_RESPONSE_INVALID')
}
return {
id: normalizeMyGenealogyId(item.genealogyId),
name,
surname: normalizeOptionalGenealogyText(item.surname, 'surname'),
hall: normalizeOptionalGenealogyText(item.ancestralHall, 'ancestralHall'),
location:
normalizeOptionalGenealogyText(item.regionFullName, 'regionFullName') ||
normalizeOptionalGenealogyText(item.regionName, 'regionName') ||
normalizeOptionalGenealogyText(item.originPlace, 'originPlace') ||
normalizeOptionalGenealogyText(item.addressDetail, 'addressDetail') ||
'地区待补',
memberCount: item.memberCount,
personCount,
accessPreset: fromApiGenealogyAccess({
visibility: normalizeOptionalGenealogyText(item.visibility, 'visibility'),
joinMode: normalizeOptionalGenealogyText(item.joinMode, 'joinMode')
}),
accessRole: item.canManage ? 'owner' : 'member',
canManage: item.canManage,
canEditContent: item.canEditContent,
intro: normalizeOptionalGenealogyText(item.intro, 'intro'),
joinTime: normalizeOptionalGenealogyText(item.joinTime, 'joinTime')
}
}
const normalizeMyGenealogies = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('我的家谱响应不是列表', 'GENEALOGY_RESPONSE_INVALID')
}
const normalized = value.map((item) => {
const genealogy = normalizeAppGenealogy(item)
return {
id: genealogy.id,
name: genealogy.name,
surname: genealogy.surname,
hall: genealogy.hall,
location: genealogy.location,
memberCount: genealogy.memberCount,
accessRole: genealogy.accessRole,
canManage: genealogy.canManage,
canEditContent: genealogy.canEditContent
}
})
if (new Set(normalized.map((item) => item.id)).size !== normalized.length) {
throw createRequestError('我的家谱响应包含重复标识', 'GENEALOGY_RESPONSE_INVALID')
}
return normalized
}
const saveLogin = (loginResult) => {
const token = loginResult?.access_token
if (!token) throw new Error('登录响应未包含会话令牌')
@@ -276,6 +370,183 @@ const toTreeNode = (person, index = 0) => {
}
}
const lineageTreeError = (message) =>
createRequestError(message, 'LINEAGE_TREE_RESPONSE_INVALID')
const normalizeLineagePersonId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineageTreeError('世系树包含无效人物标识')
}
const normalizeLineageText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineageTreeError(`世系树缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineageTreeError(`世系树${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineageTreeError(`世系树缺少${label}`)
return normalized
}
const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) throw lineageTreeError(`世系树${label}无效`)
return normalized.slice(0, 10)
}
const normalizeLineageTree = (value) => {
if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表')
const seen = new Set()
const normalized = []
const appendNode = (node, parentId, relationOverride = '') => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
const id = normalizeLineagePersonId(node.personId)
if (seen.has(id)) throw lineageTreeError('世系树包含重复人物标识')
seen.add(id)
if (seen.size > 5000) throw lineageTreeError('世系树节点数量超出客户端上限')
if (!Number.isSafeInteger(node.generation) || node.generation < 1) {
throw lineageTreeError('世系树人物世代无效')
}
const generationName = normalizeLineageText(node.generationName, '字辈')
const birthDate = lineageDatePart(node.birthDate, '出生日期')
const deathDate = lineageDatePart(node.deathDate, '逝世日期')
normalized.push({
id,
parentId,
name: normalizeLineageText(node.name, '人物姓名', { required: true }),
relation:
relationOverride ||
normalizeLineageText(node.relationName, '人物关系') ||
(parentId ? '后代' : '始祖'),
generation: node.generation,
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
years:
birthDate || deathDate
? `${birthDate}${deathDate}`
: '生卒待补',
sex: normalizeLineageText(node.sex, '性别'),
personStatus: normalizeLineageText(node.personStatus, '人物状态')
})
return id
}
const walk = (node, parentId = null, depth = 0) => {
if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限')
const id = appendNode(node, parentId)
const spouses = node.spouses ?? []
const children = node.children ?? []
if (!Array.isArray(spouses) || !Array.isArray(children)) {
throw lineageTreeError('世系树亲属集合无效')
}
spouses.forEach((spouse) => appendNode(spouse, parentId, '配偶'))
children.forEach((child) => walk(child, id, depth + 1))
}
value.forEach((root) => walk(root))
return normalized
}
const lineagePersonError = (message) =>
createRequestError(message, 'LINEAGE_PERSON_RESPONSE_INVALID')
const normalizeLineagePersonIdentity = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineagePersonError(`成员详情${label}无效`)
}
const normalizeLineagePersonText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineagePersonError(`成员详情缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineagePersonError(`成员详情${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineagePersonError(`成员详情缺少${label}`)
return normalized
}
const normalizeLineagePersonDate = (value, label) => {
const normalized = normalizeLineagePersonText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(normalized)) {
throw lineagePersonError(`成员详情${label}无效`)
}
const datePart = normalized.slice(0, 10)
const parsed = new Date(`${datePart}T00:00:00Z`)
if (
Number.isNaN(parsed.getTime()) ||
parsed.toISOString().slice(0, 10) !== datePart
) {
throw lineagePersonError(`成员详情${label}无效`)
}
return datePart
}
const normalizeLineagePersonDetail = (
value,
expectedGenealogyId,
expectedPersonId,
) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员详情响应不是对象')
}
const genealogyId = normalizeLineagePersonIdentity(value.genealogyId, '家谱标识')
const id = normalizeLineagePersonIdentity(value.personId, '人物标识')
if (genealogyId !== expectedGenealogyId || id !== expectedPersonId) {
throw lineagePersonError('成员详情响应标识与请求不匹配')
}
if (!Number.isSafeInteger(value.generation) || value.generation < 1) {
throw lineagePersonError('成员详情世代无效')
}
const name = normalizeLineagePersonText(value.name, '姓名', { required: true })
const generationName = normalizeLineagePersonText(value.generationName, '字辈')
const birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期')
const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期')
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态').toUpperCase()
const relatives = []
for (const relation of [
{ id: value.fatherId, name: value.fatherName, label: '父亲' },
{ id: value.motherId, name: value.motherName, label: '母亲' }
]) {
if (relation.id === undefined || relation.id === null) continue
const relativeId = normalizeLineagePersonIdentity(relation.id, `${relation.label}标识`)
if (relativeId === id || relatives.some((item) => item.id === relativeId)) {
throw lineagePersonError('成员详情亲属标识冲突')
}
relatives.push({
id: relativeId,
name: normalizeLineagePersonText(relation.name, `${relation.label}姓名`) || relation.label,
relation: relation.label
})
}
return {
id,
genealogyId,
genealogyName: normalizeLineagePersonText(value.genealogyName, '家谱名称') || '当前家谱',
name,
generation: value.generation,
generationName,
relation: value.generation === 1 ? '始祖' : '家谱成员',
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
sex: normalizeLineagePersonText(value.sex, '性别'),
birthDate,
deathDate,
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
birthplace: normalizeLineagePersonText(value.birthPlace, '出生地'),
biography: normalizeLineagePersonText(value.biography, '生平'),
status: ['DECEASED', 'DEAD'].includes(personStatus) ? 'deceased' : 'normal',
relatives
}
}
export const appApi = {
async getCaptchaRequirement({ sceneCode, subject }, requestOptions = {}) {
requireRemoteAuth()
@@ -373,8 +644,20 @@ export const appApi = {
async getProfile() {
return hasRemoteConfig() ? request({ url: '/genealogy/app/auth/profile' }) : currentUser
},
async getMyGenealogies() {
return hasRemoteConfig() ? request({ url: '/genealogy/app/genealogies/mine' }) : genealogies
async getMyGenealogies(requestOptions = {}) {
if (!hasRemoteConfig()) {
return genealogies.map((item) => ({
...item,
accessRole: item.membership === 'created' ? 'owner' : 'member'
}))
}
const result = await requestStrict({
url: '/genealogy/app/genealogies/mine',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeMyGenealogies(result)
},
async createGenealogy(payload) {
if (hasRemoteConfig()) return request({ url: '/genealogy/app/genealogies', method: 'POST', data: payload })
@@ -388,22 +671,47 @@ export const appApi = {
async getGenealogy(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}` }) : genealogies.find((item) => String(item.id) === String(genealogyId))
},
async getOverview(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/overview` }) : await this.getGenealogy(genealogyId)
async getOverview(genealogyId, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) return this.getGenealogy(normalizedId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/overview`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
const overview = normalizeAppGenealogy(result)
if (overview.id !== normalizedId) {
throw createRequestError('家谱概览响应标识不匹配', 'GENEALOGY_RESPONSE_INVALID')
}
return overview
},
async getTree(genealogyId) {
async getTree(genealogyId, requestOptions = {}) {
if (hasRemoteConfig()) {
const tree = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/tree` })
return (tree || []).map(toTreeNode)
const normalizedId = normalizeGenealogyPathId(genealogyId)
const tree = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/lineage/tree`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageTree(tree)
}
return treeMembers
.filter((item) => String(item.genealogyId) === String(genealogyId))
.map(toTreeNode)
},
async getPerson(genealogyId, personId) {
async getPerson(genealogyId, personId, requestOptions = {}) {
if (hasRemoteConfig()) {
const person = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/persons/${personId}` })
return toTreeNode(person)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineagePersonDetail(result, normalizedGenealogyId, normalizedPersonId)
}
const person = treeMembers.find(
(item) => String(item.id) === String(personId) &&