@liujitcn/kratos-admin-core 0.0.62 → 0.0.64

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.
Files changed (37) hide show
  1. package/README.md +2 -0
  2. package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +1 -1
  3. package/dist/package/declarations/src/components/YamlEditor/index.vue.d.ts +23 -0
  4. package/dist/package/declarations/src/locales/generated.d.ts +60 -0
  5. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +20 -0
  6. package/dist/package/declarations/src/rpc/google/protobuf/struct.d.ts +81 -0
  7. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +4 -23
  8. package/dist/package/declarations/src/runtime-messages.d.ts +2 -0
  9. package/dist/package/declarations/src/utils/conflict-message.d.ts +7 -0
  10. package/dist/package/declarations/src/utils/oauthProvider.d.ts +4 -0
  11. package/dist/package/declarations/src/utils/request-response.d.ts +9 -0
  12. package/dist/package/src/api/base/v1/file.ts +1 -1
  13. package/dist/package/src/components/YamlEditor/index.vue +107 -0
  14. package/dist/package/src/directives/modules/copy.ts +2 -1
  15. package/dist/package/src/hooks/useTable.ts +1 -1
  16. package/dist/package/src/locales/en-US.json +16 -1
  17. package/dist/package/src/locales/index.ts +3 -2
  18. package/dist/package/src/locales/ja-JP.json +16 -1
  19. package/dist/package/src/locales/zh-CN.json +16 -1
  20. package/dist/package/src/locales/zh-TW.json +16 -1
  21. package/dist/package/src/modules/index.ts +6 -5
  22. package/dist/package/src/routers/index.ts +1 -1
  23. package/dist/package/src/routers/modules/dynamicRouter.ts +1 -1
  24. package/dist/package/src/routers/modules/staticRouter.ts +2 -1
  25. package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
  26. package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
  27. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
  28. package/dist/package/src/runtime-messages.ts +39 -0
  29. package/dist/package/src/stores/modules/auth.ts +9 -4
  30. package/dist/package/src/utils/conflict-message.ts +79 -0
  31. package/dist/package/src/utils/oauthProvider.ts +9 -1
  32. package/dist/package/src/utils/request-response.ts +14 -0
  33. package/dist/package/src/utils/request.ts +10 -6
  34. package/dist/package/types/generated/components.d.ts +1 -0
  35. package/package.json +8 -1
  36. package/types/generated/components.d.ts +1 -0
  37. package/vite.config.ts +3 -4
@@ -58,6 +58,16 @@
58
58
  "common.message.request_error": "请求失败",
59
59
  "common.message.save_success": "保存成功",
60
60
  "common.message.system_error": "系统出错",
61
+ "common.error.conflict": "资源状态冲突",
62
+ "common.error.conflict.unique_location": "冲突位置:{resource};字段:{fields}",
63
+ "common.error.conflict.resource": "涉及资源:{resource}",
64
+ "common.error.conflict.operation": "接口路径:{operation}",
65
+ "common.error.conflict.related_resource": "关联位置:{parent} → {child}",
66
+ "common.error.conflict.unknown_field": "未标明字段",
67
+ "common.error.conflict.field_separator": "、",
68
+ "common.error.conflict.detail_separator": ";",
69
+ "common.error.conflict.with_details": "{message}({details})",
70
+ "common.error.rate_limited": "请求过于频繁,请稍后重试",
61
71
  "common.validation.phone_invalid": "请输入正确的手机号码",
62
72
  "common.validation.phone_required": "请输入手机号码",
63
73
  "common.title.notice": "提示",
@@ -68,6 +78,8 @@
68
78
  "common.placeholder.select": "请选择",
69
79
  "common.status.disabled": "禁用",
70
80
  "common.status.enabled": "启用",
81
+ "common.status.configured": "已配置",
82
+ "common.status.not_configured": "未配置",
71
83
  "common.value.female": "女",
72
84
  "common.value.male": "男",
73
85
  "common.message.unknown": "未知",
@@ -370,6 +382,9 @@
370
382
  "common.resource.default": "{resource}",
371
383
  "common.action.delete_resource": "删除{resource}",
372
384
  "common.action.set_field_status": "设置{field}",
385
+ "common.field.tenant_id": "租户 ID",
386
+ "common.field.user_id": "用户 ID",
373
387
  "core.home.demo_scope": "功能概览",
374
- "core.home.main_title": "管理应用"
388
+ "core.home.main_title": "管理应用",
389
+ "core.router.pending_view_unregistered": "未注册管理端静态页面:{view}"
375
390
  }
