@liujitcn/kratos-admin-system 0.0.50 → 0.0.52

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 (36) hide show
  1. package/README.md +3 -3
  2. package/dist/package/declarations/src/api/system/admin/v1/base_redact_output_policy.d.ts +4 -1
  3. package/dist/package/declarations/src/api/system/admin/v1/base_redact_storage_policy.d.ts +3 -1
  4. package/dist/package/declarations/src/components/tenant-project/TenantProjectManager.vue.d.ts +1 -2
  5. package/dist/package/declarations/src/components/tenant-project/tenant-project-manager-data.d.ts +10 -0
  6. package/dist/package/declarations/src/components/tenant-project/tenant-project-manager.d.ts +8 -3
  7. package/dist/package/declarations/src/index.d.ts +1 -1
  8. package/dist/package/declarations/src/locales/generated.d.ts +24 -0
  9. package/dist/package/declarations/src/rpc/system/admin/v1/base_redact_output_policy.d.ts +8 -0
  10. package/dist/package/declarations/src/rpc/system/admin/v1/base_redact_storage_policy.d.ts +25 -0
  11. package/dist/package/declarations/src/views/ai/chat/components/speech-recognition.d.ts +37 -0
  12. package/dist/package/src/api/system/admin/v1/base_redact_output_policy.ts +7 -0
  13. package/dist/package/src/api/system/admin/v1/base_redact_storage_policy.ts +7 -0
  14. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +77 -60
  15. package/dist/package/src/components/tenant-project/tenant-project-manager-data.ts +39 -1
  16. package/dist/package/src/components/tenant-project/tenant-project-manager.ts +19 -4
  17. package/dist/package/src/index.ts +2 -0
  18. package/dist/package/src/locales/en-US.json +6 -0
  19. package/dist/package/src/locales/ja-JP.json +6 -0
  20. package/dist/package/src/locales/zh-CN.json +6 -0
  21. package/dist/package/src/locales/zh-TW.json +6 -0
  22. package/dist/package/src/rpc/system/admin/v1/base_redact_output_policy.ts +9 -0
  23. package/dist/package/src/rpc/system/admin/v1/base_redact_storage_policy.ts +30 -0
  24. package/dist/package/src/views/ai/chat/components/XSender.vue +18 -61
  25. package/dist/package/src/views/ai/chat/components/speech-recognition.ts +177 -0
  26. package/dist/package/src/views/base/job/index.vue +82 -158
  27. package/dist/package/src/views/base/login-policy/index.vue +12 -6
  28. package/dist/package/src/views/base/redact-output-policy/index.vue +19 -7
  29. package/dist/package/src/views/base/redact-rule/index.vue +70 -53
  30. package/dist/package/src/views/base/redact-storage-policy/index.vue +18 -7
  31. package/dist/package/src/views/base/role/index.vue +1 -1
  32. package/dist/package/src/views/base/{project-grant → tenant-project-grant}/index.vue +3 -1
  33. package/package.json +2 -2
  34. /package/dist/package/declarations/src/components/{tenant-project → tenant-project-grant}/ProjectGrantDialog.vue.d.ts +0 -0
  35. /package/dist/package/declarations/src/views/base/{project-grant → tenant-project-grant}/index.vue.d.ts +0 -0
  36. /package/dist/package/src/components/{tenant-project → tenant-project-grant}/ProjectGrantDialog.vue +0 -0
@@ -22,8 +22,8 @@
22
22
  </template>
23
23
 
24
24
  <script setup lang="ts">
25
- import { computed, h, reactive, ref, type VNode } from "vue";
26
- import { ElButton, ElMessage, ElMessageBox, ElTag } from "element-plus";
25
+ import { computed, h, reactive, ref } from "vue";
26
+ import { ElMessage, ElMessageBox, ElTag } from "element-plus";
27
27
  import { CirclePlus, Delete, EditPen, Promotion, Tickets, VideoPause, VideoPlay } from "@element-plus/icons-vue";
28
28
  import type {
29
29
  ColumnProps,
@@ -172,132 +172,6 @@ function renderJobNameCell(scope: RenderScope<BaseJob>) {
172
172
  });
173
173
  }
174
174
 
