mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-30 16:56:07 +08:00
feat(client): decouple modern client URL prefix from the build output (#9674)
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
# NocoBase Client Routing
|
||||
|
||||
How the two client runtimes (legacy and modern) are mounted under the app's URL space, and the terms used to describe their path prefixes. Seeded while making the modern client's URL prefix runtime-configurable.
|
||||
|
||||
## Language
|
||||
|
||||
**Legacy client**:
|
||||
The v1 client runtime (`@nocobase/client`, `SchemaComponent`), served at the app root.
|
||||
_Avoid_: v1 (in user-facing terms), old client
|
||||
|
||||
**Modern client**:
|
||||
The v2 client runtime (`@nocobase/client-v2`, FlowEngine / FlowModel), served under a dedicated URL prefix.
|
||||
_Avoid_: v2 (in user-facing terms), new client
|
||||
|
||||
**App public path**:
|
||||
The base URL path the whole NocoBase app is mounted under, set by `APP_PUBLIC_PATH` (default `/`).
|
||||
_Avoid_: base path, root path
|
||||
|
||||
**Modern client prefix**:
|
||||
The single URL path segment, directly under the app public path, where the modern client is served — set by `APP_MODERN_CLIENT_PREFIX` (default `v`, historically the hardcoded `v2`). A segment, not a full path; accepted in any of `v` / `/v` / `/v/` and normalized to a bare segment.
|
||||
_Avoid_: v2 prefix, route prefix, base url
|
||||
|
||||
**Modern client public path**:
|
||||
The full URL prefix the modern client actually runs under = app public path + modern client prefix (e.g. `/nocobase/` + `v` → `/nocobase/v/`). Used as the React Router basename (minus trailing slash) and injected to the browser as `window.__nocobase_public_path__`.
|
||||
_Avoid_: v2 public path
|
||||
|
||||
**Modern client build directory**:
|
||||
The fixed on-disk location of the modern client's built assets (`dist/client/v/`), named from `DEFAULT_MODERN_CLIENT_PREFIX`. Internal and never user-facing; intentionally does NOT track the runtime **Modern client prefix**, so the prefix can change at runtime without a rebuild.
|
||||
_Avoid_: v2 dist, asset directory (without "build")
|
||||
|
||||
## Relationships
|
||||
|
||||
- The **Modern client public path** = **App public path** + **Modern client prefix** + `/`
|
||||
- The **Legacy client** is served at the **App public path**; the **Modern client** is served at the **Modern client public path** nested inside it
|
||||
- **App public path** and **Modern client prefix** vary independently; both default such that the modern client lands at `/v/`
|
||||
|
||||
## Example dialogue
|
||||
|
||||
> **Dev:** "If I deploy under `APP_PUBLIC_PATH=/nocobase/` and set `APP_MODERN_CLIENT_PREFIX=admin`, where does the modern client live?"
|
||||
> **Maintainer:** "At `/nocobase/admin/`. The prefix is a single segment composed under the app public path — it does not replace it."
|
||||
> **Dev:** "And the static build output folder?"
|
||||
> **Maintainer:** "That is a separate concept from the URL prefix — the folder name stays fixed regardless of what the prefix is set to."
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- **"v2"** was overloaded to mean three different things: (a) the **Modern client** runtime, (b) its URL **Modern client prefix**, and (c) the physical build-output directory name. Resolved: the runtime is the *modern client*; the URL segment is the *modern client prefix* (runtime-configurable, default `v`); the *modern client build directory* is a fixed internal constant (`v`), decoupled from the prefix so the prefix can change at runtime without rebuilding (see ADR-0001).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Modern client URL prefix is runtime-configurable, decoupled from a fixed build directory
|
||||
|
||||
The modern client (v2) was served under a hardcoded `/v2/` URL prefix. We make this prefix configurable via `APP_MODERN_CLIENT_PREFIX` (default `v`), and crucially **decouple the user-facing URL prefix from the build-output directory**: the directory name is a fixed constant (`v`, from `DEFAULT_MODERN_CLIENT_PREFIX`) baked once at build time, while the URL prefix is read at runtime from the environment. This lets operators change the prefix without rebuilding — `APP_MODERN_CLIENT_PREFIX=admin yarn start` serves the same `dist/client/v/` assets under `/admin/`.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Prefix flows into the build (directory follows the env var).** Rejected: the build-output directory would be named after the prefix, so the runtime prefix would have to match the build-time prefix — changing it would require a rebuild, breaking the "runtime effective" requirement and forcing the default literal into both build config and runtime init.
|
||||
- **Keep the build directory/sentinel named `v2` while the URL is `v`.** Rejected: the `v2`-internal / `v`-external split is needless cognitive overhead. Unifying the fixed name to `v` (same word as the default prefix) removes it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The build is prefix-agnostic: CI needs no knowledge of the prefix. `output.assetPrefix` keeps baking the fixed dist-dir sentinel (`/v/`) into the HTML; the server rewrites it per request, and `main.tsx` sets `__webpack_public_path__` from the runtime public path so dynamically-imported chunks resolve correctly under an overridden prefix / sub-path / CDN without a rebuild. (This runtime-override approach was chosen over switching `assetPrefix` to `'auto'`, which would have changed the static asset URLs the server rewrite logic depends on.)
|
||||
- The server rewrites the baked `v` segment in the served HTML to the runtime modern client prefix, and injects it to the browser as `window.__nocobase_modern_client_prefix__`.
|
||||
- The shared logic is **split by runtime** rather than placed in a single cross-cutting package (`@nocobase/utils` was deliberately avoided): client helpers live in `@nocobase/client-v2` (read `window.__nocobase_modern_client_prefix__`), SSO server helpers live in `@nocobase/plugin-auth` (read `process.env`, alongside the existing `buildRedirectPath`). Each consumer imports from a package it already depends on, so no consumer reimplements the prefix logic and a future prefix change touches none of them.
|
||||
- The literal `v` is confined to: the default-prefix constant `DEFAULT_MODERN_CLIENT_PREFIX` in `cli-v1/src/util.js` (kept local so the CLI bootstrap stays lightweight), and the fixed build-directory name in `rsbuild.config.ts` + `gateway` — the latter two are exactly where `dist/client/v2` is already hardcoded today, so this is no worse than the status quo. All helpers read `window`/`process.env` and carry no literal; runtime readers rely on `initEnv()` having populated `process.env.APP_MODERN_CLIENT_PREFIX`.
|
||||
- The build-output directory name (`dist/client/v/`) is an internal storage location, never user-facing, and intentionally does *not* track the runtime prefix.
|
||||
|
||||
## Where the change lands
|
||||
|
||||
Hardcoded `/v2/` is replaced by env-driven reads. Existing internal symbol names (`resolveV2PublicPath`, `v2PublicPath`, …) are kept to minimize churn; only behavior changes.
|
||||
|
||||
Helper homes (no shared cross-cutting package; `@nocobase/utils` deliberately untouched):
|
||||
- `@nocobase/client-v2` — client helpers `getModernClientPrefix()` / `stripModernClientPrefix()` (read `window.__nocobase_modern_client_prefix__`). Imported by app `main.tsx`, `resolveAdminRouteRuntimeTarget.ts`, and the 2 markdown plugins.
|
||||
- `@nocobase/plugin-auth` (server) — SSO redirect helper (reads `process.env`), alongside the existing `buildRedirectPath`. Imported by the 3 SSO plugins.
|
||||
- `cli-v1/src/util.js` — local `DEFAULT_MODERN_CLIENT_PREFIX`; `initEnv` adds `APP_MODERN_CLIENT_PREFIX`; `resolveV2PublicPath` reads env.
|
||||
|
||||
Server runtime:
|
||||
- `server/src/gateway/utils.ts` — `resolveV2PublicPath`, `rewriteV2AssetPublicPath` (sentinel = fixed dir `v`).
|
||||
- `server/src/gateway/index.ts` — `getV2AssetPublicPath` (CDN), `getV2IndexTemplate` path (`dist/client/v`), inject `__nocobase_modern_client_prefix__`.
|
||||
|
||||
Build:
|
||||
- `app/client-v2/rsbuild.config.ts` — fixed dir `dist/client/v`, `assetPrefix` bakes the fixed sentinel, inject window prefix, dev base reads env.
|
||||
- `app/client/rsbuild.config.ts` — v1 dev proxy base reads env.
|
||||
- `build/src/injectPublicPathPlugin.ts` — inline data-URI reads `window.__nocobase_modern_client_prefix__` (only inline exception, cannot import).
|
||||
|
||||
Client runtime:
|
||||
- `app/client-v2/src/main.tsx`, `client-v2/.../resolveAdminRouteRuntimeTarget.ts` — use the `@nocobase/client-v2` helper + window var.
|
||||
|
||||
Nginx / docker:
|
||||
- `cli-v1/nocobase.conf.tpl` — alias → `dist/client/v/assets`.
|
||||
- `cli-v1/src/commands/create-nginx-conf.js` — `otherLocation` reads env.
|
||||
- Docker relies on `initEnv()` default (no Dockerfile `ENV`). The env var is intentionally NOT documented in `.env.example` for now (not yet exposed to users), though the runtime reads it.
|
||||
|
||||
Plugins (5 code files + comment updates):
|
||||
- SSO server redirects: `plugin-auth-saml`, `plugin-auth-oidc`, `plugin-auth-cas` → shared server helper reading env.
|
||||
- Client asset base: `plugin-block-markdown`, `plugin-field-markdown-vditor` → `stripModernClientPrefix`.
|
||||
- Comments only: `plugin-auth` (`buildRedirectPath.ts`, `hooks.ts`), `plugin-file-manager` (`filePreviewTypes.tsx`).
|
||||
|
||||
Tests: fixtures build paths from the runtime helper (env/window with `v` fallback) rather than a hardcoded `/v/`, so changing the default never breaks them.
|
||||
|
||||
## Two distinct kinds of change
|
||||
|
||||
There are two very different operations, and they cost very differently:
|
||||
|
||||
1. **Change the prefix for a deployment (runtime, no rebuild).** Set `APP_MODERN_CLIENT_PREFIX=/admin/` and restart. nginx, the node gateway, and the browser all read it at runtime; the prefix detaches from the fixed `dist/client/v` directory by design. This is the common case and the whole point of this ADR — nothing below applies. See "Changing the prefix in Docker" below for the operator steps.
|
||||
|
||||
2. **Change the baked-in default itself (code change + rebuild).** Only needed when you want to rename the default value or the on-disk build directory (e.g. `v` → `console`, so artifacts land in `dist/client/console`). This is rare and is what the checklist below is for.
|
||||
|
||||
## Changing the prefix in Docker (runtime, no image rebuild)
|
||||
|
||||
The published image needs no rebuild to change the prefix. The entrypoint
|
||||
(`docker/nocobase/docker-entrypoint.sh`) runs `yarn nocobase create-nginx-conf` on **every
|
||||
container start**, which regenerates `storage/nocobase.conf` from the current environment, and
|
||||
then starts both nginx and the node server — all three go through the CLI bootstrap (`initEnv`)
|
||||
and read the same `process.env`. So:
|
||||
|
||||
1. Set `APP_MODERN_CLIENT_PREFIX=/admin/` via the container environment — `environment:` in
|
||||
docker-compose, `docker run --env`, or the mounted `/app/nocobase/.env`. Process-level env
|
||||
wins over the `.env` file (`dotenv.config()` does not overwrite an existing `process.env`).
|
||||
2. Restart the container (`docker compose restart` / `docker restart <name>`). A restart is
|
||||
required — the nginx conf is generated at entrypoint time, not hot-reloaded.
|
||||
|
||||
On restart: nginx serves the modern client under `/admin/` while its `alias` still points at the
|
||||
fixed `dist/client/v/assets` baked into the image; the gateway rewrites the served HTML and
|
||||
injects `window.__nocobase_modern_client_prefix__`; dynamic chunks resolve via
|
||||
`__webpack_public_path__`. No image rebuild, no front-end rebuild, no change to the on-disk
|
||||
`dist/client/v` directory. The image deliberately does not bake `APP_MODERN_CLIENT_PREFIX`
|
||||
(or `APP_PUBLIC_PATH`) as a Dockerfile `ENV`, so the runtime value is never shadowed.
|
||||
|
||||
## Changing the baked-in default (rare; requires a rebuild)
|
||||
|
||||
Two conceptually separate literals; decide whether you are changing one or both.
|
||||
|
||||
**A. The default URL prefix segment** (what `/` resolves to when the env var is unset). Single source of truth:
|
||||
- `packages/core/cli-v1/src/util.js` — `DEFAULT_MODERN_CLIENT_PREFIX`.
|
||||
|
||||
Every server-side reader gets it from here via `initEnv()`. You may leave the build directory as `v` and only change this — then the default URL becomes e.g. `/console/` while assets still live in `dist/client/v/` (the gateway rewrites between them, exactly as a runtime override does).
|
||||
|
||||
**B. The fixed build-output directory name** (the on-disk folder + the HTML sentinel the server rewrites). If you also want the folder renamed, change all of these to the same value and rebuild:
|
||||
- `packages/core/app/client-v2/rsbuild.config.ts` — `MODERN_CLIENT_DIST_DIR` (drives `output.distPath` + the baked sentinel).
|
||||
- `packages/core/server/src/gateway/utils.ts` — `MODERN_CLIENT_DIST_DIR` (drives the rewrite sentinel + asset-path remap; also re-exported and used by `gateway/index.ts` for the `dist/client/<dir>/index.html` read path).
|
||||
- `packages/core/cli-v1/nocobase.conf.tpl` — the `alias … /dist/client/v/assets/` line (nginx serves the physical folder).
|
||||
|
||||
**Last-resort fallbacks** that hardcode the string `v` for the "env unset AND no injected value" edge case — keep them consistent with A (they are defensive, not the source of truth, so a stale value here only affects misconfigured runtimes):
|
||||
- `packages/core/app/client-v2/src/main.tsx` (`getBuildAssetDir` fallback)
|
||||
- `packages/core/app/client/rsbuild.config.ts` (v1 dev proxy)
|
||||
- `packages/core/build/src/injectPublicPathPlugin.ts` (inline data-URI, cannot import a constant)
|
||||
- `packages/core/client-v2/src/authRedirect.ts` (`getModernClientPrefix` final fallback)
|
||||
- `packages/plugins/@nocobase/plugin-auth/src/server/utils/buildRedirectPath.ts` (`getModernClientPrefix` fallback)
|
||||
|
||||
**Do NOT need changes** when renaming the default: test fixtures (they set the env var explicitly and template off it), `.env.example` (update only the documented example for clarity), and CI (prefix-agnostic).
|
||||
|
||||
After changing B you MUST rebuild so artifacts land in the new directory; a running app pointed at the old `dist/client/v` will otherwise 404.
|
||||
@@ -18,7 +18,7 @@ AI 开发插件的能力基于 [nocobase-plugin-development](https://github.com/
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v2/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- AI 生成的代码不一定 100% 正确,建议在启用前先 review 一遍。如果运行时遇到问题,可以把错误信息发给 AI,让它继续排查和修复——通常几轮对话就能解决。
|
||||
- 推荐使用 GPT 或 Claude 系列的大模型进行开发,效果最好。其他大模型也能用,不过生成质量可能会有差异。
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ nb init --ui
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v2/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- AI 生成的代码不一定 100% 正确,建议在启用前先 review 一遍。如果运行时遇到问题,可以把错误信息发给 AI,让它继续排查和修复——通常几轮对话就能解决。
|
||||
- 推荐使用 GPT 或 Claude 系列的大模型进行开发,效果最好。其他大模型也能用,不过生成质量可能会有差异。
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ keywords: "AI 开发,水印插件,NocoBase 插件,实战案例,AI 编程"
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v2/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- NocoBase 正在从 `client`(v1)向 `client-v2` 迁移,目前 `client-v2` 还在开发中。AI 开发生成的客户端代码基于 `client-v2`,只能在 `/v/` 路径下使用,供尝鲜体验,不建议直接上生产环境。
|
||||
- AI 生成的代码不一定 100% 正确,建议在启用前先 review 一遍。如果运行时遇到问题,可以把错误信息发给 AI,让它继续排查和修复——通常几轮对话就能解决。
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ keywords: "FAQ,常见问题,排错指南,Troubleshooting,NocoBase,构建,部署,
|
||||
|
||||
### 注册的页面路由访问不到
|
||||
|
||||
NocoBase v2 的路由会默认加上 `/v2` 前缀。比如你注册了 `path: '/hello'`,实际访问地址是 `/v2/hello`:
|
||||
NocoBase v2 的路由会默认加上 `/v` 前缀。比如你注册了 `path: '/hello'`,实际访问地址是 `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // 实际访问 -> /v2/hello
|
||||
path: '/hello', // 实际访问 -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ NocoBase 的构建系统维护了一份 [external 列表](../../dependency-manag
|
||||
## 相关链接
|
||||
|
||||
- [Plugin 插件](../plugin) — 插件入口和生命周期
|
||||
- [Router 路由](../router) — 路由注册和 `/v2` 前缀
|
||||
- [Router 路由](../router) — 路由注册和 `/v` 前缀
|
||||
- [FlowEngine 概述](../flow-engine/index.md) — FlowModel 基础用法
|
||||
- [FlowEngine → 区块扩展](../flow-engine/block) — BlockModel、TableBlockModel、filterCollection
|
||||
- [FlowEngine → 字段扩展](../flow-engine/field) — FieldModel、bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
通过 `ctx.router.navigate()` 进行页面跳转:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
获取当前路由参数:
|
||||
|
||||
@@ -316,7 +316,7 @@ async load() {
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### 路由信息(ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// 组件里:页面导航
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## 上下文提供的常用能力
|
||||
|
||||
@@ -15,7 +15,7 @@ keywords: "Router,路由,router.add,pluginSettingsManager,addMenuItem,addPageTab
|
||||
|
||||
:::warning 注意
|
||||
|
||||
NocoBase v2 的插件,路由注册后会默认加上 `/v2` 前缀,访问时需要带上这个前缀。
|
||||
NocoBase v2 的插件,路由注册后会默认加上 `/v` 前缀,访问时需要带上这个前缀。
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase 已经注册了以下默认路由:
|
||||
|
||||
| 名称 | 路径 | 组件 | 说明 |
|
||||
| -------------- | --------------------- | ------------------- | -------------- |
|
||||
| admin | /v2/admin/\* | AdminLayout | 后台管理页面 |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | 动态创建的页面 |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | 插件配置页面 |
|
||||
| admin | /v/admin/\* | AdminLayout | 后台管理页面 |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | 动态创建的页面 |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | 插件配置页面 |
|
||||
|
||||
## 页面路由
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// 按需加载,访问 /v2/hello 时才加载该模块
|
||||
// 按需加载,访问 /v/hello 时才加载该模块
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// 子路由,用 componentLoader 按需加载
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class MyPlugin extends Plugin {
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
注册后,访问路径为 `/admin/settings/hello`。当菜单下只有一个页面时,顶部 tab 栏会自动隐藏。
|
||||
注册后,访问路径为 `/v/admin/settings/hello`。当菜单下只有一个页面时,顶部 tab 栏会自动隐藏。
|
||||
|
||||
### 多 Tab 设置页
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Tab 1:基础设置(key 为 'index',映射到 /admin/settings/hello)
|
||||
// Tab 1:基础设置(key 为 'index',映射到 /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Tab 2:高级设置(映射到 /admin/settings/hello/advanced)
|
||||
// Tab 2:高级设置(映射到 /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -18,7 +18,7 @@ Im Folgenden sind alle Aufgaben aufgelistet, bei denen die KI Ihnen derzeit helf
|
||||
|
||||
:::warning Hinweis
|
||||
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v2/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- Der von der KI generierte Code ist nicht zwangsläufig zu 100 % korrekt. Es wird empfohlen, ihn vor der Aktivierung zu überprüfen. Wenn zur Laufzeit Probleme auftreten, können Sie die Fehlermeldung an die KI senden, damit diese die Fehlersuche und -behebung fortsetzt – meist sind nur wenige Konversationsrunden erforderlich.
|
||||
- Für die Entwicklung werden große Sprachmodelle der GPT- oder Claude-Reihe empfohlen, da sie die besten Ergebnisse liefern. Andere Modelle funktionieren ebenfalls, die Generierungsqualität kann jedoch variieren.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Der Browser öffnet automatisch eine grafische Konfigurationsseite, die Sie durc
|
||||
|
||||
:::warning Hinweis
|
||||
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v2/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- Der von der KI generierte Code ist nicht zwangsläufig zu 100 % korrekt. Es wird empfohlen, ihn vor der Aktivierung zu überprüfen. Wenn zur Laufzeit Probleme auftreten, können Sie die Fehlermeldung an die KI senden, damit diese die Fehlersuche und -behebung fortsetzt – meist sind nur wenige Konversationsrunden erforderlich.
|
||||
- Für die Entwicklung werden große Sprachmodelle der GPT- oder Claude-Reihe empfohlen, da sie die besten Ergebnisse liefern. Andere Modelle funktionieren ebenfalls, die Generierungsqualität kann jedoch variieren.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Stellen Sie sicher, dass Sie:
|
||||
|
||||
:::warning Hinweis
|
||||
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v2/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- NocoBase befindet sich derzeit in der Migration von `client` (v1) zu `client-v2`. `client-v2` befindet sich noch in der Entwicklung. Der von der KI-Entwicklung generierte Client-Code basiert auf `client-v2` und kann nur unter dem Pfad `/v/` verwendet werden. Er ist als Vorschau gedacht und nicht für den direkten Einsatz in der Produktion empfohlen.
|
||||
- Der von der KI generierte Code ist nicht zwangsläufig zu 100 % korrekt. Es wird empfohlen, ihn vor der Aktivierung zu überprüfen. Wenn zur Laufzeit Probleme auftreten, können Sie die Fehlermeldung an die KI senden, damit diese die Fehlersuche und -behebung fortsetzt – meist sind nur wenige Konversationsrunden erforderlich.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Wenn Client-Code geändert wurde, aber kein Hot-Reload erfolgt, versuchen Sie zu
|
||||
|
||||
### Die registrierte Seitenroute ist nicht erreichbar
|
||||
|
||||
Die Routen von NocoBase v2 erhalten standardmäßig das Präfix `/v2`. Wenn Sie z. B. `path: '/hello'` registriert haben, ist die tatsächliche Adresse `/v2/hello`:
|
||||
Die Routen von NocoBase v2 erhalten standardmäßig das Präfix `/v`. Wenn Sie z. B. `path: '/hello'` registriert haben, ist die tatsächliche Adresse `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // Tatsächlicher Aufruf -> /v2/hello
|
||||
path: '/hello', // Tatsächlicher Aufruf -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ Das Build-System von NocoBase pflegt eine [external-Liste](../../dependency-mana
|
||||
## Verwandte Links
|
||||
|
||||
- [Plugin](../plugin) — Plugin-Einstiegspunkt und Lebenszyklus
|
||||
- [Router](../router) — Routen-Registrierung und `/v2`-Präfix
|
||||
- [Router](../router) — Routen-Registrierung und `/v`-Präfix
|
||||
- [FlowEngine-Übersicht](../flow-engine/index.md) — Grundlegende Verwendung von FlowModel
|
||||
- [FlowEngine → Block-Erweiterung](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Feld-Erweiterung](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Über `ctx.router.navigate()` zu einer anderen Seite navigieren:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Aktuelle Routenparameter abrufen:
|
||||
|
||||
@@ -316,7 +316,7 @@ Im Component navigieren Sie über `ctx.router.navigate()` zu einer anderen Seite
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Routen-Informationen (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// Im Component: zur Seite navigieren
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Häufig verwendete Fähigkeiten des Kontexts
|
||||
|
||||
@@ -1,92 +1,171 @@
|
||||
---
|
||||
title: "Router"
|
||||
description: "NocoBase-Client-Routing: this.router.add zur Registrierung von Seitenrouten, pluginSettingsManager zur Registrierung von Plugin-Einstellungsseiten (addMenuItem + addPageTabItem)."
|
||||
keywords: "Router,Routing,router.add,pluginSettingsManager,addMenuItem,addPageTabItem,componentLoader,Seitenregistrierung,NocoBase"
|
||||
---
|
||||
|
||||
# Router
|
||||
|
||||
Der NocoBase-Client bietet einen flexiblen Router-Manager, der es Ihnen ermöglicht, Seiten und Plugin-Einstellungsseiten mithilfe von `router.add()` und `pluginSettingsManager` zu erweitern.
|
||||
In NocoBase registrieren Plugins ihre Seiten über Routen. Es gibt zwei gängige Ansätze:
|
||||
|
||||
## Registrierte Standard-Seitenrouten
|
||||
- `this.router.add()` – Registriert reguläre Seitenrouten
|
||||
- `this.pluginSettingsManager.addMenuItem()` + `addPageTabItem()` – Registriert Plugin-Einstellungsseiten
|
||||
|
||||
| Name | Pfad | Komponente | Beschreibung |
|
||||
| -------------- | ------------------ | ------------------- | ------------------------- |
|
||||
| admin | /admin/\* | AdminLayout | Admin-Seiten |
|
||||
| admin.page | /admin/:name | AdminDynamicPage | Dynamisch erstellte Seiten |
|
||||
| admin.settings | /admin/settings/\* | AdminSettingsLayout | Plugin-Einstellungsseiten |
|
||||
Die Registrierung von Routen erfolgt üblicherweise in der `load()`-Methode des Plugins. Siehe [Plugin](./plugin) für Details.
|
||||
|
||||
## Erweitern von Standardseiten
|
||||
:::warning Hinweis
|
||||
|
||||
Sie können reguläre Seitenrouten mit `router.add()` hinzufügen. Für Seitenkomponenten sollte `componentLoader` verwendet werden, damit das Seitenmodul erst geladen wird, wenn die Route tatsächlich aufgerufen wird.
|
||||
Bei NocoBase-v2-Plugins erhalten registrierte Routen standardmäßig das Präfix `/v`. Beim Aufruf der Routen müssen Sie dieses Präfix angeben.
|
||||
|
||||
Seitendateien müssen `export default` verwenden:
|
||||
:::
|
||||
|
||||
## Standardrouten
|
||||
|
||||
NocoBase hat die folgenden Standardrouten registriert:
|
||||
|
||||
| Name | Pfad | Komponente | Beschreibung |
|
||||
| -------------- | --------------------- | ------------------- | -------------------------- |
|
||||
| admin | /v/admin/\* | AdminLayout | Admin-Seiten |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Dynamisch erstellte Seiten |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Plugin-Einstellungsseiten |
|
||||
|
||||
## Seitenrouten
|
||||
|
||||
Registrieren Sie Seitenrouten über `this.router.add()`. Für Seitenkomponenten sollte `componentLoader` zum Lazy Loading verwendet werden, damit der Seitencode erst geladen wird, wenn die Seite tatsächlich aufgerufen wird.
|
||||
|
||||
:::warning Hinweis
|
||||
|
||||
Seitendateien müssen die Komponente per `export default` exportieren.
|
||||
|
||||
:::
|
||||
|
||||
```tsx
|
||||
// routes/HomePage.tsx
|
||||
export default function HomePage() {
|
||||
return <h1>Home</h1>;
|
||||
// pages/HelloPage.tsx
|
||||
export default function HelloPage() {
|
||||
return <h1>Hello, NocoBase!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
Registrierung in der `load()`-Methode des Plugins:
|
||||
|
||||
const Layout = () => (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link> | <Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
```tsx
|
||||
import { Plugin } from '@nocobase/client-v2';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root', { element: <Layout /> });
|
||||
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
// Dynamischer Import: Das Seitenmodul wird erst geladen, wenn diese Route betreten wird
|
||||
componentLoader: () => import('./routes/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about',
|
||||
componentLoader: () => import('./routes/AboutPage'),
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Lazy Loading: Das Modul wird erst geladen, wenn /v/hello aufgerufen wird
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
Unterstützt dynamische Parameter
|
||||
Das erste Argument von `router.add()` ist der Routenname, der die Punktnotation `.` unterstützt, um Eltern-Kind-Beziehungen auszudrücken. Beispielsweise steht `root.home` für eine untergeordnete Route von `root`.
|
||||
|
||||
In Komponenten können Sie über `ctx.router.navigate('/hello')` zu einer Route navigieren.
|
||||
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default function SomeComponent() {
|
||||
const ctx = useFlowContext();
|
||||
return (
|
||||
<Button onClick={() => ctx.router.navigate('/hello')}>
|
||||
Go to Hello Page
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Weitere Details finden Sie im Abschnitt zum Routing in [Component](./component/index.md).
|
||||
|
||||
### Verschachtelte Routen
|
||||
|
||||
Verschachtelung wird über die Punktnotation umgesetzt. Übergeordnete Routen verwenden `<Outlet />`, um den Inhalt der untergeordneten Routen zu rendern:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// Übergeordnete Route, mit element als Inline-Layout
|
||||
this.router.add('root', {
|
||||
element: (
|
||||
<div>
|
||||
<nav>Navigationsleiste</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// Untergeordnete Route, mit componentLoader zum Lazy Loading
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamische Parameter
|
||||
|
||||
Routenpfade unterstützen dynamische Parameter:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id',
|
||||
element: ({ params }) => <div>User ID: {params.id}</div>
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
|
||||
Wenn eine Seite umfangreicher ist oder nicht beim ersten Rendern benötigt wird, sollte `componentLoader` bevorzugt werden. `element` eignet sich weiterhin für Layout-Routen oder sehr leichte Inline-Seiten.
|
||||
In Komponenten können Sie dynamische Parameter über `ctx.route.params` abrufen:
|
||||
|
||||
## Erweitern von Plugin-Einstellungsseiten
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
|
||||
Register plugin settings pages via `this.pluginSettingsManager`. Registration has two steps — first use `addMenuItem()` to register the menu entry, then use `addPageTabItem()` to register the actual page. Settings pages appear in the NocoBase "Plugin Settings" menu.
|
||||
export default function UserPage() {
|
||||
const ctx = useFlowContext();
|
||||
const { id } = ctx.route.params; // Dynamischen Parameter id abrufen
|
||||
return <h1>User ID: {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Weitere Details finden Sie im Abschnitt zum Routing in [Component](./component/index.md).
|
||||
|
||||
### componentLoader vs. element
|
||||
|
||||
- **`componentLoader`** (empfohlen): Lazy Loading, geeignet für Seitenkomponenten. Seitendateien benötigen `export default`.
|
||||
- **`element`**: Übergibt JSX direkt, geeignet für Layout-Komponenten oder sehr leichtgewichtige Inline-Seiten.
|
||||
|
||||
Wenn die Seite selbst umfangreiche Abhängigkeiten hat, sollte `componentLoader` bevorzugt werden.
|
||||
|
||||
## Plugin-Einstellungsseiten
|
||||
|
||||
Registrieren Sie Plugin-Einstellungsseiten über `this.pluginSettingsManager`. Die Registrierung erfolgt in zwei Schritten – verwenden Sie zuerst `addMenuItem()`, um den Menüeintrag zu registrieren, und dann `addPageTabItem()`, um die eigentliche Seite zu registrieren. Einstellungsseiten erscheinen im Menü „Plugin-Einstellungen" von NocoBase.
|
||||
|
||||

|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
export class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Menüeintrag registrieren
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('Hello Settings'),
|
||||
icon: 'ApiOutlined',
|
||||
icon: 'ApiOutlined', // Name eines Ant-Design-Icons, siehe https://5x.ant.design/components/icon
|
||||
});
|
||||
|
||||
// Seite registrieren (key 'index' wird auf den Menü-Stammpfad abgebildet)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -97,19 +176,25 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
To add multiple sub-pages under a single menu entry, register multiple `addPageTabItem` calls with the same `menuKey` — tabs will appear automatically:
|
||||
Nach der Registrierung lautet der Aufrufpfad `/v/admin/settings/hello`. Wenn unter dem Menü nur eine Seite vorhanden ist, wird die obere Tab-Leiste automatisch ausgeblendet.
|
||||
|
||||
### Einstellungsseite mit mehreren Tabs
|
||||
|
||||
Wenn die Einstellungsseite mehrere Unterseiten benötigt, registrieren Sie mehrere `addPageTabItem`-Aufrufe mit demselben `menuKey` – oben erscheint dann automatisch eine Tab-Leiste:
|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Menüeintrag registrieren
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('HelloWorld'),
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Tab 1: Allgemeine Einstellungen (key 'index' wird auf /v/admin/settings/hello abgebildet)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -117,6 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Tab 2: Erweiterte Einstellungen (wird auf /v/admin/settings/hello/advanced abgebildet)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
@@ -126,3 +212,33 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### addMenuItem-Parameter
|
||||
|
||||
| Feld | Typ | Erforderlich | Beschreibung |
|
||||
| ---------- | --------------------- | ------------ | -------------------------------------------------------------------- |
|
||||
| `key` | `string` | Ja | Eindeutige Menükennung, darf kein `.` enthalten |
|
||||
| `title` | `ReactNode` | Nein | Menütitel |
|
||||
| `icon` | `string \| ReactNode` | Nein | Menüsymbol, wird bei einem String als integriertes `Icon` gerendert |
|
||||
| `sort` | `number` | Nein | Sortierwert, kleinere Werte erscheinen weiter vorn, Standard `0` |
|
||||
| `showTabs` | `boolean` | Nein | Ob die obere Tab-Leiste angezeigt wird, standardmäßig nach Seitenanzahl bestimmt |
|
||||
| `hidden` | `boolean` | Nein | Ob der Navigationseintrag ausgeblendet wird |
|
||||
|
||||
### addPageTabItem-Parameter
|
||||
|
||||
| Feld | Typ | Erforderlich | Beschreibung |
|
||||
| ----------------- | ----------- | ------------ | -------------------------------------------------------------------- |
|
||||
| `menuKey` | `string` | Ja | Der `key` des übergeordneten Menüs, entspricht dem `key` von `addMenuItem` |
|
||||
| `key` | `string` | Ja | Eindeutige Seitenkennung. `'index'` bezeichnet die Standardseite, abgebildet auf den Menü-Stammpfad |
|
||||
| `title` | `ReactNode` | Nein | Seitentitel (wird auf dem Tab angezeigt) |
|
||||
| `componentLoader` | `Function` | Nein | Seitenkomponente per Lazy Loading (empfohlen) |
|
||||
| `Component` | `Component` | Nein | Komponente direkt übergeben (Alternative zu `componentLoader`) |
|
||||
| `sort` | `number` | Nein | Sortierwert, kleinere Werte erscheinen weiter vorn |
|
||||
| `hidden` | `boolean` | Nein | Ob in der Tab-Leiste ausgeblendet |
|
||||
| `link` | `string` | Nein | Externer Link; wenn gesetzt, führt ein Klick auf den Tab zur externen URL |
|
||||
|
||||
## Verwandte Links
|
||||
|
||||
- [Plugin](./plugin) – Routen werden in `load()` registriert
|
||||
- [Component](./component/index.md) – Wie man die von Routen eingebundenen Seitenkomponenten schreibt
|
||||
- [Plugin-Beispiel: Eine Einstellungsseite erstellen](./examples/settings-page) – Vollständiges Beispiel für eine Einstellungsseite
|
||||
|
||||
@@ -18,7 +18,7 @@ Here is a list of everything AI can currently help you do. Each capability comes
|
||||
|
||||
:::warning Note
|
||||
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v2/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- AI-generated code may not be 100% correct. We recommend reviewing it before enabling. If you encounter issues at runtime, send the error message to AI and let it investigate and fix -- it usually takes just a few rounds of conversation to resolve.
|
||||
- We recommend using GPT or Claude series models for development, as they produce the best results. Other models can also work, but generation quality may vary.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Your browser will automatically open the visual configuration page, guiding you
|
||||
|
||||
:::warning Note
|
||||
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v2/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- AI-generated code may not be 100% correct. We recommend reviewing it before enabling. If you encounter issues at runtime, send the error message to AI and let it investigate and fix -- it usually takes just a few rounds of conversation to resolve.
|
||||
- We recommend using GPT or Claude series models for development, as they produce the best results. Other models can also work, but generation quality may vary.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Make sure you have:
|
||||
|
||||
:::warning Note
|
||||
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v2/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- NocoBase is migrating from `client` (v1) to `client-v2`, and `client-v2` is still under development. The client code generated by AI development is based on `client-v2` and can only be used under the `/v/` path. It is available for early access and experimentation, but is not recommended for production use.
|
||||
- AI-generated code may not be 100% correct. We recommend reviewing it before enabling. If you encounter issues at runtime, send the error message to AI and let it investigate and fix -- it usually takes just a few rounds of conversation to resolve.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ If client code changes don't hot reload, try refreshing the browser first.
|
||||
|
||||
### Registered page route is not accessible
|
||||
|
||||
NocoBase v2 routes automatically add a `/v2` prefix. For example, if you registered `path: '/hello'`, the actual URL is `/v2/hello`:
|
||||
NocoBase v2 routes automatically add a `/v` prefix. For example, if you registered `path: '/hello'`, the actual URL is `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // actual URL -> /v2/hello
|
||||
path: '/hello', // actual URL -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ NocoBase's build system maintains an [external list](../../dependency-management
|
||||
## Related Links
|
||||
|
||||
- [Plugin](../plugin) — Plugin entry and lifecycle
|
||||
- [Router](../router) — Route registration and the `/v2` prefix
|
||||
- [Router](../router) — Route registration and the `/v` prefix
|
||||
- [FlowEngine Overview](../flow-engine/index.md) — FlowModel basics
|
||||
- [FlowEngine - Block Extension](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine - Field Extension](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Use `ctx.router.navigate()` for page navigation:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Get current route parameters:
|
||||
|
||||
@@ -316,7 +316,7 @@ Navigate between pages in components via `ctx.router.navigate()`:
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Route Information (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// In component: page navigation
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Common Capabilities Provided by Context
|
||||
|
||||
@@ -15,7 +15,7 @@ Route registration is typically done in the plugin's `load()` method. See [Plugi
|
||||
|
||||
:::warning Note
|
||||
|
||||
For NocoBase v2 plugins, registered routes automatically get a `/v2` prefix. You need to include this prefix when accessing the routes.
|
||||
For NocoBase v2 plugins, registered routes automatically get a `/v` prefix. You need to include this prefix when accessing the routes.
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase has the following default routes registered:
|
||||
|
||||
| Name | Path | Component | Description |
|
||||
| -------------- | --------------------- | ------------------- | ------------------------- |
|
||||
| admin | /v2/admin/\* | AdminLayout | Admin pages |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | Dynamically created pages |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | Plugin settings pages |
|
||||
| admin | /v/admin/\* | AdminLayout | Admin pages |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Dynamically created pages |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Plugin settings pages |
|
||||
|
||||
## Page Routes
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Lazy loading: the module is loaded only when /v2/hello is visited
|
||||
// Lazy loading: the module is loaded only when /v/hello is visited
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// Child route, using componentLoader for lazy loading
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ Route paths support dynamic parameters:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
After registration, the access path is `/admin/settings/hello`. When there is only one page under the menu, the top tab bar is automatically hidden.
|
||||
After registration, the access path is `/v/admin/settings/hello`. When there is only one page under the menu, the top tab bar is automatically hidden.
|
||||
|
||||
### Multi-Tab Settings Page
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Tab 1: General settings (key 'index' maps to /admin/settings/hello)
|
||||
// Tab 1: General settings (key 'index' maps to /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Tab 2: Advanced settings (maps to /admin/settings/hello/advanced)
|
||||
// Tab 2: Advanced settings (maps to /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -18,7 +18,7 @@ A continuación se enumeran todas las cosas que la AI puede hacer por usted actu
|
||||
|
||||
:::warning Atención
|
||||
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v2/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- El código generado por la AI no siempre es 100% correcto. Le recomendamos revisarlo antes de habilitarlo. Si encuentra problemas en tiempo de ejecución, puede enviar el mensaje de error a la AI para que continúe diagnosticando y corrigiendo. Normalmente, unas pocas rondas de conversación son suficientes para resolverlos.
|
||||
- Se recomienda utilizar modelos de la serie GPT o Claude para el desarrollo, ya que ofrecen los mejores resultados. Otros modelos también pueden funcionar, aunque la calidad de la generación puede variar.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ El navegador abrirá automáticamente la página de configuración visual y le g
|
||||
|
||||
:::warning Atención
|
||||
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v2/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- El código generado por la AI no siempre es 100% correcto. Le recomendamos revisarlo antes de habilitarlo. Si encuentra problemas en tiempo de ejecución, puede enviar el mensaje de error a la AI para que continúe diagnosticando y corrigiendo. Normalmente, unas pocas rondas de conversación son suficientes para resolverlos.
|
||||
- Se recomienda utilizar modelos de la serie GPT o Claude para el desarrollo, ya que ofrecen los mejores resultados. Otros modelos también pueden funcionar, aunque la calidad de la generación puede variar.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Asegúrese de tener:
|
||||
|
||||
:::warning Atención
|
||||
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v2/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- NocoBase está migrando de `client` (v1) a `client-v2`. Actualmente `client-v2` aún está en desarrollo. El código de cliente generado por el desarrollo con AI se basa en `client-v2` y solo puede usarse en la ruta `/v/`. Está disponible para que lo pruebe, pero no se recomienda su uso directo en producción.
|
||||
- El código generado por la AI no siempre es 100% correcto. Le recomendamos revisarlo antes de habilitarlo. Si encuentra problemas en tiempo de ejecución, puede enviar el mensaje de error a la AI para que continúe diagnosticando y corrigiendo. Normalmente, unas pocas rondas de conversación son suficientes para resolverlos.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Si modificó código del cliente y no se aplicó, primero pruebe a refrescar el
|
||||
|
||||
### No se accede a la ruta de página registrada
|
||||
|
||||
Las rutas de NocoBase v2 incluyen por defecto el prefijo `/v2`. Por ejemplo, si registra `path: '/hello'`, la URL real es `/v2/hello`:
|
||||
Las rutas de NocoBase v2 incluyen por defecto el prefijo `/v`. Por ejemplo, si registra `path: '/hello'`, la URL real es `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // URL real -> /v2/hello
|
||||
path: '/hello', // URL real -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -283,7 +283,7 @@ El sistema de build de NocoBase mantiene una [lista de externals](../../dependen
|
||||
## Enlaces relacionados
|
||||
|
||||
- [Plugin](../plugin): entrada del plugin y ciclo de vida.
|
||||
- [Router](../router): registro de rutas y prefijo `/v2`.
|
||||
- [Router](../router): registro de rutas y prefijo `/v`.
|
||||
- [Visión general de FlowEngine](../flow-engine/index.md): uso básico de FlowModel.
|
||||
- [FlowEngine → Extensión de bloques](../flow-engine/block): BlockModel, TableBlockModel, `filterCollection`.
|
||||
- [FlowEngine → Extensión de campos](../flow-engine/field): FieldModel, `bindModelToInterface`.
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Use `ctx.router.navigate()` para navegar a otra página:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Obtenga los parámetros de la ruta actual:
|
||||
|
||||
@@ -316,7 +316,7 @@ En los componentes, use `ctx.router.navigate()` para navegar:
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Información de la ruta (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// En el componente: navegación de páginas
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Capacidades comunes del contexto
|
||||
|
||||
@@ -1,123 +1,244 @@
|
||||
---
|
||||
title: "Router"
|
||||
description: "Enrutamiento del cliente de NocoBase: this.router.add para registrar rutas de página, pluginSettingsManager para registrar páginas de configuración de plugins (addMenuItem + addPageTabItem)."
|
||||
keywords: "Router,enrutamiento,router.add,pluginSettingsManager,addMenuItem,addPageTabItem,componentLoader,registro de páginas,NocoBase"
|
||||
---
|
||||
|
||||
# Router
|
||||
|
||||
El cliente de NocoBase le ofrece un gestor de rutas flexible que le permite extender páginas y páginas de configuración de plugins mediante `router.add()` y `pluginSettingsRouter.add()`.
|
||||
En NocoBase, los plugins registran páginas mediante rutas. Hay dos enfoques habituales:
|
||||
|
||||
## Rutas de página predeterminadas
|
||||
- `this.router.add()`: registra rutas de página normales
|
||||
- `this.pluginSettingsManager.addMenuItem()` + `addPageTabItem()`: registra páginas de configuración de plugins
|
||||
|
||||
| Nombre | Ruta | Componente | Descripción |
|
||||
| -------------- | ------------------ | ------------------- | --------------------------- |
|
||||
| admin | /admin/\* | AdminLayout | Páginas de administración |
|
||||
| admin.page | /admin/:name | AdminDynamicPage | Páginas creadas dinámicamente |
|
||||
| admin.settings | /admin/settings/\* | AdminSettingsLayout | Páginas de configuración de plugins |
|
||||
El registro de rutas suele realizarse en el método `load()` del plugin. Consulte [Plugin](./plugin) para más detalles.
|
||||
|
||||
## Extensión de páginas generales
|
||||
:::warning Nota
|
||||
|
||||
Agrega rutas de páginas normales mediante `router.add()`. Para los componentes de página, usa `componentLoader` para registrarlos bajo demanda, de modo que el módulo de la página solo se cargue cuando realmente se visite la ruta.
|
||||
En los plugins de NocoBase v2, las rutas registradas reciben por defecto el prefijo `/v`. Debe incluir este prefijo al acceder a las rutas.
|
||||
|
||||
Los archivos de página deben usar `export default`:
|
||||
:::
|
||||
|
||||
## Rutas predeterminadas
|
||||
|
||||
NocoBase tiene registradas las siguientes rutas predeterminadas:
|
||||
|
||||
| Nombre | Ruta | Componente | Descripción |
|
||||
| -------------- | --------------------- | ------------------- | ----------------------------- |
|
||||
| admin | /v/admin/\* | AdminLayout | Páginas de administración |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Páginas creadas dinámicamente |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Páginas de configuración de plugins |
|
||||
|
||||
## Rutas de página
|
||||
|
||||
Registre rutas de página mediante `this.router.add()`. Los componentes de página deberían usar `componentLoader` para la carga diferida, de modo que el código de la página solo se cargue cuando se visita realmente.
|
||||
|
||||
:::warning Nota
|
||||
|
||||
Los archivos de página deben exportar el componente con `export default`.
|
||||
|
||||
:::
|
||||
|
||||
```tsx
|
||||
// routes/HomePage.tsx
|
||||
export default function HomePage() {
|
||||
return <h1>Home</h1>;
|
||||
// pages/HelloPage.tsx
|
||||
export default function HelloPage() {
|
||||
return <h1>Hello, NocoBase!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
Registro en el método `load()` del plugin:
|
||||
|
||||
const Layout = () => (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link> | <Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
```tsx
|
||||
import { Plugin } from '@nocobase/client-v2';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root', { element: <Layout /> });
|
||||
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
// Importación dinámica: el módulo de la página solo se carga cuando se entra en esta ruta
|
||||
componentLoader: () => import('./routes/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about',
|
||||
componentLoader: () => import('./routes/AboutPage'),
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Carga diferida: el módulo solo se carga al visitar /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
Admite parámetros dinámicos
|
||||
El primer argumento de `router.add()` es el nombre de la ruta, que admite la notación de punto `.` para expresar relaciones padre-hijo. Por ejemplo, `root.home` representa una ruta hija de `root`.
|
||||
|
||||
En los componentes, puede navegar a una ruta mediante `ctx.router.navigate('/hello')`.
|
||||
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default function SomeComponent() {
|
||||
const ctx = useFlowContext();
|
||||
return (
|
||||
<Button onClick={() => ctx.router.navigate('/hello')}>
|
||||
Go to Hello Page
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Para más detalles, consulte la sección de enrutamiento en [Component](./component/index.md).
|
||||
|
||||
### Rutas anidadas
|
||||
|
||||
El anidamiento se implementa mediante la notación de punto. Las rutas padre usan `<Outlet />` para renderizar el contenido de las rutas hijas:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// Ruta padre, con element como diseño en línea
|
||||
this.router.add('root', {
|
||||
element: (
|
||||
<div>
|
||||
<nav>Barra de navegación</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// Ruta hija, con componentLoader para la carga diferida
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parámetros dinámicos
|
||||
|
||||
Las rutas admiten parámetros dinámicos:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id',
|
||||
element: ({ params }) => <div>User ID: {params.id}</div>
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
|
||||
Si la página es pesada o no es necesaria en el primer renderizado, se recomienda priorizar `componentLoader`; `element` sigue siendo adecuado para rutas de diseño o páginas en línea muy ligeras.
|
||||
|
||||
## Extensión de páginas de configuración de plugins
|
||||
|
||||
Agrega páginas de configuración del plugin mediante `pluginSettingsRouter.add()`. Al igual que las rutas de páginas normales, las páginas de configuración también deben usar `componentLoader` para el registro bajo demanda.
|
||||
En los componentes, puede obtener los parámetros dinámicos mediante `ctx.route.params`:
|
||||
|
||||
```tsx
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
|
||||
export class HelloPlugin extends Plugin {
|
||||
export default function UserPage() {
|
||||
const ctx = useFlowContext();
|
||||
const { id } = ctx.route.params; // Obtener el parámetro dinámico id
|
||||
return <h1>User ID: {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Para más detalles, consulte la sección de enrutamiento en [Component](./component/index.md).
|
||||
|
||||
### componentLoader vs. element
|
||||
|
||||
- **`componentLoader`** (recomendado): carga diferida, adecuada para componentes de página. Los archivos de página necesitan `export default`.
|
||||
- **`element`**: pasa JSX directamente, adecuado para componentes de diseño o páginas en línea muy ligeras.
|
||||
|
||||
Si la página tiene dependencias pesadas, es preferible usar `componentLoader`.
|
||||
|
||||
## Páginas de configuración de plugins
|
||||
|
||||
Registre páginas de configuración de plugins mediante `this.pluginSettingsManager`. El registro consta de dos pasos: primero use `addMenuItem()` para registrar la entrada de menú y luego `addPageTabItem()` para registrar la página real. Las páginas de configuración aparecen en el menú «Configuración de plugins» de NocoBase.
|
||||
|
||||

|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
export class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
this.pluginSettingsRouter.add('hello', {
|
||||
title: 'Hello', // Título de la página de configuración
|
||||
icon: 'ApiOutlined', // Icono del menú de la página de configuración
|
||||
// Importación dinámica: el módulo de la página solo se carga cuando se entra en esta página de configuración
|
||||
// Registrar la entrada de menú
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('Hello Settings'),
|
||||
icon: 'ApiOutlined', // Nombre de un icono de Ant Design, consulte https://5x.ant.design/components/icon
|
||||
});
|
||||
|
||||
// Registrar la página (la clave 'index' se asigna a la ruta raíz del menú)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
title: this.t('Hello Settings'),
|
||||
componentLoader: () => import('./settings/HelloSettingPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ejemplo de rutas multinivel
|
||||
Tras el registro, la ruta de acceso es `/v/admin/settings/hello`. Cuando solo hay una página bajo el menú, la barra de pestañas superior se oculta automáticamente.
|
||||
|
||||
### Página de configuración con varias pestañas
|
||||
|
||||
Si la página de configuración necesita varias subpáginas, registre varias llamadas a `addPageTabItem` con el mismo `menuKey`: arriba aparecerá automáticamente una barra de pestañas:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
const pluginName = 'hello';
|
||||
|
||||
class HelloPlugin extends Plugin {
|
||||
class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Ruta de nivel superior
|
||||
this.pluginSettingsRouter.add(pluginName, {
|
||||
title: 'HelloWorld',
|
||||
icon: '',
|
||||
element: <Outlet />,
|
||||
// Registrar la entrada de menú
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('HelloWorld'),
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Rutas secundarias
|
||||
this.pluginSettingsRouter.add(`${pluginName}.demo1`, {
|
||||
title: 'Demo1 Page',
|
||||
// Importación dinámica: el módulo de la página solo se carga cuando se entra en esta página de configuración
|
||||
componentLoader: () => import('./settings/Demo1Page'),
|
||||
// Pestaña 1: Configuración general (la clave 'index' se asigna a /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
title: this.t('General'),
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
this.pluginSettingsRouter.add(`${pluginName}.demo2`, {
|
||||
title: 'Demo2 Page',
|
||||
componentLoader: () => import('./settings/Demo2Page'),
|
||||
// Pestaña 2: Configuración avanzada (se asigna a /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
title: this.t('Advanced'),
|
||||
componentLoader: () => import('./settings/AdvancedPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Parámetros de addMenuItem
|
||||
|
||||
| Campo | Tipo | Obligatorio | Descripción |
|
||||
| ---------- | --------------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| `key` | `string` | Sí | Identificador único del menú, no puede contener `.` |
|
||||
| `title` | `ReactNode` | No | Título del menú |
|
||||
| `icon` | `string \| ReactNode` | No | Icono del menú; cuando es una cadena, se renderiza como `Icon` integrado |
|
||||
| `sort` | `number` | No | Valor de orden; los valores menores aparecen primero, por defecto `0` |
|
||||
| `showTabs` | `boolean` | No | Si se muestra la barra de pestañas superior; por defecto se determina según el número de páginas |
|
||||
| `hidden` | `boolean` | No | Si se oculta la entrada de navegación |
|
||||
|
||||
### Parámetros de addPageTabItem
|
||||
|
||||
| Campo | Tipo | Obligatorio | Descripción |
|
||||
| ----------------- | ----------- | ----------- | -------------------------------------------------------------------- |
|
||||
| `menuKey` | `string` | Sí | El `key` del menú padre, correspondiente al `key` de `addMenuItem` |
|
||||
| `key` | `string` | Sí | Identificador único de la página. `'index'` indica la página predeterminada, asignada a la ruta raíz del menú |
|
||||
| `title` | `ReactNode` | No | Título de la página (se muestra en la pestaña) |
|
||||
| `componentLoader` | `Function` | No | Componente de página con carga diferida (recomendado) |
|
||||
| `Component` | `Component` | No | Pasar el componente directamente (alternativa a `componentLoader`) |
|
||||
| `sort` | `number` | No | Valor de orden; los valores menores aparecen primero |
|
||||
| `hidden` | `boolean` | No | Si se oculta en la barra de pestañas |
|
||||
| `link` | `string` | No | Enlace externo; cuando se establece, al hacer clic en la pestaña se navega a la URL externa |
|
||||
|
||||
## Enlaces relacionados
|
||||
|
||||
- [Plugin](./plugin): las rutas se registran en `load()`
|
||||
- [Component](./component/index.md): cómo escribir los componentes de página que las rutas montan
|
||||
- [Ejemplo de plugin: crear una página de configuración](./examples/settings-page): ejemplo completo de página de configuración
|
||||
|
||||
@@ -18,7 +18,7 @@ Vous trouverez ci-dessous toutes les choses que l'IA peut faire pour vous. Chaqu
|
||||
|
||||
:::warning Attention
|
||||
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v2/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- Le code généré par l'IA n'est pas nécessairement correct à 100 %. Il est recommandé de le revoir avant de l'activer. Si vous rencontrez un problème à l'exécution, vous pouvez transmettre le message d'erreur à l'IA pour qu'elle continue le diagnostic et la correction — généralement quelques tours de dialogue suffisent à résoudre le problème.
|
||||
- Il est recommandé d'utiliser les modèles GPT ou Claude pour le développement, qui donnent les meilleurs résultats. D'autres modèles fonctionnent également, mais la qualité de la génération peut varier.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Le navigateur ouvrira automatiquement la page de configuration visuelle, qui vou
|
||||
|
||||
:::warning Attention
|
||||
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v2/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- Le code généré par l'IA n'est pas nécessairement correct à 100 %. Il est recommandé de le revoir avant de l'activer. Si vous rencontrez un problème à l'exécution, vous pouvez transmettre le message d'erreur à l'IA pour qu'elle continue le diagnostic et la correction — généralement quelques tours de dialogue suffisent à résoudre le problème.
|
||||
- Il est recommandé d'utiliser les modèles GPT ou Claude pour le développement, qui donnent les meilleurs résultats. D'autres modèles fonctionnent également, mais la qualité de la génération peut varier.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Assurez-vous d'avoir :
|
||||
|
||||
:::warning Attention
|
||||
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v2/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- NocoBase est en cours de migration de `client` (v1) vers `client-v2`. Actuellement, `client-v2` est encore en développement. Le code client généré par AI Development est basé sur `client-v2` et ne peut être utilisé que sous le chemin `/v/`. Il est destiné à un aperçu et n'est pas recommandé pour une mise en production directe.
|
||||
- Le code généré par l'IA n'est pas nécessairement correct à 100 %. Il est recommandé de le revoir avant de l'activer. Si vous rencontrez un problème à l'exécution, vous pouvez transmettre le message d'erreur à l'IA pour qu'elle continue le diagnostic et la correction — généralement quelques tours de dialogue suffisent à résoudre le problème.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Si le code client a été modifié mais le hot reload ne s'est pas déclenché,
|
||||
|
||||
### Impossible d'accéder à une route de page enregistrée
|
||||
|
||||
Les routes de NocoBase v2 sont préfixées par `/v2` par défaut. Par exemple, si vous enregistrez `path: '/hello'`, l'adresse réelle est `/v2/hello` :
|
||||
Les routes de NocoBase v2 sont préfixées par `/v` par défaut. Par exemple, si vous enregistrez `path: '/hello'`, l'adresse réelle est `/v/hello` :
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // accès réel -> /v2/hello
|
||||
path: '/hello', // accès réel -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ Le système de build de NocoBase maintient une [liste d'externals](../../depende
|
||||
## Liens connexes
|
||||
|
||||
- [Plugin](../plugin) — point d'entrée et cycle de vie du plugin
|
||||
- [Router](../router) — enregistrement de routes et préfixe `/v2`
|
||||
- [Router](../router) — enregistrement de routes et préfixe `/v`
|
||||
- [Aperçu de FlowEngine](../flow-engine/index.md) — utilisation de base de FlowModel
|
||||
- [FlowEngine → Extension de bloc](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Extension de champ](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
`ctx.router.navigate()` permet de naviguer vers une autre page :
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Récupérer les paramètres de la route courante :
|
||||
|
||||
@@ -316,7 +316,7 @@ Dans un composant, naviguez via `ctx.router.navigate()` :
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Informations de route (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// Dans un composant : navigation
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Capacités courantes fournies par le contexte
|
||||
|
||||
@@ -15,7 +15,7 @@ L'enregistrement des routes se fait habituellement dans la méthode `load()` du
|
||||
|
||||
:::warning Attention
|
||||
|
||||
Pour les plugins de NocoBase v2, les routes enregistrées seront préfixées par `/v2` par défaut ; il faut donc inclure ce préfixe dans l'URL d'accès.
|
||||
Pour les plugins de NocoBase v2, les routes enregistrées seront préfixées par `/v` par défaut ; il faut donc inclure ce préfixe dans l'URL d'accès.
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase a déjà enregistré les routes par défaut suivantes :
|
||||
|
||||
| Nom | Chemin | Composant | Description |
|
||||
| -------------- | --------------------- | ------------------- | --------------------- |
|
||||
| admin | /v2/admin/\* | AdminLayout | Pages d'administration |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | Pages créées dynamiquement |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | Pages de configuration des plugins |
|
||||
| admin | /v/admin/\* | AdminLayout | Pages d'administration |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Pages créées dynamiquement |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Pages de configuration des plugins |
|
||||
|
||||
## Routes de page
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Chargement à la demande : le module n'est chargé que lorsque /v2/hello est visité
|
||||
// Chargement à la demande : le module n'est chargé que lorsque /v/hello est visité
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// Sous-route : chargement à la demande via componentLoader
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ Les chemins de route prennent en charge les paramètres dynamiques :
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
Une fois enregistrée, l'URL d'accès est `/admin/settings/hello`. Lorsqu'il n'y a qu'une seule page sous le menu, la barre d'onglets en haut est masquée automatiquement.
|
||||
Une fois enregistrée, l'URL d'accès est `/v/admin/settings/hello`. Lorsqu'il n'y a qu'une seule page sous le menu, la barre d'onglets en haut est masquée automatiquement.
|
||||
|
||||
### Page de configuration multi-onglets
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Onglet 1 : Configuration de base (key vaut 'index', mappé sur /admin/settings/hello)
|
||||
// Onglet 1 : Configuration de base (key vaut 'index', mappé sur /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Onglet 2 : Configuration avancée (mappé sur /admin/settings/hello/advanced)
|
||||
// Onglet 2 : Configuration avancée (mappé sur /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -18,7 +18,7 @@ Berikut tercantum semua hal yang dapat dilakukan AI saat ini untuk Anda. Setiap
|
||||
|
||||
:::warning Perhatian
|
||||
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v2/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- Kode yang dihasilkan AI tidak selalu 100% benar, disarankan melakukan review terlebih dahulu sebelum diaktifkan. Jika menemui masalah saat runtime, Anda dapat mengirim informasi error ke AI, biarkan ia melanjutkan troubleshooting dan perbaikan — biasanya beberapa putaran dialog dapat menyelesaikannya.
|
||||
- Disarankan menggunakan model besar seri GPT atau Claude untuk pengembangan, hasilnya terbaik. Model besar lainnya juga dapat digunakan, namun kualitas pembuatan mungkin berbeda.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Browser akan secara otomatis membuka halaman konfigurasi visual, memandu Anda un
|
||||
|
||||
:::warning Perhatian
|
||||
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v2/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- Kode yang dihasilkan AI tidak selalu 100% benar, disarankan melakukan review terlebih dahulu sebelum diaktifkan. Jika menemui masalah saat runtime, Anda dapat mengirim informasi error ke AI, biarkan ia melanjutkan troubleshooting dan perbaikan — biasanya beberapa putaran dialog dapat menyelesaikannya.
|
||||
- Disarankan menggunakan model besar seri GPT atau Claude untuk pengembangan, hasilnya terbaik. Model besar lainnya juga dapat digunakan, namun kualitas pembuatan mungkin berbeda.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Pastikan Anda telah:
|
||||
|
||||
:::warning Perhatian
|
||||
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v2/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- NocoBase sedang bermigrasi dari `client` (v1) ke `client-v2`, saat ini `client-v2` masih dalam pengembangan. Kode klien yang dihasilkan oleh Pengembangan AI berbasis pada `client-v2`, hanya dapat digunakan di path `/v/`, untuk pengalaman mencoba, tidak disarankan langsung digunakan di lingkungan production.
|
||||
- Kode yang dihasilkan AI tidak selalu 100% benar, disarankan melakukan review terlebih dahulu sebelum diaktifkan. Jika menemui masalah saat runtime, Anda dapat mengirim informasi error ke AI, biarkan ia melanjutkan troubleshooting dan perbaikan — biasanya beberapa putaran dialog dapat menyelesaikannya.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Jika kode client diubah tetapi tidak hot update, coba refresh browser dulu.
|
||||
|
||||
### Route halaman yang didaftarkan tidak dapat diakses
|
||||
|
||||
Route NocoBase v2 secara default akan memiliki prefix `/v2`. Misalnya Anda mendaftarkan `path: '/hello'`, alamat akses sebenarnya adalah `/v2/hello`:
|
||||
Route NocoBase v2 secara default akan memiliki prefix `/v`. Misalnya Anda mendaftarkan `path: '/hello'`, alamat akses sebenarnya adalah `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // Akses sebenarnya -> /v2/hello
|
||||
path: '/hello', // Akses sebenarnya -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ Sistem build NocoBase memiliki [daftar external](../../dependency-management), p
|
||||
## Tautan Terkait
|
||||
|
||||
- [Plugin](../plugin) — Entry dan siklus hidup plugin
|
||||
- [Router](../router) — Registrasi route dan prefix `/v2`
|
||||
- [Router](../router) — Registrasi route dan prefix `/v`
|
||||
- [Ikhtisar FlowEngine](../flow-engine/index.md) — Penggunaan dasar FlowModel
|
||||
- [FlowEngine → Ekstensi Block](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Ekstensi Field](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Melakukan navigasi halaman melalui `ctx.router.navigate()`:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Mendapatkan parameter route saat ini:
|
||||
|
||||
@@ -316,7 +316,7 @@ Di Component, lakukan navigasi halaman melalui `ctx.router.navigate()`:
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Informasi Route (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// Di Component: navigasi halaman
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Kapabilitas Umum yang Disediakan Konteks
|
||||
|
||||
@@ -15,7 +15,7 @@ Registrasi route biasanya dilakukan dalam method `load()` plugin, lihat [Plugin]
|
||||
|
||||
:::warning Perhatian
|
||||
|
||||
Plugin NocoBase v2, route yang didaftarkan secara default akan memiliki prefix `/v2`, perlu menyertakan prefix ini saat diakses.
|
||||
Plugin NocoBase v2, route yang didaftarkan secara default akan memiliki prefix `/v`, perlu menyertakan prefix ini saat diakses.
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase telah mendaftarkan route default berikut:
|
||||
|
||||
| Nama | Path | Component | Penjelasan |
|
||||
| -------------- | --------------------- | ------------------- | -------------- |
|
||||
| admin | /v2/admin/\* | AdminLayout | Halaman admin |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | Halaman yang dibuat dinamis |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | Halaman konfigurasi plugin |
|
||||
| admin | /v/admin/\* | AdminLayout | Halaman admin |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Halaman yang dibuat dinamis |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Halaman konfigurasi plugin |
|
||||
|
||||
## Route Halaman
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Loading sesuai kebutuhan, modul ini dimuat saat mengakses /v2/hello
|
||||
// Loading sesuai kebutuhan, modul ini dimuat saat mengakses /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// Child route, menggunakan componentLoader untuk loading sesuai kebutuhan
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ Path route mendukung parameter dinamis:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
Setelah didaftarkan, path akses adalah `/admin/settings/hello`. Saat hanya ada satu halaman di bawah menu, tab bar atas akan otomatis tersembunyi.
|
||||
Setelah didaftarkan, path akses adalah `/v/admin/settings/hello`. Saat hanya ada satu halaman di bawah menu, tab bar atas akan otomatis tersembunyi.
|
||||
|
||||
### Halaman Pengaturan Multi-Tab
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Tab 1: Pengaturan Dasar (key adalah 'index', di-map ke /admin/settings/hello)
|
||||
// Tab 1: Pengaturan Dasar (key adalah 'index', di-map ke /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Tab 2: Pengaturan Lanjutan (di-map ke /admin/settings/hello/advanced)
|
||||
// Tab 2: Pengaturan Lanjutan (di-map ke /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -18,7 +18,7 @@ AI プラグイン開発の機能は [nocobase-plugin-development](https://githu
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v2/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- AI が生成するコードは必ずしも 100% 正確ではありません。有効化する前にレビューすることをお勧めします。実行時に問題が発生した場合は、エラーメッセージを AI に送信して調査・修正を続けてもらいましょう。通常、数回のやり取りで解決できます。
|
||||
- 開発には GPT または Claude シリーズの大規模言語モデルの使用を推奨します。最も良い結果が得られます。他のモデルでも使用可能ですが、生成品質に差が出る場合があります。
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ nb init --ui
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v2/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- AI が生成するコードは必ずしも 100% 正確ではありません。有効化する前にレビューすることをお勧めします。実行時に問題が発生した場合は、エラーメッセージを AI に送信して調査・修正を続けてもらいましょう。通常、数回のやり取りで解決できます。
|
||||
- 開発には GPT または Claude シリーズの大規模言語モデルの使用を推奨します。最も良い結果が得られます。他のモデルでも使用可能ですが、生成品質に差が出る場合があります。
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ keywords: "AI 開発,ウォーターマークプラグイン,NocoBase プラグ
|
||||
|
||||
:::warning 注意
|
||||
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v2/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- NocoBase は `client`(v1)から `client-v2` への移行を進めており、現在 `client-v2` はまだ開発中です。AI が生成するクライアントコードは `client-v2` ベースのため、`/v/` パスでのみ使用可能です。先行体験用であり、本番環境での使用は推奨しません。
|
||||
- AI が生成するコードは必ずしも 100% 正確ではありません。有効化する前にレビューすることをお勧めします。実行時に問題が発生した場合は、エラーメッセージを AI に送信して調査・修正を続けてもらいましょう。通常、数回のやり取りで解決できます。
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ keywords: "FAQ,よくある質問,トラブルシューティング,Troubleshoot
|
||||
|
||||
### 登録したページルートにアクセスできない
|
||||
|
||||
NocoBase v2 のルートにはデフォルトで `/v2` プレフィックスが付きます。例えば `path: '/hello'` で登録した場合、実際のアクセス URL は `/v2/hello` になります:
|
||||
NocoBase v2 のルートにはデフォルトで `/v` プレフィックスが付きます。例えば `path: '/hello'` で登録した場合、実際のアクセス URL は `/v/hello` になります:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // 実際のアクセス -> /v2/hello
|
||||
path: '/hello', // 実際のアクセス -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ NocoBase のビルドシステムは [external リスト](../../dependency-manag
|
||||
## 関連リンク
|
||||
|
||||
- [Plugin プラグイン](../plugin) — プラグインエントリとライフサイクル
|
||||
- [Router ルーティング](../router) — ルート登録と `/v2` プレフィックス
|
||||
- [Router ルーティング](../router) — ルート登録と `/v` プレフィックス
|
||||
- [FlowEngine 概要](../flow-engine/index.md) — FlowModel の基本的な使い方
|
||||
- [FlowEngine → ブロック拡張](../flow-engine/block) — BlockModel、TableBlockModel、filterCollection
|
||||
- [FlowEngine → フィールド拡張](../flow-engine/field) — FieldModel、bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
`ctx.router.navigate()` でページ遷移します:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
現在のルートパラメータを取得:
|
||||
|
||||
@@ -316,7 +316,7 @@ async load() {
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### ルート情報(ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// コンポーネント内:ページナビゲーション
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## コンテキストが提供する共通機能
|
||||
|
||||
@@ -15,7 +15,7 @@ NocoBase では、プラグインはルーティングを通じてページを
|
||||
|
||||
:::warning 注意
|
||||
|
||||
NocoBase v2 のプラグインでは、ルート登録後にデフォルトで `/v2` プレフィックスが付与されます。アクセス時にはこのプレフィックスを含める必要があります。
|
||||
NocoBase v2 のプラグインでは、ルート登録後にデフォルトで `/v` プレフィックスが付与されます。アクセス時にはこのプレフィックスを含める必要があります。
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase には以下のデフォルトルートが登録されています:
|
||||
|
||||
| 名前 | パス | コンポーネント | 説明 |
|
||||
| -------------- | --------------------- | ------------------- | ------------------ |
|
||||
| admin | /v2/admin/\* | AdminLayout | 管理画面ページ |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | 動的に作成されるページ |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | プラグイン設定ページ |
|
||||
| admin | /v/admin/\* | AdminLayout | 管理画面ページ |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | 動的に作成されるページ |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | プラグイン設定ページ |
|
||||
|
||||
## ページルート
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// オンデマンドロード、/v2/hello にアクセスした時にのみモジュールをロード
|
||||
// オンデマンドロード、/v/hello にアクセスした時にのみモジュールをロード
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// 子ルート、componentLoader でオンデマンドロード
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class MyPlugin extends Plugin {
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
登録後、アクセスパスは `/admin/settings/hello` となります。メニューにページが 1 つしかない場合、上部のタブバーは自動的に非表示になります。
|
||||
登録後、アクセスパスは `/v/admin/settings/hello` となります。メニューにページが 1 つしかない場合、上部のタブバーは自動的に非表示になります。
|
||||
|
||||
### 複数タブの設定ページ
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// タブ 1:基本設定(key が 'index'、/admin/settings/hello にマッピング)
|
||||
// タブ 1:基本設定(key が 'index'、/v/admin/settings/hello にマッピング)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// タブ 2:詳細設定(/admin/settings/hello/advanced にマッピング)
|
||||
// タブ 2:詳細設定(/v/admin/settings/hello/advanced にマッピング)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -18,7 +18,7 @@ A seguir estão listadas todas as coisas que a IA pode fazer por você atualment
|
||||
|
||||
:::warning Atenção
|
||||
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v2/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O código gerado pela IA pode não estar 100% correto. Recomenda-se revisá-lo antes de habilitar. Se encontrar problemas em tempo de execução, envie a mensagem de erro para a IA continuar investigando e corrigindo. Geralmente, algumas trocas de mensagens resolvem o problema.
|
||||
- Recomenda-se usar modelos da família GPT ou Claude para o desenvolvimento, pois oferecem os melhores resultados. Outros modelos também funcionam, mas a qualidade da geração pode variar.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ O navegador abrirá automaticamente uma página de configuração visual, guiand
|
||||
|
||||
:::warning Atenção
|
||||
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v2/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O código gerado pela IA pode não estar 100% correto. Recomenda-se revisá-lo antes de habilitar. Se encontrar problemas em tempo de execução, envie a mensagem de erro para a IA continuar investigando e corrigindo. Geralmente, algumas trocas de mensagens resolvem o problema.
|
||||
- Recomenda-se usar modelos da família GPT ou Claude para o desenvolvimento, pois oferecem os melhores resultados. Outros modelos também funcionam, mas a qualidade da geração pode variar.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Certifique-se de que você já tem:
|
||||
|
||||
:::warning Atenção
|
||||
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v2/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O NocoBase está em processo de migração de `client` (v1) para `client-v2`. No momento, o `client-v2` ainda está em desenvolvimento. O código cliente gerado pelo desenvolvimento com IA é baseado no `client-v2` e só pode ser usado no caminho `/v/`. É uma prévia experimental e não é recomendado para uso direto em produção.
|
||||
- O código gerado pela IA pode não estar 100% correto. Recomenda-se revisá-lo antes de habilitar. Se encontrar problemas em tempo de execução, envie a mensagem de erro para a IA continuar investigando e corrigindo. Geralmente, algumas trocas de mensagens resolvem o problema.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Se você alterou código do cliente mas não houve hot reload, primeiro tente at
|
||||
|
||||
### Rota de página registrada não acessível
|
||||
|
||||
As rotas no NocoBase v2 recebem o prefixo `/v2` por padrão. Por exemplo, se você registrou `path: '/hello'`, o endereço de acesso real é `/v2/hello`:
|
||||
As rotas no NocoBase v2 recebem o prefixo `/v` por padrão. Por exemplo, se você registrou `path: '/hello'`, o endereço de acesso real é `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // 实际访问 -> /v2/hello
|
||||
path: '/hello', // 实际访问 -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ O sistema de build do NocoBase mantém uma [lista external](../../dependency-man
|
||||
## Links relacionados
|
||||
|
||||
- [Plugin](../plugin) — entrada do plugin e ciclo de vida
|
||||
- [Router de rotas](../router) — registro de rotas e prefixo `/v2`
|
||||
- [Router de rotas](../router) — registro de rotas e prefixo `/v`
|
||||
- [Visão geral do FlowEngine](../flow-engine/index.md) — uso básico do FlowModel
|
||||
- [FlowEngine → Extensão de blocos](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Extensão de campos](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Use `ctx.router.navigate()` para navegar entre páginas:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Obter os parâmetros da rota atual:
|
||||
|
||||
@@ -316,7 +316,7 @@ Em componentes, use `ctx.router.navigate()` para navegar entre páginas:
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Informações de rota (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// 组件里:页面导航
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Capacidades comuns oferecidas pelo contexto
|
||||
|
||||
@@ -1,92 +1,171 @@
|
||||
# Roteador
|
||||
---
|
||||
title: "Router"
|
||||
description: "Roteamento do cliente NocoBase: this.router.add para registrar rotas de página, pluginSettingsManager para registrar páginas de configuração de plugins (addMenuItem + addPageTabItem)."
|
||||
keywords: "Router,roteamento,router.add,pluginSettingsManager,addMenuItem,addPageTabItem,componentLoader,registro de páginas,NocoBase"
|
||||
---
|
||||
|
||||
O cliente NocoBase oferece um gerenciador de roteamento flexível que permite estender páginas e páginas de configuração de **plugins** usando `router.add()` e `pluginSettingsManager`.
|
||||
# Router
|
||||
|
||||
## Rotas de Página Padrão Registradas
|
||||
No NocoBase, os plugins registram páginas por meio de rotas. Há duas abordagens comuns:
|
||||
|
||||
| Nome | Caminho | Componente | Descrição |
|
||||
| -------------- | ------------------ | ------------------- | ------------------------------- |
|
||||
| admin | /admin/\* | AdminLayout | Páginas de administração |
|
||||
| admin.page | /admin/:name | AdminDynamicPage | Páginas criadas dinamicamente |
|
||||
| admin.settings | /admin/settings/\* | AdminSettingsLayout | Páginas de configuração de **plugins** |
|
||||
- `this.router.add()` — registra rotas de página comuns
|
||||
- `this.pluginSettingsManager.addMenuItem()` + `addPageTabItem()` — registra páginas de configuração de plugins
|
||||
|
||||
## Extensão de Páginas Comuns
|
||||
O registro de rotas geralmente é feito no método `load()` do plugin. Consulte [Plugin](./plugin) para mais detalhes.
|
||||
|
||||
Adicione rotas de páginas comuns usando `router.add()`. Para componentes de página, use `componentLoader` para registro sob demanda, de modo que o módulo da página só seja carregado quando a rota for realmente acessada.
|
||||
:::warning Atenção
|
||||
|
||||
Os arquivos de página devem usar `export default`:
|
||||
Nos plugins do NocoBase v2, as rotas registradas recebem por padrão o prefixo `/v`. É necessário incluir esse prefixo ao acessar as rotas.
|
||||
|
||||
:::
|
||||
|
||||
## Rotas padrão
|
||||
|
||||
O NocoBase já tem as seguintes rotas padrão registradas:
|
||||
|
||||
| Nome | Caminho | Componente | Descrição |
|
||||
| -------------- | --------------------- | ------------------- | ----------------------------- |
|
||||
| admin | /v/admin/\* | AdminLayout | Páginas de administração |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Páginas criadas dinamicamente |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Páginas de configuração de plugins |
|
||||
|
||||
## Rotas de página
|
||||
|
||||
Registre rotas de página por meio de `this.router.add()`. Os componentes de página devem usar `componentLoader` para carregamento sob demanda, de modo que o código da página só seja carregado quando ela for realmente acessada.
|
||||
|
||||
:::warning Atenção
|
||||
|
||||
Os arquivos de página devem exportar o componente com `export default`.
|
||||
|
||||
:::
|
||||
|
||||
```tsx
|
||||
// routes/HomePage.tsx
|
||||
export default function HomePage() {
|
||||
return <h1>Home</h1>;
|
||||
// pages/HelloPage.tsx
|
||||
export default function HelloPage() {
|
||||
return <h1>Hello, NocoBase!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
Registro no método `load()` do plugin:
|
||||
|
||||
const Layout = () => (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link> | <Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
```tsx
|
||||
import { Plugin } from '@nocobase/client-v2';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root', { element: <Layout /> });
|
||||
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
// Importação dinâmica: o módulo da página só é carregado quando esta rota é realmente acessada
|
||||
componentLoader: () => import('./routes/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about',
|
||||
componentLoader: () => import('./routes/AboutPage'),
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Carregamento sob demanda: o módulo só é carregado ao acessar /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
Suporta parâmetros dinâmicos
|
||||
O primeiro argumento de `router.add()` é o nome da rota, que aceita a notação de ponto `.` para expressar relações pai-filho. Por exemplo, `root.home` representa uma rota filha de `root`.
|
||||
|
||||
Nos componentes, você pode navegar para uma rota por meio de `ctx.router.navigate('/hello')`.
|
||||
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default function SomeComponent() {
|
||||
const ctx = useFlowContext();
|
||||
return (
|
||||
<Button onClick={() => ctx.router.navigate('/hello')}>
|
||||
Go to Hello Page
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Para mais detalhes, consulte a seção de roteamento em [Component](./component/index.md).
|
||||
|
||||
### Rotas aninhadas
|
||||
|
||||
O aninhamento é implementado por meio da notação de ponto. As rotas pai usam `<Outlet />` para renderizar o conteúdo das rotas filhas:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// Rota pai, usando element como layout em linha
|
||||
this.router.add('root', {
|
||||
element: (
|
||||
<div>
|
||||
<nav>Barra de navegação</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// Rota filha, usando componentLoader para carregamento sob demanda
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parâmetros dinâmicos
|
||||
|
||||
Os caminhos de rota aceitam parâmetros dinâmicos:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id',
|
||||
element: ({ params }) => <div>User ID: {params.id}</div>
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
|
||||
Se a página for pesada ou não for necessária na renderização inicial, dê preferência a `componentLoader`; `element` continua adequado para rotas de layout ou páginas inline muito leves.
|
||||
Nos componentes, você pode obter os parâmetros dinâmicos por meio de `ctx.route.params`:
|
||||
|
||||
## Extensão de Páginas de Configuração de Plugins
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
|
||||
Register plugin settings pages via `this.pluginSettingsManager`. Registration has two steps — first use `addMenuItem()` to register the menu entry, then use `addPageTabItem()` to register the actual page. Settings pages appear in the NocoBase "Plugin Settings" menu.
|
||||
export default function UserPage() {
|
||||
const ctx = useFlowContext();
|
||||
const { id } = ctx.route.params; // Obter o parâmetro dinâmico id
|
||||
return <h1>User ID: {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Para mais detalhes, consulte a seção de roteamento em [Component](./component/index.md).
|
||||
|
||||
### componentLoader vs. element
|
||||
|
||||
- **`componentLoader`** (recomendado): carregamento sob demanda, adequado para componentes de página. Os arquivos de página precisam de `export default`.
|
||||
- **`element`**: passa o JSX diretamente, adequado para componentes de layout ou páginas em linha muito leves.
|
||||
|
||||
Se a página tiver dependências pesadas, prefira `componentLoader`.
|
||||
|
||||
## Páginas de configuração de plugins
|
||||
|
||||
Registre páginas de configuração de plugins por meio de `this.pluginSettingsManager`. O registro tem duas etapas — primeiro use `addMenuItem()` para registrar a entrada de menu e depois `addPageTabItem()` para registrar a página propriamente dita. As páginas de configuração aparecem no menu "Configuração de plugins" do NocoBase.
|
||||
|
||||

|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
export class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Registrar a entrada de menu
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('Hello Settings'),
|
||||
icon: 'ApiOutlined',
|
||||
icon: 'ApiOutlined', // Nome de um ícone do Ant Design, consulte https://5x.ant.design/components/icon
|
||||
});
|
||||
|
||||
// Registrar a página (a chave 'index' é mapeada para o caminho raiz do menu)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -97,19 +176,25 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
To add multiple sub-pages under a single menu entry, register multiple `addPageTabItem` calls with the same `menuKey` — tabs will appear automatically:
|
||||
Após o registro, o caminho de acesso é `/v/admin/settings/hello`. Quando há apenas uma página sob o menu, a barra de abas superior é ocultada automaticamente.
|
||||
|
||||
### Página de configuração com várias abas
|
||||
|
||||
Se a página de configuração precisar de várias subpáginas, registre várias chamadas a `addPageTabItem` com o mesmo `menuKey` — uma barra de abas aparecerá automaticamente no topo:
|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Registrar a entrada de menu
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('HelloWorld'),
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Aba 1: Configurações gerais (a chave 'index' é mapeada para /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -117,6 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Aba 2: Configurações avançadas (mapeada para /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
@@ -125,4 +211,34 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Parâmetros de addMenuItem
|
||||
|
||||
| Campo | Tipo | Obrigatório | Descrição |
|
||||
| ---------- | --------------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| `key` | `string` | Sim | Identificador único do menu, não pode conter `.` |
|
||||
| `title` | `ReactNode` | Não | Título do menu |
|
||||
| `icon` | `string \| ReactNode` | Não | Ícone do menu; quando é uma string, é renderizado como `Icon` integrado |
|
||||
| `sort` | `number` | Não | Valor de ordenação; valores menores aparecem primeiro, padrão `0` |
|
||||
| `showTabs` | `boolean` | Não | Se a barra de abas superior é exibida; por padrão, determinado pela quantidade de páginas |
|
||||
| `hidden` | `boolean` | Não | Se a entrada de navegação é ocultada |
|
||||
|
||||
### Parâmetros de addPageTabItem
|
||||
|
||||
| Campo | Tipo | Obrigatório | Descrição |
|
||||
| ----------------- | ----------- | ----------- | -------------------------------------------------------------------- |
|
||||
| `menuKey` | `string` | Sim | O `key` do menu pai, correspondente ao `key` de `addMenuItem` |
|
||||
| `key` | `string` | Sim | Identificador único da página. `'index'` indica a página padrão, mapeada para o caminho raiz do menu |
|
||||
| `title` | `ReactNode` | Não | Título da página (exibido na aba) |
|
||||
| `componentLoader` | `Function` | Não | Componente de página com carregamento sob demanda (recomendado) |
|
||||
| `Component` | `Component` | Não | Passar o componente diretamente (alternativa a `componentLoader`) |
|
||||
| `sort` | `number` | Não | Valor de ordenação; valores menores aparecem primeiro |
|
||||
| `hidden` | `boolean` | Não | Se é ocultado na barra de abas |
|
||||
| `link` | `string` | Não | Link externo; quando definido, clicar na aba navega para a URL externa |
|
||||
|
||||
## Links relacionados
|
||||
|
||||
- [Plugin](./plugin) — as rotas são registradas em `load()`
|
||||
- [Component](./component/index.md) — como escrever os componentes de página montados pelas rotas
|
||||
- [Exemplo de plugin: criar uma página de configuração](./examples/settings-page) — exemplo completo de página de configuração
|
||||
|
||||
@@ -18,7 +18,7 @@ keywords: "AI-разработка,возможности,разработка
|
||||
|
||||
:::warning Внимание
|
||||
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v2/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- Сгенерированный AI код не всегда корректен на 100%, рекомендуется делать review перед включением. Если в процессе работы возникают ошибки, отправьте сообщение об ошибке AI, чтобы он продолжил диагностику и исправление — обычно проблема решается за несколько итераций диалога.
|
||||
- Для разработки рекомендуется использовать большие модели серии GPT или Claude — они дают наилучшие результаты. Другие модели также можно использовать, однако качество генерации может отличаться.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ nb init --ui
|
||||
|
||||
:::warning Внимание
|
||||
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v2/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- Сгенерированный AI код не всегда корректен на 100%, рекомендуется делать review перед включением. Если в процессе работы возникают ошибки, отправьте сообщение об ошибке AI, чтобы он продолжил диагностику и исправление — обычно проблема решается за несколько итераций диалога.
|
||||
- Для разработки рекомендуется использовать большие модели серии GPT или Claude — они дают наилучшие результаты. Другие модели также можно использовать, однако качество генерации может отличаться.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ keywords: "AI-разработка,плагин водяного знака,пл
|
||||
|
||||
:::warning Внимание
|
||||
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v2/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- NocoBase сейчас переходит с `client` (v1) на `client-v2`, и `client-v2` пока находится в разработке. Клиентский код, сгенерированный AI-разработкой, основан на `client-v2` и работает только по пути `/v/`. Это для предварительного ознакомления, не рекомендуется использовать в продакшене.
|
||||
- Сгенерированный AI код не всегда корректен на 100%, рекомендуется делать review перед включением. Если в процессе работы возникают ошибки, отправьте сообщение об ошибке AI, чтобы он продолжил диагностику и исправление — обычно проблема решается за несколько итераций диалога.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ keywords: "FAQ,частые проблемы,устранение неполад
|
||||
|
||||
### Зарегистрированный маршрут страницы недоступен
|
||||
|
||||
В NocoBase v2 маршруты по умолчанию имеют префикс `/v2`. Например, если Вы зарегистрировали `path: '/hello'`, фактический адрес — `/v2/hello`:
|
||||
В NocoBase v2 маршруты по умолчанию имеют префикс `/v`. Например, если Вы зарегистрировали `path: '/hello'`, фактический адрес — `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // 实际访问 -> /v2/hello
|
||||
path: '/hello', // 实际访问 -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ TypeError: Cannot assign to read only property 'constructor' of object '[object
|
||||
## Связанные ссылки
|
||||
|
||||
- [Plugin (Плагин)](../plugin) — точка входа плагина и жизненный цикл
|
||||
- [Router (Маршрутизация)](../router) — регистрация маршрутов и префикс `/v2`
|
||||
- [Router (Маршрутизация)](../router) — регистрация маршрутов и префикс `/v`
|
||||
- [Обзор FlowEngine](../flow-engine/index.md) — базовое использование FlowModel
|
||||
- [FlowEngine → Расширение блоков](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Расширение полей](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Через `ctx.router.navigate()` выполняется переход между страницами:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Получение параметров текущего маршрута:
|
||||
|
||||
@@ -316,7 +316,7 @@ async load() {
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Информация о маршруте (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// 组件里:页面导航
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Распространённые возможности, предоставляемые контекстом
|
||||
|
||||
@@ -1,92 +1,171 @@
|
||||
# Роутер
|
||||
---
|
||||
title: "Router"
|
||||
description: "Маршрутизация клиента NocoBase: this.router.add для регистрации маршрутов страниц, pluginSettingsManager для регистрации страниц настроек плагинов (addMenuItem + addPageTabItem)."
|
||||
keywords: "Router,маршрутизация,router.add,pluginSettingsManager,addMenuItem,addPageTabItem,componentLoader,регистрация страниц,NocoBase"
|
||||
---
|
||||
|
||||
Клиент NocoBase предоставляет гибкий менеджер маршрутизации, который позволяет расширять страницы и страницы настроек плагинов с помощью `router.add()` и `pluginSettingsManager`.
|
||||
# Router
|
||||
|
||||
## Зарегистрированные маршруты страниц по умолчанию
|
||||
В NocoBase плагины регистрируют страницы через маршруты. Есть два распространённых способа:
|
||||
|
||||
| Название | Путь | Компонент | Описание |
|
||||
| :----------------- | :----------------- | :----------------------- | :---------------------------- |
|
||||
| admin | /admin/\* | AdminLayout | Страницы административной панели |
|
||||
| admin.page | /admin/:name | AdminDynamicPage | Динамически создаваемые страницы |
|
||||
| admin.settings | /admin/settings/\* | AdminSettingsLayout | Страницы настроек плагинов |
|
||||
- `this.router.add()` — регистрирует обычные маршруты страниц
|
||||
- `this.pluginSettingsManager.addMenuItem()` + `addPageTabItem()` — регистрирует страницы настроек плагинов
|
||||
|
||||
## Расширение обычных страниц
|
||||
Регистрация маршрутов обычно выполняется в методе `load()` плагина. Подробнее см. [Plugin](./plugin).
|
||||
|
||||
Добавляйте обычные маршруты страниц с помощью `router.add()`. Для компонентов страниц используйте `componentLoader`, чтобы модуль страницы загружался только при фактическом переходе на маршрут.
|
||||
:::warning Примечание
|
||||
|
||||
Файлы страниц должны использовать `export default`:
|
||||
В плагинах NocoBase v2 зарегистрированные маршруты по умолчанию получают префикс `/v`. При обращении к маршрутам необходимо указывать этот префикс.
|
||||
|
||||
:::
|
||||
|
||||
## Маршруты по умолчанию
|
||||
|
||||
В NocoBase зарегистрированы следующие маршруты по умолчанию:
|
||||
|
||||
| Имя | Путь | Компонент | Описание |
|
||||
| -------------- | --------------------- | ------------------- | ------------------------------ |
|
||||
| admin | /v/admin/\* | AdminLayout | Страницы администрирования |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Динамически создаваемые страницы |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Страницы настроек плагинов |
|
||||
|
||||
## Маршруты страниц
|
||||
|
||||
Регистрируйте маршруты страниц через `this.router.add()`. Для компонентов страниц следует использовать `componentLoader` для отложенной загрузки, чтобы код страницы загружался только при фактическом обращении к ней.
|
||||
|
||||
:::warning Примечание
|
||||
|
||||
Файлы страниц должны экспортировать компонент через `export default`.
|
||||
|
||||
:::
|
||||
|
||||
```tsx
|
||||
// routes/HomePage.tsx
|
||||
export default function HomePage() {
|
||||
return <h1>Home</h1>;
|
||||
// pages/HelloPage.tsx
|
||||
export default function HelloPage() {
|
||||
return <h1>Hello, NocoBase!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
Регистрация в методе `load()` плагина:
|
||||
|
||||
const Layout = () => (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link> | <Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
```tsx
|
||||
import { Plugin } from '@nocobase/client-v2';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root', { element: <Layout /> });
|
||||
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
// Динамический импорт: модуль страницы загружается только при переходе на этот маршрут
|
||||
componentLoader: () => import('./routes/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about',
|
||||
componentLoader: () => import('./routes/AboutPage'),
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Отложенная загрузка: модуль загружается только при обращении к /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
Поддерживает динамические параметры
|
||||
Первый аргумент `router.add()` — это имя маршрута, поддерживающее точечную нотацию `.` для выражения отношений «родитель — потомок». Например, `root.home` представляет дочерний маршрут `root`.
|
||||
|
||||
В компонентах можно перейти к маршруту через `ctx.router.navigate('/hello')`.
|
||||
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default function SomeComponent() {
|
||||
const ctx = useFlowContext();
|
||||
return (
|
||||
<Button onClick={() => ctx.router.navigate('/hello')}>
|
||||
Go to Hello Page
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Подробнее см. раздел о маршрутизации в [Component](./component/index.md).
|
||||
|
||||
### Вложенные маршруты
|
||||
|
||||
Вложенность реализуется через точечную нотацию. Родительские маршруты используют `<Outlet />` для отображения содержимого дочерних маршрутов:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// Родительский маршрут, element как встроенный макет
|
||||
this.router.add('root', {
|
||||
element: (
|
||||
<div>
|
||||
<nav>Панель навигации</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// Дочерний маршрут, componentLoader для отложенной загрузки
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Динамические параметры
|
||||
|
||||
Пути маршрутов поддерживают динамические параметры:
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id',
|
||||
element: ({ params }) => <div>User ID: {params.id}</div>
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
|
||||
Если страница тяжёлая или не нужна при первом рендере, отдавайте предпочтение `componentLoader`; `element` по-прежнему подходит для layout-маршрутов или очень лёгких inline-страниц.
|
||||
В компонентах можно получить динамические параметры через `ctx.route.params`:
|
||||
|
||||
## Расширение страниц настроек плагинов
|
||||
```tsx
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
|
||||
Register plugin settings pages via `this.pluginSettingsManager`. Registration has two steps — first use `addMenuItem()` to register the menu entry, then use `addPageTabItem()` to register the actual page. Settings pages appear in the NocoBase "Plugin Settings" menu.
|
||||
export default function UserPage() {
|
||||
const ctx = useFlowContext();
|
||||
const { id } = ctx.route.params; // Получить динамический параметр id
|
||||
return <h1>User ID: {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Подробнее см. раздел о маршрутизации в [Component](./component/index.md).
|
||||
|
||||
### componentLoader или element
|
||||
|
||||
- **`componentLoader`** (рекомендуется): отложенная загрузка, подходит для компонентов страниц. Файлам страниц нужен `export default`.
|
||||
- **`element`**: передаёт JSX напрямую, подходит для компонентов макета или очень лёгких встроенных страниц.
|
||||
|
||||
Если страница сама по себе имеет тяжёлые зависимости, предпочтительнее использовать `componentLoader`.
|
||||
|
||||
## Страницы настроек плагинов
|
||||
|
||||
Регистрируйте страницы настроек плагинов через `this.pluginSettingsManager`. Регистрация состоит из двух шагов — сначала используйте `addMenuItem()` для регистрации пункта меню, затем `addPageTabItem()` для регистрации самой страницы. Страницы настроек появляются в меню «Настройки плагинов» NocoBase.
|
||||
|
||||

|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
export class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Регистрация пункта меню
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('Hello Settings'),
|
||||
icon: 'ApiOutlined',
|
||||
icon: 'ApiOutlined', // Имя иконки Ant Design, см. https://5x.ant.design/components/icon
|
||||
});
|
||||
|
||||
// Регистрация страницы (ключ 'index' сопоставляется с корневым путём меню)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -97,19 +176,25 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
To add multiple sub-pages under a single menu entry, register multiple `addPageTabItem` calls with the same `menuKey` — tabs will appear automatically:
|
||||
После регистрации путь доступа — `/v/admin/settings/hello`. Когда под меню есть только одна страница, верхняя панель вкладок автоматически скрывается.
|
||||
|
||||
### Страница настроек с несколькими вкладками
|
||||
|
||||
Если странице настроек нужно несколько подстраниц, зарегистрируйте несколько вызовов `addPageTabItem` с одним и тем же `menuKey` — сверху автоматически появится панель вкладок:
|
||||
|
||||
```tsx
|
||||
import { Plugin, Application } from '@nocobase/client-v2';
|
||||
|
||||
class HelloPlugin extends Plugin<any, Application> {
|
||||
async load() {
|
||||
// Регистрация пункта меню
|
||||
this.pluginSettingsManager.addMenuItem({
|
||||
key: 'hello',
|
||||
title: this.t('HelloWorld'),
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Вкладка 1: Общие настройки (ключ 'index' сопоставляется с /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -117,6 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Вкладка 2: Расширенные настройки (сопоставляется с /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
@@ -125,4 +211,34 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Параметры addMenuItem
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
| ---------- | --------------------- | ----------- | ------------------------------------------------------------------- |
|
||||
| `key` | `string` | Да | Уникальный идентификатор меню, не может содержать `.` |
|
||||
| `title` | `ReactNode` | Нет | Заголовок меню |
|
||||
| `icon` | `string \| ReactNode` | Нет | Иконка меню; для строки отображается как встроенный `Icon` |
|
||||
| `sort` | `number` | Нет | Значение сортировки; меньшие значения выше, по умолчанию `0` |
|
||||
| `showTabs` | `boolean` | Нет | Показывать ли верхнюю панель вкладок; по умолчанию определяется числом страниц |
|
||||
| `hidden` | `boolean` | Нет | Скрывать ли пункт навигации |
|
||||
|
||||
### Параметры addPageTabItem
|
||||
|
||||
| Поле | Тип | Обязательно | Описание |
|
||||
| ----------------- | ----------- | ----------- | ------------------------------------------------------------------- |
|
||||
| `menuKey` | `string` | Да | `key` родительского меню, соответствует `key` из `addMenuItem` |
|
||||
| `key` | `string` | Да | Уникальный идентификатор страницы. `'index'` обозначает страницу по умолчанию, сопоставленную с корневым путём меню |
|
||||
| `title` | `ReactNode` | Нет | Заголовок страницы (отображается на вкладке) |
|
||||
| `componentLoader` | `Function` | Нет | Компонент страницы с отложенной загрузкой (рекомендуется) |
|
||||
| `Component` | `Component` | Нет | Передать компонент напрямую (альтернатива `componentLoader`) |
|
||||
| `sort` | `number` | Нет | Значение сортировки; меньшие значения выше |
|
||||
| `hidden` | `boolean` | Нет | Скрывать ли на панели вкладок |
|
||||
| `link` | `string` | Нет | Внешняя ссылка; если задана, клик по вкладке ведёт на внешний URL |
|
||||
|
||||
## Связанные ссылки
|
||||
|
||||
- [Plugin](./plugin) — маршруты регистрируются в `load()`
|
||||
- [Component](./component/index.md) — как писать компоненты страниц, монтируемые маршрутами
|
||||
- [Пример плагина: создание страницы настроек](./examples/settings-page) — полный пример страницы настроек
|
||||
|
||||
@@ -18,7 +18,7 @@ Dưới đây liệt kê tất cả những việc AI hiện có thể giúp b
|
||||
|
||||
:::warning Lưu ý
|
||||
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v2/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- Mã do AI sinh ra không phải lúc nào cũng đúng 100%, khuyến nghị review trước khi enable. Nếu gặp vấn đề khi runtime, có thể gửi thông báo lỗi cho AI để nó tiếp tục kiểm tra và sửa — thường vài lượt trao đổi là giải quyết được.
|
||||
- Khuyến nghị dùng các mô hình lớn họ GPT hoặc Claude để phát triển, hiệu quả tốt nhất. Các mô hình khác cũng có thể dùng, tuy nhiên chất lượng sinh có thể có sự khác biệt.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Trình duyệt sẽ tự động mở trang cấu hình trực quan, hướng d
|
||||
|
||||
:::warning Lưu ý
|
||||
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v2/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- Mã do AI sinh ra không phải lúc nào cũng đúng 100%, khuyến nghị review trước khi enable. Nếu gặp vấn đề khi runtime, có thể gửi thông báo lỗi cho AI để nó tiếp tục kiểm tra và sửa — thường vài lượt trao đổi là giải quyết được.
|
||||
- Khuyến nghị dùng các mô hình lớn họ GPT hoặc Claude để phát triển, hiệu quả tốt nhất. Các mô hình khác cũng có thể dùng, tuy nhiên chất lượng sinh có thể có sự khác biệt.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Sau khi enable plugin:
|
||||
|
||||
:::warning Lưu ý
|
||||
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v2/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- NocoBase đang chuyển từ `client` (v1) sang `client-v2`, hiện `client-v2` vẫn đang trong quá trình phát triển. Mã client do AI Development sinh ra dựa trên `client-v2`, chỉ có thể dùng dưới đường dẫn `/v/`, dùng để trải nghiệm trước, không khuyến nghị dùng trực tiếp trong môi trường production.
|
||||
- Mã do AI sinh ra không phải lúc nào cũng đúng 100%, khuyến nghị review trước khi enable. Nếu gặp vấn đề khi runtime, có thể gửi thông báo lỗi cho AI để nó tiếp tục kiểm tra và sửa — thường vài lượt trao đổi là giải quyết được.
|
||||
|
||||
:::
|
||||
|
||||
@@ -38,11 +38,11 @@ Nếu code client đã sửa nhưng không hot reload, thử làm mới trình d
|
||||
|
||||
### Route trang đã đăng ký không truy cập được
|
||||
|
||||
Route của NocoBase v2 mặc định sẽ thêm tiền tố `/v2`. Ví dụ bạn đã đăng ký `path: '/hello'`, địa chỉ truy cập thực tế là `/v2/hello`:
|
||||
Route của NocoBase v2 mặc định sẽ thêm tiền tố `/v`. Ví dụ bạn đã đăng ký `path: '/hello'`, địa chỉ truy cập thực tế là `/v/hello`:
|
||||
|
||||
```ts
|
||||
this.router.add('hello', {
|
||||
path: '/hello', // Truy cập thực tế -> /v2/hello
|
||||
path: '/hello', // Truy cập thực tế -> /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
```
|
||||
@@ -285,7 +285,7 @@ Hệ thống build của NocoBase duy trì một [danh sách external](../../dep
|
||||
## Liên kết liên quan
|
||||
|
||||
- [Plugin](../plugin) — Lối vào Plugin và vòng đời
|
||||
- [Router](../router) — Đăng ký route và tiền tố `/v2`
|
||||
- [Router](../router) — Đăng ký route và tiền tố `/v`
|
||||
- [Tổng quan FlowEngine](../flow-engine/index.md) — Cách dùng cơ bản FlowModel
|
||||
- [FlowEngine → Mở rộng Block](../flow-engine/block) — BlockModel, TableBlockModel, filterCollection
|
||||
- [FlowEngine → Mở rộng Field](../flow-engine/field) — FieldModel, bindModelToInterface
|
||||
|
||||
@@ -118,7 +118,7 @@ const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
|
||||
Điều hướng trang thông qua `ctx.router.navigate()`:
|
||||
|
||||
```tsx
|
||||
ctx.router.navigate('/some-page'); // -> /v2/some-page
|
||||
ctx.router.navigate('/some-page'); // -> /v/some-page
|
||||
```
|
||||
|
||||
Lấy tham số route hiện tại:
|
||||
|
||||
@@ -316,7 +316,7 @@ Trong component, điều hướng trang qua `ctx.router.navigate()`:
|
||||
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
### Thông tin route (ctx.route)
|
||||
@@ -351,7 +351,7 @@ interface RouteOptions {
|
||||
```tsx
|
||||
const ctx = useFlowContext();
|
||||
|
||||
console.log(ctx.location.pathname); // '/v2/hello'
|
||||
console.log(ctx.location.pathname); // '/v/hello'
|
||||
console.log(ctx.location.search); // '?page=1'
|
||||
console.log(ctx.location.hash); // '#section'
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ async load() {
|
||||
```tsx
|
||||
// Trong component: điều hướng trang
|
||||
const ctx = useFlowContext();
|
||||
ctx.router.navigate('/hello'); // -> /v2/hello
|
||||
ctx.router.navigate('/hello'); // -> /v/hello
|
||||
```
|
||||
|
||||
## Các năng lực phổ biến mà context cung cấp
|
||||
|
||||
@@ -15,7 +15,7 @@ Việc đăng ký route thường được thực hiện trong phương thức `
|
||||
|
||||
:::warning Lưu ý
|
||||
|
||||
Plugin của NocoBase v2, route sau khi đăng ký sẽ tự động thêm tiền tố `/v2`, khi truy cập cần kèm theo tiền tố này.
|
||||
Plugin của NocoBase v2, route sau khi đăng ký sẽ tự động thêm tiền tố `/v`, khi truy cập cần kèm theo tiền tố này.
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,9 +25,9 @@ NocoBase đã đăng ký các route mặc định sau:
|
||||
|
||||
| Tên | Đường dẫn | Component | Mô tả |
|
||||
| -------------- | --------------------- | ------------------- | -------------- |
|
||||
| admin | /v2/admin/\* | AdminLayout | Trang quản trị |
|
||||
| admin.page | /v2/admin/:name | AdminDynamicPage | Trang được tạo động |
|
||||
| admin.settings | /v2/admin/settings/\* | AdminSettingsLayout | Trang cấu hình plugin |
|
||||
| admin | /v/admin/\* | AdminLayout | Trang quản trị |
|
||||
| admin.page | /v/admin/:name | AdminDynamicPage | Trang được tạo động |
|
||||
| admin.settings | /v/admin/settings/\* | AdminSettingsLayout | Trang cấu hình plugin |
|
||||
|
||||
## Route trang
|
||||
|
||||
@@ -55,7 +55,7 @@ class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('hello', {
|
||||
path: '/hello',
|
||||
// Tải theo nhu cầu, chỉ tải module này khi truy cập /v2/hello
|
||||
// Tải theo nhu cầu, chỉ tải module này khi truy cập /v/hello
|
||||
componentLoader: () => import('./pages/HelloPage'),
|
||||
});
|
||||
}
|
||||
@@ -103,12 +103,12 @@ class MyPlugin extends Plugin {
|
||||
|
||||
// Route con, dùng componentLoader để tải theo nhu cầu
|
||||
this.router.add('root.home', {
|
||||
path: '/', // -> /v2/
|
||||
path: '/', // -> /v/
|
||||
componentLoader: () => import('./pages/HomePage'),
|
||||
});
|
||||
|
||||
this.router.add('root.about', {
|
||||
path: '/about', // -> /v2/about
|
||||
path: '/about', // -> /v/about
|
||||
componentLoader: () => import('./pages/AboutPage'),
|
||||
});
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class MyPlugin extends Plugin {
|
||||
|
||||
```tsx
|
||||
this.router.add('root.user', {
|
||||
path: '/user/:id', // -> /v2/user/:id
|
||||
path: '/user/:id', // -> /v/user/:id
|
||||
componentLoader: () => import('./pages/UserPage'),
|
||||
});
|
||||
```
|
||||
@@ -176,7 +176,7 @@ export class HelloPlugin extends Plugin<any, Application> {
|
||||
}
|
||||
```
|
||||
|
||||
Sau khi đăng ký, đường dẫn truy cập là `/admin/settings/hello`. Khi dưới menu chỉ có một trang, thanh tab phía trên sẽ tự động ẩn.
|
||||
Sau khi đăng ký, đường dẫn truy cập là `/v/admin/settings/hello`. Khi dưới menu chỉ có một trang, thanh tab phía trên sẽ tự động ẩn.
|
||||
|
||||
### Trang cài đặt nhiều Tab
|
||||
|
||||
@@ -194,7 +194,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
icon: 'ApiOutlined',
|
||||
});
|
||||
|
||||
// Tab 1: Cài đặt cơ bản (key là 'index', map đến /admin/settings/hello)
|
||||
// Tab 1: Cài đặt cơ bản (key là 'index', map đến /v/admin/settings/hello)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'index',
|
||||
@@ -202,7 +202,7 @@ class HelloPlugin extends Plugin<any, Application> {
|
||||
componentLoader: () => import('./settings/GeneralPage'),
|
||||
});
|
||||
|
||||
// Tab 2: Cài đặt nâng cao (map đến /admin/settings/hello/advanced)
|
||||
// Tab 2: Cài đặt nâng cao (map đến /v/admin/settings/hello/advanced)
|
||||
this.pluginSettingsManager.addPageTabItem({
|
||||
menuKey: 'hello',
|
||||
key: 'advanced',
|
||||
|
||||
@@ -21,8 +21,23 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
generateV2Plugins();
|
||||
|
||||
// Fixed on-disk build-output directory name for the modern (v2) client. A
|
||||
// sibling copy of this default lives in packages/core/cli-v1/src/util.js
|
||||
// (DEFAULT_MODERN_CLIENT_PREFIX) and packages/core/server/src/gateway (utils.ts
|
||||
// MODERN_CLIENT_DIST_DIR). Keep them in sync. See docs/adr/0001-modern-client-prefix.md.
|
||||
const MODERN_CLIENT_DIST_DIR = 'v';
|
||||
|
||||
// Normalize APP_MODERN_CLIENT_PREFIX (accepts `v`, `/v`, `/v/`)
|
||||
// to a bare segment, falling back to the fixed dist dir name.
|
||||
function normalizeModernClientPrefix(value: string | undefined) {
|
||||
const segment = String(value || '')
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
return segment || MODERN_CLIENT_DIST_DIR;
|
||||
}
|
||||
|
||||
function ensurePublicPath(value: string) {
|
||||
let normalized = value || '/v2/';
|
||||
let normalized = value || `/${MODERN_CLIENT_DIST_DIR}/`;
|
||||
if (!normalized.startsWith('/')) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
@@ -37,10 +52,13 @@ function toNumber(value: string | undefined, fallback: number) {
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function createRuntimeHeadScript(v2PublicPath: string, isBuild: boolean) {
|
||||
function createRuntimeHeadScript(v2PublicPath: string, isBuild: boolean, modernClientPrefix: string) {
|
||||
if (!isBuild) {
|
||||
return [
|
||||
`window['__nocobase_public_path__'] = window['__nocobase_public_path__'] || ${JSON.stringify(v2PublicPath)};`,
|
||||
`window['__nocobase_modern_client_prefix__'] = window['__nocobase_modern_client_prefix__'] || ${JSON.stringify(
|
||||
modernClientPrefix,
|
||||
)};`,
|
||||
`window['__nocobase_app_dev__'] = window['__nocobase_app_dev__'] || ${JSON.stringify(
|
||||
process.env.NOCOBASE_APP_DEV === 'true',
|
||||
)};`,
|
||||
@@ -55,6 +73,9 @@ function createRuntimeHeadScript(v2PublicPath: string, isBuild: boolean) {
|
||||
|
||||
return [
|
||||
`window['__nocobase_public_path__'] = window['__nocobase_public_path__'] || ${JSON.stringify(v2PublicPath)};`,
|
||||
`window['__nocobase_modern_client_prefix__'] = window['__nocobase_modern_client_prefix__'] || ${JSON.stringify(
|
||||
modernClientPrefix,
|
||||
)};`,
|
||||
`window['__nocobase_api_base_url__'] = window['__nocobase_api_base_url__'] || ${JSON.stringify(
|
||||
process.env.API_BASE_URL || process.env.API_BASE_PATH || '',
|
||||
)};`,
|
||||
@@ -104,7 +125,13 @@ export default defineConfig(({ command }) => {
|
||||
const apiBasePath = ensurePublicPath(process.env.API_BASE_PATH || '/api/');
|
||||
const localStorageBasePath = ensurePublicPath(`${appPublicPath.replace(/\/$/, '')}/storage/uploads/`);
|
||||
const staticBasePath = ensurePublicPath(`${appPublicPath.replace(/\/$/, '')}/static/`);
|
||||
const v2PublicPath = ensurePublicPath(`${appPublicPath.replace(/\/$/, '')}/v2/`);
|
||||
const modernClientPrefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
||||
// Build bakes the FIXED dist-dir segment (`/v/`) as a sentinel that the
|
||||
// server rewrites to the runtime prefix per request. Dev serves under the
|
||||
// actual runtime prefix so URLs line up with the v1 dev proxy.
|
||||
const modernClientDistPath = ensurePublicPath(`${appPublicPath.replace(/\/$/, '')}/${MODERN_CLIENT_DIST_DIR}/`);
|
||||
const modernClientPublicPath = ensurePublicPath(`${appPublicPath.replace(/\/$/, '')}/${modernClientPrefix}/`);
|
||||
const v2PublicPath = isBuild ? modernClientDistPath : modernClientPublicPath;
|
||||
const wsBasePath = ensurePublicPath(process.env.WS_PATH || '/ws/');
|
||||
const hmrPath = `${v2PublicPath.replace(/\/$/, '')}/__rspack_hmr`;
|
||||
const v2Port = toNumber(process.env.APP_V2_PORT, 13002);
|
||||
@@ -151,7 +178,11 @@ export default defineConfig(({ command }) => {
|
||||
},
|
||||
{
|
||||
tag: 'script',
|
||||
children: createRuntimeHeadScript(v2PublicPath, isBuild),
|
||||
children: createRuntimeHeadScript(
|
||||
v2PublicPath,
|
||||
isBuild,
|
||||
isBuild ? MODERN_CLIENT_DIST_DIR : modernClientPrefix,
|
||||
),
|
||||
head: true,
|
||||
append: false,
|
||||
},
|
||||
@@ -169,7 +200,7 @@ export default defineConfig(({ command }) => {
|
||||
output: {
|
||||
target: 'web',
|
||||
distPath: {
|
||||
root: path.resolve(__dirname, '../dist/client/v2'),
|
||||
root: path.resolve(__dirname, `../dist/client/${MODERN_CLIENT_DIST_DIR}`),
|
||||
js: 'assets',
|
||||
jsAsync: 'assets',
|
||||
css: 'assets',
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Application } from '@nocobase/client-v2';
|
||||
import { Application, getModernClientPrefix } from '@nocobase/client-v2';
|
||||
import devDynamicImport from './.plugins';
|
||||
import { NocoBaseClientPresetPluginV2 } from '@nocobase/preset-nocobase/client-v2';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__nocobase_public_path__?: string;
|
||||
__nocobase_modern_client_prefix__?: string;
|
||||
__webpack_public_path__?: string;
|
||||
__nocobase_api_base_url__?: string;
|
||||
__nocobase_api_client_storage_prefix__?: string;
|
||||
__nocobase_api_client_storage_type__?: string;
|
||||
@@ -38,8 +40,20 @@ function ensureSlash(pathname: string, fallback: string) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getModernPrefixSuffix() {
|
||||
return `/${getModernClientPrefix()}/`;
|
||||
}
|
||||
|
||||
function stripPrefixSuffix(value: string) {
|
||||
const suffix = getModernPrefixSuffix();
|
||||
if (value.endsWith(suffix)) {
|
||||
return ensureSlash(value.slice(0, value.length - suffix.length), '/');
|
||||
}
|
||||
return ensureSlash(value, '/');
|
||||
}
|
||||
|
||||
function inferRootPublicPathFromLocation() {
|
||||
const marker = '/v2/';
|
||||
const marker = getModernPrefixSuffix();
|
||||
const pathname = window.location.pathname;
|
||||
const index = pathname.indexOf(marker);
|
||||
if (index >= 0) {
|
||||
@@ -51,17 +65,18 @@ function inferRootPublicPathFromLocation() {
|
||||
function getRootPublicPath() {
|
||||
const fromWindow = window.__nocobase_public_path__;
|
||||
if (fromWindow) {
|
||||
return ensureSlash(fromWindow.replace(/\/v2\/$/, '/'), '/');
|
||||
return stripPrefixSuffix(ensureSlash(fromWindow, '/'));
|
||||
}
|
||||
const fromBase = import.meta.env.BASE_URL;
|
||||
if (fromBase) {
|
||||
return ensureSlash(fromBase.replace(/\/v2\/$/, '/'), '/');
|
||||
return stripPrefixSuffix(ensureSlash(fromBase, '/'));
|
||||
}
|
||||
return inferRootPublicPathFromLocation();
|
||||
}
|
||||
|
||||
function getV2PublicPath() {
|
||||
return ensureSlash(`${getRootPublicPath().replace(/\/$/, '')}/v2/`, '/v2/');
|
||||
const suffix = getModernPrefixSuffix();
|
||||
return ensureSlash(`${getRootPublicPath().replace(/\/$/, '')}${suffix}`, suffix);
|
||||
}
|
||||
|
||||
function parseShareToken(value: boolean | string | undefined) {
|
||||
@@ -83,6 +98,25 @@ function parseStorageType(value: string | undefined): ClientStorageType {
|
||||
const rootPublicPath = getRootPublicPath();
|
||||
const v2PublicPath = getV2PublicPath();
|
||||
|
||||
// The fixed build-output directory segment baked into asset paths at build time
|
||||
// (e.g. `v`), derived from the baked BASE_URL. Distinct from the runtime
|
||||
// modern-client prefix, which may be overridden per-deployment.
|
||||
function getBuildAssetDir() {
|
||||
const base = (import.meta.env.BASE_URL || '/v/').replace(/\/+$/, '');
|
||||
return base.split('/').pop() || 'v';
|
||||
}
|
||||
|
||||
// Point dynamically-imported chunks of the main bundle at the runtime modern
|
||||
// client asset path, so a runtime prefix override / sub-path deployment / CDN
|
||||
// works without rebuilding (mirrors AutoInjectPublicPathPlugin used for plugins).
|
||||
// `__webpack_public_path__` is a webpack magic global that must be assigned.
|
||||
declare let __webpack_public_path__: string;
|
||||
const cdnBase = window.__webpack_public_path__;
|
||||
// eslint-disable-next-line prefer-const
|
||||
__webpack_public_path__ = cdnBase
|
||||
? ensureSlash(`${cdnBase.replace(/\/$/, '')}/${getBuildAssetDir()}/`, '/')
|
||||
: v2PublicPath;
|
||||
|
||||
const app = new Application({
|
||||
publicPath: v2PublicPath,
|
||||
apiClient: {
|
||||
|
||||
@@ -99,7 +99,14 @@ export default defineConfig(({ command }) => {
|
||||
const localStorageBasePath = ensurePublicPath(`${resolvedAppPublicPath}storage/uploads/`, '/storage/uploads/');
|
||||
const staticBasePath = ensurePublicPath(`${resolvedAppPublicPath}static/`, '/static/');
|
||||
const wsBasePath = ensurePublicPath(process.env.WS_PATH, '/ws/');
|
||||
const v2BasePath = ensurePublicPath(`${resolvedAppPublicPath.replace(/\/$/, '')}/v2/`, '/v2/');
|
||||
const modernClientPrefix =
|
||||
String(process.env.APP_MODERN_CLIENT_PREFIX || 'v')
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '') || 'v';
|
||||
const v2BasePath = ensurePublicPath(
|
||||
`${resolvedAppPublicPath.replace(/\/$/, '')}/${modernClientPrefix}/`,
|
||||
`/${modernClientPrefix}/`,
|
||||
);
|
||||
const clientPort = toNumber(process.env.APP_PORT, 13001);
|
||||
const v2Port = toNumber(process.env.APP_V2_PORT, clientPort + 2);
|
||||
const hmrPath = `${resolvedAppPublicPath.replace(/\/$/, '')}/__rspack_hmr`;
|
||||
|
||||
@@ -28,15 +28,17 @@ if (!publicPath) {
|
||||
}
|
||||
}
|
||||
if (!publicPath) {
|
||||
var modernPrefix = window['__nocobase_modern_client_prefix__'] || 'v';
|
||||
modernPrefix = String(modernPrefix).replace(/^\\/+|\\/+$/g, '') || 'v';
|
||||
var marker = '/' + modernPrefix + '/';
|
||||
publicPath = window['__nocobase_public_path__'] || '';
|
||||
if (!publicPath && window.location && window.location.pathname) {
|
||||
var marker = '/v2/';
|
||||
var pathname = window.location.pathname || '/';
|
||||
var index = pathname.indexOf(marker);
|
||||
publicPath = index >= 0 ? pathname.slice(0, index + 1) : '/';
|
||||
}
|
||||
if (publicPath) {
|
||||
publicPath = publicPath.replace(/\\/v2\\/?$/, '/');
|
||||
publicPath = publicPath.replace(new RegExp('/' + modernPrefix + '/?$'), '/');
|
||||
}
|
||||
if (!publicPath) {
|
||||
publicPath = '/';
|
||||
|
||||
@@ -91,7 +91,7 @@ server {
|
||||
}
|
||||
|
||||
location {{v2PublicPath}}assets/ {
|
||||
alias {{cwd}}/node_modules/@nocobase/app/dist/client/v2/assets/;
|
||||
alias {{cwd}}/node_modules/@nocobase/app/dist/client/v/assets/;
|
||||
expires 365d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
const { resolve, posix } = require('path');
|
||||
const { storagePathJoin, resolvePublicPath, resolveV2PublicPath } = require('../util');
|
||||
const { storagePathJoin, resolvePublicPath, resolveV2PublicPath, normalizeModernClientPrefix } = require('../util');
|
||||
const { Command } = require('commander');
|
||||
const { readFileSync, writeFileSync } = require('fs');
|
||||
|
||||
@@ -21,17 +21,20 @@ module.exports = (cli) => {
|
||||
const rawAppPublicPath = process.env.APP_PUBLIC_PATH || '/';
|
||||
const appPublicPath = resolvePublicPath(rawAppPublicPath);
|
||||
const v2PublicPath = resolveV2PublicPath(rawAppPublicPath);
|
||||
const modernClientPrefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
||||
const appPublicPathWithoutTrailingSlash = appPublicPath.replace(/\/$/, '');
|
||||
const v2PublicPathWithoutTrailingSlash = v2PublicPath.replace(/\/$/, '');
|
||||
const file = resolve(__dirname, '../../nocobase.conf.tpl');
|
||||
const data = readFileSync(file, 'utf-8');
|
||||
let otherLocation = '';
|
||||
if (appPublicPath !== '/') {
|
||||
otherLocation = `location = /v2 {
|
||||
// When the app is mounted under a sub-path, redirect the root-level
|
||||
// `/<prefix>` and `/<prefix>/` to the real (sub-path-prefixed) location.
|
||||
otherLocation = `location = /${modernClientPrefix} {
|
||||
return 302 ${v2PublicPath}$is_args$args;
|
||||
}
|
||||
|
||||
location /v2/ {
|
||||
location /${modernClientPrefix}/ {
|
||||
return 302 ${appPublicPathWithoutTrailingSlash}$uri$is_args$args;
|
||||
}
|
||||
|
||||
|
||||
@@ -367,9 +367,32 @@ function resolvePublicPath(appPublicPath = '/') {
|
||||
|
||||
exports.resolvePublicPath = resolvePublicPath;
|
||||
|
||||
// Default URL segment under which the modern (v2) client is served.
|
||||
// Kept local here so the CLI bootstrap (bin/index.js -> initEnv) stays lightweight
|
||||
// and does not have to require heavier packages. A second copy of the fixed
|
||||
// build-output directory name lives in:
|
||||
// - packages/core/app/client-v2/rsbuild.config.ts (output.distPath)
|
||||
// - packages/core/server/src/gateway/index.ts (MODERN_CLIENT_DIST_DIR)
|
||||
// Keep them in sync. See docs/adr/0001-modern-client-prefix.md.
|
||||
const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
||||
|
||||
exports.DEFAULT_MODERN_CLIENT_PREFIX = DEFAULT_MODERN_CLIENT_PREFIX;
|
||||
|
||||
// Normalize APP_MODERN_CLIENT_PREFIX (accepts `v`, `/v`, `/v/`)
|
||||
// down to a bare segment like `v`.
|
||||
function normalizeModernClientPrefix(value) {
|
||||
const segment = String(value || '')
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
return segment || DEFAULT_MODERN_CLIENT_PREFIX;
|
||||
}
|
||||
|
||||
exports.normalizeModernClientPrefix = normalizeModernClientPrefix;
|
||||
|
||||
function resolveV2PublicPath(appPublicPath = '/') {
|
||||
const publicPath = resolvePublicPath(appPublicPath);
|
||||
return `${publicPath.replace(/\/$/, '')}/v2/`;
|
||||
const prefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
||||
return `${publicPath.replace(/\/$/, '')}/${prefix}/`;
|
||||
}
|
||||
|
||||
exports.resolveV2PublicPath = resolveV2PublicPath;
|
||||
@@ -533,6 +556,7 @@ exports.initEnv = function initEnv() {
|
||||
APP_BASE_URL: '',
|
||||
CDN_BASE_URL: '',
|
||||
APP_PUBLIC_PATH: '/',
|
||||
APP_MODERN_CLIENT_PREFIX: DEFAULT_MODERN_CLIENT_PREFIX,
|
||||
ESM_CDN_BASE_URL: 'https://esm.sh',
|
||||
ESM_CDN_SUFFIX: '',
|
||||
};
|
||||
|
||||
@@ -26,6 +26,10 @@ describe('nocobase buildin plugin auth redirect', () => {
|
||||
const originalLocation = globalThis.window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
// These fixtures mount the modern client under the `v2` segment; tell the
|
||||
// runtime-prefix helper so v2-runtime detection matches (server injects it
|
||||
// in production).
|
||||
(globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
|
||||
Object.defineProperty(globalThis.window, 'matchMedia', {
|
||||
configurable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
@@ -42,6 +46,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
||||
Object.defineProperty(globalThis.window, 'location', {
|
||||
configurable: true,
|
||||
value: originalLocation,
|
||||
|
||||
@@ -113,6 +113,49 @@ function getV2BasePath(app: AppLike) {
|
||||
return trimTrailingSlashes(getV2PublicPath(app)) || '/';
|
||||
}
|
||||
|
||||
type ModernClientWindow = {
|
||||
__nocobase_modern_client_prefix__?: string;
|
||||
__nocobase_public_path__?: string;
|
||||
};
|
||||
|
||||
function getModernClientWindow(): ModernClientWindow | undefined {
|
||||
return typeof window !== 'undefined' ? (window as unknown as ModernClientWindow) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime URL segment under which the modern (v2) client is served.
|
||||
* Injected by the server as `window.__nocobase_modern_client_prefix__`. Falls
|
||||
* back to the trailing segment of `window.__nocobase_public_path__`, then to
|
||||
* the default `v`. Returns a bare segment (no slashes).
|
||||
*/
|
||||
export function getModernClientPrefix(): string {
|
||||
const win = getModernClientWindow();
|
||||
const fromWindow = win?.__nocobase_modern_client_prefix__;
|
||||
if (typeof fromWindow === 'string' && fromWindow.trim()) {
|
||||
return trimLeadingSlashes(trimTrailingSlashes(fromWindow.trim()));
|
||||
}
|
||||
const publicPath = win?.__nocobase_public_path__;
|
||||
if (typeof publicPath === 'string' && publicPath.trim()) {
|
||||
const segments = trimTrailingSlashes(publicPath.trim()).split('/');
|
||||
const last = segments[segments.length - 1];
|
||||
if (last) {
|
||||
return last;
|
||||
}
|
||||
}
|
||||
return 'v';
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the trailing modern-client prefix segment from a public path,
|
||||
* recovering the app root public path (e.g. `/nocobase/v/` -> `/nocobase/`).
|
||||
*/
|
||||
export function stripModernClientPrefix(publicPath?: string): string {
|
||||
const normalized = normalizePublicPath(publicPath);
|
||||
const prefix = getModernClientPrefix();
|
||||
const suffixPattern = new RegExp(`/${escapeRegExp(prefix)}/?$`);
|
||||
return normalizePublicPath(normalized.replace(suffixPattern, '/'));
|
||||
}
|
||||
|
||||
export function getV2EffectiveBasePath(app: AppLike): string {
|
||||
const basename = app.router?.getBasename?.();
|
||||
if (basename) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
|
||||
@@ -32,6 +32,13 @@ describe('FlowRoute', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hookState.refresh = vi.fn();
|
||||
// Fixtures mount the modern client under the `v2` segment; tell the
|
||||
// runtime-prefix helper so v2-runtime detection matches.
|
||||
(globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
||||
});
|
||||
|
||||
it('should bridge page lifecycle to admin-layout-model', async () => {
|
||||
|
||||
+8
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -45,6 +45,9 @@ describe('AdminLayoutModel menu items', () => {
|
||||
let modalConfirmMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Fixtures mount the modern client under the `v2` segment; tell the
|
||||
// runtime-prefix helper so v2-runtime detection matches.
|
||||
(globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
|
||||
engine = new FlowEngine();
|
||||
modalConfirmMock = vi.fn().mockResolvedValue(true);
|
||||
engine.registerModels({
|
||||
@@ -104,6 +107,10 @@ describe('AdminLayoutModel menu items', () => {
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
||||
});
|
||||
|
||||
const createRoute = (options?: Partial<import('../../../../flow-compat').NocoBaseDesktopRoute>) => ({
|
||||
id: 1,
|
||||
title: 'Page 1',
|
||||
|
||||
+11
@@ -23,6 +23,17 @@ const app = {
|
||||
},
|
||||
} as any;
|
||||
|
||||
// These fixtures mount the modern client under the `v2` segment; tell the
|
||||
// runtime-prefix helper so `isV2AdminRuntime` detects it (the server injects
|
||||
// this in production).
|
||||
beforeAll(() => {
|
||||
(window as any).__nocobase_modern_client_prefix__ = 'v2';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as any).__nocobase_modern_client_prefix__;
|
||||
});
|
||||
|
||||
describe('resolveAdminRouteRuntimeTarget', () => {
|
||||
it('should resolve flowPage to v2 spa runtime target', () => {
|
||||
expect(
|
||||
|
||||
+2
-4
@@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { getV2EffectiveBasePath } from '../../../authRedirect';
|
||||
import { getModernClientPrefix, getV2EffectiveBasePath } from '../../../authRedirect';
|
||||
import type { BaseApplication } from '../../../BaseApplication';
|
||||
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
||||
|
||||
@@ -26,8 +26,6 @@ export type AdminRouteRuntimeTarget = {
|
||||
reason: AdminRouteRuntimeTargetReason;
|
||||
};
|
||||
|
||||
const V2_PUBLIC_PATH_SUFFIX = '/v2/';
|
||||
|
||||
type LocationLike = {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
@@ -69,7 +67,7 @@ function normalizePublicPath(value = '/') {
|
||||
}
|
||||
|
||||
export function isV2AdminRuntime(app?: ResolveAdminRouteRuntimeTargetOptions['app']) {
|
||||
return !!app?.getPublicPath && normalizePublicPath(app.getPublicPath()).endsWith(V2_PUBLIC_PATH_SUFFIX);
|
||||
return !!app?.getPublicPath && normalizePublicPath(app.getPublicPath()).endsWith(`/${getModernClientPrefix()}/`);
|
||||
}
|
||||
|
||||
export function toRouterNavigationPath(pathname: string, basename?: string) {
|
||||
|
||||
@@ -7,61 +7,87 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { injectRuntimeScript, resolveV2PublicPath, rewriteV2AssetPublicPath } from '../gateway/utils';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
injectRuntimeScript,
|
||||
MODERN_CLIENT_DIST_DIR,
|
||||
normalizeModernClientPrefix,
|
||||
resolveV2PublicPath,
|
||||
rewriteV2AssetPublicPath,
|
||||
} from '../gateway/utils';
|
||||
|
||||
const DIR = MODERN_CLIENT_DIST_DIR; // fixed build-output dir / rewrite sentinel, e.g. `v`
|
||||
|
||||
describe('gateway utils', () => {
|
||||
it('should resolve v2 public path from app public path', () => {
|
||||
expect(resolveV2PublicPath('/')).toBe('/v2/');
|
||||
expect(resolveV2PublicPath('/nocobase/')).toBe('/nocobase/v2/');
|
||||
afterEach(() => {
|
||||
delete process.env.APP_MODERN_CLIENT_PREFIX;
|
||||
});
|
||||
|
||||
it('should rewrite v2 asset paths for prefixed deployment', () => {
|
||||
it('normalizes the modern client prefix', () => {
|
||||
expect(normalizeModernClientPrefix('v')).toBe('v');
|
||||
expect(normalizeModernClientPrefix('/v')).toBe('v');
|
||||
expect(normalizeModernClientPrefix('/v/')).toBe('v');
|
||||
expect(normalizeModernClientPrefix('')).toBe(DIR);
|
||||
expect(normalizeModernClientPrefix(undefined)).toBe(DIR);
|
||||
});
|
||||
|
||||
it('should resolve modern client public path from app public path (default prefix)', () => {
|
||||
expect(resolveV2PublicPath('/')).toBe(`/${DIR}/`);
|
||||
expect(resolveV2PublicPath('/nocobase/')).toBe(`/nocobase/${DIR}/`);
|
||||
});
|
||||
|
||||
it('should resolve modern client public path honoring APP_MODERN_CLIENT_PREFIX', () => {
|
||||
process.env.APP_MODERN_CLIENT_PREFIX = '/admin/';
|
||||
expect(resolveV2PublicPath('/')).toBe('/admin/');
|
||||
expect(resolveV2PublicPath('/nocobase/')).toBe('/nocobase/admin/');
|
||||
});
|
||||
|
||||
it('should rewrite modern asset paths for prefixed deployment', () => {
|
||||
const html = [
|
||||
'<script>window.__nocobase_public_path__=window.__nocobase_public_path__||"/v2/"</script>',
|
||||
'<script src="/v2/assets/runtime.js" type="module"></script>',
|
||||
'<link rel="modulepreload" href="/v2/assets/index.js" />',
|
||||
`<script>window.__nocobase_public_path__=window.__nocobase_public_path__||"/${DIR}/"</script>`,
|
||||
`<script src="/${DIR}/assets/runtime.js" type="module"></script>`,
|
||||
`<link rel="modulepreload" href="/${DIR}/assets/index.js" />`,
|
||||
].join('');
|
||||
|
||||
const rewritten = rewriteV2AssetPublicPath(html, '/nocobase/v2/');
|
||||
const rewritten = rewriteV2AssetPublicPath(html, `/nocobase/${DIR}/`);
|
||||
|
||||
expect(rewritten).toContain('<script src="/nocobase/v2/assets/runtime.js" type="module"></script>');
|
||||
expect(rewritten).toContain('<link rel="modulepreload" href="/nocobase/v2/assets/index.js" />');
|
||||
expect(rewritten).toContain(`<script src="/nocobase/${DIR}/assets/runtime.js" type="module"></script>`);
|
||||
expect(rewritten).toContain(`<link rel="modulepreload" href="/nocobase/${DIR}/assets/index.js" />`);
|
||||
});
|
||||
|
||||
it('should support rewriting assets to cdn public path', () => {
|
||||
const html = '<script src="/v2/assets/runtime.js" type="module"></script>';
|
||||
expect(rewriteV2AssetPublicPath(html, 'https://cdn.example.com/nocobase/v2/')).toBe(
|
||||
'<script src="https://cdn.example.com/nocobase/v2/assets/runtime.js" type="module"></script>',
|
||||
const html = `<script src="/${DIR}/assets/runtime.js" type="module"></script>`;
|
||||
expect(rewriteV2AssetPublicPath(html, `https://cdn.example.com/nocobase/${DIR}/`)).toBe(
|
||||
`<script src="https://cdn.example.com/nocobase/${DIR}/assets/runtime.js" type="module"></script>`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep html unchanged for default v2 public path', () => {
|
||||
const html = '<script src="/v2/assets/runtime.js" type="module"></script>';
|
||||
expect(rewriteV2AssetPublicPath(html, '/v2/')).toBe(html);
|
||||
it('should keep html unchanged for default modern public path', () => {
|
||||
const html = `<script src="/${DIR}/assets/runtime.js" type="module"></script>`;
|
||||
expect(rewriteV2AssetPublicPath(html, `/${DIR}/`)).toBe(html);
|
||||
});
|
||||
|
||||
it('should inject runtime script before module script', () => {
|
||||
const html = '<html><head><script src="/v2/assets/runtime.js" type="module"></script></head></html>';
|
||||
const runtimeScript = '<script>window.__nocobase_public_path__="/nocobase/v2/";</script>';
|
||||
const html = `<html><head><script src="/${DIR}/assets/runtime.js" type="module"></script></head></html>`;
|
||||
const runtimeScript = `<script>window.__nocobase_public_path__="/nocobase/${DIR}/";</script>`;
|
||||
|
||||
expect(injectRuntimeScript(html, runtimeScript)).toContain(
|
||||
`${runtimeScript}\n<script src="/v2/assets/runtime.js" type="module"></script>`,
|
||||
`${runtimeScript}\n<script src="/${DIR}/assets/runtime.js" type="module"></script>`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should inject runtime script before browser-checker script', () => {
|
||||
const html = [
|
||||
'<html><head>',
|
||||
'<script>window.__nocobase_public_path__=window.__nocobase_public_path__||"/v2/"</script>',
|
||||
'<script src="/nocobase/v2/browser-checker.js?v=1"></script>',
|
||||
'<script src="/v2/assets/runtime.js" type="module"></script>',
|
||||
`<script>window.__nocobase_public_path__=window.__nocobase_public_path__||"/${DIR}/"</script>`,
|
||||
`<script src="/nocobase/${DIR}/browser-checker.js?v=1"></script>`,
|
||||
`<script src="/${DIR}/assets/runtime.js" type="module"></script>`,
|
||||
'</head></html>',
|
||||
].join('');
|
||||
const runtimeScript = '<script>window.__nocobase_public_path__="/nocobase/v2/";</script>';
|
||||
const runtimeScript = `<script>window.__nocobase_public_path__="/nocobase/${DIR}/";</script>`;
|
||||
|
||||
expect(injectRuntimeScript(html, runtimeScript)).toContain(
|
||||
`${runtimeScript}\n<script src="/nocobase/v2/browser-checker.js?v=1"></script>`,
|
||||
`${runtimeScript}\n<script src="/nocobase/${DIR}/browser-checker.js?v=1"></script>`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,14 @@ import { applyErrorWithArgs, getErrorWithCode } from './errors';
|
||||
import { IPCSocketClient } from './ipc-socket-client';
|
||||
import { IPCSocketServer } from './ipc-socket-server';
|
||||
import { getStorageUploadSecurityHeaders } from './static-file-security';
|
||||
import { injectRuntimeScript, resolvePublicPath, resolveV2PublicPath, rewriteV2AssetPublicPath } from './utils';
|
||||
import {
|
||||
injectRuntimeScript,
|
||||
MODERN_CLIENT_DIST_DIR,
|
||||
normalizeModernClientPrefix,
|
||||
resolvePublicPath,
|
||||
resolveV2PublicPath,
|
||||
rewriteV2AssetPublicPath,
|
||||
} from './utils';
|
||||
import { WSServer } from './ws-server';
|
||||
import { isMainThread, workerData } from 'node:worker_threads';
|
||||
import process from 'node:process';
|
||||
@@ -339,6 +346,7 @@ export class Gateway extends EventEmitter {
|
||||
private getV2RuntimeConfig() {
|
||||
return {
|
||||
__nocobase_public_path__: this.getV2PublicPath(),
|
||||
__nocobase_modern_client_prefix__: normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX),
|
||||
__webpack_public_path__: process.env.CDN_BASE_URL ? `${process.env.CDN_BASE_URL.replace(/\/+$/, '')}/` : '',
|
||||
__nocobase_api_base_url__: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
||||
__nocobase_api_client_storage_prefix__: process.env.API_CLIENT_STORAGE_PREFIX,
|
||||
@@ -363,14 +371,15 @@ export class Gateway extends EventEmitter {
|
||||
|
||||
private getV2AssetPublicPath() {
|
||||
if (process.env.CDN_BASE_URL) {
|
||||
return `${process.env.CDN_BASE_URL.replace(/\/+$/, '')}/v2/`;
|
||||
// CDN hosts the assets under the fixed build-output directory name.
|
||||
return `${process.env.CDN_BASE_URL.replace(/\/+$/, '')}/${MODERN_CLIENT_DIST_DIR}/`;
|
||||
}
|
||||
|
||||
return this.getV2PublicPath();
|
||||
}
|
||||
|
||||
private getV2IndexTemplate() {
|
||||
const file = `${process.env.APP_PACKAGE_ROOT}/dist/client/v2/index.html`;
|
||||
const file = `${process.env.APP_PACKAGE_ROOT}/dist/client/${MODERN_CLIENT_DIST_DIR}/index.html`;
|
||||
if (!fs.existsSync(file)) {
|
||||
return null;
|
||||
}
|
||||
@@ -486,6 +495,14 @@ export class Gateway extends EventEmitter {
|
||||
}
|
||||
|
||||
req.url = req.url.substring(APP_PUBLIC_PATH.length - 1);
|
||||
// Map the runtime modern-client prefix segment back to the fixed
|
||||
// on-disk build directory (e.g. /admin/assets/x.js -> /v/assets/x.js)
|
||||
// so assets resolve when serving standalone (no nginx) and the runtime
|
||||
// prefix differs from the dist dir. No-op when they match (the default).
|
||||
const modernPrefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
||||
if (modernPrefix !== MODERN_CLIENT_DIST_DIR && req.url.startsWith(`/${modernPrefix}/`)) {
|
||||
req.url = `/${MODERN_CLIENT_DIST_DIR}/${req.url.slice(modernPrefix.length + 2)}`;
|
||||
}
|
||||
await compress(req, res);
|
||||
return handler(req, res, {
|
||||
public: `${process.env.APP_PACKAGE_ROOT}/dist/client`,
|
||||
|
||||
@@ -10,15 +10,32 @@
|
||||
import { IncomingMessage } from 'http';
|
||||
import { IncomingRequest } from '.';
|
||||
|
||||
// Fixed on-disk build-output directory name for the modern (v2) client, and
|
||||
// the sentinel segment baked into its HTML at build time. NOT the runtime URL
|
||||
// prefix (that is APP_MODERN_CLIENT_PREFIX, read per-request). A sibling copy
|
||||
// of this default lives in packages/core/cli-v1/src/util.js
|
||||
// (DEFAULT_MODERN_CLIENT_PREFIX). See docs/adr/0001-modern-client-prefix.md.
|
||||
export const MODERN_CLIENT_DIST_DIR = 'v';
|
||||
|
||||
export function resolvePublicPath(appPublicPath = '/') {
|
||||
const normalized = String(appPublicPath || '/').trim() || '/';
|
||||
const withLeadingSlash = normalized.startsWith('/') ? normalized : `/${normalized}`;
|
||||
return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`;
|
||||
}
|
||||
|
||||
// Normalize APP_MODERN_CLIENT_PREFIX (accepts `v`, `/v`, `/v/`)
|
||||
// down to a bare segment. Falls back to the fixed dist dir name.
|
||||
export function normalizeModernClientPrefix(value?: string) {
|
||||
const segment = String(value || '')
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
return segment || MODERN_CLIENT_DIST_DIR;
|
||||
}
|
||||
|
||||
export function resolveV2PublicPath(appPublicPath = '/') {
|
||||
const publicPath = resolvePublicPath(appPublicPath);
|
||||
return `${publicPath.replace(/\/$/, '')}/v2/`;
|
||||
const prefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
||||
return `${publicPath.replace(/\/$/, '')}/${prefix}/`;
|
||||
}
|
||||
|
||||
function ensureTrailingSlash(value: string) {
|
||||
@@ -27,11 +44,15 @@ function ensureTrailingSlash(value: string) {
|
||||
|
||||
export function rewriteV2AssetPublicPath(html: string, assetPublicPath: string) {
|
||||
const normalizedAssetPublicPath = ensureTrailingSlash(assetPublicPath);
|
||||
if (normalizedAssetPublicPath === '/v2/') {
|
||||
// HTML is built with the fixed dist-dir sentinel (`/v/`) baked into
|
||||
// asset URLs; rewrite it to the runtime asset path when they differ.
|
||||
const sentinel = `/${MODERN_CLIENT_DIST_DIR}/`;
|
||||
if (normalizedAssetPublicPath === sentinel) {
|
||||
return html;
|
||||
}
|
||||
|
||||
return html.replace(/((?:src|href)=["'])\/v2\//g, `$1${normalizedAssetPublicPath}`);
|
||||
const sentinelPattern = new RegExp(`((?:src|href)=["'])${sentinel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g');
|
||||
return html.replace(sentinelPattern, `$1${normalizedAssetPublicPath}`);
|
||||
}
|
||||
|
||||
export function injectRuntimeScript(html: string, runtimeScript: string) {
|
||||
|
||||
@@ -12,10 +12,10 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useApp } from '@nocobase/client-v2';
|
||||
|
||||
/**
|
||||
* 把 `?redirect=` 上带 v2 basename 的目标(例如 `/nocobase/v2/admin`)规约成 react-router
|
||||
* 接受的、相对 basename 的路径(`/admin`)。如果 target 已经是相对路径(不带 basename,
|
||||
* 例如服务端 2FA 中间件返回的 `/admin`),原样返回——react-router `navigate` 会自动加上
|
||||
* basename。
|
||||
* 把 `?redirect=` 上带 modern client basename 的目标(例如 `/nocobase/v/admin`)规约成
|
||||
* react-router 接受的、相对 basename 的路径(`/admin`)。如果 target 已经是相对路径(不带
|
||||
* basename, 例如服务端 2FA 中间件返回的 `/admin`),原样返回——react-router `navigate` 会自动
|
||||
* 加上 basename。
|
||||
*/
|
||||
function stripV2Basename(target: string, basename?: string): string {
|
||||
if (!basename || basename === '/') {
|
||||
@@ -28,7 +28,7 @@ function stripV2Basename(target: string, basename?: string): string {
|
||||
if (target.startsWith(`${normalized}/`)) {
|
||||
return target.slice(normalized.length) || '/';
|
||||
}
|
||||
// target 不在 v2 basename 下,当作相对路径,交给 react-router 自动 prepend basename。
|
||||
// target 不在 modern client basename 下,当作相对路径,交给 react-router 自动 prepend basename。
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,14 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { AuthModel, BasicAuth, buildRedirectPath, defaultTokenPolicyConfig, default, presetAuthType } from './server';
|
||||
export type { BuildRedirectPathOptions } from './server';
|
||||
export {
|
||||
AuthModel,
|
||||
BasicAuth,
|
||||
buildRedirectPath,
|
||||
defaultTokenPolicyConfig,
|
||||
default,
|
||||
getModernClientPrefix,
|
||||
presetAuthType,
|
||||
resolveSigninPrefix,
|
||||
} from './server';
|
||||
export type { BuildRedirectPathOptions, ResolveSigninPrefixOptions } from './server';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user