@jcoder-stack/registry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/auth/app/app-env.d.ts +24 -0
- package/auth/app/env.ts +17 -0
- package/auth/auth.config.ts +15 -0
- package/auth/index.ts +10 -0
- package/auth/manifest.json +9 -0
- package/auth/middleware.ts +21 -0
- package/auth/root/.env.example +23 -0
- package/auth/routes/api.auth.callback.ts +7 -0
- package/auth/routes/api.auth.login.ts +7 -0
- package/auth/routes/api.auth.logout.ts +7 -0
- package/auth/routes/api.culture.ts +6 -0
- package/auth/routes/api.tenant.ts +6 -0
- package/auth/runtime.ts +10 -0
- package/auth/server-fns.ts +119 -0
- package/package.json +43 -0
- package/public/r/abp-crud.json +51 -0
- package/public/r/abp-layout.json +93 -0
- package/public/r/abp-login.json +31 -0
- package/public/r/abp-permission-sheet.json +42 -0
- package/public/r/abp-sheet.json +25 -0
- package/public/r/abp-table.json +80 -0
- package/public/r/admin-pages.json +74 -0
- package/public/r/app-shell.json +120 -0
- package/public/r/combobox.json +39 -0
- package/public/r/data-table.json +92 -0
- package/public/r/date-picker.json +51 -0
- package/public/r/form.json +75 -0
- package/public/r/registry.json +723 -0
- package/public/r/tree.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the abp-react-start authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @jcoder-stack/registry
|
|
2
|
+
|
|
3
|
+
认证外壳的 copy-in 源,工厂化到极简:**`auth.config.ts` 是你唯一要看的文件**——一句 `createAbpAuthRuntime(process.env, {…覆盖项})`,默认即现行为,每个覆盖项(cookie 名/寿命、策略启停、登出回跳、身份解析器…)都带默认值,取消注释即生效。其余是薄接线:`runtime.ts`(进程单例)、`server-fns.ts` / `middleware.ts`(TanStack 接线,编译约束必留 copy-in)、`index.ts`(barrel)。
|
|
4
|
+
|
|
5
|
+
装配与机制全部下沉包:ABP auth 运行时工厂 `createAbpAuthRuntime` 与登录/回调/登出/文化/租户 handler → `@jcoder-stack/abp-react/proxy`;路由守卫 `requireAuth`/`requirePermission` → `@jcoder-stack/abp-react/router`(TanStack Router beforeLoad,经 `@/auth` barrel 重导出);ABP 代理调用、身份读取、returnUrl/culture 纯函数 → `@jcoder-stack/abp-react/proxy` / `@jcoder-stack/abp-react/auth`。
|
|
6
|
+
|
|
7
|
+
由 `jc-abp add auth` 按 manifest 落位到应用 `src/`;属 ABP React Start 框架,总览见仓库根 README。
|
|
8
|
+
|
|
9
|
+
## UI blocks
|
|
10
|
+
|
|
11
|
+
`ui/blocks/` 下是可用官方 shadcn CLI 安装的块,产物在 `public/r/`:
|
|
12
|
+
|
|
13
|
+
| 块 | 内容 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `abp-layout` | 侧栏布局(AppSidebar / SiteHeader / 面包屑 / 语言与主题切换 / 品牌标识) |
|
|
16
|
+
| `abp-login` | 密码登录卡片,含 OIDC 入口 |
|
|
17
|
+
| `app-shell` | 应用壳胶水:`_layout`/`_authed`/首页/login 路由 + `abp-fetch.ts` + 起点 `menu.tsx` |
|
|
18
|
+
| `data-table` | 通用服务端分页/排序/搜索表格 |
|
|
19
|
+
| `combobox` | 单选/多选 combobox(本地过滤或防抖远程 loadOptions) |
|
|
20
|
+
| `date-picker` | 单日期 / 日期区间 / 日期时间选择器 |
|
|
21
|
+
| `form` | 表单壳 + 字段组件 + 服务端字段错误映射 |
|
|
22
|
+
| `abp-crud` | ABP CRUD 协议(`createCrudService` / `AbpTableSource`)与共享件 |
|
|
23
|
+
| `abp-table` | `useAbpTable`——表格 + 筛选面板 + 批量操作 |
|
|
24
|
+
| `abp-sheet` | `useAbpSheet`——增删改抽屉 |
|
|
25
|
+
| `tree` | 通用树形块(展开收起 / 勾选,级联策略留给消费方) |
|
|
26
|
+
| `abp-permission-sheet` | 权限树 Sheet(组=Accordion,组内 Tree) |
|
|
27
|
+
| `admin-pages` | users / roles / tenants / settings / profile 五个管理页 |
|
|
28
|
+
|
|
29
|
+
装法、依赖顺序与各块的前置条件见 [`docs/guides/install-blocks.md`](../docs/guides/install-blocks.md)。源码改动后用 `bun run build:registry` 重新生成产物。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface ImportMetaEnv {
|
|
3
|
+
readonly VITE_APP_TITLE?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ImportMeta {
|
|
7
|
+
readonly env: ImportMetaEnv;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
namespace NodeJS {
|
|
11
|
+
interface ProcessEnv {
|
|
12
|
+
readonly OIDC_ISSUER: string;
|
|
13
|
+
readonly OIDC_CLIENT_ID: string;
|
|
14
|
+
readonly OIDC_CLIENT_SECRET?: string;
|
|
15
|
+
readonly OIDC_SCOPE?: string;
|
|
16
|
+
readonly API_BASE_URL: string;
|
|
17
|
+
readonly AUTH_SESSION_SECRET: string;
|
|
18
|
+
readonly OIDC_REDIRECT_URI: string;
|
|
19
|
+
readonly OIDC_POST_LOGOUT_REDIRECT_URI?: string;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {};
|
package/auth/app/env.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createServerOnlyFn } from "@tanstack/react-start";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// AUTH_* 必需 env 由 @jcoder-stack/abp-react/proxy 的 resolveAbpAuthEnv 校验,勿在此重复。
|
|
5
|
+
const serverEnvSchema = z.object({
|
|
6
|
+
// 按需加应用自己的 server 变量,例:STRIPE_SECRET_KEY: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const clientEnvSchema = z.object({
|
|
10
|
+
VITE_APP_TITLE: z.string().default("ABP React Start"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** 应用自有 server env;每次调用重新读取,边缘运行时的 env 按请求注入,不可缓存。 */
|
|
14
|
+
export const getServerEnv = createServerOnlyFn(() => serverEnvSchema.parse(process.env));
|
|
15
|
+
|
|
16
|
+
/** 应用自有公开 env;VITE_ 前缀构建期内联,SSR 与浏览器两端可读,勿放机密。 */
|
|
17
|
+
export const clientEnv = clientEnvSchema.parse(import.meta.env);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createAbpAuthRuntime } from "@jcoder-stack/abp-react/proxy";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auth 策略声明。本目录你唯一需要改的文件。
|
|
5
|
+
* 默认即现行为;每个覆盖项都有默认值,取消注释即生效。
|
|
6
|
+
*/
|
|
7
|
+
export const createRuntime = () =>
|
|
8
|
+
createAbpAuthRuntime(process.env, {
|
|
9
|
+
// 覆盖会话 cookie(默认名 "auth_session",寿命 7 天):
|
|
10
|
+
// cookies: { session: { name: "myapp_session", maxAge: 60 * 60 * 24 * 14 } },
|
|
11
|
+
// 只留 OIDC、关掉 password 策略(默认两者都开):
|
|
12
|
+
// strategies: { password: false },
|
|
13
|
+
// 覆盖 IdP 登出后回跳(默认取 AUTH_POST_LOGOUT_REDIRECT_URI):
|
|
14
|
+
// postLogoutRedirectUri: "https://app.example/goodbye",
|
|
15
|
+
});
|
package/auth/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { type GuardContext, requireAuth, requirePermission } from "@jcoder-stack/abp-react/router";
|
|
2
|
+
export { authMiddleware } from "./middleware";
|
|
3
|
+
export { getAuthRuntime } from "./runtime";
|
|
4
|
+
export {
|
|
5
|
+
abpRequestFn,
|
|
6
|
+
getAppStateFn,
|
|
7
|
+
getIdentityFn,
|
|
8
|
+
loginWithPasswordFn,
|
|
9
|
+
logoutFn,
|
|
10
|
+
} from "./server-fns";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
import { getRequestHeader, setResponseHeader } from "@tanstack/react-start/server";
|
|
3
|
+
import { getAuthRuntime } from "./runtime";
|
|
4
|
+
|
|
5
|
+
/** 官方主推模式:每个受保护 server fn 经此拿 context.session;过期时在这里统一刷新并回写 cookie。 */
|
|
6
|
+
export const authMiddleware = createMiddleware({ type: "function" }).server(async ({ next }) => {
|
|
7
|
+
const rt = getAuthRuntime();
|
|
8
|
+
const cookieHeader = getRequestHeader("cookie") ?? null;
|
|
9
|
+
let session = await rt.auth.session.current(cookieHeader);
|
|
10
|
+
if (session !== null && rt.auth.session.isExpired(session)) {
|
|
11
|
+
const fresh = await rt.auth.session.refresh(session, cookieHeader);
|
|
12
|
+
if (fresh !== null) {
|
|
13
|
+
session = fresh.session;
|
|
14
|
+
setResponseHeader("Set-Cookie", fresh.setCookies);
|
|
15
|
+
} else {
|
|
16
|
+
// 刷新失败=匿名化:不带着过期 token 去打后端,请求本身不炸。
|
|
17
|
+
session = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return next({ context: { session, cookieHeader } });
|
|
21
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 下面的地址与 client id 是占位,八成跟你的后端对不上:client 必须是后端 OpenIddict 里已注册的
|
|
2
|
+
# 那一个,回调地址也要登记在它名下。填错时报错发生在后端的登录页上,前端只看到一次跳转,不好查。
|
|
3
|
+
# 先用授权端点探一次,三种回应对应三种情况:
|
|
4
|
+
# curl -sk "<你的后端>/connect/authorize?client_id=<猜的名字>&response_type=code&scope=openid\
|
|
5
|
+
# &redirect_uri=http://localhost:3000/api/auth/callback&state=x" | head -3
|
|
6
|
+
# "'client_id' is invalid" → 这个 client 不存在,换名字再试
|
|
7
|
+
# "'redirect_uri' is not valid" → client 对了,回调没登记,去后端补
|
|
8
|
+
# 302 到 /Account/Login → 两个都对
|
|
9
|
+
AUTH_ISSUER=https://localhost:44300
|
|
10
|
+
AUTH_CLIENT_ID=WebApp
|
|
11
|
+
# 机密客户端才需要:
|
|
12
|
+
# AUTH_CLIENT_SECRET=
|
|
13
|
+
# 除 openid/profile/offline_access 外还要带后端的 API 资源 scope(ABP 模板里通常与项目同名,在
|
|
14
|
+
# /.well-known/openid-configuration 的 scopes_supported 里能看到)。漏了它登录照样过,但换到的
|
|
15
|
+
# token 没有 API 的 audience,代理请求一律 401——表现为每个管理页都是空的,不报错。
|
|
16
|
+
AUTH_SCOPE="openid profile offline_access"
|
|
17
|
+
AUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback
|
|
18
|
+
AUTH_POST_LOGOUT_REDIRECT_URI=http://localhost:3000/
|
|
19
|
+
# ≥32 字符随机串:openssl rand -base64 32
|
|
20
|
+
AUTH_SESSION_SECRET=
|
|
21
|
+
AUTH_ABP_BASE_URL=https://localhost:44300
|
|
22
|
+
# 开发排障:置 true 打开 auth 全链路 debug 日志(永不输出 token 原文)
|
|
23
|
+
AUTH_DEBUG=false
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { handleCallback } from "@jcoder-stack/abp-react/proxy";
|
|
2
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
3
|
+
import { getAuthRuntime } from "@/auth/runtime";
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute("/api/auth/callback")({
|
|
6
|
+
server: { handlers: { GET: ({ request }) => handleCallback(request, getAuthRuntime()) } },
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { handleLogin } from "@jcoder-stack/abp-react/proxy";
|
|
2
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
3
|
+
import { getAuthRuntime } from "@/auth/runtime";
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute("/api/auth/login")({
|
|
6
|
+
server: { handlers: { GET: ({ request }) => handleLogin(request, getAuthRuntime()) } },
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { handleLogout } from "@jcoder-stack/abp-react/proxy";
|
|
2
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
3
|
+
import { getAuthRuntime } from "@/auth/runtime";
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute("/api/auth/logout")({
|
|
6
|
+
server: { handlers: { GET: ({ request }) => handleLogout(request, getAuthRuntime()) } },
|
|
7
|
+
});
|
package/auth/runtime.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AuthRuntime } from "@jcoder-stack/abp-react/proxy";
|
|
2
|
+
import { createRuntime } from "./auth.config";
|
|
3
|
+
|
|
4
|
+
let runtime: AuthRuntime | undefined;
|
|
5
|
+
|
|
6
|
+
/** 进程级 auth 运行时单例;仅在 server 侧 import(路由 handler / server fn / middleware)。 */
|
|
7
|
+
export function getAuthRuntime(): AuthRuntime {
|
|
8
|
+
runtime ??= createRuntime();
|
|
9
|
+
return runtime;
|
|
10
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthError,
|
|
3
|
+
type AuthErrorCode,
|
|
4
|
+
parseCookieHeader,
|
|
5
|
+
parseCultureCookie,
|
|
6
|
+
} from "@jcoder-stack/abp-react/auth";
|
|
7
|
+
import {
|
|
8
|
+
AbpProxyError,
|
|
9
|
+
CULTURE_COOKIE,
|
|
10
|
+
callAbpWithSession,
|
|
11
|
+
loadAppState,
|
|
12
|
+
TENANT_COOKIE,
|
|
13
|
+
} from "@jcoder-stack/abp-react/proxy";
|
|
14
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
15
|
+
import { getRequestHeader, setResponseHeader } from "@tanstack/react-start/server";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { authMiddleware } from "./middleware";
|
|
18
|
+
import { getAuthRuntime } from "./runtime";
|
|
19
|
+
|
|
20
|
+
/** SSR 一次取数喂两张嘴:config(AppConfigProvider)+ identity(SessionProvider)。 */
|
|
21
|
+
export const getAppStateFn = createServerFn({ method: "GET" })
|
|
22
|
+
.middleware([authMiddleware])
|
|
23
|
+
.handler(async ({ context }) => {
|
|
24
|
+
const rt = getAuthRuntime();
|
|
25
|
+
const state = await loadAppState(rt, context.session, context.cookieHeader);
|
|
26
|
+
if (state.setCookies.length > 0) setResponseHeader("Set-Cookie", state.setCookies);
|
|
27
|
+
return { config: state.config, identity: state.identity };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** 轻量身份重取(useSession().reload 的服务侧一半)。 */
|
|
31
|
+
export const getIdentityFn = createServerFn({ method: "GET" })
|
|
32
|
+
.middleware([authMiddleware])
|
|
33
|
+
.handler(async ({ context }) => {
|
|
34
|
+
const rt = getAuthRuntime();
|
|
35
|
+
const state = await loadAppState(rt, context.session, context.cookieHeader);
|
|
36
|
+
if (state.setCookies.length > 0) setResponseHeader("Set-Cookie", state.setCookies);
|
|
37
|
+
return state.identity;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const passwordLoginSchema = z.object({
|
|
41
|
+
userName: z.string().min(1),
|
|
42
|
+
password: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** password 策略登录;凭据错误是预期业务态 → 返回值而非 throw。成功后客户端 reload() 取新身份。 */
|
|
46
|
+
export const loginWithPasswordFn = createServerFn({ method: "POST" })
|
|
47
|
+
.validator(passwordLoginSchema)
|
|
48
|
+
.handler(async ({ data }) => {
|
|
49
|
+
const rt = getAuthRuntime();
|
|
50
|
+
const cookieHeader = getRequestHeader("cookie") ?? null;
|
|
51
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
52
|
+
try {
|
|
53
|
+
const result = await rt.auth.strategy("password").complete({
|
|
54
|
+
kind: "credentials",
|
|
55
|
+
userName: data.userName,
|
|
56
|
+
password: data.password,
|
|
57
|
+
tenant: cookies[TENANT_COOKIE] ?? null,
|
|
58
|
+
});
|
|
59
|
+
const setCookies = await rt.auth.session.establish(result, {
|
|
60
|
+
tenant: cookies[TENANT_COOKIE] ?? null,
|
|
61
|
+
culture: parseCultureCookie(cookies[CULTURE_COOKIE]),
|
|
62
|
+
cookieHeader,
|
|
63
|
+
});
|
|
64
|
+
setResponseHeader("Set-Cookie", setCookies);
|
|
65
|
+
return { ok: true as const };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const code: AuthErrorCode = error instanceof AuthError ? error.code : "exchange_failed";
|
|
68
|
+
rt.logger.debug("password login failed", { code });
|
|
69
|
+
return { ok: false as const, error: code };
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** 本地登出(password 模式);OIDC 全局登出走 /api/auth/logout 路由。 */
|
|
74
|
+
export const logoutFn = createServerFn({ method: "POST" }).handler(async () => {
|
|
75
|
+
const rt = getAuthRuntime();
|
|
76
|
+
setResponseHeader(
|
|
77
|
+
"Set-Cookie",
|
|
78
|
+
await rt.auth.session.destroy(getRequestHeader("cookie") ?? null),
|
|
79
|
+
);
|
|
80
|
+
return { ok: true as const };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const abpRequestSchema = z.object({
|
|
84
|
+
path: z.string(),
|
|
85
|
+
method: z.string().optional(),
|
|
86
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
87
|
+
body: z.string().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** 业务 API 的唯一请求边界:orval mutator → 此 server fn → 代理网关。 */
|
|
91
|
+
export const abpRequestFn = createServerFn({ method: "POST" })
|
|
92
|
+
.validator(abpRequestSchema)
|
|
93
|
+
.middleware([authMiddleware])
|
|
94
|
+
.handler(async ({ data, context }) => {
|
|
95
|
+
const rt = getAuthRuntime();
|
|
96
|
+
let res: Awaited<ReturnType<typeof callAbpWithSession>>;
|
|
97
|
+
try {
|
|
98
|
+
res = await callAbpWithSession(rt, context.session, context.cookieHeader, data);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// 失败也要把过程中刷新出的会话 cookie 落到响应,否则轮换型 IdP 下用户被静默登出。
|
|
101
|
+
if (error instanceof AbpProxyError && error.setCookies.length > 0)
|
|
102
|
+
setResponseHeader("Set-Cookie", error.setCookies);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
if (res.setCookies.length > 0) setResponseHeader("Set-Cookie", res.setCookies);
|
|
106
|
+
return {
|
|
107
|
+
status: res.status,
|
|
108
|
+
contentType: res.headers.get("content-type"),
|
|
109
|
+
body: typeof res.body === "string" ? res.body : undefined,
|
|
110
|
+
bodyBase64: typeof res.body === "string" ? undefined : toBase64(res.body),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// server fn 边界只序列化 JSON,二进制体转 base64 过桥,abpFetch 端解码还原字节。
|
|
115
|
+
function toBase64(buffer: ArrayBuffer): string {
|
|
116
|
+
let binary = "";
|
|
117
|
+
for (const byte of new Uint8Array(buffer)) binary += String.fromCharCode(byte);
|
|
118
|
+
return btoa(binary);
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jcoder-stack/registry",
|
|
3
|
+
"description": "认证外壳的 copy-in 源:登录/回调/登出 handler、callAbp 代理、路由守卫、TanStack 接线",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"auth",
|
|
8
|
+
"public/r"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build:registry": "node scripts/check-registry-deps.mjs && npx shadcn@latest build",
|
|
12
|
+
"check-registry-deps": "node scripts/check-registry-deps.mjs",
|
|
13
|
+
"prepack": "node ../scripts/apply-publish-config.mjs",
|
|
14
|
+
"postpack": "node ../scripts/restore-publish-config.mjs"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@jcoder-stack/abp-react": "^0.1.0",
|
|
18
|
+
"@tanstack/react-router": "^1.0.0",
|
|
19
|
+
"@tanstack/react-start": "^1.0.0",
|
|
20
|
+
"zod": "^4.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@tanstack/react-form": "^1.33.2",
|
|
24
|
+
"@tanstack/react-table": "9.0.0-beta.59",
|
|
25
|
+
"date-fns": "^4.4.0",
|
|
26
|
+
"lucide-react": "^0.525.0",
|
|
27
|
+
"radix-ui": "^1.6.4",
|
|
28
|
+
"sonner": "^2.0.7",
|
|
29
|
+
"vite": "^7.0.0"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/jcoder-stack/abp-react-start.git",
|
|
35
|
+
"directory": "registry"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "abp-crud",
|
|
4
|
+
"title": "ABP CRUD Protocol",
|
|
5
|
+
"description": "ABP CRUD service protocol (createCrudService, AbpTableSource), ABP-flavored form options/error mapping, and the bound-components factory shared by the abp-table and abp-sheet blocks.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@jcoder-stack/abp-react",
|
|
8
|
+
"@tanstack/react-query",
|
|
9
|
+
"@tanstack/react-form"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "ui/blocks/abp-crud/crud-service.ts",
|
|
15
|
+
"content": "import type { AbpListParams, PagedResult } from \"@jcoder-stack/abp-react/core\";\nimport type { keepPreviousData } from \"@tanstack/react-query\";\n\nexport interface PagedResultLike<T> {\n items?: T[] | null;\n totalCount?: number;\n}\n\n/** ABP 分页协议 4 字段 + 端点自有查询参数的透传位:交叉 Record 才能把端点自有参数一起传下去。\n * interface 没有隐式索引签名,所以 `AbpListParams` 类型的变量赋不进来;推断出的匿名对象类型可以。 */\nexport type ListParams = AbpListParams & Record<string, unknown>;\n\n/** 这 4 个参数名由表格状态机独占。端点自有参数撞上其中任何一个都不会生效,\n * 这条规则是固定的,不随当前的排序或搜索状态变化。 */\nexport const ABP_RESERVED_LIST_PARAM_NAMES: ReadonlySet<string> = new Set([\n \"SkipCount\",\n \"MaxResultCount\",\n \"Sorting\",\n \"Filter\",\n]);\n\ninterface MutationCbs {\n onSuccess?: () => void;\n onError?: () => void;\n}\n\n/** 写操作的两个 hook。只读 service 不必满足,`WritableCrudService`(文件尾)交叉它把两者钉成必填。\n * 用方法简写而非属性签名,是为了拿到参数双变,让 orval 生成的 hook 赋得进来。 */\nexport interface CrudWriteHooks<TCreate, TUpdate> {\n useCreate(options?: { mutation?: MutationCbs }): {\n mutateAsync: (variables: { data: TCreate }) => Promise<unknown>;\n isPending: boolean;\n };\n useUpdate(options?: { mutation?: MutationCbs }): {\n mutateAsync: (variables: { id: string; data: TUpdate }) => Promise<unknown>;\n isPending: boolean;\n };\n}\n\n/** 删除 hook,单独拆分:读写但不可删(如仅管理员可删)的 service 场景下无需满足。 */\nexport interface CrudDeleteHook {\n useDelete(options?: { mutation?: MutationCbs }): {\n mutate: (variables: { id: string }) => void;\n mutateAsync: (variables: { id: string }) => Promise<unknown>;\n isPending: boolean;\n };\n}\n\n/** orval 为 CRUD 端点生成的 hook 形状。返回类型只列 `useAbpTable`/`useAbpSheet` 真正读的字段,\n * 其余靠协变兼容。`useList` 必填,只读 service 也要能查;写/删各自可选,\n * 由 `createCrudService` 按实传逐个钉回。 */\nexport interface CrudHooks<TDto extends { id?: string }, TCreate, TUpdate>\n extends Partial<CrudWriteHooks<TCreate, TUpdate>>,\n Partial<CrudDeleteHook> {\n useList(\n params: ListParams,\n options?: {\n query?: {\n placeholderData?: typeof keepPreviousData;\n select?: (raw: PagedResultLike<TDto>) => PagedResult<TDto>;\n };\n },\n ): { data?: PagedResult<TDto>; isPending: boolean; isFetching: boolean; isError: boolean };\n}\n\n/** 端点参数类型决定 supportsFilter 能取什么值:端点没有 Filter 时必填 false。漏填会落到缺省的\n * true,做出一个能打字能回车、什么都不发生的搜索框。\n *\n * 违规时报错多半落在 `useCreate`/`useUpdate` 行,只有一条直接指向 supportsFilter,先查这条。\n * 有两种看着像修复、实际是把约束关掉的改法:补回显式类型参数,或把描述符先标注成\n * `CrudServiceDef<…>` 再传入(标注会换成带索引签名的 `CrudHooks[\"useList\"]`,\n * `\"Filter\" extends keyof` 于是恒真)。 */\nexport type FilterConfig<TListParams> = \"Filter\" extends keyof TListParams\n ? { supportsFilter?: boolean }\n : { supportsFilter: false };\n\n/** `CrudServiceDef` 与 `createCrudService` 入参共用的元数据槽位,抽出来免得两处各写一份。 */\ninterface CrudServiceMeta {\n /** react-query 失效前缀(orval 的 getGetXxxQueryKey)。invalidate 用它一次清掉所有分页态。 */\n listKey: () => readonly unknown[];\n policy?: string;\n policies?: Partial<{ create: string; update: string; delete: string }>;\n}\n\ndeclare const crudServiceCapabilities: unique symbol;\n\n/** 类型层反解用的幽灵字段。运行时从不写它(可选,缺失照样满足结构类型),但悬浮提示里会显示成\n * `CrudServiceTypeWitness<...>` 这个噪音字段。已知无害,别当 bug 删掉。\n *\n * `useCreate`/`useUpdate` 不在 `CrudServiceDef` 的结构体里(只读 service 上要求它们彻底不存在),\n * `CrudService<infer TDto, infer TCreate, …>` 这类反解就没有真实属性可以落点。三个类型参数都收进\n * 这一个字段,反解才不受\"hook 有没有实传\"\"返回类型交没交叉 Capability\"的影响。 */\ninterface CrudServiceTypeWitness<TCreate, TUpdate, TListParams> {\n readonly [crudServiceCapabilities]?: {\n create: TCreate;\n update: TUpdate;\n listParams: TListParams;\n };\n}\n\n/** `CrudService` 展开后的形状,不是 `createCrudService` 的入参契约。别拿它标注描述符再传进去,\n * 那会关掉 `supportsFilter` 的约束(见 `FilterConfig`)。这里只含 `useList`,\n * 写/删 hook 由 `createCrudService` 的返回类型交叉 `CrudCapability` 钉回。 */\ntype CrudServiceDef<TDto extends { id?: string }, TCreate, TUpdate, TListParams> = Pick<\n CrudHooks<TDto, TCreate, TUpdate>,\n \"useList\"\n> &\n CrudServiceMeta &\n FilterConfig<ListParams> &\n CrudServiceTypeWitness<TCreate, TUpdate, TListParams>;\n\n/** 第 4 个类型参数是端点自有的查询参数类型,经 `CrudServiceTypeWitness` 在别名体内被引用;\n * `use-abp-table.ts` 里 `t.queryForm.AppField` 的字段名收窄靠它反解。\n *\n * 默认不含 useCreate/useUpdate/useDelete:只读 service 上这些成员不存在,而不是类型为 undefined。\n * `createCrudService` 按实传交叉回来,`WritableCrudService` 再把 create/update 钉成必填。 */\nexport type CrudService<\n TDto extends { id?: string },\n TCreate,\n TUpdate,\n TListParams extends Record<string, unknown> = Record<string, unknown>,\n> = CrudServiceDef<TDto, TCreate, TUpdate, TListParams> & {\n resolvedPolicies: { create?: string; update?: string; delete?: string };\n};\n\n/** 要求 service 可写。`CrudService` 本身不保证 useCreate/useUpdate 存在,\n * 需要写操作的调用方(如 `useAbpSheet`)用它约束入参。 */\nexport type WritableCrudService<\n TDto extends { id?: string },\n TCreate,\n TUpdate,\n TListParams extends Record<string, unknown> = Record<string, unknown>,\n> = CrudService<TDto, TCreate, TUpdate, TListParams> & CrudWriteHooks<TCreate, TUpdate>;\n\n/** 类型级严格相等。`CrudCapability` 拿它跟 `WildcardMutationHook` 比,\n * 普通的互相 `extends` 会把两个不同的具体签名判成相等。 */\ntype Equals<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;\n\n/** 三个可选 mutation hook 的类型参数缺省值。`options: never` 处在逆变位,任何真实 hook 都赋得进来。\n * 没用 `undefined` 当缺省:那会把\"没传这个 hook\"判成\"要求 hook 的类型是 undefined\",入参直接报错。\n * 代价是显式指定类型实参时会被误判成未传,本文件之外没有这种用法。 */\ntype WildcardMutationHook = (options?: never) => unknown;\n\n/** 可选 hook 的存在性投影:`TWitness` 等于缺省值就当没传,产出空对象(键彻底不存在,\n * 而不是类型为 undefined);否则产出一个必填键。`createCrudService` 的返回类型逐个交叉它。 */\ntype CrudCapability<TKey extends string, TWitness, TValue> =\n Equals<TWitness, WildcardMutationHook> extends true ? Record<never, never> : Record<TKey, TValue>;\n\n/** 服务描述符:绑定 orval 生成的 hook,按 ABP 命名约定派生权限策略(X → X.Create/.Update/.Delete)。\n * 纯数据,零执行。\n *\n * `useList` 写成 `TUseList &` 具体签名的交叉,三个 mutation hook 同样处理,且各自嵌在自己的属性里。\n * 这个形状别动。合成单个签名,TS 会在 orval 的重载源上放弃泛型推断、静默回退缺省值,\n * `supportsFilter` 的约束就永远走不到 false 分支;把裸类型参数提到顶层交叉,同级那些自带类型参数\n * 的函数属性也会推不出来。真改了编译器会响,只是报错位置离这里很远(TDto 塌成 `{ id?: string }`)。 */\nexport function createCrudService<\n TDto extends { id?: string },\n TCreate = unknown,\n TUpdate = TCreate,\n TUseList extends (params: never, ...rest: never[]) => unknown = (params: ListParams) => {\n data?: PagedResult<TDto>;\n isPending: boolean;\n isFetching: boolean;\n isError: boolean;\n },\n TUseCreate extends (options?: never) => unknown = WildcardMutationHook,\n TUseUpdate extends (options?: never) => unknown = WildcardMutationHook,\n TUseDelete extends (options?: never) => unknown = WildcardMutationHook,\n>(\n def: {\n useList: TUseList & CrudHooks<TDto, TCreate, TUpdate>[\"useList\"];\n } & Partial<{\n useCreate: TUseCreate & CrudWriteHooks<TCreate, TUpdate>[\"useCreate\"];\n useUpdate: TUseUpdate & CrudWriteHooks<TCreate, TUpdate>[\"useUpdate\"];\n useDelete: TUseDelete & CrudDeleteHook[\"useDelete\"];\n }> &\n FilterConfig<NonNullable<Parameters<TUseList>[0]>> &\n CrudServiceMeta,\n): CrudService<TDto, TCreate, TUpdate, NonNullable<Parameters<TUseList>[0]>> &\n CrudCapability<\"useCreate\", TUseCreate, CrudWriteHooks<TCreate, TUpdate>[\"useCreate\"]> &\n CrudCapability<\"useUpdate\", TUseUpdate, CrudWriteHooks<TCreate, TUpdate>[\"useUpdate\"]> &\n CrudCapability<\"useDelete\", TUseDelete, CrudDeleteHook[\"useDelete\"]> {\n const base = def.policy;\n return {\n ...def,\n resolvedPolicies: {\n create: def.policies?.create ?? (base ? `${base}.Create` : undefined),\n update: def.policies?.update ?? (base ? `${base}.Update` : undefined),\n delete: def.policies?.delete ?? (base ? `${base}.Delete` : undefined),\n },\n } as unknown as CrudService<TDto, TCreate, TUpdate, NonNullable<Parameters<TUseList>[0]>> &\n CrudCapability<\"useCreate\", TUseCreate, CrudWriteHooks<TCreate, TUpdate>[\"useCreate\"]> &\n CrudCapability<\"useUpdate\", TUseUpdate, CrudWriteHooks<TCreate, TUpdate>[\"useUpdate\"]> &\n CrudCapability<\"useDelete\", TUseDelete, CrudDeleteHook[\"useDelete\"]>;\n}\n",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "components/abp/crud/crud-service.ts"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "ui/blocks/abp-crud/abp-table-source.ts",
|
|
21
|
+
"content": "import type { PagedResult } from \"@jcoder-stack/abp-react/core\";\n\n/**\n * `useAbpTable` 的数据源契约。不绑定 `CrudService`,非 ABP 后端或测试替身满足这个形状就能接。\n *\n * `delete` 可选:只读 service 上这个键不存在,而不是值为 undefined。所以\n * `source.delete?.mutate(id)` 是安全的,不会得到一个点了没反应的删除按钮。\n *\n * `can` 要求 hook 存在且 ABP 策略通过,缺一就隐藏入口。\n */\nexport interface AbpTableSource<TDto> {\n listQuery: {\n data?: PagedResult<TDto>;\n isPending: boolean;\n isFetching: boolean;\n isError: boolean;\n /** 同参数重试出口。瞬时错误(网络抖动、500)下缓存键没变,点「查询」不会重发,只有它能恢复。\n * react-query 数据源天然有;自实现的可以不给,不给就不渲染重试按钮。 */\n refetch?: () => void;\n };\n pageCount: number;\n totalCount: number;\n delete?: {\n mutate: (id: string) => void;\n /** 整批删除:逐条删、汇总失败 id、结束后失效一次列表。别逐条 toast 或逐条失效,\n * 删 N 条会变成 N 个提示和 N 次重取;提示由 `t.BulkDelete` 按整批结果给一条。\n * 不提供则 `t.BulkDelete` 不渲染,DEV 下告警。 */\n many?: (ids: string[]) => Promise<{ failed: string[] }>;\n };\n can: { create: boolean; update: boolean; delete: boolean };\n supportsFilter: boolean;\n}\n",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "components/abp/crud/abp-table-source.ts"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "ui/blocks/abp-crud/create-bound-components.ts",
|
|
27
|
+
"content": "import { useRef } from \"react\";\n\n/** 组件身份跨渲染稳定 + 接线值读活:绑定成员的唯一产地。create 只在首渲染调用一次,\n * 闭包经 read() 拿当前接线值。StrictMode 双调用下 create 可能跑两次,须无副作用。 */\nexport function useBoundComponents<W, T>(wiring: W, create: (read: () => W) => T): T {\n const wiringRef = useRef(wiring);\n wiringRef.current = wiring;\n const componentsRef = useRef<T | null>(null);\n if (componentsRef.current === null) {\n const read = () => wiringRef.current;\n componentsRef.current = create(read);\n }\n return componentsRef.current;\n}\n",
|
|
28
|
+
"type": "registry:component",
|
|
29
|
+
"target": "components/abp/crud/create-bound-components.ts"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "ui/blocks/abp-crud/abp-form-errors.ts",
|
|
33
|
+
"content": "import { AbpApiError } from \"@/api/mutator\";\nimport {\n type FieldErrors,\n type ServerErrorMap,\n serverSubmitValidator,\n} from \"@/components/form/server-errors\";\n\ninterface AbpValidationErrorLike {\n message?: string | null;\n members?: string[] | null;\n}\n\n/** ABP 后端 member 名是 PascalCase(如 \"Name\"、\"Details.Email\"),前端表单字段是 camelCase;逐段转。 */\nfunction memberToFieldName(member: string): string {\n return member\n .split(\".\")\n .map((segment) => segment.charAt(0).toLowerCase() + segment.slice(1))\n .join(\".\");\n}\n\n/** 解包 AbpApiError.body.error → validationErrors 展开为通用 FieldErrors。 */\nexport function abpErrorToFieldErrors(error: unknown): FieldErrors {\n const source = (error instanceof AbpApiError ? error.body?.error : error) as {\n validationErrors?: AbpValidationErrorLike[] | null;\n message?: string;\n } | null;\n const validation = source?.validationErrors;\n if (!Array.isArray(validation) || validation.length === 0) {\n return source?.message ? [{ message: source.message }] : [];\n }\n const out: FieldErrors = [];\n for (const item of validation) {\n if (!item.message) continue;\n if (Array.isArray(item.members) && item.members.length > 0) {\n for (const member of item.members) {\n out.push({ field: memberToFieldName(member), message: item.message });\n }\n } else {\n out.push({ message: item.message });\n }\n }\n return out;\n}\n\n/** ABP 版 onSubmitAsync 校验器:serverSubmitValidator 预注入 ABP 错误映射。页面提交一律用它包 mutation。 */\nexport function abpSubmitValidator<TValue>(\n submit: (value: TValue) => Promise<void>,\n): (arg: { value: TValue }) => Promise<ServerErrorMap | null> {\n return serverSubmitValidator(submit, abpErrorToFieldErrors);\n}\n",
|
|
34
|
+
"type": "registry:component",
|
|
35
|
+
"target": "components/abp/crud/abp-form-errors.ts"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "ui/blocks/abp-crud/abp-form-options.ts",
|
|
39
|
+
"content": "import {\n type FormAsyncValidateOrFn,\n type FormValidateOrFn,\n revalidateLogic,\n} from \"@tanstack/react-form\";\nimport { abpSubmitValidator } from \"@/components/abp/crud/abp-form-errors\";\n\nexport interface AbpFormConfig<TValues> {\n defaultValues: TValues;\n /** 表单级 zod schema,落 validators.onDynamic;缺省则无客户端 schema 校验。 */\n schema?: FormValidateOrFn<TValues>;\n /** 业务提交;抛出的 ABP 错误自动落字段/表单级。 */\n submit: (value: TValues) => Promise<void>;\n /** 提交成功后回调。 */\n onSuccess?: () => void;\n /** 额外校验器逃生舱;onDynamic/onSubmitAsync 由预设独占,类型上不可传入。 */\n validators?: {\n onMount?: FormValidateOrFn<TValues>;\n onChange?: FormValidateOrFn<TValues>;\n onChangeAsync?: FormAsyncValidateOrFn<TValues>;\n onBlur?: FormValidateOrFn<TValues>;\n onBlurAsync?: FormAsyncValidateOrFn<TValues>;\n };\n}\n\n/** 用 schema 的 transform 输出替换原始表单值(如 zod `.trim()`)。绕开「校验不回写转换值」\n * (TanStack Form 官方 submission-handling:onSubmit 拿到的永远是 input data)。函数校验器、\n * 异步 schema、或产生 issue 时原样返回(onDynamic 已在提交前拦过校验)。`FormValidateOrFn` 的\n * Standard Schema 分支 Output 记为 unknown,故 value 需一次显式下转。 */\nfunction normalizeWithSchema<TValues>(\n schema: AbpFormConfig<TValues>[\"schema\"],\n value: TValues,\n): TValues {\n if (schema === undefined || typeof schema === \"function\") return value;\n const result = schema[\"~standard\"].validate(value);\n if (result instanceof Promise || \"issues\" in result) return value;\n return result.value as TValues;\n}\n\n/** ABP 表单标准姿势:必填三件套 validationLogic + 服务端错误通道,返回值直接交给 useAppForm。 */\nexport function abpFormOptions<TValues>(config: AbpFormConfig<TValues>) {\n const submit = (value: TValues) => config.submit(normalizeWithSchema(config.schema, value));\n return {\n defaultValues: config.defaultValues,\n validationLogic: revalidateLogic({ mode: \"submit\", modeAfterSubmission: \"change\" }),\n validators: {\n ...config.validators,\n onDynamic: config.schema,\n onSubmitAsync: abpSubmitValidator<TValues>(submit),\n },\n onSubmit: config.onSuccess,\n };\n}\n",
|
|
40
|
+
"type": "registry:component",
|
|
41
|
+
"target": "components/abp/crud/abp-form-options.ts"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"path": "ui/blocks/abp-crud/crud-messages.json",
|
|
45
|
+
"content": "{\n \"en\": {\n \"\": {\n \"Crud:Create\": \"Create\",\n \"Crud:EditTitle\": \"Edit\",\n \"Crud:ViewTitle\": \"Details\",\n \"Crud:Delete\": \"Delete\",\n \"Crud:DeleteConfirmTitle\": \"Confirm deletion\",\n \"Crud:DeleteConfirmBody\": \"This action cannot be undone.\",\n \"Crud:Saved\": \"Saved\",\n \"Crud:Deleted\": \"Deleted\",\n \"Crud:BulkDeletePartialFailure\": \"Deleted {0}, {1} failed\",\n \"Crud:OperationFailed\": \"Operation failed\",\n \"Crud:RangeFrom\": \"{0} from\",\n \"Crud:RangeTo\": \"{0} to\",\n \"Crud:Retry\": \"Retry\"\n }\n },\n \"zh-Hans\": {\n \"\": {\n \"Crud:Create\": \"新增\",\n \"Crud:EditTitle\": \"编辑\",\n \"Crud:ViewTitle\": \"详情\",\n \"Crud:Delete\": \"删除\",\n \"Crud:DeleteConfirmTitle\": \"确认删除\",\n \"Crud:DeleteConfirmBody\": \"此操作不可撤销。\",\n \"Crud:Saved\": \"已保存\",\n \"Crud:Deleted\": \"已删除\",\n \"Crud:BulkDeletePartialFailure\": \"已删除 {0} 项,{1} 项失败\",\n \"Crud:OperationFailed\": \"操作失败\",\n \"Crud:RangeFrom\": \"{0}起\",\n \"Crud:RangeTo\": \"{0}止\",\n \"Crud:Retry\": \"重试\"\n }\n }\n}\n",
|
|
46
|
+
"type": "registry:file",
|
|
47
|
+
"target": "components/abp/crud/crud-messages.json"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"type": "registry:block"
|
|
51
|
+
}
|