@liujitcn/kratos-admin-system 0.0.51 → 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.
package/README.md CHANGED
@@ -153,7 +153,7 @@ export const adminModules = [systemAdminModule];
153
153
 
154
154
  跨模块跳转使用 Vue Router;复用 System 代码时只引用 `package.json#exports` 公开的 npm 子路径。
155
155
 
156
- 租户项目管理组件通过 `TenantProjectManager` 公开。外部业务模块可以传入 `extraColumns`、`extraActions` 和 `loadExtraData`:追加列继续使用 `ColumnProps`,数据加载器接收当前页的 `tenant_id + project_id` 集合,单条操作回调接收同一组双键以及项目基础数据。组件也会透传表格具名插槽,并向行插槽增加 `tenantProject` 上下文;保存业务配置后可通过组件实例的 `refresh()` 刷新列表。按外部业务字段搜索或排序时,应由外部接口参与分页查询,不能只在当前页加载后过滤。
156
+ 租户项目管理组件通过 `TenantProjectManager` 公开。默认租户创建项目时在表单中选择目标租户,普通租户由服务端自动使用当前租户;默认租户查询全部项目,普通租户按项目授权范围查询。外部业务模块可以传入 `extraColumns`、`extraActions` 和 `loadExtraData`:扩展列可通过 `after` 指定插入到哪个基础字段后面,列的同名表格插槽会沿用该位置,未配置时默认插入备注后面,例如 `{ prop: "address", after: "name" }` 配合 `#address` 插槽即可把内容放到项目名称后面;数据加载器接收当前页的 `tenant_id + project_id` 集合,单条操作回调接收同一组双键以及项目基础数据。组件也会透传表格具名插槽,并向行插槽增加 `tenantProject` 上下文;保存业务配置后可通过组件实例的 `refresh()` 刷新列表。按外部业务字段搜索或排序时,应由外部接口参与分页查询,不能只在当前页加载后过滤。
157
157
 
158
158
  外部项目可以在自己的页面中包装该组件,并通过菜单将项目管理页面的 `component` 指向包装页面:
159
159
 
@@ -1,4 +1,3 @@
1
- import type { ColumnProps } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
2
1
  import { type TenantProjectAction, type TenantProjectManagerProps } from "./tenant-project-manager";
3
2
  /** 刷新项目表格。 */
4
3
  declare function refreshTable(): Promise<void>;
