837 lines
27 KiB
JavaScript
837 lines
27 KiB
JavaScript
import {
|
|
NOTICE_TARGETS,
|
|
ROOT_ROUTE_KEYS,
|
|
getRoute,
|
|
getRouteKeyByPath,
|
|
} from "./navigation-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 snapshotDataRecord = (name, value) => {
|
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new TypeError(`${name} 必须是对象`);
|
|
}
|
|
const prototype = Object.getPrototypeOf(value);
|
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
throw new TypeError(`${name} 必须是普通对象`);
|
|
}
|
|
const snapshot = Object.create(null);
|
|
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
if (typeof key !== "string") {
|
|
throw new TypeError(`${name} 不接受 Symbol 字段`);
|
|
}
|
|
const descriptor = descriptors[key];
|
|
if (!descriptor.enumerable) {
|
|
throw new TypeError(`${name} 字段 ${key} 必须可枚举`);
|
|
}
|
|
if (!hasOwn(descriptor, "value")) {
|
|
throw new TypeError(`${name} 字段 ${key} 不得使用访问器`);
|
|
}
|
|
Object.defineProperty(snapshot, key, {
|
|
value: descriptor.value,
|
|
enumerable: true,
|
|
writable: false,
|
|
configurable: false,
|
|
});
|
|
}
|
|
return Object.freeze(snapshot);
|
|
};
|
|
|
|
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 = snapshotDataRecord(`${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) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(candidate, name);
|
|
if (!descriptor) continue;
|
|
if (!descriptor.enumerable) {
|
|
throw new TypeError(`页面导航参数 ${name} 必须可枚举`);
|
|
}
|
|
if (!hasOwn(descriptor, "value")) {
|
|
throw new TypeError(`页面导航参数 ${name} 不得使用访问器`);
|
|
}
|
|
params[name] = descriptor.value;
|
|
}
|
|
}
|
|
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 = snapshotDataRecord(`${routeKey} 导航结果`, result);
|
|
|
|
const fields = Reflect.ownKeys(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);
|
|
|
|
// T03 是当前唯一 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 result = 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,
|
|
result,
|
|
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);
|
|
});
|
|
|
|
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 = snapshotDataRecord("通知目标参数", 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 = snapshotDataRecord("返回守卫上下文", 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 resolveBackAction = (context = {}) =>
|
|
resolveBackActionFromContext(validateBackContext(context));
|
|
|
|
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();
|
|
};
|