修改未完成

This commit is contained in:
rain
2026-07-29 18:15:59 +08:00
parent 6fbcf21024
commit 8c2355a52c
59 changed files with 7995 additions and 1118 deletions
+121 -174
View File
@@ -181,12 +181,6 @@ const request = (options, {
if (!settled && requestController) requestController.bind(abortRequest)
})
const authPayload = (payload) => ({
clientId: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...payload
})
// 已核对接口使用这一严格边界:只接受 HTTP 200 与完整 JSON envelope
// 并统一限制弱网等待时间。尚未逐页治理的旧业务读取仍沿用上方通用 request。
const requestStrict = (options, {
@@ -241,7 +235,7 @@ const parseStrictUploadResponse = (responseText, requireData) => {
}
const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (!globalThis.plus?.uploader?.createUpload) {
if (typeof globalThis.uni?.uploadFile !== 'function') {
reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE'))
return
}
@@ -254,6 +248,7 @@ const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, {
}
let settled = false
let task = null
let cancelled = false
const release = () => requestController?.release(abort)
const resolveOnce = (value) => {
if (settled) return
@@ -269,34 +264,56 @@ const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, {
}
const abort = () => {
if (settled) return
cancelled = true
task?.abort?.()
rejectOnce(createRequestCancelledError())
}
const query = `uploadId=${encodeURIComponent(uploadId)}&chunkIndex=${encodeURIComponent(chunkIndex)}&chunkMd5=${encodeURIComponent(chunkMd5)}`
const token = session.getToken()
try {
task = globalThis.plus.uploader.createUpload(
`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk?${query}`,
{ method: 'POST' },
(upload, status) => {
if (status !== 200) {
rejectOnce(createRequestError(`文件分片上传失败(HTTP ${status}`, 'HTTP_ERROR', { httpStatus: status }))
task = globalThis.uni.uploadFile({
url: `${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`,
filePath,
name: 'file',
formData: {
uploadId,
chunkIndex: String(chunkIndex),
chunkMd5
},
header: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
timeout: REQUEST_TIMEOUT_MS,
success: (upload) => {
if (upload.statusCode !== 200) {
rejectOnce(createRequestError(`文件分片上传失败(HTTP ${upload.statusCode}`, 'HTTP_ERROR', { httpStatus: upload.statusCode }))
return
}
try {
parseStrictUploadResponse(upload.responseText, false)
parseStrictUploadResponse(upload.data, false)
resolveOnce(null)
} catch (error) {
rejectOnce(error)
}
},
fail: (error) => {
if (settled) return
if (cancelled) {
rejectOnce(createRequestCancelledError())
return
}
const message = typeof error?.errMsg === 'string' && error.errMsg
? error.errMsg
: '文件分片上传失败'
const timeout = /timeout/i.test(message)
rejectOnce(createRequestError(
timeout ? '文件分片上传超时' : message,
timeout ? 'REQUEST_TIMEOUT' : 'UPLOAD_FAILED'
))
}
)
task.setRequestHeader('clientid', runtimeConfig.clientId)
task.setRequestHeader('tenantId', runtimeConfig.tenantId)
const token = session.getToken()
if (token) task.setRequestHeader('Authorization', `Bearer ${token}`)
task.addFile(filePath, { key: 'file' })
})
if (requestController) requestController.bind(abort)
task.start()
} catch (error) {
rejectOnce(error)
}
@@ -315,17 +332,25 @@ const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { req
return
}
let settled = false
let timeoutId = null
const abortController = new AbortController()
const release = () => requestController?.release(abort)
const clearRequestTimeout = () => {
if (timeoutId === null) return
clearTimeout(timeoutId)
timeoutId = null
}
const resolveOnce = (value) => {
if (settled) return
settled = true
clearRequestTimeout()
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
clearRequestTimeout()
release()
reject(error)
}
@@ -334,12 +359,18 @@ const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { req
abortController.abort()
rejectOnce(createRequestCancelledError())
}
const query = `uploadId=${encodeURIComponent(uploadId)}&chunkIndex=${encodeURIComponent(chunkIndex)}&chunkMd5=${encodeURIComponent(chunkMd5)}`
const token = session.getToken()
const formData = new globalThis.FormData()
formData.append('uploadId', uploadId)
formData.append('chunkIndex', String(chunkIndex))
formData.append('chunkMd5', chunkMd5)
formData.append('file', file, file.name || 'image')
if (requestController) requestController.bind(abort)
globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk?${query}`, {
timeoutId = setTimeout(() => {
abortController.abort()
rejectOnce(createRequestError('文件分片上传超时', 'REQUEST_TIMEOUT'))
}, REQUEST_TIMEOUT_MS)
globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`, {
method: 'POST',
headers: {
clientid: runtimeConfig.clientId,
@@ -360,67 +391,6 @@ const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { req
})
})
const requestNativeSingleFileUpload = ({ filePath }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('文件上传控制器格式无效'))
return
}
let settled = false
let task = null
const release = () => requestController?.release(abort)
const resolveOnce = (value) => {
if (settled) return
settled = true
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
release()
reject(error)
}
const abort = () => {
if (settled) return
task?.abort?.()
rejectOnce(createRequestCancelledError())
}
try {
const token = session.getToken()
task = uni.uploadFile({
url: `${runtimeConfig.baseUrl}/genealogy/app/files/upload`,
filePath,
name: 'file',
header: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
success: ({ data, statusCode }) => {
if (statusCode !== 200) {
rejectOnce(createRequestError(`文件上传失败(HTTP ${statusCode}`, 'HTTP_ERROR', { httpStatus: statusCode }))
return
}
try {
resolveOnce(normalizeResumableCompleteResult(parseStrictUploadResponse(data, true)))
} catch (error) {
rejectOnce(error)
}
},
fail: (error) => {
const message = error?.errMsg || '文件上传网络连接失败'
rejectOnce(createRequestError(message, /timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR'))
}
})
if (requestController) requestController.bind(abort)
} catch (error) {
rejectOnce(error)
}
})
const requireRemoteAuth = () => {
if (resolveRuntimeMode() !== 'remote') {
const error = new Error('当前为本地预览模式,真实认证服务未启用')
@@ -1095,21 +1065,18 @@ const normalizeLineageText = (value, label, { required = false } = {}) => {
const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) throw lineageTreeError(`世系树${label}无效`)
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
throw lineageTreeError(`世系树${label}无效`)
}
if (normalized.length === 10) return normalized
const instant = new Date(normalized)
if (Number.isNaN(instant.getTime())) throw lineageTreeError(`世系树${label}无效`)
const parts = Object.fromEntries(
new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(instant)
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value])
)
return `${parts.year}-${parts.month}-${parts.day}`
const chinaTime = new Date(instant.getTime() + 8 * 60 * 60 * 1000)
const year = chinaTime.getUTCFullYear()
const month = String(chinaTime.getUTCMonth() + 1).padStart(2, '0')
const day = String(chinaTime.getUTCDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const normalizeLineageTree = (value) => {
@@ -1870,7 +1837,7 @@ const normalizePhoneChangePayload = (payload) => {
if (typeof payload.phone !== 'string' || !/^1\d{10}$/.test(payload.phone.trim())) {
throw new TypeError('新手机号格式无效')
}
return authPayload({ phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode) })
return { phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode) }
}
const normalizeMemberUpdatePayload = (payload) => {
@@ -1898,36 +1865,23 @@ const normalizeVipOrderPayload = (payload) => {
return data
}
const normalizeFileReferencePayload = (payload) => {
assertPlainPayload(payload, new Set(['bizType', 'bizName', 'bizTable', 'bizId', 'bizField', 'ossId', 'ossIds', 'usageScene', 'usageName']), '文件引用请求')
const normalizeSiteArticleQuery = (payload = {}) => {
assertPlainPayload(payload, new Set(['articleType', 'limit']), '官网文章查询')
const data = {}
for (const field of ['bizType', 'bizName', 'bizTable', 'bizField', 'usageScene', 'usageName']) {
const value = normalizeOptionalAuthText(payload[field], field)
if (value) data[field] = value
const articleType = normalizeOptionalAuthText(payload.articleType, '官网文章类型')
if (articleType) data.articleType = articleType
if (payload.limit !== undefined && payload.limit !== null && payload.limit !== '') {
const limit = typeof payload.limit === 'number' ? payload.limit : Number(payload.limit)
if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('官网文章数量上限必须是正整数')
data.limit = limit
}
for (const field of ['bizType', 'bizTable', 'bizField']) {
if (!data[field]) throw new TypeError(`文件引用缺少 ${field}`)
}
data.bizId = Number(normalizeResourcePathId(payload.bizId, '业务标识'))
if (payload.ossId !== undefined && payload.ossId !== null && payload.ossId !== '') {
data.ossId = Number(normalizeResourcePathId(payload.ossId, '文件标识'))
}
const ossIds = normalizeOptionalOssIdList(payload.ossIds)
if (ossIds) data.ossIds = ossIds
if (!data.ossId && !data.ossIds) throw new TypeError('文件引用需要 ossId 或 ossIds')
return data
}
const normalizeFileReferenceReleasePayload = (payload) => {
assertPlainPayload(payload, new Set(['bizTable', 'bizId', 'bizField']), '文件引用释放请求')
const bizTable = normalizeOptionalAuthText(payload.bizTable, 'bizTable')
const bizField = normalizeOptionalAuthText(payload.bizField, 'bizField')
if (!bizTable || !bizField) throw new TypeError('文件引用释放缺少 bizTable 或 bizField')
return {
bizTable,
bizId: normalizeResourcePathId(payload.bizId, '业务标识'),
bizField
}
const normalizeSitePageKey = (pageKey) => {
const normalized = normalizeOptionalAuthText(pageKey, '官网页面标识')
if (!normalized) throw new TypeError('官网页面标识不能为空')
return normalized
}
const normalizeJoinApplicationPayload = (payload) => {
@@ -2252,20 +2206,6 @@ export const appApi = {
}
}, requestOptions)
},
async sendLegacySmsCode({ phone, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
return requestAuthVoid({
url: '/genealogy/app/auth/sms/code',
method: 'POST',
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'sms',
phone,
...(normalizedValidToken ? { validToken: normalizedValidToken } : {})
}
}, requestOptions)
},
async loginWithPassword({ phone, passwordHash, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
@@ -2287,7 +2227,7 @@ export const appApi = {
const result = await requestAuth({
url: '/genealogy/app/auth/login/sms',
method: 'POST',
data: authPayload({ grantType: 'sms', phone, smsCode: assertSmsCode(smsCode) })
data: { tenantId: runtimeConfig.tenantId, grantType: 'sms', phone, smsCode: assertSmsCode(smsCode) }
}, requestOptions)
return saveLogin(result)
},
@@ -2297,13 +2237,14 @@ export const appApi = {
const result = await requestAuth({
url: '/genealogy/app/auth/register',
method: 'POST',
data: authPayload({
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'password',
phone,
password: assertPasswordHash(passwordHash),
smsCode: assertSmsCode(smsCode),
...(normalizedNickName ? { nickName: normalizedNickName } : {})
})
}
}, requestOptions)
return saveLogin(result)
},
@@ -2312,12 +2253,13 @@ export const appApi = {
return requestAuthVoid({
url: '/genealogy/app/auth/password/reset',
method: 'PUT',
data: authPayload({
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'password',
phone,
newPassword: assertPasswordHash(passwordHash),
smsCode: assertSmsCode(smsCode)
})
}
}, requestOptions)
},
async changePassword({ oldPasswordHash, newPasswordHash }, requestOptions = {}) {
@@ -2340,7 +2282,7 @@ export const appApi = {
await requestStrict({
url: '/genealogy/app/auth/account/deactivate',
method: 'POST',
data: { clientId: runtimeConfig.clientId, smsCode: assertSmsCode(smsCode) }
data: { smsCode: assertSmsCode(smsCode) }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
@@ -2477,27 +2419,6 @@ export const appApi = {
})
return normalizeResumableCompleteResult(result)
},
async uploadSingleFile({ filePath }, requestOptions = {}) {
if (typeof filePath !== 'string' || !filePath.trim()) {
throw new TypeError('filePath必须是非空字符串')
}
requireRemoteResource('文件上传', '写入')
return requestNativeSingleFileUpload({ filePath: filePath.trim() }, requestOptions)
},
async createFileReference(payload, requestOptions = {}) {
return writeRemoteVoid(
'/genealogy/app/files/reference',
'POST',
normalizeFileReferencePayload(payload),
'文件引用',
requestOptions
)
},
async deleteFileReference(payload, requestOptions = {}) {
const data = normalizeFileReferenceReleasePayload(payload)
const query = `bizTable=${encodeURIComponent(data.bizTable)}&bizId=${encodeURIComponent(data.bizId)}&bizField=${encodeURIComponent(data.bizField)}`
return writeRemoteVoid(`/genealogy/app/files/reference?${query}`, 'DELETE', undefined, '文件引用', requestOptions)
},
async getRegionChildren(parentCode, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('地区选择需要真实读取服务,当前本地预览不会伪造地区列表')
@@ -2505,7 +2426,7 @@ export const appApi = {
throw error
}
const result = await requestStrict({
url: '/genealogy/region/children',
url: '/genealogy/app/region/children',
method: 'GET',
data: { parentCode: normalizeRegionParentCode(parentCode) }
}, {
@@ -2516,7 +2437,7 @@ export const appApi = {
async getRegionPath(regionCode, requestOptions = {}) {
requireRemoteResource('行政区划路径', '读取')
const result = await requestStrict({
url: `/genealogy/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
url: `/genealogy/app/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
@@ -2529,7 +2450,7 @@ export const appApi = {
async searchRegions(payload, requestOptions = {}) {
requireRemoteResource('行政区划搜索', '读取')
const result = await requestStrict({
url: '/genealogy/region/search',
url: '/genealogy/app/region/search',
method: 'GET',
data: normalizeRegionSearchPayload(payload)
}, {
@@ -2543,7 +2464,7 @@ export const appApi = {
async getRegion(regionCode, requestOptions = {}) {
requireRemoteResource('行政区划详情', '读取')
const result = await requestStrict({
url: `/genealogy/region/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
url: `/genealogy/app/region/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
@@ -3444,11 +3365,15 @@ export const appApi = {
},
async changePhone(payload, requestOptions = {}) {
requireRemoteAuth()
return requestAuthVoid({
await requestStrict({
url: '/genealogy/app/auth/phone',
method: 'PUT',
data: normalizePhoneChangePayload(payload)
}, requestOptions)
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
async getHelpArticles(requestOptions = {}) {
return readRemoteList('/genealogy/app/help-articles', '帮助文章', requestOptions)
@@ -3504,10 +3429,32 @@ export const appApi = {
const id = normalizeGenealogyPathId(genealogyId)
return readRemoteList(`/genealogy/app/genealogies/${id}/lineage/persons/options`, '世系人物选项', requestOptions)
},
async getContent(type, genealogyId, requestOptions = {}) {
const loaders = { article: this.getArticles, album: this.getAlbums, ceremony: this.getCeremonies, record: this.getGrowthRecords }
if (!loaders[type]) throw new Error('未知内容类型')
return loaders[type].call(this, genealogyId, requestOptions)
async getSiteArticles(payload = {}, requestOptions = {}) {
const result = await requestStrict({
url: '/genealogy/app/site/articles',
method: 'GET',
data: normalizeSiteArticleQuery(payload)
}, {
authenticated: false,
requestController: requestOptions.requestController ?? null
})
if (!Array.isArray(result)) {
throw createRequestError('官网文章列表响应不是数组', 'SITE_ARTICLE_LIST_RESPONSE_INVALID')
}
return result
},
async getSitePage(pageKey, requestOptions = {}) {
const result = await requestStrict({
url: `/genealogy/app/site/pages/${encodeURIComponent(normalizeSitePageKey(pageKey))}`,
method: 'GET'
}, {
authenticated: false,
requestController: requestOptions.requestController ?? null
})
if (!result || typeof result !== 'object' || Array.isArray(result)) {
throw createRequestError('官网单页响应不是对象', 'SITE_PAGE_RESPONSE_INVALID')
}
return result
},
async getNotifications(requestOptions = {}) {
if (!hasRemoteConfig()) return listNotificationFixtures()
+2
View File
@@ -3,6 +3,8 @@ export const AUTH_VERIFICATION_OPERATION = Object.freeze({
SMS_LOGIN: "sms-login",
REGISTER: "register",
FORGOT_PASSWORD: "forgot-password",
PHONE_CHANGE: "phone-change",
ACCOUNT_DEACTIVATE: "account-deactivate",
});
const SUPPORTED_TAC_TYPES = new Set([
+15 -7
View File
@@ -106,7 +106,15 @@ export const ROUTES = Object.freeze({
parent: "G05",
requiredParams: ["genealogyId"],
optionalParams: ["selectedId"],
allowedSources: ["G01", "G05", "T04", "T06", "T07"],
allowedSources: ["G01", "G05", "T02", "T04", "T06", "T07"],
}),
T02: defineRoute({
path: "/pages/tree/t02-pedigree-overview",
kind: "page",
parent: "G05",
requiredParams: ["genealogyId"],
optionalParams: ["selectedId"],
allowedSources: ["G01", "G05", "T01"],
}),
T03: defineRoute({
path: "/pages/tree/t03-member-profile",
@@ -114,8 +122,8 @@ export const ROUTES = Object.freeze({
parent: "T01",
parentParamMap: { selectedId: "personId" },
requiredParams: ["genealogyId", "personId"],
allowedSources: ["T01", "T07", "R02"],
resultOperations: ["member-open-requested"],
allowedSources: ["T01", "T02", "T07", "R02"],
resultOperations: ["member-open-requested", "member-updated"],
}),
T04: defineRoute({
path: "/pages/tree/t04-add-relative",
@@ -124,14 +132,14 @@ export const ROUTES = Object.freeze({
parentParamMap: { selectedId: "personId" },
requiredParams: ["genealogyId"],
optionalParams: ["personId", "mode", "relationType"],
allowedSources: ["T01"],
allowedSources: ["T01", "T02"],
}),
T05: defineRoute({
path: "/pages/tree/t05-edit-member",
kind: "flow",
parent: "T03",
requiredParams: ["genealogyId", "personId"],
allowedSources: ["T01", "T03"],
allowedSources: ["T01", "T02", "T03"],
}),
T06: defineRoute({
path: "/pages/tree/t06-edit-relationship",
@@ -140,14 +148,14 @@ export const ROUTES = Object.freeze({
parentParamMap: { selectedId: "personId" },
requiredParams: ["genealogyId", "personId"],
optionalParams: ["mode"],
allowedSources: ["T01"],
allowedSources: ["T01", "T02"],
}),
T07: defineRoute({
path: "/pages/tree/t07-member-directory",
kind: "page",
parent: "T01",
requiredParams: ["genealogyId"],
allowedSources: ["T01"],
allowedSources: ["T01", "T02"],
}),
T08: defineRoute({
path: "/pages/tree/t08-member-states",
+23 -2
View File
@@ -134,6 +134,27 @@ const pickBrowserVideo = () => new Promise((resolve, reject) => {
});
const readNativeFile = (filePath) => new Promise((resolve, reject) => {
if (typeof globalThis.uni?.getFileInfo === 'function') {
const normalizedPath = String(filePath || '')
const fileName = normalizedPath.split(/[\\/]/).pop() || 'file'
try {
globalThis.uni.getFileInfo({
filePath,
digestAlgorithm: 'md5',
success: (file) => resolve({
filePath,
fileName,
size: file.size,
contentType: 'application/octet-stream',
md5: String(file.digest || '').toLowerCase()
}),
fail: () => reject(createUploadError('读取图片信息失败', 'IMAGE_READ_FAILED'))
})
} catch (error) {
reject(error)
}
return
}
if (!globalThis.plus?.io?.resolveLocalFileSystemURL || !globalThis.plus?.io?.FileReader) {
reject(createUploadError("当前运行环境不支持读取图片", "IMAGE_READ_UNAVAILABLE"));
return;
@@ -202,7 +223,7 @@ export const pickAndUploadImage = async ({ requestController = null } = {}) => {
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID");
}
const fileMd5 = calcMD5Bytes(file.data);
const fileMd5 = file.md5 || calcMD5Bytes(file.data);
const initPayload = {
uploadId: createUploadId(),
fileName: file.fileName,
@@ -245,7 +266,7 @@ export const pickAndUploadVideo = async ({ requestController = null } = {}) => {
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
throw createUploadError("所选视频大小无效", "VIDEO_SIZE_INVALID");
}
const fileMd5 = calcMD5Bytes(file.data);
const fileMd5 = file.md5 || calcMD5Bytes(file.data);
const initPayload = {
uploadId: createUploadId(),
fileName,