175
- /**
176
- * 渲染定时任务操作列。
177
- */
178
- function renderOperationCell(scope: RenderScope<BaseJob>) {
179
- const row = scope.row;
180
- const actionNodes: VNode[] = [];
181
-
182
- if (BUTTONS.value["base:job:update"]) {
183
- actionNodes.push(
184
- h(
185
- ElButton,
186
- {
187
- key: `edit-${row.id}`,
188
- type: "primary",
189
- link: true,
190
- icon: EditPen,
191
- onClick: () => handleOpenDialog(row.id)
192
- },
193
- () => t("common.action.edit")
194
- )
195
- );
196
- }
197
-
198
- if (BUTTONS.value["base:job:delete"]) {
199
- actionNodes.push(
200
- h(
201
- ElButton,
202
- {
203
- key: `delete-${row.id}`,
204
- type: "danger",
205
- link: true,
206
- icon: Delete,
207
- onClick: () => handleDelete(row)
208
- },
209
- () => t("common.action.delete")
210
- )
211
- );
212
- }
213
-
214
- if (
215
- row.status === Status.STATUS_ENABLE &&
216
- (row.entry_id === undefined || row.entry_id === 0) &&
217
- BUTTONS.value["base:job:start"]
218
- ) {
219
- actionNodes.push(
220
- h(
221
- ElButton,
222
- {
223
- key: `start-${row.id}`,
224
- type: "primary",
225
- link: true,
226
- icon: VideoPlay,
227
- class: "job-action job-action--start",
228
- onClick: () => handleStart(row.id, row.name)
229
- },
230
- () => t("system.base.job.action.start")
231
- )
232
- );
233
- }
234
-
235
- if (row.status === Status.STATUS_ENABLE && row.entry_id > 0 && BUTTONS.value["base:job:stop"]) {
236
- actionNodes.push(
237
- h(
238
- ElButton,
239
- {
240
- key: `stop-${row.id}`,
241
- type: "warning",
242
- link: true,
243
- icon: VideoPause,
244
- class: "job-action job-action--stop",
245
- onClick: () => handleStop(row.id, row.name)
246
- },
247
- () => t("system.base.job.action.stop")
248
- )
249
- );
250
- }
251
-
252
- if (
253
- row.status === Status.STATUS_ENABLE &&
254
- (row.entry_id === undefined || row.entry_id === 0) &&
255
- BUTTONS.value["base:job:exec"]
256
- ) {
257
- actionNodes.push(
258
- h(
259
- ElButton,
260
- {
261
- key: `exec-${row.id}`,
262
- type: "success",
263
- link: true,
264
- icon: Promotion,
265
- class: "job-action job-action--exec",
266
- onClick: () => handleExec(row.id, row.name)
267
- },
268
- () => t("system.base.job.action.execute")
269
- )
270
- );
271
- }
272
-
273
- if (BUTTONS.value["base:job:log"]) {
274
- actionNodes.push(
275
- h(
276
- ElButton,
277
- {
278
- key: `log-${row.id}`,
279
- type: "primary",
280
- link: true,
281
- icon: Tickets,
282
- class: "job-action job-action--log",
283
- onClick: () => handleOpenBaseJob(row.id, row.name)
284
- },
285
- () => t("system.base.job.action.log")
286
- )
287
- );
288
- }
289
-
290
- if (!actionNodes.length) return "--";
291
- return h(
292
- "div",
293
- {
294
- class: "job-operation",
295
- key: `job-operation-${row.id}`
296
- },
297
- actionNodes
298
- );
299
- }
300
-
301
175
  /** 定时任务表单字段配置。 */