@@ -58,6 +58,16 @@
58
58
  "common.message.request_error": "請求失敗",
59
59
  "common.message.save_success": "儲存成功",
60
60
  "common.message.system_error": "系統出錯",
61
+ "common.error.conflict": "資源狀態衝突",
62
+ "common.error.conflict.unique_location": "衝突位置:{resource};欄位:{fields}",
63
+ "common.error.conflict.resource": "涉及資源:{resource}",
64
+ "common.error.conflict.operation": "API 路徑:{operation}",
65
+ "common.error.conflict.related_resource": "關聯位置:{parent} → {child}",
66
+ "common.error.conflict.unknown_field": "未標明欄位",
67
+ "common.error.conflict.field_separator": "、",
68
+ "common.error.conflict.detail_separator": ";",
69
+ "common.error.conflict.with_details": "{message}({details})",
70
+ "common.error.rate_limited": "請求過於頻繁,請稍後再試。",
61
71
  "common.validation.phone_invalid": "請輸入正確的手機號碼",
62
72
  "common.validation.phone_required": "請輸入手機號碼",
63
73
  "common.title.notice": "提示",
@@ -68,6 +78,8 @@
68
78
  "common.placeholder.select": "請選擇",
69
79
  "common.status.disabled": "禁用",
70
80
  "common.status.enabled": "啟用",
81
+ "common.status.configured": "已設定",
82
+ "common.status.not_configured": "未設定",
71
83
  "common.value.female": "女",
72
84
  "common.value.male": "男",
73
85
  "common.message.unknown": "未知",
@@ -370,6 +382,9 @@
370
382
  "common.resource.default": "{resource}",
371
383
  "common.action.delete_resource": "刪除{resource}",
372
384
  "common.action.set_field_status": "設定{field}",
385
+ "common.field.tenant_id": "租戶 ID",
386
+ "common.field.user_id": "使用者 ID",
373
387
  "core.home.demo_scope": "功能概覽",
374
- "core.home.main_title": "管理應用程式"
388
+ "core.home.main_title": "管理應用程式",
389
+ "core.router.pending_view_unregistered": "未註冊管理端靜態頁面:{view}"
375
390
  }
@@ -1,6 +1,7 @@
1
1
  import type { Component } from "vue";
2
2
  import type { RouteLocationNormalizedLoaded } from "vue-router";
3
3
  import type { LocaleMessages, SupportedLocale } from "../locales";
4
+ import { runtimeMessage } from "../runtime-messages.js";
4
5
 
5
6
  /**
6
7
  * 管理端视图加载器。
@@ -100,10 +101,10 @@ export function registerAdminModule(module: AdminModule): void {
100
101
  */
101
102
  export function registerAdminModules(modules: AdminModule[]): void {
102
103
  const nextModules = [...registeredModules.values(), ...modules];
103
- assertUniqueAdminModuleKeys(nextModules, "模块", module => [module.name]);
104
- assertUniqueAdminModuleKeys(nextModules, "顶部工具", module => module.headerTools?.map(tool => tool.name) ?? []);
105
- assertUniqueAdminModuleKeys(nextModules, "用户菜单", module => module.userMenuActions?.map(action => action.name) ?? []);
106
- assertUniqueAdminModuleKeys(nextModules, "路由配置", module => Object.keys(module.routeOptions ?? {}));
104
+ assertUniqueAdminModuleKeys(nextModules, runtimeMessage("label.module"), module => [module.name]);
105
+ assertUniqueAdminModuleKeys(nextModules, runtimeMessage("label.header_tool"), module => module.headerTools?.map(tool => tool.name) ?? []);
106
+ assertUniqueAdminModuleKeys(nextModules, runtimeMessage("label.user_menu"), module => module.userMenuActions?.map(action => action.name) ?? []);
107
+ assertUniqueAdminModuleKeys(nextModules, runtimeMessage("label.route_option"), module => Object.keys(module.routeOptions ?? {}));
107
108
  modules.forEach(module => registeredModules.set(module.name, module));
108
109
  }
109
110
 
