新增 有家相册 uni-app 前端(uniapp/youjiala,H5 / 微信小程序 / App 三端)
从旧 WebVue 移植的地基部分: - 登录页(RSA+MD5 密码加密、redirect 回跳、中英文切换)与登录后落地页 - 401 / 403 / 404 错误页 + error-panel 通用外壳(文案走 ErrorPage 语言包) - 登录守卫双链路:uni.addInterceptor 拦站内跳转,页面级 onShow 复核冷启动深链 - 单一请求链路 common/http.interceptor:JWT 唯一来源、401 并发刷新单例、 业务异常识别、HTTP 403 跳无权限页 - H5 干净短地址:initRouter 前向 __uniRoutes 追加克隆记录(router/h5-clean-url) - 路由登记表 router/routes.ts(PAGE_PATHS / pageUrl / PAGE_PERMISSIONS) 与新增页面四步登记约定(src/md/page-routing.md) - i18n:uView Pro 语言包 + 后端词条合并,双取值(后端 zh-cn/en,包名 zh-CN/en-US) - 后端接口层 service/ 与数据模型 data/auto/ 为生成器产物,不手改 不入库的内容: - 摘除脚手架残留的 Harmony 签名密码(manifest.json signingConfigs 与 harmony-configs/build-profile.json5 的 keyPassword / storePassword 已置空) - 忽略 node_modules、dist、unpackage、.hbuilderx、.agents、pnpm-lock.yaml 及根目录临时截图
This commit is contained in:
1 parent
74757f9eb4
commit
6a8258ac4b
274 files changed
+30291
No files matched your search
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* H5 干净短地址:为每个页面追加一条短路径路由记录
|
||||
*
|
||||
* 背景:uni-h5-vite 的 generatePageRoute 生成 `path:'/${isEntry ? '' : path}'`,
|
||||
* 路由 path 就是 pages.json 里 path 的字面值,除入口页外没有任何 URL 重命名配置。
|
||||
* uni-h5 的 initRouter(app) 在插件 install 阶段才读取 window.__uniRoutes 去 createRouter,
|
||||
* 且读的是同一个数组引用,因此在 createRouter 之前往数组里追加记录即可生效。
|
||||
*
|
||||
* 为什么是"追加克隆记录"而不是"给原记录挂 alias":
|
||||
* 1. uni-h5 的 getRouteOptions(path, true) 用 `route.alias === path` 字符串全等匹配,
|
||||
* 数组形式的 alias 会让 uni.reLaunch 报 `page is not found`(深链能进、站内跳不进);
|
||||
* 2. 带 meta.isEntry 的记录在导航层会被 `url.replace(routeOptions.alias, "/")` 强改回根路径,
|
||||
* 地址栏到不了 /login;克隆体去掉 isEntry 才能保留短地址;
|
||||
* 3. 克隆体沿用 meta.route = 真实路径,uni 的页面栈、导航栏样式等仍按真实路径工作。
|
||||
*
|
||||
* 仅在 H5 引入(main.ts 用 #ifdef H5 包裹),其他端不会打进包里。
|
||||
*/
|
||||
import type { PageName } from "@/router/routes";
|
||||
import { PAGE_PATHS, toCleanPath } from "@/router/routes";
|
||||
|
||||
interface UniRouteMeta {
|
||||
route?: string;
|
||||
isEntry?: boolean;
|
||||
isTabBar?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface UniRouteRecord {
|
||||
path: string;
|
||||
alias?: string | string[];
|
||||
meta?: UniRouteMeta;
|
||||
/** 标记本记录由本模块追加,避免被二次注册 */
|
||||
isCleanClone?: boolean;
|
||||
}
|
||||
|
||||
/** 已占用的地址:真实 path + 既有 alias(入口页的 alias 就是它的真实路径) */
|
||||
function collectTaken(routes: UniRouteRecord[]): Set<string> {
|
||||
const taken = new Set<string>();
|
||||
routes.forEach((route) => {
|
||||
taken.add(route.path);
|
||||
if (typeof route.alias === "string") {
|
||||
taken.add(route.alias);
|
||||
}
|
||||
else if (Array.isArray(route.alias)) {
|
||||
route.alias.forEach(item => taken.add(item));
|
||||
}
|
||||
});
|
||||
return taken;
|
||||
}
|
||||
|
||||
/** pages.json 的 path -> 页面名,用于套用 PAGE_PATHS 的短地址规则 */
|
||||
function toPageName(realPath: string): PageName | null {
|
||||
const entry = (Object.entries(PAGE_PATHS) as [PageName, string][]).find(
|
||||
([, value]) =>
|
||||
`/${value}` === realPath || value === realPath.replace(/^\//, ""),
|
||||
);
|
||||
return entry ? entry[0] : null;
|
||||
}
|
||||
|
||||
/** 复用原记录的 component/loader/meta,仅把 path 换成短地址 */
|
||||
function buildCleanRoute(route: UniRouteRecord, clean: string): UniRouteRecord {
|
||||
const clone: UniRouteRecord = { ...route, path: clean, isCleanClone: true };
|
||||
delete clone.alias;
|
||||
clone.meta = { ...route.meta, isEntry: undefined };
|
||||
return clone;
|
||||
}
|
||||
|
||||
function applyCleanUrls(routes: UniRouteRecord[]): void {
|
||||
const taken = collectTaken(routes);
|
||||
const additions: UniRouteRecord[] = [];
|
||||
routes.forEach((route) => {
|
||||
if (route.isCleanClone) {
|
||||
// 本模块追加的克隆体不参与二次注册
|
||||
return;
|
||||
}
|
||||
// 入口页 path 是 "/",真实路径记在 meta.route(生成规则:(alias || path).slice(1))
|
||||
const realPath = route.meta?.route ? `/${route.meta.route}` : route.path;
|
||||
const name = toPageName(realPath);
|
||||
if (!name) {
|
||||
// 漏登记不会导致页面坏掉(真实路径照常可用),但会丢掉短地址,显式提示
|
||||
console.warn(
|
||||
"[h5-clean-url] 页面未登记到 PAGE_PATHS(router/routes.ts),仅真实路径可访问:",
|
||||
realPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (route.meta?.isTabBar) {
|
||||
// tabBar 的路径判定依赖 route.path.slice(1),克隆记录会干扰 switchTab/preloadPage
|
||||
console.warn("[h5-clean-url] tabBar 页面不注册短地址:", realPath);
|
||||
return;
|
||||
}
|
||||
const clean = toCleanPath(name);
|
||||
if (taken.has(clean)) {
|
||||
console.warn(
|
||||
"[h5-clean-url] 短地址冲突,已跳过:",
|
||||
clean,
|
||||
"<-",
|
||||
realPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
taken.add(clean);
|
||||
additions.push(buildCleanRoute(route, clean));
|
||||
});
|
||||
// 遍历结束后再追加,避免边遍历边改数组
|
||||
additions.forEach(route => routes.push(route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 必须在 uni-h5 创建 router 之前完成注册。
|
||||
* pages 虚拟模块与本模块的求值先后不由我们控制,故两种情况都要覆盖:
|
||||
* 已赋值则直接改数组;未赋值则拦一次 setter,在赋值瞬间完成注册。
|
||||
*/
|
||||
export function registerCleanUrls(): void {
|
||||
const store = window as unknown as { __uniRoutes?: UniRouteRecord[] };
|
||||
if (Array.isArray(store.__uniRoutes)) {
|
||||
applyCleanUrls(store.__uniRoutes);
|
||||
return;
|
||||
}
|
||||
let routes: UniRouteRecord[] | undefined;
|
||||
Object.defineProperty(window, "__uniRoutes", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => routes,
|
||||
set: (value: UniRouteRecord[]) => {
|
||||
routes = value;
|
||||
applyCleanUrls(value);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 全局导航守卫(对应旧 WebVue 的 setupLoginGuard + catch-all 404)
|
||||
*
|
||||
* uni-app 没有 router.beforeEach,导航拦截靠 uni.addInterceptor:
|
||||
* invoke 钩子返回 false 时本次导航被静默丢弃(uni-h5 的 queue 见 res === false 直接返回空 thenable,
|
||||
* 既不执行原调用也不产生 promise rejection),所以取消导航不会给调用方留下未捕获异常。
|
||||
*
|
||||
* 覆盖范围(两条链路都要,缺一漏一半):
|
||||
* 1. 站内跳转:本文件的 addInterceptor,拦住 navigateTo/redirectTo/reLaunch/switchTab;
|
||||
* 2. 冷启动直达:H5 地址栏输入 / 分享链接 / 小程序编译进某个页面,不经过任何导航 API,
|
||||
* 拦截器拦不到,必须由页面自己调用 composables/useAuthGuard(页面级 onShow 校验)。
|
||||
*/
|
||||
import type { ErrorPageName, PageName } from "@/router/routes";
|
||||
import { getJwtInfo } from "@/common/http.interceptor";
|
||||
import {
|
||||
errorPageUrl,
|
||||
isAuthFree,
|
||||
normalizePath,
|
||||
pageUrl,
|
||||
resolvePageName,
|
||||
} from "@/router/routes";
|
||||
|
||||
/** 需要拦的导航 API(switchTab 目前没有 tabBar 页,留着以免将来漏) */
|
||||
const NAV_APIS = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
|
||||
|
||||
/** 是否已登录:只认请求层的 AccessToken(令牌唯一来源,与 store 口径一致) */
|
||||
function isLoggedIn(): boolean {
|
||||
return !!getJwtInfo()?.AccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定目标地址该被拦成哪个错误页;返回 null 表示放行
|
||||
*
|
||||
* 顺序即语义:地址不存在(404) -> 没登录(401) -> 没权限(403)
|
||||
* 403 这层要等 PAGE_PERMISSIONS 与 PermissionContext 移植后接在这里
|
||||
*/
|
||||
function evaluate(url: string): ErrorPageName | null {
|
||||
const name: PageName | null = resolvePageName(url);
|
||||
if (!name) {
|
||||
return "error404";
|
||||
}
|
||||
if (isAuthFree(name)) {
|
||||
return null;
|
||||
}
|
||||
if (!isLoggedIn()) {
|
||||
return "error401";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 跳错误页;redirectTo 保住返回栈("返回上一页"还有内容),页面栈为空时退化成 reLaunch */
|
||||
function jumpToError(blocked: ErrorPageName, from: string): void {
|
||||
const url = errorPageUrl(blocked, from);
|
||||
uni.redirectTo({
|
||||
url,
|
||||
fail: () => {
|
||||
uni.reLaunch({ url });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 冷启动/页面级校验:当前页面对象已由 uni 解析好,按真实路径判定
|
||||
* 返回 false 表示已被拦走,调用方(useAuthGuard)不必再做后续初始化
|
||||
*/
|
||||
export function ensureCurrentPageAuthorized(): boolean {
|
||||
const pages = getCurrentPages();
|
||||
const current = pages[pages.length - 1];
|
||||
if (!current) {
|
||||
// 页面栈还没建立(启动极早期),交给页面自己的 useAuthGuard 再判一次
|
||||
return true;
|
||||
}
|
||||
const path = `/${current.route}`;
|
||||
const blocked = evaluate(path);
|
||||
if (!blocked) {
|
||||
return true;
|
||||
}
|
||||
// getCurrentPages 只会给出真实路径,而 ?from= 是给用户看的:换成当前端的地址形态
|
||||
const name = resolvePageName(path);
|
||||
jumpToError(blocked, name ? pageUrl(name) : path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 注册导航拦截器;在 App.vue 的 onLaunch 调用,确保首次站内跳转就被管住 */
|
||||
export function setupNavGuard(): void {
|
||||
NAV_APIS.forEach((api) => {
|
||||
uni.addInterceptor(api, {
|
||||
invoke(args: { url?: string }) {
|
||||
const url = args?.url ?? "";
|
||||
const blocked = evaluate(url);
|
||||
if (!blocked) {
|
||||
return true;
|
||||
}
|
||||
jumpToError(blocked, url);
|
||||
// 取消本次导航
|
||||
return false;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* H5 专属:地址栏直接访问一个不存在的地址时,uni 路由表里没有对应页面,
|
||||
* 结果是白屏 + 一条 vue-router "No match found",且这条路径不经过导航 API,拦截器也管不到,
|
||||
* 只能在启动时按 location.pathname 兜底跳 404。
|
||||
*
|
||||
* 前提:manifest 的 h5.router.base 为 "/";改成子路径部署时这里要先剥掉 base 前缀。
|
||||
*/
|
||||
export function setupUnknownPathFallback(): void {
|
||||
// #ifdef H5
|
||||
if (resolvePageName(window.location.pathname)) {
|
||||
return;
|
||||
}
|
||||
const from = `${window.location.pathname}${window.location.search}`;
|
||||
// 延到下一个任务:此时 uni 已完成首屏路由解析,reLaunch 才作用在真实页面栈上
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: errorPageUrl("error404", from) });
|
||||
}, 0);
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 供页面把用户送回登录页(带 redirect,登录成功后回到原地址;缺省回 dashboard)
|
||||
* redirect 与 from 一样不做预编码,理由见 routes.ts 的 errorPageUrl
|
||||
*/
|
||||
export function goLoginWithRedirect(from?: string): void {
|
||||
const target = from && resolvePageName(from) ? from : pageUrl("dashboard");
|
||||
uni.reLaunch({
|
||||
url: `${pageUrl("login")}?redirect=${normalizePath(target)}`,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 页面路径的唯一来源
|
||||
*
|
||||
* 约束(勿改):
|
||||
* - PAGE_PATHS 的值必须与 pages.json 里的 path 字面一致;
|
||||
* - 小程序 / App 端导航只能使用真实路径,且 getCurrentPages() 返回的 route 也是真实路径,
|
||||
* 因此"路径比较"必须用 toRealPath(),只有"跳转用的 URL"才允许用 pageUrl();
|
||||
* - 详细设计与源码证据见 src/md/page-routing.md。
|
||||
*/
|
||||
export const PAGE_PATHS = {
|
||||
login: "pages/login/login",
|
||||
dashboard: "pages/dashboard/dashboard",
|
||||
error401: "pages/error/page401",
|
||||
error403: "pages/error/page403",
|
||||
error404: "pages/error/page404",
|
||||
} as const;
|
||||
|
||||
export type PageName = keyof typeof PAGE_PATHS;
|
||||
|
||||
/** 错误页名(守卫与请求层按状态码跳页时只可能是这三个) */
|
||||
export type ErrorPageName = "error401" | "error403" | "error404";
|
||||
|
||||
/**
|
||||
* 短地址覆盖表:默认短地址取路径最后一段(pages/album/album => /album),
|
||||
* 错误页要与他系统(旧 WebVue、WebSitePolicy.Web401/403/404)逐字一致,故显式指定
|
||||
*/
|
||||
const CLEAN_PATH_OVERRIDES: Partial<Record<PageName, string>> = {
|
||||
error401: "/error/page401",
|
||||
error403: "/error/page403",
|
||||
error404: "/error/page404",
|
||||
};
|
||||
|
||||
/**
|
||||
* 免登录页面:登录页 + 三个错误页。
|
||||
* 错误页必须放行——401 页就是给"没登录/登录失效"的人看的,拦掉它只能看到登录框,看不到出错原因
|
||||
*/
|
||||
export const AUTH_FREE_PAGES: PageName[] = [
|
||||
"login",
|
||||
"error401",
|
||||
"error403",
|
||||
"error404",
|
||||
];
|
||||
|
||||
/**
|
||||
* 页面所需权限名(权限守卫用,格式同旧系统 meta.permission)。
|
||||
* 暂未接入:等菜单/功能权限体系移植时逐条填进来,缺省表示不校验
|
||||
*/
|
||||
export const PAGE_PERMISSIONS: Partial<Record<PageName, string>> = {};
|
||||
|
||||
/** 带前导斜杠的真实路径,供 uni.* API 之外的路径比较、日志使用 */
|
||||
export function toRealPath(name: PageName): string {
|
||||
return `/${PAGE_PATHS[name]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* H5 干净短地址:优先取覆盖表,否则取真实路径的最后一段
|
||||
* 依赖项目约定"目录名与文件名同名"(pages/dashboard/dashboard.vue => /dashboard)
|
||||
*/
|
||||
export function toCleanPath(name: PageName): string {
|
||||
const override = CLEAN_PATH_OVERRIDES[name];
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
const segments = PAGE_PATHS[name].split("/").filter(Boolean);
|
||||
return `/${segments[segments.length - 1]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 供 uni.navigateTo / reLaunch / redirectTo 使用的目标地址
|
||||
* H5 返回短地址(由 router/h5-clean-url 追加短地址路由记录,地址栏因此显示 /dashboard),
|
||||
* 其他端返回真实路径
|
||||
*/
|
||||
export function pageUrl(name: PageName): string {
|
||||
let url = toRealPath(name);
|
||||
// #ifdef H5
|
||||
url = toCleanPath(name);
|
||||
// #endif
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去 query/hash 并补前导斜杠:H5 入口页的路由 path 是 "/",也归一化成有效值。
|
||||
* 也是放进 ?from= / ?redirect= 前的清洗——保证参数值里只剩路径字符
|
||||
*/
|
||||
export function normalizePath(url: string): string {
|
||||
const pure = String(url || "")
|
||||
.split("?")[0]
|
||||
.split("#")[0];
|
||||
if (pure === "/" || pure === "") {
|
||||
return "/";
|
||||
}
|
||||
return pure.startsWith("/") ? pure : `/${pure}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意 URL(真实路径 / H5 短地址 / 带 query 或 hash / 无前导斜杠)-> 页面名
|
||||
* 认不出即返回 null,调用方据此判定"地址不存在"
|
||||
*/
|
||||
export function resolvePageName(url: string): PageName | null {
|
||||
const path = normalizePath(url);
|
||||
const names = Object.keys(PAGE_PATHS) as PageName[];
|
||||
// "/" 是 H5 入口页(登录页)的路由 path,不对应任何字面配置
|
||||
if (path === "/") {
|
||||
return "login";
|
||||
}
|
||||
const hit = names.find(
|
||||
name => path === toRealPath(name) || path === toCleanPath(name),
|
||||
);
|
||||
return hit ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误页地址:把出错的原地址用 ?from= 带过去,用户能一眼看出是哪个页面被拦。
|
||||
* 参数值只做 normalizePath 清洗、不做 encodeURIComponent:uni 不会先解这一层,
|
||||
* 而 vue-router 拼 URL 用的是 encodeURI(不编码 /,但会把 % 编成 %25),
|
||||
* 预编码只会让地址栏出现 ?from=%252Fdashboard 这种二次编码
|
||||
*/
|
||||
export function errorPageUrl(name: ErrorPageName, from?: string): string {
|
||||
const url = pageUrl(name);
|
||||
if (!from) {
|
||||
return url;
|
||||
}
|
||||
return `${url}?from=${normalizePath(from)}`;
|
||||
}
|
||||
|
||||
/** 当前页面真实路径(带前导斜杠);页面栈为空(启动极早期)返回空串 */
|
||||
export function currentRealPath(): string {
|
||||
const pages = getCurrentPages();
|
||||
const current = pages[pages.length - 1];
|
||||
return current ? `/${current.route}` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取守卫用 ?from= 带过来的地址。
|
||||
* 各端对 query 的解码时机不完全一致(H5 由 vue-router 解,小程序由平台解),
|
||||
* 故兜底再解一次;残留孤立 % 时 decodeURIComponent 会抛 URIError,原样返回即可
|
||||
*/
|
||||
export function decodeQueryPath(value: unknown): string {
|
||||
const raw = typeof value === "string" ? value : "";
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(raw);
|
||||
}
|
||||
catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否免登录页 */
|
||||
export function isAuthFree(name: PageName): boolean {
|
||||
return AUTH_FREE_PAGES.includes(name);
|
||||
}
|
||||
Reference in new issue
Block a user