feat: migrate app routes and business modules
This commit is contained in:
@@ -0,0 +1,852 @@
|
||||
import {
|
||||
NOTICE_TARGETS,
|
||||
ROOT_ROUTE_KEYS,
|
||||
getRoute,
|
||||
getRouteKeyByPath,
|
||||
} from "./routes.js";
|
||||
|
||||
const navigationResults = new Map();
|
||||
const pageInstanceTokens = new WeakMap();
|
||||
let navigationInFlight = null;
|
||||
let pageInstanceSequence = 0;
|
||||
|
||||
const hasOwn = (value, key) =>
|
||||
Object.prototype.hasOwnProperty.call(value, key);
|
||||
|
||||
const getPageInstanceToken = (page) => {
|
||||
if ((typeof page !== "object" || page === null) && typeof page !== "function") {
|
||||
return null;
|
||||
}
|
||||
let token = pageInstanceTokens.get(page);
|
||||
if (!token) {
|
||||
pageInstanceSequence += 1;
|
||||
token = pageInstanceSequence;
|
||||
pageInstanceTokens.set(page, token);
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const copyDataRecord = (name, value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError(`${name} 必须是对象`);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
};
|
||||
|
||||
const assertScalarString = (name, value) => {
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
throw new TypeError(`导航参数 ${name} 必须是非空字符串`);
|
||||
}
|
||||
};
|
||||
|
||||
const validateRouteParams = (routeKey, params, requireAll) => {
|
||||
const route = getRoute(routeKey);
|
||||
if (!route) throw new Error(`未知路由键:${routeKey}`);
|
||||
const normalizedParams = copyDataRecord(`${routeKey} 导航参数`, params);
|
||||
|
||||
const allowed = new Set([...route.requiredParams, ...route.optionalParams]);
|
||||
for (const name of Object.keys(normalizedParams)) {
|
||||
if (!allowed.has(name)) {
|
||||
throw new Error(`${routeKey} 不接受导航参数 ${name}`);
|
||||
}
|
||||
assertScalarString(name, normalizedParams[name]);
|
||||
}
|
||||
|
||||
if (requireAll) {
|
||||
for (const name of route.requiredParams) {
|
||||
if (!hasOwn(normalizedParams, name)) {
|
||||
throw new Error(`${routeKey} 缺少导航参数 ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { route, params: normalizedParams };
|
||||
};
|
||||
|
||||
const encodeQuery = (entries) =>
|
||||
entries
|
||||
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
|
||||
const buildValidatedRouteUrl = (routeKey, route, params, sourceKey = "") => {
|
||||
if (sourceKey !== "") {
|
||||
assertScalarString("sourceKey", sourceKey);
|
||||
if (!route.allowedSources.includes(sourceKey)) {
|
||||
throw new Error(`${sourceKey} 不能进入 ${routeKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
const entries = [...route.requiredParams, ...route.optionalParams]
|
||||
.filter((name) => hasOwn(params, name))
|
||||
.map((name) => [name, params[name]]);
|
||||
if (sourceKey !== "") entries.push(["sourceKey", sourceKey]);
|
||||
const query = encodeQuery(entries);
|
||||
return query ? `${route.path}?${query}` : route.path;
|
||||
};
|
||||
|
||||
export const buildRouteUrl = (routeKey, params = {}, sourceKey = "") => {
|
||||
const validated = validateRouteParams(routeKey, params, true);
|
||||
return buildValidatedRouteUrl(
|
||||
routeKey,
|
||||
validated.route,
|
||||
validated.params,
|
||||
sourceKey,
|
||||
);
|
||||
};
|
||||
|
||||
const getPagePath = (page) => {
|
||||
const rawPath = page?.route || page?.$page?.route || page?.$page?.fullPath;
|
||||
if (typeof rawPath !== "string" || rawPath.length === 0) return "";
|
||||
return rawPath.split("?")[0];
|
||||
};
|
||||
|
||||
const getPageParams = (page, route) => {
|
||||
const params = Object.create(null);
|
||||
const routeFields = [...route.requiredParams, ...route.optionalParams];
|
||||
const candidates = [page?.$page?.query, page?.$page?.options, page?.options];
|
||||
|
||||
// UniApp 各端暴露页面参数的位置不同。只投影注册表声明字段,避免把
|
||||
// sourceKey 等传输字段误当成目标业务参数;后出现的容器拥有更高优先级,
|
||||
// 但空的顶层 options 不会遮蔽 $page.query/$page.options。
|
||||
for (const candidate of candidates) {
|
||||
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
||||
continue;
|
||||
}
|
||||
for (const name of routeFields) {
|
||||
if (hasOwn(candidate, name)) params[name] = candidate[name];
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
const readPageStack = () => {
|
||||
if (typeof getCurrentPages !== "function") {
|
||||
throw new Error("当前运行环境不支持页面栈读取");
|
||||
}
|
||||
const stack = getCurrentPages();
|
||||
if (!Array.isArray(stack)) throw new Error("页面栈格式无效");
|
||||
return stack;
|
||||
};
|
||||
|
||||
const getPageRouteKey = (page) => getRouteKeyByPath(getPagePath(page));
|
||||
|
||||
const assertActualSource = (routeKey, sourceKey) => {
|
||||
const route = getRoute(routeKey);
|
||||
if (!route) throw new Error(`未知路由键:${routeKey}`);
|
||||
assertScalarString("sourceKey", sourceKey);
|
||||
if (!route.allowedSources.includes(sourceKey)) {
|
||||
throw new Error(`${sourceKey} 不能进入 ${routeKey}`);
|
||||
}
|
||||
|
||||
const stack = readPageStack();
|
||||
const actualSource = stack.length > 0
|
||||
? getPageRouteKey(stack[stack.length - 1])
|
||||
: null;
|
||||
if (actualSource !== sourceKey) {
|
||||
throw new Error(
|
||||
`当前真实页面 ${actualSource || "UNKNOWN"} 与声明来源 ${sourceKey} 不一致`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isCurrentTarget = (routeKey, params) => {
|
||||
const stack = readPageStack();
|
||||
if (stack.length === 0) return false;
|
||||
const currentPage = stack[stack.length - 1];
|
||||
if (getPageRouteKey(currentPage) !== routeKey) return false;
|
||||
|
||||
const route = getRoute(routeKey);
|
||||
const currentParams = getPageParams(currentPage, route);
|
||||
for (const name of [...route.requiredParams, ...route.optionalParams]) {
|
||||
const targetHasParam = hasOwn(params, name);
|
||||
const currentHasParam = hasOwn(currentParams, name);
|
||||
if (targetHasParam !== currentHasParam) return false;
|
||||
if (targetHasParam && currentParams[name] !== params[name]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const runUniNavigation = (
|
||||
key,
|
||||
invoke,
|
||||
onStart = null,
|
||||
onAbort = null,
|
||||
onSuccess = null,
|
||||
) => {
|
||||
if (navigationInFlight?.key === key) return navigationInFlight.promise;
|
||||
if (navigationInFlight) return Promise.resolve(false);
|
||||
|
||||
let resolvePromise;
|
||||
let rejectPromise;
|
||||
let settled = false;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
resolvePromise = resolve;
|
||||
rejectPromise = reject;
|
||||
});
|
||||
const flight = { key, promise };
|
||||
navigationInFlight = flight;
|
||||
|
||||
const releaseFlight = () => {
|
||||
if (navigationInFlight === flight) navigationInFlight = null;
|
||||
};
|
||||
const resolveFlight = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
releaseFlight();
|
||||
resolvePromise(true);
|
||||
};
|
||||
const rejectFlight = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
let rejection = error;
|
||||
try {
|
||||
onAbort?.();
|
||||
} catch (abortError) {
|
||||
rejection = abortError;
|
||||
}
|
||||
releaseFlight();
|
||||
rejectPromise(rejection);
|
||||
};
|
||||
|
||||
try {
|
||||
onStart?.();
|
||||
invoke({
|
||||
success: () => {
|
||||
try {
|
||||
onSuccess?.();
|
||||
resolveFlight();
|
||||
} catch (error) {
|
||||
rejectFlight(error);
|
||||
}
|
||||
},
|
||||
fail: (error) => rejectFlight(
|
||||
new Error(error?.errMsg || "页面跳转失败"),
|
||||
),
|
||||
complete: () => {
|
||||
if (!settled) {
|
||||
rejectFlight(new Error("页面跳转未返回 success 或 fail"));
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
rejectFlight(error);
|
||||
}
|
||||
return promise;
|
||||
};
|
||||
|
||||
const asNavigationPromise = (execute) => {
|
||||
try {
|
||||
return execute();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateNavigationResult = (routeKey, result) => {
|
||||
const route = getRoute(routeKey);
|
||||
if (!route) throw new Error(`未知路由键:${routeKey}`);
|
||||
const normalizedResult = copyDataRecord(`${routeKey} 导航结果`, result);
|
||||
|
||||
const fields = Object.keys(normalizedResult);
|
||||
const allowedFields = new Set(["operation", "entityId", "refresh"]);
|
||||
for (const field of fields) {
|
||||
if (typeof field !== "string" || !allowedFields.has(field)) {
|
||||
throw new Error(`${routeKey} 导航结果不接受字段 ${String(field)}`);
|
||||
}
|
||||
}
|
||||
for (const requiredField of ["operation", "refresh"]) {
|
||||
if (!hasOwn(normalizedResult, requiredField)) {
|
||||
throw new Error(`${routeKey} 导航结果缺少 ${requiredField}`);
|
||||
}
|
||||
}
|
||||
|
||||
assertScalarString("operation", normalizedResult.operation);
|
||||
if (!route.resultOperations.includes(normalizedResult.operation)) {
|
||||
throw new Error(`${routeKey} 不接受结果 operation=${normalizedResult.operation}`);
|
||||
}
|
||||
if (hasOwn(normalizedResult, "entityId")) {
|
||||
assertScalarString("entityId", normalizedResult.entityId);
|
||||
}
|
||||
if (typeof normalizedResult.refresh !== "boolean") {
|
||||
throw new TypeError("导航结果 refresh 必须是布尔值");
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
operation: normalizedResult.operation,
|
||||
...(hasOwn(normalizedResult, "entityId")
|
||||
? { entityId: normalizedResult.entityId }
|
||||
: {}),
|
||||
refresh: normalizedResult.refresh,
|
||||
});
|
||||
};
|
||||
|
||||
const runWithNavigationResult = (
|
||||
routeKey,
|
||||
targetPage,
|
||||
sourcePage,
|
||||
targetParams,
|
||||
result,
|
||||
key,
|
||||
invoke,
|
||||
) => {
|
||||
const envelope = Object.seal({
|
||||
targetPageToken: getPageInstanceToken(targetPage),
|
||||
sourcePageToken: getPageInstanceToken(sourcePage),
|
||||
targetParams,
|
||||
result,
|
||||
});
|
||||
let started = false;
|
||||
|
||||
const rollback = () => {
|
||||
if (navigationResults.get(routeKey) !== envelope) return;
|
||||
navigationResults.delete(routeKey);
|
||||
};
|
||||
|
||||
const bindCreatedTarget = () => {
|
||||
if (navigationResults.get(routeKey) !== envelope) return;
|
||||
const stack = readPageStack();
|
||||
const currentPage = stack.length > 0 ? stack[stack.length - 1] : null;
|
||||
if (getPageRouteKey(currentPage) !== routeKey) {
|
||||
throw new Error(`${routeKey} 导航成功后未找到规范目标页`);
|
||||
}
|
||||
const currentPageToken = getPageInstanceToken(currentPage);
|
||||
const route = getRoute(routeKey);
|
||||
const currentParams = validateRouteParams(
|
||||
routeKey,
|
||||
getPageParams(currentPage, route),
|
||||
true,
|
||||
).params;
|
||||
const paramsMatch = routeParamsEqual(
|
||||
route,
|
||||
currentParams,
|
||||
envelope.targetParams,
|
||||
);
|
||||
if (envelope.targetPageToken !== null) {
|
||||
if (currentPageToken !== envelope.targetPageToken || !paramsMatch) {
|
||||
throw new Error(`${routeKey} 导航成功后的目标实例或参数不匹配`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentPageToken === envelope.sourcePageToken || !paramsMatch) {
|
||||
throw new Error(`${routeKey} 导航成功后的目标实例或参数不匹配`);
|
||||
}
|
||||
envelope.targetPageToken = currentPageToken;
|
||||
};
|
||||
|
||||
const promise = runUniNavigation(
|
||||
key,
|
||||
invoke,
|
||||
() => {
|
||||
started = true;
|
||||
// 任意新完成流程取得全局转场锁后,所有旧结果都已经错过各自目标页的
|
||||
// onShow 生命周期,必须先整体淘汰再写入当前结果;当前转场失败时只删除
|
||||
// 当前 envelope,不复活任何路由的陈旧结果。
|
||||
navigationResults.clear();
|
||||
navigationResults.set(routeKey, envelope);
|
||||
},
|
||||
rollback,
|
||||
bindCreatedTarget,
|
||||
);
|
||||
if (!started) return promise;
|
||||
|
||||
// 清理副作用挂在原 Promise 上,但仍返回原 Promise,保证相同语义调用的
|
||||
// Promise 身份一致;失败处理分支吞掉派生链结果,不改变调用方收到的拒绝。
|
||||
promise.then(
|
||||
(completed) => {
|
||||
if (!completed) rollback();
|
||||
},
|
||||
() => rollback(),
|
||||
);
|
||||
return promise;
|
||||
};
|
||||
|
||||
const pushPage = (url) =>
|
||||
runUniNavigation(`push:${url}`, (callbacks) =>
|
||||
uni.navigateTo({ url, ...callbacks }));
|
||||
|
||||
const activateExistingSinglePage = (routeKey, params, url) => {
|
||||
const stack = readPageStack();
|
||||
const targetIndexes = [];
|
||||
for (let index = stack.length - 2; index >= 0; index -= 1) {
|
||||
if (getPageRouteKey(stack[index]) === routeKey) {
|
||||
targetIndexes.push(index);
|
||||
}
|
||||
}
|
||||
if (targetIndexes.length > 1) {
|
||||
const error = new Error("T03_STACK_CONFLICT:页面栈中存在多个成员页实例");
|
||||
error.code = "T03_STACK_CONFLICT";
|
||||
throw error;
|
||||
}
|
||||
const targetIndex = targetIndexes[0] ?? -1;
|
||||
if (targetIndex < 0) return pushPage(url);
|
||||
|
||||
// 成员档案是当前唯一 single 页面。复用实例前必须锁定家谱上下文;若跨家谱强行
|
||||
// 复用,成员轨迹和一次性结果都会串到错误领域,因此明确失败而不是猜测回退。
|
||||
const existingParams = validateRouteParams(
|
||||
routeKey,
|
||||
getPageParams(stack[targetIndex], getRoute(routeKey)),
|
||||
true,
|
||||
).params;
|
||||
if (existingParams.genealogyId !== params.genealogyId) {
|
||||
const error = new Error("T03_CONTEXT_CONFLICT:既有成员页属于其他家谱");
|
||||
error.code = "T03_CONTEXT_CONFLICT";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const navigationResult = validateNavigationResult(routeKey, {
|
||||
operation: "member-open-requested",
|
||||
entityId: params.personId,
|
||||
refresh: false,
|
||||
});
|
||||
const delta = stack.length - 1 - targetIndex;
|
||||
const transitionKey = `single:${routeKey}:${delta}:${params.personId}`;
|
||||
return runWithNavigationResult(
|
||||
routeKey,
|
||||
stack[targetIndex],
|
||||
stack[stack.length - 1],
|
||||
existingParams,
|
||||
navigationResult,
|
||||
transitionKey,
|
||||
(callbacks) => uni.navigateBack({ delta, ...callbacks }),
|
||||
);
|
||||
};
|
||||
|
||||
export const openPage = (routeKey, params = {}, sourceKey = "") =>
|
||||
asNavigationPromise(() => {
|
||||
const validated = validateRouteParams(routeKey, params, true);
|
||||
const url = buildValidatedRouteUrl(
|
||||
routeKey,
|
||||
validated.route,
|
||||
validated.params,
|
||||
sourceKey,
|
||||
);
|
||||
assertActualSource(routeKey, sourceKey);
|
||||
if (isCurrentTarget(routeKey, validated.params)) return Promise.resolve(false);
|
||||
if (validated.route.kind === "single") {
|
||||
return activateExistingSinglePage(routeKey, validated.params, url);
|
||||
}
|
||||
return pushPage(url);
|
||||
});
|
||||
|
||||
const confirmExternalSiteContentTarget = (targetUrl) => new Promise((resolve, reject) => {
|
||||
const host = targetUrl.match(/^https:\/\/([^/?#]+)/)?.[1] || "外部网站";
|
||||
uni.showModal({
|
||||
title: "即将离开应用",
|
||||
content: `将打开外部网站 ${host},请确认链接来源可信。`,
|
||||
confirmText: "继续访问",
|
||||
cancelText: "取消",
|
||||
success: ({ confirm }) => resolve(confirm === true),
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
|
||||
export const openSiteContentTarget = async (targetUrl, onExternalFailure = null) => {
|
||||
if (
|
||||
typeof targetUrl !== "string" ||
|
||||
targetUrl.trim() !== targetUrl ||
|
||||
!(
|
||||
/^\/(?!\/)[^\s]*$/.test(targetUrl) ||
|
||||
/^https:\/\/[^\s/?#]+(?:[/?#][^\s]*)?$/.test(targetUrl)
|
||||
)
|
||||
) {
|
||||
return Promise.reject(new TypeError("站点内容跳转地址无效"));
|
||||
}
|
||||
if (onExternalFailure !== null && typeof onExternalFailure !== "function") {
|
||||
return Promise.reject(new TypeError("外部链接失败回调必须是函数"));
|
||||
}
|
||||
|
||||
if (targetUrl.startsWith("/")) {
|
||||
return runUniNavigation(`site-content:${targetUrl}`, (callbacks) =>
|
||||
uni.navigateTo({ url: targetUrl, ...callbacks }));
|
||||
}
|
||||
|
||||
if (!await confirmExternalSiteContentTarget(targetUrl)) return false;
|
||||
|
||||
if (typeof plus !== "undefined" && typeof plus.runtime?.openURL === "function") {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
plus.runtime.openURL(targetUrl, onExternalFailure || (() => {}));
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof window !== "undefined" && typeof window.location?.assign === "function") {
|
||||
window.location.assign(targetUrl);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.reject(new Error("当前运行环境不支持打开外部链接"));
|
||||
};
|
||||
|
||||
export const goRoot = (routeKey, params = {}) =>
|
||||
asNavigationPromise(() => {
|
||||
if (!ROOT_ROUTE_KEYS.includes(routeKey)) {
|
||||
throw new Error(`${routeKey} 不是根语义`);
|
||||
}
|
||||
const validated = validateRouteParams(routeKey, params, true);
|
||||
const url = buildValidatedRouteUrl(
|
||||
routeKey,
|
||||
validated.route,
|
||||
validated.params,
|
||||
);
|
||||
if (isCurrentTarget(routeKey, validated.params)) return Promise.resolve(false);
|
||||
return runUniNavigation(`root:${url}`, (callbacks) =>
|
||||
uni.reLaunch({ url, ...callbacks }));
|
||||
});
|
||||
|
||||
export const openNoticeTarget = (targetType, params, sourceKey = "N02") =>
|
||||
asNavigationPromise(() => {
|
||||
assertScalarString("通知目标类型", targetType);
|
||||
assertScalarString("通知来源", sourceKey);
|
||||
if (!hasOwn(NOTICE_TARGETS, targetType)) {
|
||||
throw new Error(`未知通知目标:${targetType}`);
|
||||
}
|
||||
if (!["N01", "N02"].includes(sourceKey)) {
|
||||
throw new Error(`${sourceKey} 不能进入通知目标`);
|
||||
}
|
||||
|
||||
const target = NOTICE_TARGETS[targetType];
|
||||
const normalizedParams = copyDataRecord("通知目标参数", params);
|
||||
const expectedParams = new Set(target.params);
|
||||
for (const name of Object.keys(normalizedParams)) {
|
||||
if (!expectedParams.has(name)) {
|
||||
throw new Error(`${targetType} 通知目标不接受参数 ${name}`);
|
||||
}
|
||||
assertScalarString(name, normalizedParams[name]);
|
||||
}
|
||||
for (const name of target.params) {
|
||||
if (!hasOwn(normalizedParams, name)) {
|
||||
throw new Error(`${targetType} 通知目标缺少参数 ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
return target.routeKey === "G01"
|
||||
? goRoot(target.routeKey, normalizedParams)
|
||||
: openPage(target.routeKey, normalizedParams, sourceKey);
|
||||
});
|
||||
|
||||
const collectParentParams = (childRoute, childParams, parentRoute) => {
|
||||
const parentParams = {};
|
||||
const parentFields = [
|
||||
...parentRoute.requiredParams,
|
||||
...parentRoute.optionalParams,
|
||||
];
|
||||
for (const name of parentFields) {
|
||||
if (
|
||||
hasOwn(childParams, name) &&
|
||||
typeof childParams[name] === "string" &&
|
||||
childParams[name].length > 0
|
||||
) {
|
||||
parentParams[name] = childParams[name];
|
||||
}
|
||||
}
|
||||
for (const [parentParam, childParam] of Object.entries(childRoute.parentParamMap)) {
|
||||
if (
|
||||
hasOwn(childParams, childParam) &&
|
||||
typeof childParams[childParam] === "string" &&
|
||||
childParams[childParam].length > 0
|
||||
) {
|
||||
parentParams[parentParam] = childParams[childParam];
|
||||
}
|
||||
}
|
||||
return parentParams;
|
||||
};
|
||||
|
||||
const runParentFallback = (routeKey, params) => {
|
||||
let childRoute = getRoute(routeKey);
|
||||
let childParams = params;
|
||||
|
||||
// 深链没有可信历史。逐级只继承注册表声明的同名字段和 parentParamMap;任何
|
||||
// 必填字段不足的中间页都不能伪造,继续上溯到首个可合法构造的父语义。
|
||||
while (childRoute?.parent) {
|
||||
const parentKey = childRoute.parent;
|
||||
const parentRoute = getRoute(parentKey);
|
||||
const parentParams = collectParentParams(childRoute, childParams, parentRoute);
|
||||
const hasRequiredParams = parentRoute.requiredParams.every((name) =>
|
||||
hasOwn(parentParams, name));
|
||||
|
||||
if (hasRequiredParams) {
|
||||
const url = buildRouteUrl(parentKey, parentParams);
|
||||
if (ROOT_ROUTE_KEYS.includes(parentKey)) {
|
||||
return runUniNavigation(`fallback-root:${url}`, (callbacks) =>
|
||||
uni.reLaunch({ url, ...callbacks }));
|
||||
}
|
||||
return runUniNavigation(`fallback-replace:${url}`, (callbacks) =>
|
||||
uni.redirectTo({ url, ...callbacks }));
|
||||
}
|
||||
|
||||
childRoute = parentRoute;
|
||||
childParams = parentParams;
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
};
|
||||
|
||||
export const goBack = () =>
|
||||
asNavigationPromise(() => {
|
||||
const stack = readPageStack();
|
||||
if (stack.length === 0) throw new Error("当前页面栈为空");
|
||||
if (stack.length > 1) {
|
||||
return runUniNavigation("back:1", (callbacks) =>
|
||||
uni.navigateBack({ delta: 1, ...callbacks }));
|
||||
}
|
||||
|
||||
const currentPage = stack[0];
|
||||
const currentRouteKey = getPageRouteKey(currentPage);
|
||||
if (!currentRouteKey) throw new Error("当前页面不在活动路由注册表中");
|
||||
if (ROOT_ROUTE_KEYS.includes(currentRouteKey)) return Promise.resolve(false);
|
||||
const currentRoute = getRoute(currentRouteKey);
|
||||
return runParentFallback(
|
||||
currentRouteKey,
|
||||
getPageParams(currentPage, currentRoute),
|
||||
);
|
||||
});
|
||||
|
||||
const findNearestPageIndex = (stack, routeKey) => {
|
||||
for (let index = stack.length - 2; index >= 0; index -= 1) {
|
||||
if (getPageRouteKey(stack[index]) === routeKey) return index;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getResultTransitionKey = (result) => {
|
||||
if (!result) return "";
|
||||
const entityKey = hasOwn(result, "entityId")
|
||||
? `1:${encodeURIComponent(result.entityId)}`
|
||||
: "0";
|
||||
return `:${encodeURIComponent(result.operation)}:${entityKey}:${result.refresh ? "1" : "0"}`;
|
||||
};
|
||||
|
||||
const routeParamsEqual = (route, left, right) => {
|
||||
for (const name of [...route.requiredParams, ...route.optionalParams]) {
|
||||
const leftHasParam = hasOwn(left, name);
|
||||
const rightHasParam = hasOwn(right, name);
|
||||
if (leftHasParam !== rightHasParam) return false;
|
||||
if (leftHasParam && left[name] !== right[name]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const assertResultSourceContext = (targetRoute, targetParams, sourcePage) => {
|
||||
const sourceRouteKey = getPageRouteKey(sourcePage);
|
||||
const sourceRoute = getRoute(sourceRouteKey);
|
||||
if (!sourceRoute) return;
|
||||
|
||||
const sourceParams = validateRouteParams(
|
||||
sourceRouteKey,
|
||||
getPageParams(sourcePage, sourceRoute),
|
||||
true,
|
||||
).params;
|
||||
const targetFields = new Set([
|
||||
...targetRoute.requiredParams,
|
||||
...targetRoute.optionalParams,
|
||||
]);
|
||||
for (const name of [...sourceRoute.requiredParams, ...sourceRoute.optionalParams]) {
|
||||
if (
|
||||
targetFields.has(name) &&
|
||||
hasOwn(sourceParams, name) &&
|
||||
hasOwn(targetParams, name) &&
|
||||
sourceParams[name] !== targetParams[name]
|
||||
) {
|
||||
throw new Error(`完成来源上下文 ${name} 与目标不一致`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isImplicitRootOptionalContext = (
|
||||
routeKey,
|
||||
route,
|
||||
existingParams,
|
||||
targetParams,
|
||||
name,
|
||||
sourcePage,
|
||||
) => {
|
||||
if (
|
||||
!ROOT_ROUTE_KEYS.includes(routeKey) ||
|
||||
!route.optionalParams.includes(name) ||
|
||||
hasOwn(existingParams, name) ||
|
||||
!hasOwn(targetParams, name)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sourceRouteKey = getPageRouteKey(sourcePage);
|
||||
const sourceRoute = getRoute(sourceRouteKey);
|
||||
if (!sourceRoute) return false;
|
||||
const sourceParams = validateRouteParams(
|
||||
sourceRouteKey,
|
||||
getPageParams(sourcePage, sourceRoute),
|
||||
true,
|
||||
).params;
|
||||
return hasOwn(sourceParams, name) && sourceParams[name] === targetParams[name];
|
||||
};
|
||||
|
||||
const returnToValidated = (routeKey, targetParams, result) => {
|
||||
const validatedTarget = validateRouteParams(
|
||||
routeKey,
|
||||
targetParams,
|
||||
result !== null,
|
||||
);
|
||||
const route = validatedTarget.route;
|
||||
const normalizedTargetParams = validatedTarget.params;
|
||||
const stack = readPageStack();
|
||||
const targetIndex = findNearestPageIndex(stack, routeKey);
|
||||
const sourcePage = stack.length > 0 ? stack[stack.length - 1] : null;
|
||||
let targetPage = null;
|
||||
let targetIdentity;
|
||||
let transitionKey;
|
||||
let invoke;
|
||||
|
||||
if (targetIndex >= 0) {
|
||||
targetPage = stack[targetIndex];
|
||||
const existingParams = getPageParams(stack[targetIndex], route);
|
||||
const validatedExisting = validateRouteParams(routeKey, existingParams, true);
|
||||
targetIdentity = validatedExisting.params;
|
||||
for (const [name, value] of Object.entries(normalizedTargetParams)) {
|
||||
if (
|
||||
(!hasOwn(validatedExisting.params, name) ||
|
||||
validatedExisting.params[name] !== value) &&
|
||||
!isImplicitRootOptionalContext(
|
||||
routeKey,
|
||||
route,
|
||||
validatedExisting.params,
|
||||
normalizedTargetParams,
|
||||
name,
|
||||
sourcePage,
|
||||
)
|
||||
) {
|
||||
throw new Error(`${routeKey} 显式目标参数 ${name} 与最近实例不一致`);
|
||||
}
|
||||
}
|
||||
const delta = stack.length - 1 - targetIndex;
|
||||
transitionKey = `return:${routeKey}:${delta}${getResultTransitionKey(result)}`;
|
||||
invoke = (callbacks) => uni.navigateBack({ delta, ...callbacks });
|
||||
} else {
|
||||
const completeTarget = validateRouteParams(
|
||||
routeKey,
|
||||
normalizedTargetParams,
|
||||
true,
|
||||
);
|
||||
targetIdentity = completeTarget.params;
|
||||
const url = buildValidatedRouteUrl(routeKey, route, completeTarget.params);
|
||||
const resultKey = getResultTransitionKey(result);
|
||||
if (ROOT_ROUTE_KEYS.includes(routeKey)) {
|
||||
transitionKey = `return-root:${url}${resultKey}`;
|
||||
invoke = (callbacks) => uni.reLaunch({ url, ...callbacks });
|
||||
} else {
|
||||
transitionKey = `return-replace:${url}${resultKey}`;
|
||||
invoke = (callbacks) => uni.redirectTo({ url, ...callbacks });
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
assertResultSourceContext(route, targetIdentity, sourcePage);
|
||||
}
|
||||
|
||||
return result
|
||||
? runWithNavigationResult(
|
||||
routeKey,
|
||||
targetPage,
|
||||
sourcePage,
|
||||
targetIdentity,
|
||||
result,
|
||||
transitionKey,
|
||||
invoke,
|
||||
)
|
||||
: runUniNavigation(transitionKey, invoke);
|
||||
};
|
||||
|
||||
export const returnTo = (routeKey, targetParams = {}, ...unsupportedArgs) =>
|
||||
asNavigationPromise(() => {
|
||||
if (unsupportedArgs.length > 0) {
|
||||
throw new Error("returnTo 不接受流程结果,请使用 finishPage");
|
||||
}
|
||||
return returnToValidated(routeKey, targetParams, null);
|
||||
});
|
||||
|
||||
export const finishPage = (routeKey, targetParams = {}, result) =>
|
||||
asNavigationPromise(() => {
|
||||
const normalizedResult = validateNavigationResult(routeKey, result);
|
||||
return returnToValidated(routeKey, targetParams, normalizedResult);
|
||||
});
|
||||
|
||||
export const consumeNavigationResult = (routeKey) => {
|
||||
const route = getRoute(routeKey);
|
||||
if (!route) throw new Error(`未知路由键:${routeKey}`);
|
||||
const stack = readPageStack();
|
||||
if (
|
||||
stack.length === 0 ||
|
||||
getPageRouteKey(stack[stack.length - 1]) !== routeKey
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!navigationResults.has(routeKey)) return null;
|
||||
const envelope = navigationResults.get(routeKey);
|
||||
const currentPage = stack[stack.length - 1];
|
||||
const currentPageToken = getPageInstanceToken(currentPage);
|
||||
if (envelope.targetPageToken !== null) {
|
||||
if (currentPageToken !== envelope.targetPageToken) return null;
|
||||
} else if (currentPageToken === envelope.sourcePageToken) {
|
||||
return null;
|
||||
}
|
||||
const currentParams = validateRouteParams(
|
||||
routeKey,
|
||||
getPageParams(currentPage, route),
|
||||
true,
|
||||
).params;
|
||||
if (!routeParamsEqual(route, currentParams, envelope.targetParams)) return null;
|
||||
navigationResults.delete(routeKey);
|
||||
return envelope.result;
|
||||
};
|
||||
|
||||
const resolveBackActionFromContext = ({
|
||||
transientOpen = false,
|
||||
internalTrail = false,
|
||||
dirty = false,
|
||||
submitting = false,
|
||||
}) => {
|
||||
if (transientOpen) return "close-transient";
|
||||
if (internalTrail) return "pop-internal-trail";
|
||||
if (submitting) return "block-submitting";
|
||||
if (dirty) return "confirm-discard";
|
||||
return "go-back";
|
||||
};
|
||||
|
||||
const validateBackContext = (context) => {
|
||||
const normalizedContext = copyDataRecord("返回守卫上下文", context);
|
||||
for (const flag of ["transientOpen", "internalTrail", "dirty", "submitting"]) {
|
||||
if (hasOwn(normalizedContext, flag) && typeof normalizedContext[flag] !== "boolean") {
|
||||
throw new TypeError(`返回守卫状态 ${flag} 必须是布尔值`);
|
||||
}
|
||||
}
|
||||
return normalizedContext;
|
||||
};
|
||||
|
||||
// onBackPress 必须同步返回布尔值;而网关自己的 navigateBack 会再次触发该钩子。
|
||||
// 这里统一放行网关回调来源,并把页面异步守卫从同步平台钩子中安全分离。
|
||||
export const handleBackPress = (event, requestBack) => {
|
||||
if (event?.from === "navigateBack") return false;
|
||||
if (typeof requestBack !== "function") {
|
||||
throw new TypeError("handleBackPress 的 requestBack 必须是函数");
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(requestBack)
|
||||
.catch((error) => console.error("页面返回守卫执行失败", error));
|
||||
return true;
|
||||
};
|
||||
|
||||
export const runBackGuard = async (context = {}) => {
|
||||
const normalizedContext = validateBackContext(context);
|
||||
const action = resolveBackActionFromContext(normalizedContext);
|
||||
if (action === "go-back") return goBack();
|
||||
|
||||
const callback = normalizedContext[action];
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(`返回守卫缺少 ${action} 回调`);
|
||||
}
|
||||
const outcome = await callback();
|
||||
if (action !== "confirm-discard") {
|
||||
return true;
|
||||
}
|
||||
if (outcome !== true) return false;
|
||||
return goBack();
|
||||
};
|
||||
@@ -0,0 +1,439 @@
|
||||
// 本文件是活动页面导航语义的唯一运行时所有者。页面只能使用路由键,
|
||||
// 不得自行复制路径、父页、允许来源、参数或流程结果规则。
|
||||
const defineRoute = (route) =>
|
||||
Object.freeze({
|
||||
...route,
|
||||
parentParamMap: Object.freeze(route.parentParamMap || {}),
|
||||
requiredParams: Object.freeze(route.requiredParams || []),
|
||||
optionalParams: Object.freeze(route.optionalParams || []),
|
||||
allowedSources: Object.freeze(route.allowedSources || []),
|
||||
resultOperations: Object.freeze(route.resultOperations || []),
|
||||
});
|
||||
|
||||
const defineNoticeTarget = (routeKey, params) =>
|
||||
Object.freeze({ routeKey, params: Object.freeze(params) });
|
||||
|
||||
export const NOTICE_TARGETS = Object.freeze({
|
||||
GENEALOGY_REVIEW: defineNoticeTarget("G10", ["genealogyId"]),
|
||||
GENEALOGY_HOME: defineNoticeTarget("G01", ["genealogyId"]),
|
||||
FAMILY_FEED: defineNoticeTarget("F03", ["genealogyId", "feedId"]),
|
||||
MEMO_REMINDER: defineNoticeTarget("R10", ["genealogyId", "memoId"]),
|
||||
CEREMONY_INVITE: defineNoticeTarget("M11", []),
|
||||
});
|
||||
|
||||
export const ROUTES = Object.freeze({
|
||||
A01: defineRoute({
|
||||
path: "/pages/auth/sign-in",
|
||||
kind: "auth-root",
|
||||
parent: null,
|
||||
resultOperations: ["password-reset"],
|
||||
}),
|
||||
A04: defineRoute({
|
||||
path: "/pages/auth/register",
|
||||
kind: "page",
|
||||
parent: "A01",
|
||||
allowedSources: ["A01"],
|
||||
}),
|
||||
A05: defineRoute({
|
||||
path: "/pages/auth/reset-password",
|
||||
kind: "page",
|
||||
parent: "A01",
|
||||
allowedSources: ["A01"],
|
||||
}),
|
||||
G01: defineRoute({
|
||||
path: "/pages/genealogy/my-genealogies",
|
||||
kind: "root",
|
||||
parent: null,
|
||||
optionalParams: ["genealogyId"],
|
||||
resultOperations: ["genealogy-created"],
|
||||
}),
|
||||
G03: defineRoute({
|
||||
path: "/pages/genealogy/create",
|
||||
kind: "flow",
|
||||
parent: "G01",
|
||||
allowedSources: ["G01"],
|
||||
}),
|
||||
G05: defineRoute({
|
||||
path: "/pages/genealogy/overview",
|
||||
kind: "page",
|
||||
parent: "G01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G01", "G03", "G06", "G09"],
|
||||
}),
|
||||
G06: defineRoute({
|
||||
path: "/pages/genealogy/search",
|
||||
kind: "page",
|
||||
parent: "G01",
|
||||
optionalParams: ["mode"],
|
||||
allowedSources: ["G01", "G03", "G09"],
|
||||
}),
|
||||
G08: defineRoute({
|
||||
path: "/pages/genealogy/join-application",
|
||||
kind: "flow",
|
||||
parent: "G06",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["source"],
|
||||
allowedSources: ["G01", "G05", "G06", "G09"],
|
||||
}),
|
||||
G09: defineRoute({
|
||||
path: "/pages/genealogy/my-applications",
|
||||
kind: "page",
|
||||
parent: "G01",
|
||||
optionalParams: ["status"],
|
||||
allowedSources: ["G01", "G05", "G06", "G08"],
|
||||
}),
|
||||
G10: defineRoute({
|
||||
path: "/pages/genealogy/application-review",
|
||||
kind: "page",
|
||||
parent: "G01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G01", "G05", "N01", "N02"],
|
||||
}),
|
||||
G11: defineRoute({
|
||||
path: "/pages/genealogy/settings",
|
||||
kind: "flow",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G05"],
|
||||
}),
|
||||
G12: defineRoute({
|
||||
path: "/pages/genealogy/generation-poems",
|
||||
kind: "flow",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G01", "G05"],
|
||||
}),
|
||||
G13: defineRoute({
|
||||
path: "/pages/genealogy/members",
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G05"],
|
||||
}),
|
||||
T01: defineRoute({
|
||||
path: "/pages/tree/overview",
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["selectedId"],
|
||||
allowedSources: ["G01", "G05", "T02", "T04", "T06", "T07"],
|
||||
}),
|
||||
T02: defineRoute({
|
||||
path: "/pages/tree/pedigree",
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["selectedId"],
|
||||
allowedSources: ["G01", "G05", "T01"],
|
||||
}),
|
||||
T03: defineRoute({
|
||||
path: "/pages/tree/member-profile",
|
||||
kind: "single",
|
||||
parent: "T01",
|
||||
parentParamMap: { selectedId: "personId" },
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
allowedSources: ["T01", "T02", "T07", "R02"],
|
||||
resultOperations: ["member-open-requested", "member-updated"],
|
||||
}),
|
||||
T04: defineRoute({
|
||||
path: "/pages/tree/add-relative",
|
||||
kind: "flow",
|
||||
parent: "T01",
|
||||
parentParamMap: { selectedId: "personId" },
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["personId", "mode", "relationType"],
|
||||
allowedSources: ["T01", "T02"],
|
||||
}),
|
||||
T05: defineRoute({
|
||||
path: "/pages/tree/edit-member",
|
||||
kind: "flow",
|
||||
parent: "T03",
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
allowedSources: ["T01", "T02", "T03"],
|
||||
}),
|
||||
T06: defineRoute({
|
||||
path: "/pages/tree/member-rank",
|
||||
kind: "flow",
|
||||
parent: "T01",
|
||||
parentParamMap: { selectedId: "personId" },
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
optionalParams: ["mode"],
|
||||
allowedSources: ["T01", "T02"],
|
||||
}),
|
||||
T07: defineRoute({
|
||||
path: "/pages/tree/member-directory",
|
||||
kind: "page",
|
||||
parent: "T01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["T01", "T02"],
|
||||
}),
|
||||
T08: defineRoute({
|
||||
path: "/pages/tree/member-states",
|
||||
kind: "page",
|
||||
parent: "T03",
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
allowedSources: ["T03"],
|
||||
}),
|
||||
F01: defineRoute({
|
||||
path: "/pages/family/feed",
|
||||
kind: "root",
|
||||
parent: null,
|
||||
optionalParams: ["genealogyId"],
|
||||
}),
|
||||
F02: defineRoute({
|
||||
path: "/pages/family/feed-editor",
|
||||
kind: "flow",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId", "mode"],
|
||||
optionalParams: ["feedId"],
|
||||
allowedSources: ["F01", "F03"],
|
||||
}),
|
||||
F03: defineRoute({
|
||||
path: "/pages/family/feed-detail",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId", "feedId"],
|
||||
allowedSources: ["F01", "F02", "N02"],
|
||||
}),
|
||||
F04: defineRoute({
|
||||
path: "/pages/family/articles",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
F05: defineRoute({
|
||||
path: "/pages/family/article-detail",
|
||||
kind: "page",
|
||||
parent: "F04",
|
||||
requiredParams: ["genealogyId", "articleId"],
|
||||
allowedSources: ["F04"],
|
||||
}),
|
||||
F06: defineRoute({
|
||||
path: "/pages/family/article-editor",
|
||||
kind: "flow",
|
||||
parent: "F04",
|
||||
requiredParams: ["genealogyId", "mode"],
|
||||
optionalParams: ["articleId"],
|
||||
allowedSources: ["F04", "F05"],
|
||||
}),
|
||||
F07: defineRoute({
|
||||
path: "/pages/family/albums",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
F08: defineRoute({
|
||||
path: "/pages/family/album-detail",
|
||||
kind: "page",
|
||||
parent: "F07",
|
||||
requiredParams: ["genealogyId", "albumId"],
|
||||
allowedSources: ["F07"],
|
||||
}),
|
||||
F09: defineRoute({
|
||||
path: "/pages/family/add-photo",
|
||||
kind: "flow",
|
||||
parent: "F08",
|
||||
requiredParams: ["genealogyId", "albumId"],
|
||||
allowedSources: ["F08"],
|
||||
}),
|
||||
F10: defineRoute({
|
||||
path: "/pages/family/videos",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
R01: defineRoute({
|
||||
path: "/pages/records/people",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
R02: defineRoute({
|
||||
path: "/pages/records/person-detail",
|
||||
kind: "flow",
|
||||
parent: "R01",
|
||||
requiredParams: ["genealogyId", "mode"],
|
||||
optionalParams: ["personId"],
|
||||
allowedSources: ["R01"],
|
||||
}),
|
||||
R03: defineRoute({
|
||||
path: "/pages/records/relative-records",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
R04: defineRoute({
|
||||
path: "/pages/records/relative-record-editor",
|
||||
kind: "flow",
|
||||
parent: "R03",
|
||||
requiredParams: ["genealogyId", "mode"],
|
||||
optionalParams: ["relativeId"],
|
||||
allowedSources: ["R03"],
|
||||
}),
|
||||
R05: defineRoute({
|
||||
path: "/pages/records/ceremonies",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
R06: defineRoute({
|
||||
path: "/pages/records/ceremony-detail",
|
||||
kind: "page",
|
||||
parent: "R05",
|
||||
requiredParams: ["genealogyId", "ceremonyId"],
|
||||
allowedSources: ["R05"],
|
||||
}),
|
||||
R07: defineRoute({
|
||||
path: "/pages/records/ceremony-editor",
|
||||
kind: "flow",
|
||||
parent: "R05",
|
||||
requiredParams: ["genealogyId", "mode"],
|
||||
optionalParams: ["ceremonyId"],
|
||||
allowedSources: ["R05", "R06"],
|
||||
}),
|
||||
R08: defineRoute({
|
||||
path: "/pages/records/growth-journal",
|
||||
kind: "page",
|
||||
parent: "R02",
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
allowedSources: ["R02", "T03"],
|
||||
}),
|
||||
R09: defineRoute({
|
||||
path: "/pages/records/life-events",
|
||||
kind: "page",
|
||||
parent: "R02",
|
||||
requiredParams: ["genealogyId", "personId"],
|
||||
allowedSources: ["R02", "T03"],
|
||||
}),
|
||||
R10: defineRoute({
|
||||
path: "/pages/records/memos",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["memoId"],
|
||||
allowedSources: ["F01", "N02"],
|
||||
}),
|
||||
R11: defineRoute({
|
||||
path: "/pages/records/merit-records",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
N01: defineRoute({
|
||||
path: "/pages/notification/message-center",
|
||||
kind: "page",
|
||||
parent: "G01",
|
||||
optionalParams: ["genealogyId"],
|
||||
allowedSources: ["G01", "M01"],
|
||||
}),
|
||||
N02: defineRoute({
|
||||
path: "/pages/notification/message-detail",
|
||||
kind: "page",
|
||||
parent: "N01",
|
||||
requiredParams: ["id"],
|
||||
allowedSources: ["N01"],
|
||||
}),
|
||||
M01: defineRoute({
|
||||
path: "/pages/profile/home",
|
||||
kind: "root",
|
||||
parent: null,
|
||||
}),
|
||||
M02: defineRoute({
|
||||
path: "/pages/profile/edit-profile",
|
||||
kind: "flow",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M03: defineRoute({
|
||||
path: "/pages/profile/security",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M04: defineRoute({
|
||||
path: "/pages/profile/change-password",
|
||||
kind: "flow",
|
||||
parent: "M03",
|
||||
allowedSources: ["M03"],
|
||||
}),
|
||||
M05: defineRoute({
|
||||
path: "/pages/profile/change-phone",
|
||||
kind: "flow",
|
||||
parent: "M03",
|
||||
allowedSources: ["M03"],
|
||||
}),
|
||||
M06: defineRoute({
|
||||
path: "/pages/profile/help",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M07: defineRoute({
|
||||
path: "/pages/profile/feedback",
|
||||
kind: "flow",
|
||||
parent: "M06",
|
||||
allowedSources: ["M01", "M06"],
|
||||
}),
|
||||
M08: defineRoute({
|
||||
path: "/pages/profile/promotions",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M09: defineRoute({
|
||||
path: "/pages/profile/vip",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M10: defineRoute({
|
||||
path: "/pages/profile/settings",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M11: defineRoute({
|
||||
path: "/pages/profile/ceremony-invitations",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01", "N02"],
|
||||
}),
|
||||
M12: defineRoute({
|
||||
path: "/pages/profile/earnings",
|
||||
kind: "page",
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
M13: defineRoute({
|
||||
path: "/pages/profile/compliance-document",
|
||||
kind: "page",
|
||||
parent: "M10",
|
||||
requiredParams: ["documentKey"],
|
||||
allowedSources: ["M10"],
|
||||
}),
|
||||
});
|
||||
|
||||
export const ROOT_ROUTE_KEYS = Object.freeze(["A01", "G01", "F01", "M01"]);
|
||||
|
||||
export const getRoute = (routeKey) =>
|
||||
typeof routeKey === "string" &&
|
||||
Object.prototype.hasOwnProperty.call(ROUTES, routeKey)
|
||||
? ROUTES[routeKey]
|
||||
: null;
|
||||
|
||||
export const getRouteKeyByPath = (path) => {
|
||||
if (typeof path !== "string") return null;
|
||||
const normalizedPath = `/${path.replace(/^\/+/, "")}`;
|
||||
return (
|
||||
Object.keys(ROUTES).find(
|
||||
(routeKey) => ROUTES[routeKey].path === normalizedPath,
|
||||
) || null
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user