feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+68
View File
@@ -0,0 +1,68 @@
// 当前两个产品选项同时决定 OpenAPI GenealogyCreate/UpdateBody 的
// visibility 与 joinMode,因此它们是“访问预设”,不是单一可见范围。
export const GENEALOGY_ACCESS_PRESET = Object.freeze({
MEMBER_ONLY: "MEMBER_ONLY",
PUBLIC_APPLY: "PUBLIC_APPLY",
});
export const GENEALOGY_VISIBILITY = Object.freeze({
PRIVATE: "0",
PUBLIC: "1",
MEMBER_ONLY: "2",
});
export const GENEALOGY_JOIN_MODE = Object.freeze({
CLOSED: "0",
REVIEW: "1",
INVITATION: "2",
});
export const GENEALOGY_ACCESS_PRESET_OPTIONS = Object.freeze([
Object.freeze({
value: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
label: "仅成员可见",
}),
Object.freeze({
value: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY,
label: "公开可申请",
}),
]);
export const isGenealogyAccessPreset = (preset) =>
Object.values(GENEALOGY_ACCESS_PRESET).includes(preset);
export const isGenealogyVisibility = (value) =>
Object.values(GENEALOGY_VISIBILITY).includes(value);
export const isGenealogyJoinMode = (value) =>
Object.values(GENEALOGY_JOIN_MODE).includes(value);
// OpenAPI 为两个字段分别声明了 0/1/2,但当前产品只提供以下两个完整预设。
// 其他合法组合不能被悄悄改写成相近选项,页面应明确显示“访问规则待确认”。
const apiAccessByPreset = Object.freeze({
[GENEALOGY_ACCESS_PRESET.MEMBER_ONLY]: Object.freeze({
visibility: GENEALOGY_VISIBILITY.MEMBER_ONLY,
joinMode: GENEALOGY_JOIN_MODE.CLOSED,
}),
[GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY]: Object.freeze({
visibility: GENEALOGY_VISIBILITY.PUBLIC,
joinMode: GENEALOGY_JOIN_MODE.REVIEW,
}),
});
export const getGenealogyAccessPresetLabel = (preset) =>
GENEALOGY_ACCESS_PRESET_OPTIONS.find((item) => item.value === preset)
?.label || "访问规则待确认";
export const toApiGenealogyAccess = (preset) => {
const access = apiAccessByPreset[preset];
return access ? { ...access } : null;
};
export const fromApiGenealogyAccess = ({ visibility, joinMode } = {}) => {
const match = Object.entries(apiAccessByPreset).find(
([, access]) =>
access.visibility === visibility && access.joinMode === joinMode,
);
return match?.[0] || null;
};
+89
View File
@@ -0,0 +1,89 @@
const CURRENT_GENEALOGY_KEY = 'jiapu_current_genealogy_id'
const CURRENT_GENEALOGY_INVALIDATED_KEY = 'jiapu_current_genealogy_invalidated'
const normalizeGenealogyId = (genealogyId) => {
if (typeof genealogyId !== 'string') {
throw new TypeError('当前家谱 ID 必须是词法字符串')
}
if (!genealogyId || genealogyId.trim() !== genealogyId || /[\u0000-\u001f\u007f]/.test(genealogyId)) {
throw new TypeError('当前家谱 ID 必须是无边界空白的非空字符串')
}
return genealogyId
}
const markCurrentGenealogyInvalidated = () => {
uni.removeStorageSync(CURRENT_GENEALOGY_KEY)
uni.setStorageSync(CURRENT_GENEALOGY_INVALIDATED_KEY, '1')
}
const readCurrentGenealogyInvalidated = () => {
const stored = uni.getStorageSync(CURRENT_GENEALOGY_INVALIDATED_KEY)
if (stored === '' || stored === null || stored === undefined) return false
if (stored !== '1') uni.setStorageSync(CURRENT_GENEALOGY_INVALIDATED_KEY, '1')
return true
}
const readCurrentGenealogyId = () => {
const stored = uni.getStorageSync(CURRENT_GENEALOGY_KEY)
if (stored === '' || stored === null || stored === undefined) return ''
try {
return normalizeGenealogyId(stored)
} catch {
// 旧版本或损坏的存储值不应继续流入路由和权限判断。发现异常时只清理
// 这一唯一键;下一次取得可用家谱列表后再按确定顺序建立新上下文。
markCurrentGenealogyInvalidated()
return ''
}
}
export const genealogyContext = {
getCurrentGenealogyId: readCurrentGenealogyId,
setCurrentGenealogyId: (genealogyId) => {
const normalizedId = normalizeGenealogyId(genealogyId)
uni.setStorageSync(CURRENT_GENEALOGY_KEY, normalizedId)
uni.removeStorageSync(CURRENT_GENEALOGY_INVALIDATED_KEY)
return normalizedId
},
invalidateCurrentGenealogyId: () => {
markCurrentGenealogyInvalidated()
return ''
},
isCurrentGenealogyInvalidated: readCurrentGenealogyInvalidated,
clearCurrentGenealogyId: () => {
uni.removeStorageSync(CURRENT_GENEALOGY_KEY)
uni.removeStorageSync(CURRENT_GENEALOGY_INVALIDATED_KEY)
},
reconcileCurrentGenealogyId: (availableIds, preferredId = '') => {
if (!Array.isArray(availableIds)) {
throw new TypeError('可用家谱 ID 必须是数组')
}
const normalizedIds = availableIds.map(normalizeGenealogyId)
if (new Set(normalizedIds).size !== normalizedIds.length) {
throw new TypeError('可用家谱列表包含重复 ID')
}
const normalizedPreferredId = preferredId === ''
? ''
: normalizeGenealogyId(preferredId)
const storedId = readCurrentGenealogyId()
if (normalizedPreferredId) {
if (!normalizedIds.includes(normalizedPreferredId)) {
markCurrentGenealogyInvalidated()
return ''
}
return genealogyContext.setCurrentGenealogyId(normalizedPreferredId)
}
if (storedId) {
if (!normalizedIds.includes(storedId)) {
markCurrentGenealogyInvalidated()
return ''
}
return genealogyContext.setCurrentGenealogyId(storedId)
}
if (readCurrentGenealogyInvalidated()) return ''
if (!normalizedIds.length) {
uni.removeStorageSync(CURRENT_GENEALOGY_KEY)
return ''
}
return genealogyContext.setCurrentGenealogyId(normalizedIds[0])
}
}
+71
View File
@@ -0,0 +1,71 @@
import { GENERATION_POEM_STATUS } from '@/services/api/generation-poem-contract.js'
export { GENERATION_POEM_STATUS }
// 三个限制逐一对应 GenerationPoemBatchBody:输入总长 26000、单代文字
// 最多 50 字符、单批最多 500 个世代,不能再合并成一个含义模糊的“长度”。
export const MAX_GENERATION_COUNT = 500
export const MAX_GENERATION_TEXT_LENGTH = 50
export const MAX_GENERATION_POEM_INPUT_LENGTH = 26000
const separatorPattern = /[\s,;;、/|]/u
const separatorRunPattern = /[\s,;;、/|]+/u
const forbiddenControlPattern = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u
const codePointLength = (value) => Array.from(value).length
const normalizeGenerationText = (value) => {
const text = String(value ?? '').trim()
if (
!text ||
codePointLength(text) > MAX_GENERATION_TEXT_LENGTH ||
forbiddenControlPattern.test(text)
) {
return null
}
return text
}
// 按 OpenAPI 的批量输入规则解析:没有分隔符时每个 Unicode code point 对应
// 一代;存在空格、逗号、分号、顿号、斜杠或竖线时,每个分段可包含多字。
export const validateGenerationPoemText = (value) => {
const input = String(value ?? '')
if (!input.trim()) {
return Object.freeze({ valid: false, generations: [], message: '请录入字辈内容' })
}
if (codePointLength(input) > MAX_GENERATION_POEM_INPUT_LENGTH) {
return Object.freeze({
valid: false,
generations: [],
message: `字辈输入最多 ${MAX_GENERATION_POEM_INPUT_LENGTH} 个字符`,
})
}
if (forbiddenControlPattern.test(input)) {
return Object.freeze({
valid: false,
generations: [],
message: '字辈内容包含不支持的控制字符',
})
}
const generations = separatorPattern.test(input)
? input.split(separatorRunPattern).map((item) => item.trim()).filter(Boolean)
: Array.from(input)
if (!generations.length) {
return Object.freeze({ valid: false, generations, message: '请录入字辈内容' })
}
if (generations.length > MAX_GENERATION_COUNT) {
return Object.freeze({
valid: false,
generations,
message: `一次最多录入 ${MAX_GENERATION_COUNT} 个世代`,
})
}
if (generations.some((item) => normalizeGenerationText(item) === null)) {
return Object.freeze({
valid: false,
generations,
message: `每代字辈文字不能为空且最多 ${MAX_GENERATION_TEXT_LENGTH} 个字符`,
})
}
return Object.freeze({ valid: true, generations, message: '' })
}