302
176
  const formFields = computed<ProFormField[]>(() => [
303
177
  {
@@ -372,7 +246,86 @@ const columns = computed<ColumnProps[]>(() => [
372
246
  {
373
247
  prop: "operation",
374
248
  label: t("common.field.action"),
375
- render: scope => renderOperationCell(scope as unknown as RenderScope<BaseJob>)
249
+ cellType: "actions",
250
+ actions: [
251
+ {
252
+ label: t("common.action.edit"),
253
+ type: "primary",
254
+ link: true,
255
+ icon: EditPen,
256
+ hidden: () => !BUTTONS.value["base:job:update"],
257
+ onClick: scope => handleOpenDialog((scope.row as BaseJob).id)
258
+ },
259
+ {
260
+ label: t("common.action.delete"),
261
+ type: "danger",
262
+ link: true,
263
+ icon: Delete,
264
+ hidden: () => !BUTTONS.value["base:job:delete"],
265
+ onClick: scope => handleDelete(scope.row as BaseJob)
266
+ },
267
+ {
268
+ label: t("system.base.job.action.start"),
269
+ type: "primary",
270
+ link: true,
271
+ icon: VideoPlay,
272
+ hidden: scope => {
273
+ const row = scope.row as BaseJob;
274
+ return (
275
+ row.status !== Status.STATUS_ENABLE ||
276
+ (row.entry_id !== undefined && row.entry_id !== 0) ||
277
+ !BUTTONS.value["base:job:start"]
278
+ );
279
+ },
280
+ onClick: scope => {
281
+ const row = scope.row as BaseJob;
282
+ return handleStart(row.id, row.name);
283
+ }
284
+ },
285
+ {
286
+ label: t("system.base.job.action.stop"),
287
+ type: "warning",
288
+ link: true,
289
+ icon: VideoPause,
290
+ hidden: scope => {
291
+ const row = scope.row as BaseJob;
292
+ return row.status !== Status.STATUS_ENABLE || !(row.entry_id > 0) || !BUTTONS.value["base:job:stop"];
293
+ },
294
+ onClick: scope => {
295
+ const row = scope.row as BaseJob;
296
+ return handleStop(row.id, row.name);
297
+ }
298
+ },
299
+ {
300
+ label: t("system.base.job.action.execute"),
301
+ type: "success",
302
+ link: true,
303
+ icon: Promotion,
304
+ hidden: scope => {
305
+ const row = scope.row as BaseJob;
306
+ return (
307
+ row.status !== Status.STATUS_ENABLE ||
308
+ (row.entry_id !== undefined && row.entry_id !== 0) ||
309
+ !BUTTONS.value["base:job:exec"]
310
+ );
311
+ },
312
+ onClick: scope => {
313
+ const row = scope.row as BaseJob;
314
+ return handleExec(row.id, row.name);
315
+ }
316
+ },
317
+ {
318
+ label: t("system.base.job.action.log"),
319
+ type: "info",
320
+ link: true,
321
+ icon: Tickets,
322
+ hidden: () => !BUTTONS.value["base:job:log"],
323
+ onClick: scope => {
324
+ const row = scope.row as BaseJob;
325
+ return handleOpenBaseJob(row.id, row.name);
326
+ }
327
+ }
328
+ ]
376
329
  }
377
330
  ]);
378
331
 
@@ -608,32 +561,3 @@ function handleOpenBaseJob(id: number, name: string) {
608
561
  navigateTo(router, "/base/job/log", { jobId: id, title: t("system.base.job.title.log", { name }) });
609
562
  }
610
563
  </script>
611
-
612
- <style scoped>
613
- .job-operation {
614
- display: flex;
615
- flex-wrap: wrap;
616
- gap: 4px 10px;
617
- align-items: center;
618
- white-space: nowrap;
619
- }
620
- .job-action {
621
- margin-left: 0;
622
- font-weight: 500;
623
- }
624
- .job-action:deep(.el-icon) {
625
- margin-right: 4px;
626
- }
627
- .job-action--start {
628
- --el-button-text-color: var(--el-color-primary);
629
- }
630
- .job-action--stop {
631
- --el-button-text-color: var(--el-color-warning);
632
- }
633
- .job-action--exec {
634
- --el-button-text-color: var(--el-color-success);
635
- }
636
- .job-action--log {
637
- --el-button-text-color: var(--el-color-info);
638
- }
639
- </style>
@@ -94,7 +94,11 @@ import {
94
94
  } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_login_policy";
95
95
 
96
96
  /** 登录策略表单状态,初始化密码仅在提交时转换为一次性密码密文。 */
97
- interface BaseLoginPolicyFormState extends Omit<BaseLoginPolicyForm, "initial_password"> {
97
+ interface BaseLoginPolicyFormState extends Omit<BaseLoginPolicyForm, "initial_password" | "tenant_id" | "user_id"> {
98
+ /** 租户作用域目标,未选择时保持未定义。 */
99
+ tenant_id?: number;
100
+ /** 用户作用域目标,未选择时保持未定义。 */
101
+ user_id?: number;
98
102
  /** 初始化密码明文只保留在当前表单,留空表示不修改已有值。 */
99
103
  initial_password: string;
100
104
  }
@@ -370,13 +374,13 @@ watch(
370
374
  () => formData.scope_type,
371
375
  value => {
372
376
  if (value === BaseLoginPolicyScopeType.BASE_LOGIN_POLICY_SCOPE_TYPE_GLOBAL) {
373
- formData.tenant_id = 0;
374
- formData.user_id = 0;
377
+ formData.tenant_id = undefined;
378
+ formData.user_id = undefined;
375
379
  userOptions.value = [];
376
380
  return;
377
381
  }
378
382
  if (value === BaseLoginPolicyScopeType.BASE_LOGIN_POLICY_SCOPE_TYPE_TENANT) {
379
- formData.user_id = 0;
383
+ formData.user_id = undefined;
380
384
  userOptions.value = [];
381
385
  }
382
386
  }
@@ -399,8 +403,8 @@ function defaultForm(): BaseLoginPolicyFormState {
399
403
  return {
400
404
  id: 0,
401
405
  scope_type: BaseLoginPolicyScopeType.BASE_LOGIN_POLICY_SCOPE_TYPE_GLOBAL,
402
- tenant_id: 0,
403
- user_id: 0,
406
+ tenant_id: undefined,
407
+ user_id: undefined,
404
408
  max_failed_attempts: 5,
405
409
  lock_duration_minutes: 15,
406
410
  allow_concurrent_login: false,
@@ -464,6 +468,8 @@ async function handleSubmit() {
464
468
 
465
469
  const baseLoginPolicy: BaseLoginPolicyForm = {
466
470
  ...formData,
471
+ tenant_id: formData.tenant_id ?? 0,
472
+ user_id: formData.user_id ?? 0,
467
473
  initial_password: formData.initial_password
468
474
  ? await encryptPassword(formData.initial_password, PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY)
469
475
  : undefined
@@ -56,6 +56,7 @@
56
56
  <template v-if="row.rule_type === 'MASK'">
57
57
  <ParameterNumber v-model="row.params.keep_first" :label="t('system.base.redact_rule.parameter.keep_first')" />
58
58
  <ParameterNumber v-model="row.params.keep_last" :label="t('system.base.redact_rule.parameter.keep_last')" />
59
+ <ParameterNumber v-model="row.params.min_mask" :label="t('system.base.redact_rule.parameter.min_mask')" />
59
60
  <ParameterText v-model="row.params.mask_char" :label="t('system.base.redact_rule.parameter.mask_char')" />
60
61
  </template>
61
62
  <template v-else-if="row.rule_type === 'EMAIL'">
@@ -119,6 +120,7 @@ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
119
120
  interface RuleParams {
120
121
  keep_first?: number;
121
122
  keep_last?: number;
123
+ min_mask?: number;
122
124
  mask_char?: string;
123
125
  keep_local_first?: number;
124
126
  mask_domain?: boolean;
@@ -260,7 +262,7 @@ function handleServiceChange(serviceName?: string) { form.service_name = service
260
262
  /** API 变更后重新加载返回字段。 */
261
263
  async function handleApiChange(apiId?: number) { const api = interfaceOptions.value.find(item => item.id === apiId); form.service_name = api?.service_name ?? form.service_name; form.operation = api?.operation ?? ""; form.field_rows = []; if (apiId !== undefined) await loadResponseFields(apiId); }
262
264
  /** 加载 API 返回字段并初始化表格行。 */
263
- async function loadResponseFields(apiId: number) { const api = apiCatalog.value.find(item => item.id === apiId); if (!api) return; const doc = await defBaseApiService.GetBaseApiDoc({ id: apiId }); const rows: OutputFieldRow[] = []; for (const response of doc.responses ?? []) { if (response.body) collectFields(response.body, "", "", rows, true); } form.field_rows = [...new Map(rows.map(item => [String(item.value), item])).values()]; }
265
+ async function loadResponseFields(apiId: number) { const api = apiCatalog.value.find(item => item.id === apiId); if (!api) return; const doc = await defBaseRedactOutputPolicyService.GetBaseRedactOutputFieldDoc({ api_id: apiId }); const rows: OutputFieldRow[] = []; for (const response of doc.responses ?? []) { if (response.body) collectFields(response.body, "", "", rows, true); } form.field_rows = [...new Map(rows.map(item => [String(item.value), item])).values()]; }
264
266
  /** 递归收集可配置的返回叶子字段。 */
265
267
  function collectFields(schema: BaseApiDocSchema, parentRef: string, parentPath: string, result: OutputFieldRow[], root: boolean) { const schemaRef = normalizeRef(schema.ref); const messageRef = schemaRef || parentRef; const startsNewMessage = Boolean(schemaRef && parentRef && schemaRef !== parentRef); const rootContainer = root || (!parentRef && (schema.name === "body" || schema.name === "body[]")); const path = rootContainer || startsNewMessage ? "" : schema.name ? (parentPath ? `${parentPath}.${schema.name}` : schema.name) : parentPath; if (!schema.children?.length) { const fieldName = path.split(".").pop()?.toLowerCase() ?? ""; if (path && !OUTPUT_SYSTEM_FIELD_NAMES.has(fieldName)) result.push(createFieldRow({ label: `${messageRef}.${path}${schema.description ? ` (${schema.description})` : ""}`, value: `${messageRef}\u0000${path}`, message_ref: messageRef, field_path: path, description: schema.description })); return; } for (const child of schema.children) collectFields(child, messageRef, path, result, false); }
266
268
  /** 规范化 OpenAPI 引用名称。 */
@@ -274,7 +276,7 @@ async function submit() { const valid = await dialogRef.value?.validate(); if (!
274
276
  /** 将表格行转换为出库策略请求。 */
275
277
  function buildPayload(row: OutputFieldRow): BaseRedactOutputPolicyForm { syncRowParams(row); return { id: row.id, operation: form.operation, service_name: form.service_name, message_ref: row.message_ref, field_path: row.field_path, mode: row.mode, rule_id: row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_RULE ? row.rule_id ?? 0 : 0, rule_params: row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_RULE ? row.rule_params : "{}", status: form.status, remark: form.remark }; }
276
278
  /** 将已保存的策略回填到对应字段行。 */
277
- function applyPolicy(row: OutputFieldRow, data: BaseRedactOutputPolicyForm) { row.id = data.id; row.mode = data.mode || BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_FULL; row.rule_id = data.rule_id || undefined; row.rule_params = data.rule_params; if (row.rule_id) { const rule = ruleCatalog.value.find(item => item.id === row.rule_id); row.rule_type = rule?.rule_type ?? ""; row.params = parseParams(row.rule_type, row.rule_params); } }
279
+ function applyPolicy(row: OutputFieldRow, data: BaseRedactOutputPolicyForm) { row.id = data.id; row.mode = data.mode || BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_FULL; row.rule_id = data.rule_id || undefined; row.rule_params = data.rule_params; if (row.rule_id) { const rule = ruleCatalog.value.find(item => item.id === row.rule_id); row.rule_type = rule?.rule_type ?? ""; row.params = rule ? parseParams(row.rule_type, row.rule_params, rule.rule) : {}; } }
278
280
  /** 处理行模式变更。 */
279
281
  function handleModeChange(row: OutputTableRow) { if (row.mode !== BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_RULE) { row.rule_id = undefined; row.rule_type = ""; row.params = {}; row.rule_params = "{}"; } }
280
282
  /** 处理行规则变更。 */
@@ -285,7 +287,7 @@ function syncRowParams(row: OutputFieldRow) { const rule = ruleCatalog.value.fin
285
287
  function isConfiguredRow(row: OutputFieldRow) { return row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_HIDE || row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_FULL || (row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_RULE && Boolean(row.rule_id)); }
286
288
  /** 判断字段表格是否至少配置了一行。 */
287
289
  function isConfiguredRowList(rows: OutputFieldRow[] | undefined) { return Boolean(rows?.some(isConfiguredRow)); }
288
- /** 判断字段行是否使用规则模板。 */
290
+ /** 判断字段行是否使用脱敏规则。 */
289
291
  function isRuleRow(row: OutputTableRow) { return row.mode === BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_RULE && Boolean(row.rule_id); }
290
292
  /** 格式化 API 下拉项。 */
291
293
  function apiLabel(api: BaseApi) { return api.desc && api.desc !== api.operation ? `${api.desc}(${api.operation})` : api.operation; }
@@ -293,10 +295,20 @@ function apiLabel(api: BaseApi) { return api.desc && api.desc !== api.operation
293
295
  function isGetApi(api: BaseApi) { return api.method.toUpperCase() === "GET"; }
294
296
  /** 格式化服务下拉项。 */
295
297
  function serviceLabel(serviceName: string) { const service = apiCatalog.value.find(item => item.service_name === serviceName); return service?.service_desc && service.service_desc !== serviceName ? `${service.service_desc}(${serviceName})` : serviceName; }
296
- /** 解析规则参数。 */
297
- function parseParams(ruleType: string, raw: string): RuleParams { try { const value = JSON.parse(raw) as Record<string, RuleParams>; return value[ruleType.toLowerCase()] ?? defaultParams(ruleType); } catch { return defaultParams(ruleType); } }
298
- /** 返回规则默认参数。 */
299
- function defaultParams(ruleType: string): RuleParams { switch (ruleType) { case "MASK": return { keep_first: 3, keep_last: 4, mask_char: "*" }; case "EMAIL": return { keep_local_first: 2, mask_domain: false, mask_char: "*" }; case "REGEX": return { pattern: "(?s).+", replacement: "[REDACTED]" }; case "TRUNCATE": return { length: 10, suffix: "..." }; case "HASH": return { algo: "SHA256" }; case "IP": return { keep_octets: 2, mask_char: "x" }; case "URL": return { mask_query: true, mask_char: "*" }; case "FIXED_LENGTH": return { char: "X" }; default: return {}; } }
298
+ /** 解析规则参数,空策略参数时回退到数据库规则参数。 */
299
+ function parseParams(ruleType: string, raw: string, fallbackRaw = ""): RuleParams {
300
+ const candidates = fallbackRaw && fallbackRaw !== raw ? [raw, fallbackRaw] : [raw];
301
+ for (const candidate of candidates) {
302
+ try {
303
+ const value = JSON.parse(candidate) as Record<string, RuleParams>;
304
+ const params = value[ruleType.toLowerCase()];
305
+ if (params && typeof params === "object" && !Array.isArray(params)) return params;
306
+ } catch {
307
+ continue;
308
+ }
309
+ }
310
+ return {};
311
+ }
300
312
  /** 创建空的出库字段行。 */
301
313
  function createFieldRow(option: Pick<OutputFieldRow, "label" | "value" | "message_ref" | "field_path"> & { description?: string }): OutputFieldRow { return { ...option, id: 0, mode: BaseRedactOutputPolicyMode.BASE_REDACT_OUTPUT_POLICY_MODE_FULL, rule_id: undefined, rule_type: "", params: {}, rule_params: "{}" }; }
302
314
  /** 创建默认出库表单。 */
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="table-box">
3
- <ProTable ref="table" row-key="id" :columns="columns" :request-api="requestTable" />
3
+ <ProTable ref="table" row-key="id" :columns="columns" :header-actions="headerActions" :request-api="requestTable" />
4
4
  <FormDialog
5
5
  v-model="dialog.visible"
6
6
  ref="dialogRef"
@@ -14,7 +14,7 @@
14
14
  >
15
15
  <template #rule>
16
16
  <div class="rule-editor">
17
- <div class="rule-description">{{ t(templateDescriptionKey) }}</div>
17
+ <div v-if="form.remark" class="rule-description">{{ form.remark }}</div>
18
18
  <div v-if="form.rule_type === 'MASK'" class="parameter-grid">
19
19
  <div class="parameter-item">
20
20
  <span>{{ t("system.base.redact_rule.parameter.keep_first") }}</span>
@@ -24,6 +24,10 @@
24
24
  <span>{{ t("system.base.redact_rule.parameter.keep_last") }}</span>
25
25
  <el-input-number v-model="form.params.keep_last" :min="0" :precision="0" controls-position="right" />
26
26
  </div>
27
+ <div class="parameter-item">
28
+ <span>{{ t("system.base.redact_rule.parameter.min_mask") }}</span>
29
+ <el-input-number v-model="form.params.min_mask" :min="0" :precision="0" controls-position="right" />
30
+ </div>
27
31
  <div class="parameter-item">
28
32
  <span>{{ t("system.base.redact_rule.parameter.mask_char") }}</span>
29
33
  <el-input v-model="form.params.mask_char" maxlength="8" />
@@ -109,13 +113,14 @@
109
113
  <script setup lang="ts">
110
114
  import { computed, reactive, ref } from "vue";
111
115
  import { ElMessage, ElMessageBox } from "element-plus";
112
- import { EditPen } from "@element-plus/icons-vue";
113
- import type { ColumnProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
116
+ import type { FormRules } from "element-plus";
117
+ import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
118
+ import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
114
119
  import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
115
120
  import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
116
121
  import type { ProFormField } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
117
122
  import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
118
- import { buildPageRequest } from "@liujitcn/kratos-admin-core/table";
123
+ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
119
124
  import { t } from "@liujitcn/kratos-admin-core";
120
125
  import { defBaseRedactRuleService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_redact_rule";
121
126
  import type { BaseRedactRule, BaseRedactRuleForm, PageBaseRedactRuleRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_redact_rule";
@@ -124,6 +129,7 @@ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
124
129
  interface RuleParams {
125
130
  keep_first?: number;
126
131
  keep_last?: number;
132
+ min_mask?: number;
127
133
  mask_char?: string;
128
134
  keep_local_first?: number;
129
135
  mask_domain?: boolean;
@@ -152,18 +158,21 @@ const statusOptions = computed(() => [
152
158
  { label: t("common.status.enabled"), value: Status.STATUS_ENABLE },
153
159
  { label: t("common.status.disabled"), value: Status.STATUS_DISABLE }
154
160
  ]);
155
- const templateDescriptionKey = computed(() => `system.base.redact_rule.description.${form.code || "unknown"}`);
156
-
161
+ /** 运行时支持的规则类型选项。 */
162
+ const ruleTypeOptions = ["MASK", "EMAIL", "REGEX", "TRUNCATE", "HASH", "UUID", "IP", "URL", "FIXED_LENGTH"].map(value => ({ label: value, value }));
157
163
  const fields = computed<ProFormField[]>(() => [
158
- { prop: "code", label: t("system.base.redact_rule.field.code"), component: "input", props: { disabled: true } },
159
- { prop: "name", label: t("system.base.redact_rule.field.name"), component: "input", props: { disabled: true } },
160
- { prop: "rule_type", label: t("system.base.redact_rule.field.rule_type"), component: "input", props: { disabled: true } },
164
+ { prop: "code", label: t("system.base.redact_rule.field.code"), component: "input", props: { disabled: Boolean(form.id) } },
165
+ { prop: "name", label: t("system.base.redact_rule.field.name"), component: "input", props: { disabled: Boolean(form.id) } },
166
+ { prop: "rule_type", label: t("system.base.redact_rule.field.rule_type"), component: "select", options: ruleTypeOptions, props: { disabled: Boolean(form.id), onChange: handleRuleTypeChange } },
161
167
  { prop: "rule", label: t("system.base.redact_rule.field.parameters"), component: "slot", slotName: "rule", colSpan: 24 },
162
168
  { prop: "status", label: t("common.field.status"), component: "radio-group", options: statusOptions.value },
163
169
  { prop: "remark", label: t("common.field.remark"), component: "textarea" }
164
170
  ]);
165
171
 
166
- const rules = computed(() => ({
172
+ const rules = computed<FormRules>(() => ({
173
+ code: [{ required: true, message: t("system.base.redact_rule.validation.code"), trigger: "blur" }],
174
+ name: [{ required: true, message: t("system.base.redact_rule.validation.name"), trigger: "blur" }],
175
+ rule_type: [{ required: true, message: t("system.base.redact_rule.validation.rule_type"), trigger: "change" }],
167
176
  rule: [{ required: true, message: t("system.base.redact_rule.validation.parameters"), trigger: "change" }]
168
177
  }));
169
178
 
@@ -190,36 +199,40 @@ const columns = computed<ColumnProps[]>(() => [
190
199
  prop: "actions",
191
200
  label: t("common.field.operation"),
192
201
  cellType: "actions",
193
- actions: [{ label: t("common.action.edit"), type: "primary", link: true, icon: EditPen, hidden: () => !BUTTONS.value["base:redact-rule:update"], onClick: scope => openDialog((scope.row as BaseRedactRule).id) }]
202
+ actions: [{ label: t("common.action.edit"), type: "primary", link: true, icon: EditPen, hidden: () => !BUTTONS.value["base:redact-rule:update"], onClick: scope => openDialog((scope.row as BaseRedactRule).id) }, { label: t("common.action.delete"), type: "danger", link: true, icon: Delete, hidden: () => !BUTTONS.value["base:redact-rule:delete"], onClick: scope => deleteItems(scope.row as BaseRedactRule) }]
194
203
  }
195
204
  ]);
205
+ const headerActions = computed<HeaderActionProps[]>(() => [{ label: t("common.action.create"), type: "success", icon: CirclePlus, hidden: () => !BUTTONS.value["base:redact-rule:create"], onClick: () => openDialog() }, { label: t("common.action.delete"), type: "danger", icon: Delete, hidden: () => !BUTTONS.value["base:redact-rule:delete"], disabled: scope => !scope.selectedList.length, onClick: scope => deleteItems(scope.selectedList as BaseRedactRule[]) }]);
196
206
 
197
- /** 请求固定规则模板列表。 */
207
+ /** 请求固定规则列表。 */
198
208
  async function requestTable(params: PageBaseRedactRuleRequest) {
199
209
  const data = await defBaseRedactRuleService.PageBaseRedactRule(buildPageRequest(params));
200
210
  return { data: { list: data.base_redact_rules ?? [], total: data.total } };
201
211
  }
202
212
 
203
- /** 打开固定规则模板编辑弹窗。 */
204
- async function openDialog(id: number) {
213
+ /** 打开新增或编辑弹窗。 */
214
+ async function openDialog(id?: number) {
205
215
  resetForm();
206
- const data = await defBaseRedactRuleService.GetBaseRedactRule({ id });
207
- Object.assign(form, data, { params: parseRuleParams(data.rule_type, data.rule) });
216
+ if (id !== undefined) {
217
+ const data = await defBaseRedactRuleService.GetBaseRedactRule({ id });
218
+ Object.assign(form, data, { params: parseRuleParams(data.rule_type, data.rule) });
219
+ }
220
+ dialog.titleKey = id !== undefined ? "common.action.edit_resource" : "common.action.create_resource";
208
221
  dialog.visible = true;
209
222
  }
210
223
 
211
- /** 重置规则模板编辑表单。 */
224
+ /** 重置规则编辑表单。 */
212
225
  function resetForm() {
213
226
  dialog.visible = false;
214
227
  dialogRef.value?.resetFields();
215
228
  Object.assign(form, defaultForm());
216
229
  }
217
230
 
218
- /** 提交固定规则模板参数。 */
231
+ /** 提交新增或编辑的规则。 */
219
232
  async function submit() {
233
+ syncRule();
220
234
  const valid = await dialogRef.value?.validate();
221
235
  if (!valid) return;
222
- syncRule();
223
236
  const payload: BaseRedactRuleForm = {
224
237
  id: form.id,
225
238
  code: form.code,
@@ -229,13 +242,17 @@ async function submit() {
229
242
  status: form.status,
230
243
  remark: form.remark
231
244
  };
232
- await defBaseRedactRuleService.UpdateBaseRedactRule({ base_redact_rule: payload });
233
- ElMessage.success(t("common.message.update_success", { resource: t("system.base.redact_rule.title") }));
245
+ if (form.id) {
246
+ await defBaseRedactRuleService.UpdateBaseRedactRule({ base_redact_rule: payload });
247
+ } else {
248
+ await defBaseRedactRuleService.CreateBaseRedactRule({ base_redact_rule: payload });
249
+ }
250
+ ElMessage.success(t(form.id ? "common.message.update_success" : "common.message.create_success", { resource: t("system.base.redact_rule.title") }));
234
251
  resetForm();
235
252
  table.value?.getTableList();
236
253
  }
237
254
 
238
- /** 切换固定规则模板状态。 */
255
+ /** 切换固定规则状态。 */
239
256
  async function changeStatus(row: BaseRedactRule) {
240
257
  const next = row.status === Status.STATUS_ENABLE ? Status.STATUS_DISABLE : Status.STATUS_ENABLE;
241
258
  try {
@@ -257,46 +274,46 @@ async function changeStatus(row: BaseRedactRule) {
257
274
  }
258
275
  }
259
276
 
277
+ /** 删除未被引用的规则。 */
278
+ function deleteItems(selected?: BaseRedactRule | BaseRedactRule[] | number | string | Array<number | string>) {
279
+ const items = Array.isArray(selected) ? selected.filter((item): item is BaseRedactRule => typeof item === "object") : selected && typeof selected === "object" ? [selected] : [];
280
+ const ids = items.length ? items.map(item => item.id) : normalizeSelectedIds(selected as number | string | Array<number | string>);
281
+ if (!ids.length) {
282
+ ElMessage.warning(t("common.message.select_delete_item"));
283
+ return;
284
+ }
285
+ ElMessageBox.confirm(t("common.dialog.delete_selected", { resource: t("system.base.redact_rule.title") }), t("common.title.warning"), { type: "warning" }).then(async () => {
286
+ await defBaseRedactRuleService.DeleteBaseRedactRule({ id: ids.join(",") });
287
+ ElMessage.success(t("common.message.delete_success", { resource: t("system.base.redact_rule.title") }));
288
+ table.value?.getTableList();
289
+ });
290
+ }
291
+
260
292
  /** 将参数表单同步为运行时规则 JSON。 */
261
293
  function syncRule() {
262
- form.params = { ...defaultRuleParams(form.rule_type), ...form.params };
294
+ if (!form.rule_type) {
295
+ form.rule = "";
296
+ return;
297
+ }
263
298
  form.rule = JSON.stringify({ [form.rule_type.toLowerCase()]: form.params }, null, 2);
264
299
  }
265
300
 
266
- /** 解析规则 JSON 中当前类型的参数。 */
301
+ /** 切换规则类型时重置参数。 */
302
+ function handleRuleTypeChange(ruleType?: string) {
303
+ form.rule_type = ruleType ?? "";
304
+ form.params = {};
305
+ }
306
+
307
+ /** 解析数据库规则 JSON 中当前类型的参数。 */
267
308
  function parseRuleParams(ruleType: string, rawRule: string): RuleParams {
268
309
  try {
269
310
  const value = JSON.parse(rawRule) as Record<string, unknown>;
270
311
  const params = value[ruleType.toLowerCase()];
271
312
  if (params && typeof params === "object" && !Array.isArray(params)) return params as RuleParams;
272
313
  } catch {
273
- return defaultRuleParams(ruleType);
274
- }
275
- return defaultRuleParams(ruleType);
276
- }
277
-
278
- /** 返回固定规则模板的默认参数。 */
279
- function defaultRuleParams(ruleType: string): RuleParams {
280
- switch (ruleType) {
281
- case "MASK":
282
- return { keep_first: 3, keep_last: 4, mask_char: "*" };
283
- case "EMAIL":
284
- return { keep_local_first: 2, mask_domain: false, mask_char: "*" };
285
- case "REGEX":
286
- return { pattern: "(?s).+", replacement: "[REDACTED]" };
287
- case "TRUNCATE":
288
- return { length: 10, suffix: "..." };
289
- case "HASH":
290
- return { algo: "SHA256" };
291
- case "IP":
292
- return { keep_octets: 2, mask_char: "x" };
293
- case "URL":
294
- return { mask_query: true, mask_char: "*" };
295
- case "FIXED_LENGTH":
296
- return { char: "X" };
297
- default:
298
- return {};
314
+ return {};
299
315
  }
316
+ return {};
300
317
  }
301
318
 
302
319
  /** 返回规则参数的紧凑展示文本。 */
@@ -304,9 +321,9 @@ function ruleSummary(row: BaseRedactRule) {
304
321
  return JSON.stringify(parseRuleParams(row.rule_type, row.rule));
305
322
  }
306
323
 
307
- /** 返回规则模板表单的初始值。 */
324
+ /** 返回规则表单的初始值。 */
308
325
  function defaultForm(): RuleFormState {
309
- return { id: 0, code: "", name: "", rule_type: "MASK", rule: "", status: Status.STATUS_ENABLE, remark: "", params: defaultRuleParams("MASK") };
326
+ return { id: 0, code: "", name: "", rule_type: "", rule: "", status: Status.STATUS_ENABLE, remark: "", params: {} };
310
327
  }
311
328
  </script>
312
329