/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { FlowEngineContext } from '../flowContext'; import { define, observable } from '../reactive'; import { encodeOpenViewRouteState, ViewParam as SharedViewParam } from '../utils'; type ViewParams = Omit & { viewUid?: string }; export interface GeneratePathnameFromViewParamsOptions { prefix?: string; basePath?: string; } export interface ViewNavigationOptions { basePath?: string; layoutBasePath?: string; } function encodeFilterByTk(val: SharedViewParam['filterByTk']): string { if (val === undefined || val === null) return ''; // 1.x 兼容:对象按 key1=v1&key2=v2 拼接后整体 encodeURIComponent if (val && typeof val === 'object' && !Array.isArray(val)) { const pairs = Object.entries(val).map(([k, v]) => { return `${encodeURIComponent(String(k))}=${encodeURIComponent(String(v))}`; }); return encodeURIComponent(pairs.join('&')); } // 其它情况按原单值编码(字符串) return encodeURIComponent(String(val)); } function hasUsableSourceId(sourceId: unknown): sourceId is string | number { return sourceId !== undefined && sourceId !== null && String(sourceId) !== ''; } function normalizeBasePath(basePath?: string) { const value = basePath || '/admin'; return `/${value.replace(/^\/+/, '').replace(/\/+$/, '')}`; } /** * 将 ViewParam 数组转换为 pathname * * @param viewParams - ViewParam 数组 * @returns 生成的 pathname * * @example * ```typescript * generatePathnameFromViewParams([{ viewUid: 'xxx' }]) // '/admin/xxx' * generatePathnameFromViewParams([{ viewUid: 'xxx', tabUid: 'yyy' }]) // '/admin/xxx/tab/yyy' * generatePathnameFromViewParams([{ viewUid: 'xxx' }, { viewUid: 'yyy' }]) // '/admin/xxx/view/yyy' * ``` */ export function generatePathnameFromViewParams( viewParams: ViewParams[], options: GeneratePathnameFromViewParamsOptions = {}, ): string { const basePath = normalizeBasePath(options.basePath || options.prefix); if (!viewParams || viewParams.length === 0) { return basePath; } const segments = basePath.replace(/^\/+/, '').split('/').filter(Boolean); viewParams.forEach((viewParam, index) => { // 如果不是第一个视图,添加 'view' 关键字 if (index > 0) { segments.push('view'); } // 添加视图 UID segments.push(viewParam.viewUid); const openViewRouteStateToken = encodeOpenViewRouteState(viewParam.viewUid, viewParam.openViewRouteState); if (openViewRouteStateToken) { segments.push('opts', openViewRouteStateToken); } // 添加参数 if (viewParam.tabUid) { segments.push('tab', viewParam.tabUid); } if (viewParam.filterByTk != null) { const encoded = encodeFilterByTk(viewParam.filterByTk); if (encoded) { segments.push('filterbytk', encoded); } } if (hasUsableSourceId(viewParam.sourceId)) { segments.push('sourceid', String(viewParam.sourceId)); } }); return '/' + segments.join('/'); } export class ViewNavigation { viewStack: ReadonlyArray; // 只能通过 setViewStack 修改 ctx: FlowEngineContext; viewParams: ViewParams; private readonly basePath?: string; constructor(ctx: FlowEngineContext, viewParams: ViewParams[], options: ViewNavigationOptions = {}) { this.setViewStack(viewParams); this.ctx = ctx; this.basePath = options.basePath || options.layoutBasePath; define(this, { viewParams: observable, }); } setViewStack(viewParams: ViewParams[]) { this.viewStack = Object.freeze([...viewParams]); this.viewParams = this.viewStack[this.viewStack.length - 1] || {}; } changeTo(viewParam: ViewParams) { // 1. 根据传入的参数,合并成新的 viewStack const newViewStack = this.viewStack.map((item, index) => { if (index === this.viewStack.length - 1) { return { ...item, ...viewParam }; } return { ...item }; }); // 2. 根据 viewStack 生成新的 pathname const newPathname = generatePathnameFromViewParams(newViewStack, { basePath: this.getLayoutBasePath() }); // 3. 触发一次跳转。使用 replace 的方式 this.ctx.router.navigate(newPathname, { replace: true }); } navigateTo(viewParam: ViewParams, opts?: { replace?: boolean; state?: any }) { // 1. 基于当前 viewStack 生成一个 pathname // 2. 将当前传入的参数转为 path string const newViewPathname = generatePathnameFromViewParams([...this.viewStack, viewParam], { basePath: this.getLayoutBasePath(), }); // 3. 与 pathname 拼接成新的 pathname(这里直接使用新生成的 pathname) const newPathname = newViewPathname; // 4. 如果新的 pathname 与当前 ctx.route.pathname 不同,则触发一次跳转。使用 push 的方式 this.ctx.router.navigate(newPathname, opts); } back() { const prevStack = this.viewStack.slice(0, -1); const prevPath = generatePathnameFromViewParams(prevStack, { basePath: this.getLayoutBasePath() }); this.ctx.router.navigate(prevPath, { replace: true }); } private getLayoutBasePath() { const routePath = (this.ctx as any).layout?.routePath; return ( this.basePath || (this.ctx as any).layoutRoute?.basePathname || (routePath?.startsWith('/') ? routePath : '/admin') ); } }