@@ -228,7 +229,7 @@ function assertUniqueAdminModuleKeys(modules: AdminModule[], label: string, getK
228
229
  getKeys(module).forEach(key => {
229
230
  const owner = owners.get(key);
230
231
  if (owner) {
231
- throw new Error(`管理端${label}名称重复: ${key}(模块 ${owner} 与 ${module.name})`);
232
+ throw new Error(runtimeMessage("module_duplicate", { label, key, owner, module: module.name }));
232
233
  }
233
234
  owners.set(key, module.name);
234
235
  });
@@ -140,7 +140,7 @@ function getFirstAccessibleRoutePath() {
140
140
  * */
141
141
  router.onError(error => {
142
142
  NProgress.done();
143
- console.warn("路由错误", error.message);
143
+ console.warn("Navigation failed:", error.message);
144
144
  });
145
145
 
146
146
  /**
@@ -89,7 +89,7 @@ function resolveRouteComponent(component?: string) {
89
89
  if (viewLoader) return viewLoader;
90
90
 
91
91
  const pendingViewLoader = viewRegistry.resolve(ADMIN_STATIC_VIEWS.PENDING);
92
- if (!pendingViewLoader) throw new Error(`未注册管理端静态页面:${ADMIN_STATIC_VIEWS.PENDING}`);
92
+ if (!pendingViewLoader) throw new Error(t("core.router.pending_view_unregistered", { view: ADMIN_STATIC_VIEWS.PENDING }));
93
93
  return pendingViewLoader;
94
94
  }
95
95
 
@@ -1,12 +1,13 @@
1
1
  import { RouteRecordRaw } from "vue-router";
2
2
  import { HOME_URL, LOGIN_URL } from "../../config";
3
3
  import { ADMIN_STATIC_VIEWS, getAdminViewRegistry } from "../../modules";
4
+ import { t } from "../../locales";
4
5
 
5
6
  /** 创建通过视图注册表解析的静态页面加载器。 */
6
7
  function createAdminStaticViewLoader(viewPath: string) {
7
8
  return () => {
8
9
  const viewLoader = getAdminViewRegistry().resolve(viewPath);
9
- if (!viewLoader) throw new Error(`未注册管理端静态页面:${viewPath}`);
10
+ if (!viewLoader) throw new Error(t("core.router.pending_view_unregistered", { view: viewPath }));
10
11
  return viewLoader();
11
12
  };
12
13
  }
@@ -191,14 +191,30 @@ export interface UnbindOauthAccountRequest {
191
191
  export interface OauthProvider {
192
192
  /** 登录方式标识 */
193
193
  provider: string;
194
+ /** 登录方式名称 */
195
+ name: string;
196
+ /** 登录方式提示语 */
197
+ description: string;
198
+ /** 图标键或图片地址 */
199
+ icon: string;
200
+ /** Provider个性化配置JSON对象 */
201
+ config: { [key: string]: any } | undefined;
194
202
  }
195
203
 
196
204
  /** 个人中心三方账号绑定信息 */
197
205
  export interface OauthBinding {
198
206
  /** 登录方式标识 */
199
207
  provider: string;
208
+ /** 登录方式名称 */
209
+ name: string;
210
+ /** 登录方式提示语 */
211
+ description: string;
200
212
  /** 是否已绑定 */
201
213
  bound: boolean;
214
+ /** 图标键或图片地址 */
215
+ icon: string;
216
+ /** Provider个性化配置JSON对象 */
217
+ config: { [key: string]: any } | undefined;
202
218
  }
203
219
 
204
220
  /** Base三方登录公共服务 */
@@ -0,0 +1,80 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.8
4
+ // protoc unknown
5
+ // source: google/protobuf/struct.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /**
10
+ * `NullValue` is a singleton enumeration to represent the null value for the
11
+ * `Value` type union.
12
+ *
13
+ * The JSON representation for `NullValue` is JSON `null`.
14
+ */
15
+ export enum NullValue {
16
+ /** NULL_VALUE - Null value. */
17
+ NULL_VALUE = 0,
18
+ }
19
+
20
+ /**
21
+ * `Struct` represents a structured data value, consisting of fields
22
+ * which map to dynamically typed values. In some languages, `Struct`
23
+ * might be supported by a native representation. For example, in
24
+ * scripting languages like JS a struct is represented as an
25
+ * object. The details of that representation are described together
26
+ * with the proto support for the language.
27
+ *
28
+ * The JSON representation for `Struct` is JSON object.
29
+ */
30
+ export interface Struct {
31
+ /** Unordered map of dynamically typed values. */
32
+ fields: Map<string, any | undefined>;
33
+ }
34
+
35
+ export interface Struct_FieldsEntry {
36
+ key: string;
37
+ value: any | undefined;
38
+ }
39
+
40
+ /**
41
+ * `Value` represents a dynamically typed value which can be either
42
+ * null, a number, a string, a boolean, a recursive struct value, or a
43
+ * list of values. A producer of value is expected to set one of these
44
+ * variants. Absence of any variant indicates an error.
45
+ *
46
+ * The JSON representation for `Value` is JSON value.
47
+ */
48
+ export interface Value {
49
+ /** The kind of value. */
50
+ kind:
51
+ | //
52
+ /** Represents a null value. */
53
+ { $case: "null_value"; null_value: NullValue }
54
+ | //
55
+ /** Represents a double value. */
56
+ { $case: "number_value"; number_value: number }
57
+ | //
58
+ /** Represents a string value. */
59
+ { $case: "string_value"; string_value: string }
60
+ | //
61
+ /** Represents a boolean value. */
62
+ { $case: "bool_value"; bool_value: boolean }
63
+ | //
64
+ /** Represents a structured value. */
65
+ { $case: "struct_value"; struct_value: { [key: string]: any } | undefined }
66
+ | //
67
+ /** Represents a repeated `Value`. */
68
+ { $case: "list_value"; list_value: Array<any> | undefined }
69
+ | undefined;
70
+ }
71
+
72
+ /**
73
+ * `ListValue` is a wrapper around a repeated field of values.
74
+ *
75
+ * The JSON representation for `ListValue` is JSON array.
76
+ */
77
+ export interface ListValue {
78
+ /** Repeated field of dynamically typed values. */
79
+ values: any[];
80
+ }
@@ -7,26 +7,6 @@
7
7
  /* eslint-disable */
8
8
  import type { Empty } from "../../../google/protobuf/empty";
9
9
 
10
- /** 统一翻译表目标类型,和 base_i18n.target_type 的值一一对应。 */
11
- export enum I18nTargetType {
12
- /** I18N_TARGET_TYPE_UNSPECIFIED - 未指定统一翻译表目标类型。 */
13
- I18N_TARGET_TYPE_UNSPECIFIED = 0,
14
- /** I18N_TARGET_TYPE_BASE_CONFIG_VALUE - 系统配置值。 */
15
- I18N_TARGET_TYPE_BASE_CONFIG_VALUE = 1,
16
- /** I18N_TARGET_TYPE_BASE_CONFIG_NAME - 系统配置名称。 */
17
- I18N_TARGET_TYPE_BASE_CONFIG_NAME = 2,
18
- /** I18N_TARGET_TYPE_BASE_DICT_NAME - 字典名称。 */
19
- I18N_TARGET_TYPE_BASE_DICT_NAME = 3,
20
- /** I18N_TARGET_TYPE_BASE_DICT_ITEM_LABEL - 字典项标签。 */
21
- I18N_TARGET_TYPE_BASE_DICT_ITEM_LABEL = 4,
22
- /** I18N_TARGET_TYPE_BASE_MENU_META_TITLE - 菜单标题。 */
23
- I18N_TARGET_TYPE_BASE_MENU_META_TITLE = 5,
24
- /** I18N_TARGET_TYPE_BASE_JOB_NAME - 定时任务名称。 */
25
- I18N_TARGET_TYPE_BASE_JOB_NAME = 6,
26
- /** I18N_TARGET_TYPE_BASE_MIGRATION_DESCRIPTION - 数据库迁移说明。 */
27
- I18N_TARGET_TYPE_BASE_MIGRATION_DESCRIPTION = 7,
28
- }
29
-
30
10
  /** 翻译单个文本请求。 */
31
11
  export interface DraftBaseI18nRequest {
32
12
  /** 待翻译文本 */
@@ -53,8 +33,8 @@ export interface DraftBaseI18nItem {
53
33
  export interface UpdateBaseI18nRequest {
54
34
  /** 翻译记录ID,大于零时优先按ID更新;ID不存在时根据目标信息更新或新增 */
55
35
  id: number;
56
- /** 翻译目标类型,ID为零时用于查询或新增 */
57
- target_type: I18nTargetType;
36
+ /** 翻译目标键,格式为表名.字段名 */
37
+ target_key: string;
58
38
  /** 目标资源ID,ID为零时用于查询或新增 */
59
39
  target_id: number;
60
40
  /** 目标语言区域,ID为零时用于查询或新增 */
@@ -67,8 +47,8 @@ export interface UpdateBaseI18nRequest {
67
47
  export interface BaseI18n {
68
48
  /** 翻译记录ID */
69
49
  id: number;
70
- /** 翻译目标类型 */
71
- target_type: I18nTargetType;
50
+ /** 翻译目标键,格式为表名.字段名 */
51
+ target_key: string;
72
52
  /** 目标资源ID */
73
53
  target_id: number;
74
54
  /** 语言区域 */
@@ -0,0 +1,39 @@
1
+ type RuntimeLocale = "zh-CN" | "en-US";
2
+
3
+ const messages: Record<RuntimeLocale, Record<string, string>> = {
4
+ "zh-CN": {
5
+ missing_source_entry: "管理端模块缺少源码入口:{root}",
6
+ https_certificate_missing: "VITE_HTTPS 已开启,但未找到证书文件,请先在仓库根目录运行 scripts/generate-dev-cert.sh;期望路径:{keyPath} 和 {certPath}",
7
+ "label.module": "模块",
8
+ "label.header_tool": "顶部工具",
9
+ "label.user_menu": "用户菜单",
10
+ "label.route_option": "路由配置",
11
+ module_duplicate: "管理端{label}名称重复: {key}(模块 {owner} 与 {module})",
12
+ locale_duplicate: "{locale} 语言键重复: {key}",
13
+ locale_namespace_invalid: "{module} 的语言键命名空间无效: {key}"
14
+ },
15
+ "en-US": {
16
+ missing_source_entry: "The admin module is missing a source entry: {root}",
17
+ https_certificate_missing: "VITE_HTTPS is enabled, but certificate files were not found. Run scripts/generate-dev-cert.sh from the repository root; expected paths: {keyPath} and {certPath}",
18
+ "label.module": "module",
19
+ "label.header_tool": "header tool",
20
+ "label.user_menu": "user menu",
21
+ "label.route_option": "route configuration",
22
+ module_duplicate: "Duplicate admin {label} name: {key} (modules {owner} and {module})",
23
+ locale_duplicate: "Duplicate locale key in {locale}: {key}",
24
+ locale_namespace_invalid: "Invalid locale key namespace for {module}: {key}"
25
+ }
26
+ };
27
+
28
+ /** 根据环境变量选择管理端启动期错误语言。 */
29
+ function resolveRuntimeLocale(): RuntimeLocale {
30
+ const value = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;
31
+ const locale = value?.KRATOS_ADMIN_LOCALE ?? value?.LC_ALL ?? value?.LANG ?? "";
32
+ return locale.toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
33
+ }
34
+
35
+ /** 渲染管理端无框架运行时错误。 */
36
+ export function runtimeMessage(key: string, params: Record<string, string> = {}): string {
37
+ const template = messages[resolveRuntimeLocale()][key] ?? messages["en-US"][key] ?? key;
38
+ return template.replace(/\{([A-Za-z0-9_]+)\}/g, (_, name: string) => params[name] ?? `{${name}}`);
39
+ }
@@ -1,5 +1,6 @@
1
1
  import { defineStore } from "pinia";
2
2
  import { defAuthService } from "../../api/system/admin/v1/auth";
3
+ import { useConfigStore } from "../../stores/modules/config";
3
4
  import { AuthState } from "../../stores/interface";
4
5
  import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList, isExternalPath } from "../../utils";
5
6
  import type { RouteItem } from "../../rpc/system/admin/v1/auth";
@@ -19,14 +20,18 @@ function normalizeRoutePath(path?: string, parentPath = "") {
19
20
  return `/${pathSegments.join("/")}`;
20
21
  }
21
22
 
22
- /** 递归规范化菜单树,避免菜单点击时发生相对路径拼接。 */
23
- function normalizeRouteTree(menuList: RouteItem[], parentPath = ""): RouteItem[] {
23
+ /** 递归规范化菜单树,并根据 AI 模型可用状态控制会话菜单显隐。 */
24
+ function normalizeRouteTree(menuList: RouteItem[], parentPath = "", aiEnabled = false): RouteItem[] {
24
25
  return menuList.map(item => {
25
26
  const currentPath = normalizeRoutePath(item.path, parentPath);
26
27
  return {
27
28
  ...item,
29
+ meta:
30
+ item.name === "AiChat"
31
+ ? { ...item.meta, params: item.meta?.params ?? [], hidden: !aiEnabled }
32
+ : item.meta,
28
33
  path: currentPath,
29
- children: normalizeRouteTree(item.children ?? [], currentPath)
34
+ children: normalizeRouteTree(item.children ?? [], currentPath, aiEnabled)
30
35
  };
31
36
  });
32
37
  }
@@ -63,7 +68,7 @@ export const useAuthStore = defineStore("admin-auth", {
63
68
  /** 获取菜单权限列表 */
64
69
  async getAuthMenuList() {
65
70
  const data = await defAuthService.TreeUserMenu({});
66
- this.authMenuList = normalizeRouteTree(data.routes ?? []);
71
+ this.authMenuList = normalizeRouteTree(data.routes ?? [], "", useConfigStore().aiEnabled);
67
72
  },
68
73
  /** 设置当前路由名称 */
69
74
  async setRouteName(name: string) {
@@ -0,0 +1,79 @@
1
+ type ConflictMessageTranslator = (key: string, params?: Record<string, string>) => string;
2
+
3
+ /** 格式化结构化冲突错误,优先显示资源和字段,缺少业务元数据时回退到接口路径。 */
4
+ export function formatConflictMessage(
5
+ message: string,
6
+ error: { reason?: string; metadata?: Record<string, string> } | undefined,
7
+ translate: ConflictMessageTranslator
8
+ ): string {
9
+ if (error?.reason !== "CONFLICT") return message;
10
+
11
+ const metadata = error.metadata ?? {};
12
+ const resource = metadata.resource;
13
+ const childResource = metadata.child_resource;
14
+ const conflictType = metadata.conflict_type;
15
+ const details: string[] = [];
16
+ const fields = resource
17
+ ? (metadata.field ?? "")
18
+ .split(",")
19
+ .map(field => field.trim())
20
+ .filter(Boolean)
21
+ .map(field => conflictFieldLabel(resource, field, translate))
22
+ : [];
23
+
24
+ if (resource && childResource && conflictType === "has_children") {
25
+ details.push(
26
+ translate("common.error.conflict.related_resource", {
27
+ parent: conflictResourceLabel(resource, translate),
28
+ child: conflictResourceLabel(childResource, translate)
29
+ })
30
+ );
31
+ } else if (resource && conflictType === "unique_violation") {
32
+ details.push(
33
+ translate("common.error.conflict.unique_location", {
34
+ resource: conflictResourceLabel(resource, translate),
35
+ fields: fields.join(translate("common.error.conflict.field_separator")) || translate("common.error.conflict.unknown_field")
36
+ })
37
+ );
38
+ } else if (resource) {
39
+ details.push(
40
+ fields.length
41
+ ? translate("common.error.conflict.unique_location", {
42
+ resource: conflictResourceLabel(resource, translate),
43
+ fields: fields.join(translate("common.error.conflict.field_separator"))
44
+ })
45
+ : translate("common.error.conflict.resource", { resource: conflictResourceLabel(resource, translate) })
46
+ );
47
+ } else if (metadata.operation) {
48
+ details.push(translate("common.error.conflict.operation", { operation: metadata.operation }));
49
+ }
50
+
51
+ if (!details.length) return message;
52
+
53
+ if (!message || metadata.message_key === "common.error.conflict" || message === translate("common.error.conflict")) {
54
+ return details.join(translate("common.error.conflict.detail_separator"));
55
+ }
56
+
57
+ return translate("common.error.conflict.with_details", {
58
+ message,
59
+ details: details.join(translate("common.error.conflict.detail_separator"))
60
+ });
61
+ }
62
+
63
+ function conflictResourceLabel(resource: string, translate: ConflictMessageTranslator): string {
64
+ const name = resource.startsWith("base_") ? resource.slice("base_".length) : resource;
65
+ return translatedIdentifier([`system.base.${name}.resource`, `system.base.${name}.title`], resource, translate);
66
+ }
67
+
68
+ function conflictFieldLabel(resource: string, field: string, translate: ConflictMessageTranslator): string {
69
+ const name = resource.startsWith("base_") ? resource.slice("base_".length) : resource;
70
+ return translatedIdentifier([`system.base.${name}.field.${field}`, `common.field.${field}`], field, translate);
71
+ }
72
+
73
+ function translatedIdentifier(keys: string[], fallback: string, translate: ConflictMessageTranslator): string {
74
+ for (const key of keys) {
75
+ const value = translate(key);
76
+ if (value && value !== key) return value;
77
+ }
78
+ return fallback;
79
+ }
@@ -22,6 +22,10 @@ export interface OauthProviderDisplay {
22
22
  export interface OauthProviderSource {
23
23
  /** 登录方式标识。 */
24
24
  provider: string;
25
+ /** 接口返回的动态名称。 */
26
+ name?: string;
27
+ /** 接口返回的动态图标键或图片地址。 */
28
+ icon?: string;
25
29
  }
26
30
 
27
31
  const oauthProviderDisplayMap: Record<string, Omit<OauthProviderDisplay, "name">> = {
@@ -72,13 +76,17 @@ function createOauthFallbackIcon(provider: OauthProviderSource & OauthProviderDi
72
76
 
73
77
  /** 获取三方登录真实品牌图标组件。 */
74
78
  export function getOauthProviderIcon(provider: OauthProviderSource & OauthProviderDisplay): Component {
79
+ if (/^(https?:\/\/|\/)/.test(provider.icon)) return createOauthImageIcon(provider.icon, provider.provider);
75
80
  return oauthIconMap[provider.icon] || oauthIconMap[provider.provider] || createOauthFallbackIcon(provider);
76
81
  }
77
82
 
78
83
  /** 为接口返回的三方登录数据补齐前端展示信息。 */
79
84
  export function withOauthProviderDisplay<T extends OauthProviderSource>(item: T): T & OauthProviderDisplay {
85
+ const fallback = getOauthProviderDisplay(item.provider);
80
86
  return {
81
87
  ...item,
82
- ...getOauthProviderDisplay(item.provider)
88
+ name: item.name || fallback.name,
89
+ nameKey: fallback.nameKey,
90
+ icon: item.icon || fallback.icon
83
91
  };
84
92
  }
@@ -0,0 +1,14 @@
1
+ /** 判断响应是否包含结构化错误码。 */
2
+ export function hasStructuredErrorCode<T extends object>(value: T): value is T & { code: number } {
3
+ return typeof value === "object" && !Array.isArray(value) && "code" in value && typeof value.code === "number";
4
+ }
5
+
6
+ /** 返回结构化错误提示,并为缺少 message 的限流响应提供本地化回退。 */
7
+ export function resolveStructuredErrorMessage(
8
+ value: { message?: string; reason?: string },
9
+ translate: (key: string) => string
10
+ ): string {
11
+ if (value.message) return value.message;
12
+ if (value.reason === "RATE_LIMITED") return translate("common.error.rate_limited");
13
+ return "";
14
+ }
@@ -6,6 +6,8 @@ import { LOGIN_URL } from "../config";
6
6
  import pinia from "../stores";
7
7
  import { useUserStore } from "../stores/modules/user";
8
8
  import { getLocaleRequestHeaders, t } from "../locales";
9
+ import { formatConflictMessage } from "./conflict-message";
10
+ import { hasStructuredErrorCode, resolveStructuredErrorMessage } from "./request-response";
9
11
 
10
12
  const apiBasePath = import.meta.env.VITE_APP_BASE_API || "";
11
13
  const apiTargetUrl = import.meta.env.VITE_API_URL || import.meta.env.VITE_APP_API_URL || "";
@@ -340,12 +342,12 @@ service.interceptors.response.use(
340
342
  }
341
343
 
342
344
  const responseData = response.data as ErrorResponseData;
343
- const { code, message } = responseData;
344
- // 成功响应不携带错误码;只要存在 code 即视为结构化错误响应。
345
- if (code === undefined) {
345
+ if (!hasStructuredErrorCode(responseData)) {
346
346
  return response.data;
347
347
  }
348
348
 
349
+ const message = formatConflictMessage(resolveStructuredErrorMessage(responseData, key => t(key)), responseData, t);
350
+
349
351
  if (isPasswordChangeRequired(responseData)) {
350
352
  handlePasswordChangeRequired(message || t("common.message.system_error"));
351
353
  } else {
@@ -361,7 +363,9 @@ service.interceptors.response.use(
361
363
  const status = error.response?.status;
362
364
  const data = error.response?.data as ErrorResponseData | undefined;
363
365
  const code = data?.code;
364
- const message = data?.message;
366
+ const message = data
367
+ ? formatConflictMessage(resolveStructuredErrorMessage(data, key => t(key)), data, t)
368
+ : undefined;
365
369
  const requestConfig = error.config as RetryableRequestConfig | undefined;
366
370
 
367
371
  // 退出或认证失效后,页面中的在途请求返回 401 属于预期结果,不再重复刷新令牌、弹窗或提示错误。
@@ -380,7 +384,7 @@ service.interceptors.response.use(
380
384
  requestConfig.headers.Authorization = getUserStore().token.trim();
381
385
  return service(requestConfig);
382
386
  } catch (refreshError) {
383
- console.log("token 刷新失败", refreshError);
387
+ console.log("Token refresh failed:", refreshError);
384
388
  }
385
389
  }
386
390
  handleAuthExpired();
@@ -432,7 +436,7 @@ async function handleTokenRefresh(promptOnFailure = true) {
432
436
  getUserStore().clearAuthData();
433
437
  }
434
438
  if (promptOnFailure) {
435
- console.log("token 刷新失败", error);
439
+ console.log("Token refresh failed:", error);
436
440
  handleAuthExpired();
437
441
  }
438
442
  throw error;
@@ -19,6 +19,7 @@ declare module 'vue' {
19
19
  ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
20
20
  ElButton: typeof import('element-plus/es')['ElButton']
21
21
  ElCard: typeof import('element-plus/es')['ElCard']
22
+ ElCascader: typeof import('element-plus/es')['ElCascader']
22
23
  ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
23
24
  ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
24
25
  ElCol: typeof import('element-plus/es')['ElCol']
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-core",
3
3
  "private": false,
4
- "version": "0.0.62",
4
+ "version": "0.0.64",
5
5
  "type": "module",
6
6
  "description": "kratos-admin frontend runtime and base modules",
7
7
  "author": {
@@ -19,6 +19,8 @@
19
19
  "url": "https://github.com/liujitcn/kratos-admin/issues"
20
20
  },
21
21
  "dependencies": {
22
+ "@codemirror/lang-yaml": "^6.1.3",
23
+ "@codemirror/view": "^6.43.10",
22
24
  "@element-plus/icons-vue": "^2.3.1",
23
25
  "@lucide/vue": "^1.28.0",
24
26
  "@vitejs/plugin-vue": "^6.0.7",
@@ -29,6 +31,7 @@
29
31
  "asmcrypto.js": "2.3.2",
30
32
  "axios": "^1.7.2",
31
33
  "code-inspector-plugin": "^1.5.1",
34
+ "codemirror": "^6.0.2",
32
35
  "dayjs": "^1.11.21",
33
36
  "dompurify": "^3.4.5",
34
37
  "driver.js": "^1.3.1",
@@ -137,6 +140,10 @@
137
140
  "types": "./dist/package/declarations/src/components/PasswordStrength/index.vue.d.ts",
138
141
  "default": "./dist/package/src/components/PasswordStrength/index.vue"
139
142
  },
143
+ "./components/YamlEditor/index.vue": {
144
+ "types": "./dist/package/declarations/src/components/YamlEditor/index.vue.d.ts",
145
+ "default": "./dist/package/src/components/YamlEditor/index.vue"
146
+ },
140
147
  "./components/ProForm/index.vue": {
141
148
  "types": "./dist/package/declarations/src/components/ProForm/index.vue.d.ts",
142
149
  "default": "./dist/package/src/components/ProForm/index.vue"
@@ -19,6 +19,7 @@ declare module 'vue' {
19
19
  ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
20
20
  ElButton: typeof import('element-plus/es')['ElButton']
21
21
  ElCard: typeof import('element-plus/es')['ElCard']
22
+ ElCascader: typeof import('element-plus/es')['ElCascader']
22
23
  ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
23
24
  ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
24
25
  ElCol: typeof import('element-plus/es')['ElCol']
package/vite.config.ts CHANGED
@@ -9,6 +9,7 @@ import { wrapperEnv } from "./build/getEnv";
9
9
  import { createProxy } from "./build/proxy";
10
10
  import { createVitePlugins } from "./build/plugins";
11
11
  import { createSourcePatterns } from "./build/source-patterns";
12
+ import { runtimeMessage } from "./src/runtime-messages";
12
13
 
13
14
  const coreRoot = dirname(fileURLToPath(import.meta.url));
14
15
  const viteLogger = createLogger();
@@ -199,9 +200,7 @@ function resolveHttpsOptions(viteEnv: ViteEnv, root: string): ServerOptions | un
199
200
  const keyPath = resolve(root, viteEnv.VITE_HTTPS_KEY || "../../../../certs/dev-key.pem");
200
201
  const certPath = resolve(root, viteEnv.VITE_HTTPS_CERT || "../../../../certs/dev-cert.pem");
201
202
  if (!existsSync(keyPath) || !existsSync(certPath)) {
202
- throw new Error(
203
- `VITE_HTTPS 已开启,但未找到证书文件,请先在仓库根目录运行 scripts/generate-dev-cert.sh;期望路径:${keyPath} 和 ${certPath}`
204
- );
203
+ throw new Error(runtimeMessage("https_certificate_missing", { keyPath, certPath }));
205
204
  }
206
205
 
207
206
  return {
@@ -225,7 +224,7 @@ function resolveSourceRoot(packageRoot: string): string {
225
224
 
226
225
  const publishedSourceRoot = resolve(packageRoot, "dist/package/src");
227
226
  if (existsSync(resolve(publishedSourceRoot, "index.ts"))) return publishedSourceRoot;
228
- throw new Error(`管理端模块缺少源码入口: ${packageRoot}`);
227
+ throw new Error(runtimeMessage("missing_source_entry", { root: packageRoot }));
229
228
  }
230
229
 
231
230
  /** 根据 package.json exports 创建源码别名。 */