@@ -16,7 +15,7 @@ declare const __VLS_base: import("vue").DefineComponent<TenantProjectManagerProp
16
15
  }, string, import("vue").PublicProps, Readonly<TenantProjectManagerProps> & Readonly<{
17
16
  "onExtra-data-error"?: ((error: unknown) => any) | undefined;
18
17
  }>, {
19
- extraColumns: ColumnProps[];
18
+ extraColumns: import("./tenant-project-manager").TenantProjectExtraColumn[];
20
19
  extraActions: TenantProjectAction[];
21
20
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
21
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,4 +1,9 @@
1
1
  import type { BaseTenantProject } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
2
+ /** 租户项目表格列的最小结构,用于在不依赖表格组件的情况下排列扩展列。 */
3
+ type TenantProjectColumnLike = {
4
+ prop?: string;
5
+ type?: string;
6
+ };
2
7
  /** 租户项目业务数据查询键。 */
3
8
  export interface TenantProjectKey {
4
9
  /** 租户 ID。 */
@@ -8,7 +13,12 @@ export interface TenantProjectKey {
8
13
  }
9
14
  /** 租户项目扩展数据,外层键由租户 ID 和项目 ID 组成。 */
10
15
  export type TenantProjectExtraData = Record<string, Record<string, unknown>>;
16
+ /** 按基础字段位置排列租户项目管理的扩展列。 */
17
+ export declare function arrangeTenantProjectColumns<T extends TenantProjectColumnLike>(baseColumns: T[], extraColumns: Array<T & {
18
+ after?: string;
19
+ }>): T[];
11
20
  /** 创建租户项目扩展数据的稳定键。 */
12
21
  export declare function tenantProjectKey(tenantId: number, projectId: number): string;
13
22
  /** 将外部业务字段合并到项目行,同时保护公共项目字段。 */
14
23
  export declare function mergeTenantProjectExtraData(project: BaseTenantProject, extra?: Record<string, unknown>): BaseTenantProject & Record<string, unknown>;
24
+ export {};
@@ -1,7 +1,7 @@
1
1
  import type { BaseTenantProject, PageBaseTenantProjectRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
2
2
  import type { ColumnProps, TableActionProps } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
3
3
  import type { TenantProjectExtraData, TenantProjectKey } from "./tenant-project-manager-data";
4
- export { mergeTenantProjectExtraData, tenantProjectKey, type TenantProjectExtraData, type TenantProjectKey } from "./tenant-project-manager-data";
4
+ export { arrangeTenantProjectColumns, mergeTenantProjectExtraData, tenantProjectKey, type TenantProjectExtraData, type TenantProjectKey } from "./tenant-project-manager-data";
5
5
  /** 租户项目单条业务上下文。 */
6
6
  export interface TenantProjectContext<TData extends Record<string, unknown> = Record<string, unknown>> extends TenantProjectKey {
7
7
  /** 公共项目基础数据。 */
@@ -29,10 +29,15 @@ export type TenantProjectAction = Omit<TableActionProps, "disabled" | "hidden" |
29
29
  /** 执行项目扩展操作。 */
30
30
  onClick: (context: TenantProjectContext, params?: Record<string, any>) => void | Promise<void>;
31
31
  };
32
+ /** 租户项目管理的扩展列配置。 */
33
+ export type TenantProjectExtraColumn = ColumnProps & {
34
+ /** 插入到指定基础字段后面,未配置时默认插入备注字段后面。 */
35
+ after?: string;
36
+ };
32
37
  /** 租户项目管理组件属性。 */
33
38
  export interface TenantProjectManagerProps {
34
- /** 追加到基础列表的表格列。 */
35
- extraColumns?: ColumnProps[];
39
+ /** 追加到基础列表的表格列,可通过 after 指定插入位置。 */
40
+ extraColumns?: TenantProjectExtraColumn[];
36
41
  /** 追加到基础操作列的业务操作。 */
37
42
  extraActions?: TenantProjectAction[];
38
43
  /** 加载当前页外部业务字段。 */
@@ -1,5 +1,5 @@
1
1
  export { ADMIN_AI_EXTENSION, getAdminAiExtension } from "./ai";
2
2
  export type { AdminAiExtension } from "./ai";
3
3
  export { default as TenantProjectManager } from "./components/tenant-project/TenantProjectManager.vue";
4
- export { mergeTenantProjectExtraData, tenantProjectKey, type TenantProjectAction, type TenantProjectContext, type TenantProjectExtraData, type TenantProjectExtraDataLoadContext, type TenantProjectExtraDataLoader, type TenantProjectKey, type TenantProjectManagerProps } from "./components/tenant-project/tenant-project-manager";
4
+ export { mergeTenantProjectExtraData, arrangeTenantProjectColumns, tenantProjectKey, type TenantProjectAction, type TenantProjectContext, type TenantProjectExtraColumn, type TenantProjectExtraData, type TenantProjectExtraDataLoadContext, type TenantProjectExtraDataLoader, type TenantProjectKey, type TenantProjectManagerProps } from "./components/tenant-project/tenant-project-manager";
5
5
  export { systemAdminModule } from "./module";
@@ -3,6 +3,7 @@ export type SpeechResultList = ArrayLike<{
3
3
  0?: {
4
4
  transcript: string;
5
5
  };
6
+ isFinal?: boolean;
6
7
  length: number;
7
8
  }>;
8
9
  /** 语音识别错误的最小结构,兼容浏览器事件和不支持错误。 */
@@ -21,8 +22,11 @@ type SpeechRecognitionOptions = {
21
22
  /** 识别结果回调。 */
22
23
  onResult?: (text: string) => void;
23
24
  };
24
- /** 收集一次识别事件中的完整文本。 */
25
- export declare function collectSpeechResultText(results: SpeechResultList): string;
25
+ /** 增量累积语音识别结果,避免连续模式下 interim 结果重复计入已确认文本。 */
26
+ export declare function accumulateSpeechResultText(results: SpeechResultList, resultIndex: number, previousFinalText: string): {
27
+ finalText: string;
28
+ text: string;
29
+ };
26
30
  /** 创建浏览器语音识别实例,并统一结果、状态和卸载处理。 */
27
31
  export declare function useSpeechRecognition({ onError, onStart, onEnd, onResult }?: SpeechRecognitionOptions): {
28
32
  loading: globalThis.Ref<boolean, boolean>;
@@ -28,20 +28,31 @@
28
28
  </template>
29
29
 
30
30
  <script setup lang="ts">
31
- import { computed, reactive, ref, useSlots } from "vue";
31
+ import { computed, onMounted, reactive, ref, useSlots } from "vue";
32
32
  import { ElMessage, ElMessageBox } from "element-plus";
33
33
  import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
34
- import type { ColumnProps, HeaderActionProps, ProTableInstance, RenderScope, TableActionProps } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
34
+ import type {
35
+ ColumnProps,
36
+ HeaderActionProps,
37
+ ProTableInstance,
38
+ RenderScope,
39
+ TableActionProps
40
+ } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
35
41
  import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
36
42
  import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
37
43
  import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
38
44
  import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
39
45
  import { defBaseTenantProjectService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_tenant_project";
40
- import type { BaseTenantProject, BaseTenantProjectForm, PageBaseTenantProjectRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
46
+ import type {
47
+ BaseTenantProject,
48
+ BaseTenantProjectForm,
49
+ PageBaseTenantProjectRequest
50
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
41
51
  import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
42
52
  import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
43
53
  import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
44
54
  import { t } from "@liujitcn/kratos-admin-core";
55
+ import { arrangeTenantProjectColumns } from "./tenant-project-manager-data";
45
56
  import {
46
57
  mergeTenantProjectExtraData,
47
58
  tenantProjectKey,
@@ -66,8 +77,11 @@ const emit = defineEmits<{
66
77
  "extra-data-error": [error: unknown];
67
78
  }>();
68
79
 
69
- /** 项目表单状态,租户由服务端租户上下文自动补全。 */
70
- type BaseTenantProjectFormState = Omit<BaseTenantProjectForm, "tenant_id">;
80
+ /** 项目表单状态,默认租户创建时必须选择目标租户。 */
81
+ type BaseTenantProjectFormState = Omit<BaseTenantProjectForm, "tenant_id"> & {
82
+ /** 项目所属租户。 */
83
+ tenant_id?: number;
84
+ };
71
85
 
72
86
  const { BUTTONS } = useAuthButtons();
73
87
  const slots = useSlots();
@@ -88,6 +102,8 @@ const statusOptions = computed<ProFormOption[]>(() => [
88
102
  const formData = reactive<BaseTenantProjectFormState>({
89
103
  /** 项目ID。 */
90
104
  id: 0,
105
+ /** 项目所属租户。 */
106
+ tenant_id: undefined,
91
107
  /** 项目名称。 */
92
108
  name: "",
93
109
  /** 项目编号。 */
@@ -100,6 +116,13 @@ const formData = reactive<BaseTenantProjectFormState>({
100
116
  remark: ""
101
117
  });
102
118
  const rules = computed(() => ({
119
+ tenant_id: [
120
+ {
121
+ required: true,
122
+ message: t("common.validation.required_select", { field: t("common.field.tenant") }),
123
+ trigger: "change"
124
+ }
125
+ ],
103
126
  name: [
104
127
  {
105
128
  required: true,
@@ -141,10 +164,14 @@ const rules = computed(() => ({
141
164
  ]
142
165
  }));
143
166
 
144
- const { isDefaultTenant } = useTenantScope();
167
+ const { isDefaultTenant, tenantColumns, tenantFormField, toRequestTenantId, loadTenantOptions } = useTenantScope();
168
+ onMounted(() => {
169
+ if (isDefaultTenant.value) void loadTenantOptions();
170
+ });
145
171
 
146
172
  /** 项目表单字段配置。 */
147
173
  const formFields = computed<ProFormField[]>(() => [
174
+ tenantFormField({ label: t("common.field.tenant"), disabledOnEdit: true }),
148
175
  {
149
176
  prop: "name",
150
177
  label: t("system.base.tenant_project.field.name"),
@@ -202,56 +229,60 @@ function resolveProjectBoolean(value: boolean | ((context: TenantProjectContext)
202
229
  }
203
230
 
204
231
  /** 项目表格列配置。 */
205
- const columns = computed<ColumnProps[]>(() => [
206
- { type: "selection", width: 55 },
207
- { prop: "name", label: t("system.base.tenant_project.field.name"), minWidth: 180, search: { el: "input" } },
208
- { prop: "code", label: t("system.base.tenant_project.field.code"), minWidth: 140, search: { el: "input" } },
209
- { prop: "sort", label: t("common.field.sort"), minWidth: 90, align: "right" },
210
- {
211
- prop: "status",
212
- label: t("common.field.status"),
213
- minWidth: 100,
214
- search: { el: "select" },
215
- cellType: "status",
216
- statusProps: {
217
- activeValue: Status.STATUS_ENABLE,
218
- inactiveValue: Status.STATUS_DISABLE,
219
- activeText: t("common.status.enabled"),
220
- inactiveText: t("common.status.disabled"),
221
- disabled: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:status"],
222
- beforeChange: scope => handleBeforeSetStatus(scope.row as BaseTenantProject)
223
- }
224
- },
225
- { prop: "remark", label: t("common.field.remark"), minWidth: 160 },
226
- ...props.extraColumns,
227
- { prop: "created_at", align: "center", label: t("common.field.created_at"), minWidth: 180 },
228
- { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 180, align: "center" },
229
- {
230
- prop: "operation",
231
- label: t("common.field.operation"),
232
- cellType: "actions",
233
- actions: [
234
- ...props.extraActions.map(toTableAction),
235
- {
236
- label: t("common.action.edit"),
237
- type: "primary",
238
- link: true,
239
- icon: EditPen,
240
- hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:update"],
241
- params: scope => ({ projectId: scope.row.id }),
242
- onClick: (scope, params) => handleOpenDialog((params?.projectId as number | undefined) ?? (scope.row as BaseTenantProject).id)
243
- },
244
- {
245
- label: t("common.action.delete"),
246
- type: "danger",
247
- link: true,
248
- icon: Delete,
249
- hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:delete"],
250
- onClick: scope => handleDelete(scope.row as BaseTenantProject)
232
+ const columns = computed<ColumnProps[]>(() => {
233
+ const baseColumns: ColumnProps[] = [
234
+ { type: "selection", width: 55 },
235
+ ...tenantColumns({ label: t("common.field.tenant"), order: 1 }),
236
+ { prop: "name", label: t("system.base.tenant_project.field.name"), minWidth: 180, search: { el: "input" } },
237
+ { prop: "code", label: t("system.base.tenant_project.field.code"), minWidth: 140, search: { el: "input" } },
238
+ { prop: "sort", label: t("common.field.sort"), minWidth: 90, align: "right" },
239
+ {
240
+ prop: "status",
241
+ label: t("common.field.status"),
242
+ minWidth: 100,
243
+ search: { el: "select" },
244
+ cellType: "status",
245
+ statusProps: {
246
+ activeValue: Status.STATUS_ENABLE,
247
+ inactiveValue: Status.STATUS_DISABLE,
248
+ activeText: t("common.status.enabled"),
249
+ inactiveText: t("common.status.disabled"),
250
+ disabled: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:status"],
251
+ beforeChange: scope => handleBeforeSetStatus(scope.row as BaseTenantProject)
251
252
  }
252
- ]
253
- }
254
- ]);
253
+ },
254
+ { prop: "remark", label: t("common.field.remark"), minWidth: 160 },
255
+ { prop: "created_at", align: "center", label: t("common.field.created_at"), minWidth: 180 },
256
+ { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 180, align: "center" },
257
+ {
258
+ prop: "operation",
259
+ label: t("common.field.operation"),
260
+ cellType: "actions",
261
+ actions: [
262
+ ...props.extraActions.map(toTableAction),
263
+ {
264
+ label: t("common.action.edit"),
265
+ type: "primary",
266
+ link: true,
267
+ icon: EditPen,
268
+ hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:update"],
269
+ params: scope => ({ projectId: scope.row.id }),
270
+ onClick: (scope, params) =>
271
+ handleOpenDialog((params?.projectId as number | undefined) ?? (scope.row as BaseTenantProject).id)
272
+ },
273
+ {
274
+ label: t("common.action.delete"),
275
+ type: "danger",
276
+ link: true,
277
+ icon: Delete,
278
+ hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:tenant:project:delete"],
279
+ onClick: scope => handleDelete(scope.row as BaseTenantProject)
280
+ }
281
+ ]
282
+ }
283
+ ];
284
+ return arrangeTenantProjectColumns(baseColumns, props.extraColumns);
285
+ });
255
286
 
256
287
  /** 项目顶部按钮配置。 */
257
288
  const headerActions = computed<HeaderActionProps[]>(() => [
@@ -275,7 +306,10 @@ const headerActions = computed<HeaderActionProps[]>(() => [
275
306
  /** 请求项目分页列表并加载当前页外部业务字段。 */
276
307
  async function requestBaseTenantProjectTable(params: PageBaseTenantProjectRequest) {
277
308
  const revision = ++extraDataRequestRevision;
278
- const request = buildPageRequest(params) as PageBaseTenantProjectRequest;
309
+ const request = {
310
+ ...buildPageRequest(params),
311
+ tenant_id: toRequestTenantId(params.tenant_id)
312
+ } as PageBaseTenantProjectRequest;
279
313
  const data = await defBaseTenantProjectService.PageBaseTenantProject(request);
280
314
  const projects = data.base_tenant_projects ?? [];
281
315
  if (!props.loadExtraData || !projects.length) {
@@ -316,6 +350,7 @@ function resolveTenantProjectSlotContext(scope: Record<string, any>) {
316
350
  /** 打开项目编辑弹窗。 */
317
351
  async function handleOpenDialog(id?: number) {
318
352
  resetForm();
353
+ await loadTenantOptions();
319
354
  dialog.titleKey = id ? "common.action.edit_resource" : "common.action.create_resource";
320
355
  dialog.visible = true;
321
356
  if (id) Object.assign(formData, await defBaseTenantProjectService.GetBaseTenantProject({ id }));
@@ -332,6 +367,7 @@ function resetForm() {
332
367
  formDialogRef.value?.resetFields();
333
368
  formDialogRef.value?.clearValidate();
334
369
  formData.id = 0;
370
+ formData.tenant_id = undefined;
335
371
  formData.name = "";
336
372
  formData.code = "";
337
373
  formData.sort = 1;
@@ -344,7 +380,9 @@ function handleSubmit() {
344
380
  formDialogRef.value?.validate()?.then(valid => {
345
381
  if (!valid) return;
346
382
  const submitData = JSON.parse(JSON.stringify(formData)) as Partial<BaseTenantProjectForm>;
347
- delete submitData.tenant_id;
383
+ const tenantId = toRequestTenantId(formData.tenant_id);
384
+ if (tenantId === undefined) delete submitData.tenant_id;
385
+ else submitData.tenant_id = tenantId;
348
386
  const request = submitData.id
349
387
  ? defBaseTenantProjectService.UpdateBaseTenantProject({ base_tenant_project: submitData as BaseTenantProjectForm })
350
388
  : defBaseTenantProjectService.CreateBaseTenantProject({ base_tenant_project: submitData as BaseTenantProjectForm });
@@ -1,5 +1,11 @@
1
1
  import type { BaseTenantProject } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
2
2
 
3
+ /** 租户项目表格列的最小结构,用于在不依赖表格组件的情况下排列扩展列。 */
4
+ type TenantProjectColumnLike = {
5
+ prop?: string;
6
+ type?: string;
7
+ };
8
+
3
9
  /** 租户项目业务数据查询键。 */
4
10
  export interface TenantProjectKey {
5
11
  /** 租户 ID。 */
@@ -11,13 +17,45 @@ export interface TenantProjectKey {
11
17
  /** 租户项目扩展数据,外层键由租户 ID 和项目 ID 组成。 */
12
18
  export type TenantProjectExtraData = Record<string, Record<string, unknown>>;
13
19
 
20
+ /** 按基础字段位置排列租户项目管理的扩展列。 */
21
+ export function arrangeTenantProjectColumns<T extends TenantProjectColumnLike>(
22
+ baseColumns: T[],
23
+ extraColumns: Array<T & { after?: string }>
24
+ ): T[] {
25
+ const baseKeys = new Set(baseColumns.map(column => column.prop ?? column.type));
26
+ const extrasByAnchor = new Map<string | undefined, T[]>();
27
+ for (const extraColumn of extraColumns) {
28
+ const { after, ...column } = extraColumn;
29
+ const anchor = after && baseKeys.has(after) ? after : "remark";
30
+ const columns = extrasByAnchor.get(anchor) ?? [];
31
+ columns.push(column as T);
32
+ extrasByAnchor.set(anchor, columns);
33
+ }
34
+
35
+ const result: T[] = [];
36
+ for (const baseColumn of baseColumns) {
37
+ result.push(baseColumn);
38
+ const anchor = baseColumn.prop ?? baseColumn.type;
39
+ const columns = extrasByAnchor.get(anchor);
40
+ if (!columns) continue;
41
+ result.push(...columns);
42
+ extrasByAnchor.delete(anchor);
43
+ }
44
+ const remaining: T[] = [];
45
+ for (const columns of extrasByAnchor.values()) remaining.push(...columns);
46
+ return [...result, ...remaining];
47
+ }
48
+
14
49
  /** 创建租户项目扩展数据的稳定键。 */
15
50
  export function tenantProjectKey(tenantId: number, projectId: number): string {
16
51
  return `${tenantId}:${projectId}`;
17
52
  }
18
53
 
19
54
  /** 将外部业务字段合并到项目行,同时保护公共项目字段。 */
20
- export function mergeTenantProjectExtraData(project: BaseTenantProject, extra?: Record<string, unknown>): BaseTenantProject & Record<string, unknown> {
55
+ export function mergeTenantProjectExtraData(
56
+ project: BaseTenantProject,
57
+ extra?: Record<string, unknown>
58
+ ): BaseTenantProject & Record<string, unknown> {
21
59
  if (!extra) return { ...project } as BaseTenantProject & Record<string, unknown>;
22
60
 
23
61
  const protectedFields = new Set(["id", "tenant_id", "code", "name", "status", "sort", "remark", "created_at", "updated_at"]);
@@ -1,8 +1,17 @@
1
- import type { BaseTenantProject, PageBaseTenantProjectRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
1
+ import type {
2
+ BaseTenantProject,
3
+ PageBaseTenantProjectRequest
4
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
2
5
  import type { ColumnProps, TableActionProps } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
3
6
  import type { TenantProjectExtraData, TenantProjectKey } from "./tenant-project-manager-data";
4
7
 
5
- export { mergeTenantProjectExtraData, tenantProjectKey, type TenantProjectExtraData, type TenantProjectKey } from "./tenant-project-manager-data";
8
+ export {
9
+ arrangeTenantProjectColumns,
10
+ mergeTenantProjectExtraData,
11
+ tenantProjectKey,
12
+ type TenantProjectExtraData,
13
+ type TenantProjectKey
14
+ } from "./tenant-project-manager-data";
6
15
 
7
16
  /** 租户项目单条业务上下文。 */
8
17
  export interface TenantProjectContext<TData extends Record<string, unknown> = Record<string, unknown>> extends TenantProjectKey {
@@ -37,10 +46,16 @@ export type TenantProjectAction = Omit<TableActionProps, "disabled" | "hidden" |
37
46
  onClick: (context: TenantProjectContext, params?: Record<string, any>) => void | Promise<void>;
38
47
  };
39
48
 
49
+ /** 租户项目管理的扩展列配置。 */
50
+ export type TenantProjectExtraColumn = ColumnProps & {
51
+ /** 插入到指定基础字段后面,未配置时默认插入备注字段后面。 */
52
+ after?: string;
53
+ };
54
+
40
55
  /** 租户项目管理组件属性。 */
41
56
  export interface TenantProjectManagerProps {
42
- /** 追加到基础列表的表格列。 */
43
- extraColumns?: ColumnProps[];
57
+ /** 追加到基础列表的表格列,可通过 after 指定插入位置。 */
58
+ extraColumns?: TenantProjectExtraColumn[];
44
59
  /** 追加到基础操作列的业务操作。 */
45
60
  extraActions?: TenantProjectAction[];
46
61
  /** 加载当前页外部业务字段。 */
@@ -3,9 +3,11 @@ export type { AdminAiExtension } from "./ai";
3
3
  export { default as TenantProjectManager } from "./components/tenant-project/TenantProjectManager.vue";
4
4
  export {
5
5
  mergeTenantProjectExtraData,
6
+ arrangeTenantProjectColumns,
6
7
  tenantProjectKey,
7
8
  type TenantProjectAction,
8
9
  type TenantProjectContext,
10
+ type TenantProjectExtraColumn,
9
11
  type TenantProjectExtraData,
10
12
  type TenantProjectExtraDataLoadContext,
11
13
  type TenantProjectExtraDataLoader,
@@ -236,42 +236,7 @@ function setRecordText(recordText: string) {
236
236
 
237
237
  /** 标准化语音识别文本,压掉多余空白。 */
238
238
  function normalizeRecordText(recordText: string) {
239
- return collapseCumulativeRecordText(recordText.replace(/\s+/g, " ").trim());
240
- }
241
-
242
- /** 压缩 useRecord 在中文连续识别时返回的前缀累计文本。 */
243
- function collapseCumulativeRecordText(text: string) {
244
- if (text.length < 4) return text;
245
- for (let candidateLength = 2; candidateLength <= Math.floor(text.length / 2); candidateLength++) {
246
- const candidate = text.slice(-candidateLength);
247
- const matchResult = matchCumulativeRecordCandidate(text, candidate);
248
- if (matchResult.matched && (matchResult.hasPartial || matchResult.segmentCount >= 3)) {
249
- return candidate;
250
- }
251
- }
252
- return text;
253
- }
254
-
255
- /** 判断文本是否由同一识别结果的前缀片段累计组成。 */
256
- function matchCumulativeRecordCandidate(text: string, candidate: string) {
257
- let index = 0;
258
- let segmentCount = 0;
259
- let hasPartial = false;
260
- while (index < text.length) {
261
- let matchedLength = 0;
262
- const maxLength = Math.min(candidate.length, text.length - index);
263
- for (let length = maxLength; length >= 1; length--) {
264
- if (candidate.startsWith(text.slice(index, index + length))) {
265
- matchedLength = length;
266
- break;
267
- }
268
- }
269
- if (!matchedLength) return { matched: false, segmentCount, hasPartial };
270
- if (matchedLength < candidate.length) hasPartial = true;
271
- segmentCount++;
272
- index += matchedLength;
273
- }
274
- return { matched: true, segmentCount, hasPartial };
239
+ return recordText.replace(/\s+/g, " ").trim();
275
240
  }
276
241
 
277
242
  /** 根据浏览器语音识别错误类型生成用户可理解的提示。 */
@@ -5,6 +5,7 @@ export type SpeechResultList = ArrayLike<{
5
5
  0?: {
6
6
  transcript: string;
7
7
  };
8
+ isFinal?: boolean;
8
9
  length: number;
9
10
  }>;
10
11
 
@@ -17,6 +18,7 @@ export type SpeechRecognitionError = {
17
18
 
18
19
  type SpeechRecognitionResultEvent = {
19
20
  results: SpeechResultList;
21
+ resultIndex: number;
20
22
  };
21
23
 
22
24
  type SpeechRecognitionInstance = {
@@ -49,13 +51,28 @@ type SpeechRecognitionOptions = {
49
51
  onResult?: (text: string) => void;
50
52
  };
51
53
 
52
- /** 收集一次识别事件中的完整文本。 */
53
- export function collectSpeechResultText(results: SpeechResultList) {
54
- let text = "";
55
- for (let index = 0; index < results.length; index++) {
56
- text += results[index]?.[0]?.transcript ?? "";
54
+ /** 增量累积语音识别结果,避免连续模式下 interim 结果重复计入已确认文本。 */
55
+ export function accumulateSpeechResultText(
56
+ results: SpeechResultList,
57
+ resultIndex: number,
58
+ previousFinalText: string
59
+ ): { finalText: string; text: string } {
60
+ let finalText = previousFinalText;
61
+ let interimText = "";
62
+ for (let index = resultIndex; index < results.length; index++) {
63
+ const result = results[index];
64
+ const transcript = result?.[0]?.transcript ?? "";
65
+ if (result?.isFinal) {
66
+ finalText += transcript;
67
+ } else {
68
+ interimText += transcript;
69
+ }
70
+ }
71
+ // Chrome 连续模式下 interim 结果会累计包含已确认的 final 文本,去除重复前缀。
72
+ if (interimText && finalText && interimText.startsWith(finalText)) {
73
+ interimText = interimText.slice(finalText.length);
57
74
  }
58
- return text;
75
+ return { finalText, text: finalText + interimText };
59
76
  }
60
77
 
61
78
  /** 创建浏览器语音识别实例,并统一结果、状态和卸载处理。 */
@@ -64,6 +81,7 @@ export function useSpeechRecognition({ onError, onStart, onEnd, onResult }: Spee
64
81
  const value = ref("");
65
82
  let recognition: SpeechRecognitionInstance | null = null;
66
83
  let recognitionSession = 0;
84
+ let finalText = "";
67
85
 
68
86
  /** 获取当前浏览器支持的语音识别构造函数。 */
69
87
  function resolveRecognitionConstructor() {
@@ -84,6 +102,7 @@ export function useSpeechRecognition({ onError, onStart, onEnd, onResult }: Spee
84
102
  const nextRecognition = new RecognitionConstructor();
85
103
  const currentSession = ++recognitionSession;
86
104
  recognition = nextRecognition;
105
+ finalText = "";
87
106
  const isCurrentRecognition = () => recognition === nextRecognition && recognitionSession === currentSession;
88
107
  nextRecognition.continuous = true;
89
108
  nextRecognition.interimResults = true;
@@ -96,7 +115,9 @@ export function useSpeechRecognition({ onError, onStart, onEnd, onResult }: Spee
96
115
  };
97
116
  nextRecognition.onresult = event => {
98
117
  if (!isCurrentRecognition()) return;
99
- value.value = collectSpeechResultText(event.results);
118
+ const accumulated = accumulateSpeechResultText(event.results, event.resultIndex, finalText);
119
+ finalText = accumulated.finalText;
120
+ value.value = accumulated.text;
100
121
  onResult?.(value.value);
101
122
  };
102
123
  nextRecognition.onerror = error => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-system",
3
3
  "private": false,
4
- "version": "0.0.51",
4
+ "version": "0.0.52",
5
5
  "type": "module",
6
6
  "description": "kratos-admin system management module",
7
7
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "sortablejs": "^1.15.2",
17
17
  "swagger-ui-dist": "^5.32.15",
18
18
  "vue-element-plus-x": "^2.0.3",
19
- "@liujitcn/kratos-admin-core": "0.0.51"
19
+ "@liujitcn/kratos-admin-core": "0.0.52"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@element-plus/icons-vue": "^2.3.1",