@liujitcn/kratos-admin-system 0.0.49 → 0.0.50

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 (94) hide show
  1. package/README.md +44 -8
  2. package/dist/package/declarations/src/api/system/admin/v1/base_i18n_custom.d.ts +18 -0
  3. package/dist/package/declarations/src/api/system/admin/v1/base_tenant.d.ts +2 -2
  4. package/dist/package/declarations/src/api/system/admin/v1/base_tenant_project.d.ts +23 -0
  5. package/dist/package/declarations/src/api/system/admin/v1/base_tenant_project_grant.d.ts +16 -0
  6. package/dist/package/declarations/src/components/tenant-project/ProjectGrantDialog.vue.d.ts +8 -0
  7. package/dist/package/declarations/src/components/tenant-project/TenantProjectManager.vue.d.ts +29 -0
  8. package/dist/package/declarations/src/components/tenant-project/TenantProjectSelect.vue.d.ts +31 -0
  9. package/dist/package/declarations/src/components/tenant-project/TenantProjectText.vue.d.ts +19 -0
  10. package/dist/package/declarations/src/components/tenant-project/tenant-project-manager-data.d.ts +14 -0
  11. package/dist/package/declarations/src/components/tenant-project/tenant-project-manager.d.ts +40 -0
  12. package/dist/package/declarations/src/index.d.ts +2 -0
  13. package/dist/package/declarations/src/locales/generated.d.ts +172 -0
  14. package/dist/package/declarations/src/rpc/base/v1/config.d.ts +11 -0
  15. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n_custom.d.ts +105 -0
  16. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +10 -1
  17. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant_project.d.ts +141 -0
  18. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant_project_grant.d.ts +105 -0
  19. package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +2 -2
  20. package/dist/package/declarations/src/views/base/i18n-custom/index.vue.d.ts +3 -0
  21. package/dist/package/declarations/src/views/base/project-grant/index.vue.d.ts +3 -0
  22. package/dist/package/declarations/src/views/base/tenant-project/index.vue.d.ts +3 -0
  23. package/dist/package/src/api/system/admin/v1/base_i18n_custom.ts +72 -0
  24. package/dist/package/src/api/system/admin/v1/base_tenant.ts +3 -2
  25. package/dist/package/src/api/system/admin/v1/base_tenant_project.ts +94 -0
  26. package/dist/package/src/api/system/admin/v1/base_tenant_project_grant.ts +63 -0
  27. package/dist/package/src/components/tenant-project/ProjectGrantDialog.vue +108 -0
  28. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +452 -0
  29. package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +83 -0
  30. package/dist/package/src/components/tenant-project/TenantProjectText.vue +37 -0
  31. package/dist/package/src/components/tenant-project/tenant-project-manager-data.ts +26 -0
  32. package/dist/package/src/components/tenant-project/tenant-project-manager.ts +48 -0
  33. package/dist/package/src/index.ts +12 -0
  34. package/dist/package/src/locales/README.md +1 -1
  35. package/dist/package/src/locales/en-US.json +45 -2
  36. package/dist/package/src/locales/ja-JP.json +45 -2
  37. package/dist/package/src/locales/zh-CN.json +45 -2
  38. package/dist/package/src/locales/zh-TW.json +45 -2
  39. package/dist/package/src/module.ts +3 -3
  40. package/dist/package/src/rpc/base/v1/config.ts +12 -0
  41. package/dist/package/src/rpc/system/admin/v1/base_i18n_custom.ts +126 -0
  42. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +11 -1
  43. package/dist/package/src/rpc/system/admin/v1/base_tenant_project.ts +166 -0
  44. package/dist/package/src/rpc/system/admin/v1/base_tenant_project_grant.ts +128 -0
  45. package/dist/package/src/views/ai/chat/components/XSender.vue +4 -3
  46. package/dist/package/src/views/base/api-log/index.vue +3 -3
  47. package/dist/package/src/views/base/config/index.vue +3 -3
  48. package/dist/package/src/views/base/data-access-log/index.vue +3 -3
  49. package/dist/package/src/views/base/dept/index.vue +10 -45
  50. package/dist/package/src/views/base/dict/index.vue +3 -3
  51. package/dist/package/src/views/base/dict/item.vue +3 -3
  52. package/dist/package/src/views/base/file/index.vue +12 -3
  53. package/dist/package/src/views/base/i18n-custom/index.vue +321 -0
  54. package/dist/package/src/views/base/job/index.vue +3 -3
  55. package/dist/package/src/views/base/login-log/index.vue +13 -4
  56. package/dist/package/src/views/base/login-policy/index.vue +97 -34
  57. package/dist/package/src/views/base/menu/index.vue +3 -3
  58. package/dist/package/src/views/base/message/index.vue +9 -32
  59. package/dist/package/src/views/base/message-category/index.vue +2 -4
  60. package/dist/package/src/views/base/oauth-client/index.vue +8 -42
  61. package/dist/package/src/views/base/online-session/index.vue +8 -27
  62. package/dist/package/src/views/base/operation-log/index.vue +3 -3
  63. package/dist/package/src/views/base/permission-log/index.vue +3 -3
  64. package/dist/package/src/views/base/policy-evaluation-log/index.vue +3 -3
  65. package/dist/package/src/views/base/post/index.vue +9 -42
  66. package/dist/package/src/views/base/project-grant/index.vue +336 -0
  67. package/dist/package/src/views/base/role/index.vue +13 -47
  68. package/dist/package/src/views/base/tenant/index.vue +163 -19
  69. package/dist/package/src/views/base/tenant-project/index.vue +12 -0
  70. package/dist/package/src/views/base/user/index.vue +30 -51
  71. package/dist/package/src/views/profile/components/login-log.vue +15 -2
  72. package/dist/package/src/views/profile/components/password.vue +1 -1
  73. package/dist/package/src/views/profile/components/security.vue +2 -2
  74. package/dist/package/src/views/tool/code-gen/components/CodeGenLocaleEditor.vue +2 -2
  75. package/dist/package/src/views/tool/code-gen/config.ts +2 -1
  76. package/package.json +2 -2
  77. /package/dist/package/declarations/src/components/{ChangePasswordForm.vue.d.ts → account/ChangePasswordForm.vue.d.ts} +0 -0
  78. /package/dist/package/declarations/src/components/{ForcedPasswordDialog.vue.d.ts → account/ForcedPasswordDialog.vue.d.ts} +0 -0
  79. /package/dist/package/declarations/src/components/{Ai.vue.d.ts → ai/Ai.vue.d.ts} +0 -0
  80. /package/dist/package/declarations/src/components/{DynamicI18nCell.vue.d.ts → i18n/DynamicI18nCell.vue.d.ts} +0 -0
  81. /package/dist/package/declarations/src/components/{DynamicI18nEditor.vue.d.ts → i18n/DynamicI18nEditor.vue.d.ts} +0 -0
  82. /package/dist/package/declarations/src/components/{dynamicI18n.d.ts → i18n/dynamicI18n.d.ts} +0 -0
  83. /package/dist/package/declarations/src/components/{LogTable.vue.d.ts → log/LogTable.vue.d.ts} +0 -0
  84. /package/dist/package/declarations/src/components/{log.d.ts → log/log.d.ts} +0 -0
  85. /package/dist/package/declarations/src/components/{Notification.vue.d.ts → notification/Notification.vue.d.ts} +0 -0
  86. /package/dist/package/src/components/{ChangePasswordForm.vue → account/ChangePasswordForm.vue} +0 -0
  87. /package/dist/package/src/components/{ForcedPasswordDialog.vue → account/ForcedPasswordDialog.vue} +0 -0
  88. /package/dist/package/src/components/{Ai.vue → ai/Ai.vue} +0 -0
  89. /package/dist/package/src/components/{DynamicI18nCell.vue → i18n/DynamicI18nCell.vue} +0 -0
  90. /package/dist/package/src/components/{DynamicI18nEditor.vue → i18n/DynamicI18nEditor.vue} +0 -0
  91. /package/dist/package/src/components/{dynamicI18n.ts → i18n/dynamicI18n.ts} +0 -0
  92. /package/dist/package/src/components/{LogTable.vue → log/LogTable.vue} +0 -0
  93. /package/dist/package/src/components/{log.ts → log/log.ts} +0 -0
  94. /package/dist/package/src/components/{Notification.vue → notification/Notification.vue} +0 -0
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <FormDialog
3
+ v-model="visible"
4
+ :title="t('system.base.tenant_project.grant')"
5
+ :model="form"
6
+ :fields="fields"
7
+ :rules="rules"
8
+ width="640px"
9
+ @confirm="save"
10
+ @close="reset"
11
+ />
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed, reactive, ref, watch } from "vue";
16
+ import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
17
+ import type { ProFormField } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
18
+ import { t } from "@liujitcn/kratos-admin-core";
19
+ import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
20
+ import { defBaseTenantProjectService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_tenant_project";
21
+ import { defBaseTenantProjectGrantService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_tenant_project_grant";
22
+ import type {
23
+ BaseTenantProjectGrant,
24
+ BaseTenantProjectGrantSubjectType
25
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project_grant";
26
+ import type { SelectOptionResponse_Option } from "@liujitcn/kratos-admin-system/rpc/common/v1/common";
27
+
28
+ const visible = ref(false);
29
+ const loading = ref(false);
30
+ const projectOptions = ref<SelectOptionResponse_Option[]>([]);
31
+ const form = reactive({ tenant_id: 0, all: false, project_id: [] as number[] });
32
+ const { tenantFormField, loadTenantOptions } = useTenantScope();
33
+ let subjectType: BaseTenantProjectGrantSubjectType;
34
+ let subjectId = 0;
35
+ let revision = 0;
36
+ const rules = computed(() => ({ tenant_id: [{ required: true, type: "number", min: 1, message: t("common.validation.required_select", { field: t("common.field.tenant") }), trigger: "change" }] }));
37
+ const fields = computed<ProFormField[]>(() => [
38
+ tenantFormField({ label: t("common.field.tenant") }),
39
+ { prop: "all", label: t("system.base.tenant_project.all"), component: "switch", props: { disabled: loading.value } },
40
+ { prop: "project_id", label: t("system.base.tenant_project.projects"), component: "select", options: projectOptions.value, visible: () => !form.all, props: { multiple: true, filterable: true, disabled: loading.value } }
41
+ ]);
42
+
43
+ /** 打开指定主体的授权表单,目标租户切换后重新加载直接授权。 */
44
+ async function open(type: BaseTenantProjectGrantSubjectType, id: number, tenantId: number) {
45
+ reset();
46
+ subjectType = type;
47
+ subjectId = id;
48
+ await loadTenantOptions(true);
49
+ form.tenant_id = tenantId;
50
+ visible.value = true;
51
+ await load();
52
+ }
53
+
54
+ /** 加载目标租户项目和当前主体的直接授权,避免快速切换时旧响应覆盖新结果。 */
55
+ async function load() {
56
+ const current = ++revision;
57
+ form.all = false;
58
+ form.project_id = [];
59
+ projectOptions.value = [];
60
+ if (!visible.value || form.tenant_id <= 0) return;
61
+ loading.value = true;
62
+ try {
63
+ const request = { tenant_id: form.tenant_id, subject_type: subjectType, subject_id: subjectId };
64
+ const [options, grant] = await Promise.all([
65
+ defBaseTenantProjectService.OptionBaseTenantProject({ tenant_id: request.tenant_id }),
66
+ defBaseTenantProjectGrantService.GetBaseTenantProjectGrant(request)
67
+ ]);
68
+ if (current !== revision) return;
69
+ projectOptions.value = options.list ?? [];
70
+ form.all = grant.project_id.length === 1 && grant.project_id[0] === 0;
71
+ form.project_id = form.all ? [] : grant.project_id;
72
+ } finally {
73
+ if (current === revision) loading.value = false;
74
+ }
75
+ }
76
+
77
+ /** 保存当前来源的授权,其他三个来源保持独立。 */
78
+ async function save() {
79
+ if (loading.value) return;
80
+ const grant: BaseTenantProjectGrant = {
81
+ tenant_id: form.tenant_id,
82
+ subject_type: subjectType,
83
+ subject_id: subjectId,
84
+ project_id: form.all ? [0] : form.project_id,
85
+ tenant_name: "",
86
+ subject_name: "",
87
+ subject_code: "",
88
+ project_names: [],
89
+ grant_key: ""
90
+ };
91
+ await defBaseTenantProjectGrantService.UpdateBaseTenantProjectGrant({ base_tenant_project_grant: grant });
92
+ visible.value = false;
93
+ }
94
+
95
+ /** 关闭后清空主体及项目,阻止未完成请求覆盖下次表单。 */
96
+ function reset() {
97
+ ++revision;
98
+ visible.value = false;
99
+ subjectId = 0;
100
+ form.tenant_id = 0;
101
+ form.all = false;
102
+ form.project_id = [];
103
+ loading.value = false;
104
+ }
105
+
106
+ watch(() => form.tenant_id, () => { if (visible.value) void load(); });
107
+ defineExpose({ open });
108
+ </script>
@@ -0,0 +1,452 @@
1
+ <template>
2
+ <div class="table-box">
3
+ <ProTable
4
+ ref="proTable"
5
+ :key="isDefaultTenant ? 'default-tenant' : 'normal-tenant'"
6
+ row-key="id"
7
+ :columns="columns"
8
+ :header-actions="headerActions"
9
+ :request-api="requestBaseTenantProjectTable"
10
+ >
11
+ <template v-for="slotName in forwardedSlotNames" #[slotName]="scope">
12
+ <slot :name="slotName" v-bind="scope" :tenant-project="resolveTenantProjectSlotContext(scope)" />
13
+ </template>
14
+ </ProTable>
15
+
16
+ <FormDialog
17
+ v-model="dialog.visible"
18
+ ref="formDialogRef"
19
+ :title="t(dialog.titleKey, { resource: t('common.field.project') })"
20
+ width="560px"
21
+ :model="formData"
22
+ :fields="formFields"
23
+ :rules="rules"
24
+ @confirm="handleSubmit"
25
+ @close="handleCloseDialog"
26
+ />
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed, onMounted, reactive, ref, useSlots } from "vue";
32
+ import { ElMessage, ElMessageBox } from "element-plus";
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";
35
+ import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
36
+ import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
37
+ import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
38
+ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
39
+ 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";
41
+ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
42
+ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
43
+ import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
44
+ import { t } from "@liujitcn/kratos-admin-core";
45
+ import {
46
+ mergeTenantProjectExtraData,
47
+ tenantProjectKey,
48
+ type TenantProjectAction,
49
+ type TenantProjectContext,
50
+ type TenantProjectExtraData,
51
+ type TenantProjectManagerProps
52
+ } from "./tenant-project-manager";
53
+
54
+ defineOptions({
55
+ name: "TenantProjectManager",
56
+ inheritAttrs: false
57
+ });
58
+
59
+ const props = withDefaults(defineProps<TenantProjectManagerProps>(), {
60
+ extraColumns: () => [],
61
+ extraActions: () => []
62
+ });
63
+
64
+ const emit = defineEmits<{
65
+ /** 外部业务字段加载失败。 */
66
+ "extra-data-error": [error: unknown];
67
+ }>();
68
+
69
+ /** 项目表单状态,新增时租户由默认租户管理员选择。 */
70
+ type BaseTenantProjectFormState = Omit<BaseTenantProjectForm, "tenant_id"> & {
71
+ /** 租户ID。 */
72
+ tenant_id?: number;
73
+ };
74
+
75
+ const { BUTTONS } = useAuthButtons();
76
+ const slots = useSlots();
77
+ const forwardedSlotNames = computed(() => Object.keys(slots).filter(name => name !== "default"));
78
+ const proTable = ref<ProTableInstance>();
79
+ const formDialogRef = ref<InstanceType<typeof FormDialog>>();
80
+ const extraDataByProject = ref<TenantProjectExtraData>({});
81
+ let extraDataRequestRevision = 0;
82
+
83
+ const dialog = reactive({
84
+ titleKey: "common.action.create_resource",
85
+ visible: false
86
+ });
87
+ const statusOptions = computed<ProFormOption[]>(() => [
88
+ { label: t("common.status.enabled"), value: Status.STATUS_ENABLE },
89
+ { label: t("common.status.disabled"), value: Status.STATUS_DISABLE }
90
+ ]);
91
+ const formData = reactive<BaseTenantProjectFormState>({
92
+ /** 项目ID。 */
93
+ id: 0,
94
+ /** 租户ID。 */
95
+ tenant_id: undefined,
96
+ /** 项目名称。 */
97
+ name: "",
98
+ /** 项目编号。 */
99
+ code: "",
100
+ /** 显示顺序。 */
101
+ sort: 1,
102
+ /** 状态。 */
103
+ status: Status.STATUS_ENABLE,
104
+ /** 备注。 */
105
+ remark: ""
106
+ });
107
+ const rules = computed(() => ({
108
+ tenant_id: [
109
+ {
110
+ required: true,
111
+ message: t("common.validation.required_select", { field: t("common.field.tenant") }),
112
+ trigger: "change"
113
+ }
114
+ ],
115
+ name: [
116
+ {
117
+ required: true,
118
+ message: t("common.validation.required_input", { field: t("system.base.tenant_project.field.name") }),
119
+ trigger: "blur"
120
+ },
121
+ {
122
+ max: 100,
123
+ message: t("common.validation.max_length", { field: t("system.base.tenant_project.field.name"), max: 100 }),
124
+ trigger: "blur"
125
+ }
126
+ ],
127
+ code: [
128
+ {
129
+ required: true,
130
+ message: t("common.validation.required_input", { field: t("system.base.tenant_project.field.code") }),
131
+ trigger: "blur"
132
+ },
133
+ {
134
+ max: 50,
135
+ message: t("common.validation.max_length", { field: t("system.base.tenant_project.field.code"), max: 50 }),
136
+ trigger: "blur"
137
+ }
138
+ ],
139
+ sort: [{ required: true, type: "number", min: 1, message: t("common.validation.sort_positive"), trigger: "blur" }],
140
+ status: [
141
+ {
142
+ required: true,
143
+ message: t("common.validation.required_select", { field: t("common.field.status") }),
144
+ trigger: "change"
145
+ }
146
+ ],
147
+ remark: [
148
+ {
149
+ max: 500,
150
+ message: t("common.validation.max_length", { field: t("common.field.remark"), max: 500 }),
151
+ trigger: "blur"
152
+ }
153
+ ]
154
+ }));
155
+
156
+ const { isDefaultTenant, tenantColumns, tenantFormField, toRequestTenantId, loadTenantOptions } = useTenantScope();
157
+ onMounted(() => {
158
+ if (isDefaultTenant.value) void loadTenantOptions();
159
+ });
160
+
161
+ /** 项目表单字段配置。 */
162
+ const formFields = computed<ProFormField[]>(() => [
163
+ tenantFormField({ label: t("common.field.tenant"), disabledOnEdit: true }),
164
+ {
165
+ prop: "name",
166
+ label: t("system.base.tenant_project.field.name"),
167
+ component: "input",
168
+ props: { placeholder: t("common.validation.required_input", { field: t("system.base.tenant_project.field.name") }) }
169
+ },
170
+ {
171
+ prop: "code",
172
+ label: t("system.base.tenant_project.field.code"),
173
+ component: "input",
174
+ props: { placeholder: t("common.validation.required_input", { field: t("system.base.tenant_project.field.code") }) }
175
+ },
176
+ {
177
+ prop: "sort",
178
+ label: t("common.field.sort"),
179
+ component: "input-number",
180
+ props: { min: 1, precision: 0, step: 1, controlsPosition: "right", style: { width: "100%" } }
181
+ },
182
+ { prop: "status", label: t("common.field.status"), component: "radio-group", options: statusOptions.value },
183
+ {
184
+ prop: "remark",
185
+ label: t("common.field.remark"),
186
+ component: "textarea",
187
+ props: { placeholder: t("common.placeholder.remark") }
188
+ }
189
+ ]);
190
+
191
+ /** 将项目行转换成外部业务模块可用的单条上下文。 */
192
+ function toTenantProjectContext(row: BaseTenantProject): TenantProjectContext {
193
+ const key = tenantProjectKey(row.tenant_id, row.id);
194
+ return {
195
+ tenant_id: row.tenant_id,
196
+ project_id: row.id,
197
+ project: row,
198
+ data: extraDataByProject.value[key] ?? {}
199
+ };
200
+ }
201
+
202
+ /** 将租户项目扩展操作适配成 ProTable 操作。 */
203
+ function toTableAction(action: TenantProjectAction): TableActionProps {
204
+ const { disabled, hidden, params, onClick, ...rest } = action;
205
+ const result: TableActionProps = {
206
+ ...rest,
207
+ onClick: (scope, resolvedParams) => onClick(toTenantProjectContext(scope.row as BaseTenantProject), resolvedParams)
208
+ };
209
+ if (disabled !== undefined) result.disabled = scope => resolveProjectBoolean(disabled, scope);
210
+ if (hidden !== undefined) result.hidden = scope => resolveProjectBoolean(hidden, scope);
211
+ if (params) result.params = scope => params(toTenantProjectContext(scope.row as BaseTenantProject));
212
+ return result;
213
+ }
214
+
215
+ /** 解析项目扩展操作的布尔配置。 */
216
+ function resolveProjectBoolean(value: boolean | ((context: TenantProjectContext) => boolean), scope: RenderScope): boolean {
217
+ return typeof value === "function" ? value(toTenantProjectContext(scope.row as BaseTenantProject)) : value;
218
+ }
219
+
220
+ /** 项目表格列配置。 */
221
+ const columns = computed<ColumnProps[]>(() => [
222
+ { type: "selection", width: 55 },
223
+ ...tenantColumns({ label: t("common.field.tenant"), order: 1 }),
224
+ { prop: "name", label: t("system.base.tenant_project.field.name"), minWidth: 180, search: { el: "input" } },
225
+ { prop: "code", label: t("system.base.tenant_project.field.code"), minWidth: 140, search: { el: "input" } },
226
+ { prop: "sort", label: t("common.field.sort"), minWidth: 90, align: "right" },
227
+ {
228
+ prop: "status",
229
+ label: t("common.field.status"),
230
+ minWidth: 100,
231
+ search: { el: "select" },
232
+ cellType: "status",
233
+ statusProps: {
234
+ activeValue: Status.STATUS_ENABLE,
235
+ inactiveValue: Status.STATUS_DISABLE,
236
+ activeText: t("common.status.enabled"),
237
+ inactiveText: t("common.status.disabled"),
238
+ disabled: () => !BUTTONS.value["base:tenant:project:status"],
239
+ beforeChange: scope => handleBeforeSetStatus(scope.row as BaseTenantProject)
240
+ }
241
+ },
242
+ { prop: "remark", label: t("common.field.remark"), minWidth: 160 },
243
+ ...props.extraColumns,
244
+ { prop: "created_at", align: "center", label: t("common.field.created_at"), minWidth: 180 },
245
+ { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 180, align: "center" },
246
+ {
247
+ prop: "operation",
248
+ label: t("common.field.operation"),
249
+ cellType: "actions",
250
+ actions: [
251
+ ...props.extraActions.map(toTableAction),
252
+ {
253
+ label: t("common.action.edit"),
254
+ type: "primary",
255
+ link: true,
256
+ icon: EditPen,
257
+ hidden: () => !BUTTONS.value["base:tenant:project:update"],
258
+ params: scope => ({ projectId: scope.row.id }),
259
+ onClick: (scope, params) => handleOpenDialog((params?.projectId as number | undefined) ?? (scope.row as BaseTenantProject).id)
260
+ },
261
+ {
262
+ label: t("common.action.delete"),
263
+ type: "danger",
264
+ link: true,
265
+ icon: Delete,
266
+ hidden: () => !BUTTONS.value["base:tenant:project:delete"],
267
+ onClick: scope => handleDelete(scope.row as BaseTenantProject)
268
+ }
269
+ ]
270
+ }
271
+ ]);
272
+
273
+ /** 项目顶部按钮配置。 */
274
+ const headerActions = computed<HeaderActionProps[]>(() => [
275
+ {
276
+ label: t("common.action.create"),
277
+ type: "success",
278
+ icon: CirclePlus,
279
+ hidden: () => !BUTTONS.value["base:tenant:project:create"],
280
+ onClick: () => handleOpenDialog()
281
+ },
282
+ {
283
+ label: t("common.action.delete"),
284
+ type: "danger",
285
+ icon: Delete,
286
+ hidden: () => !BUTTONS.value["base:tenant:project:delete"],
287
+ disabled: scope => !scope.selectedList.length,
288
+ onClick: scope => handleDelete(scope.selectedList as BaseTenantProject[])
289
+ }
290
+ ]);
291
+
292
+ /** 请求项目分页列表并加载当前页外部业务字段。 */
293
+ async function requestBaseTenantProjectTable(params: PageBaseTenantProjectRequest) {
294
+ const revision = ++extraDataRequestRevision;
295
+ const request = {
296
+ ...buildPageRequest(params),
297
+ tenant_id: toRequestTenantId(params.tenant_id)
298
+ } as PageBaseTenantProjectRequest;
299
+ const data = await defBaseTenantProjectService.PageBaseTenantProject(request);
300
+ const projects = data.base_tenant_projects ?? [];
301
+ if (!props.loadExtraData || !projects.length) {
302
+ if (revision === extraDataRequestRevision) extraDataByProject.value = {};
303
+ return { data: { list: projects, total: data.total } };
304
+ }
305
+
306
+ try {
307
+ const extraData = (await props.loadExtraData({
308
+ projects: projects.map(row => ({ tenant_id: row.tenant_id, project_id: row.id })),
309
+ query: request
310
+ })) ?? {};
311
+ if (revision === extraDataRequestRevision) extraDataByProject.value = extraData;
312
+ return {
313
+ data: {
314
+ list: projects.map(row => mergeTenantProjectExtraData(row, extraData[tenantProjectKey(row.tenant_id, row.id)])),
315
+ total: data.total
316
+ }
317
+ };
318
+ } catch (error) {
319
+ if (revision === extraDataRequestRevision) extraDataByProject.value = {};
320
+ emit("extra-data-error", error);
321
+ return { data: { list: projects, total: data.total } };
322
+ }
323
+ }
324
+
325
+ /** 刷新项目表格。 */
326
+ async function refreshTable() {
327
+ await proTable.value?.getTableList();
328
+ }
329
+
330
+ /** 将表格插槽作用域补充为项目上下文。 */
331
+ function resolveTenantProjectSlotContext(scope: Record<string, any>) {
332
+ const row = scope?.row as BaseTenantProject | undefined;
333
+ return row?.id && row.tenant_id ? toTenantProjectContext(row) : undefined;
334
+ }
335
+
336
+ /** 打开项目编辑弹窗。 */
337
+ async function handleOpenDialog(id?: number) {
338
+ resetForm();
339
+ await loadTenantOptions();
340
+ dialog.titleKey = id ? "common.action.edit_resource" : "common.action.create_resource";
341
+ dialog.visible = true;
342
+ if (id) Object.assign(formData, await defBaseTenantProjectService.GetBaseTenantProject({ id }));
343
+ }
344
+
345
+ /** 关闭项目弹窗并清理表单。 */
346
+ function handleCloseDialog() {
347
+ dialog.visible = false;
348
+ resetForm();
349
+ }
350
+
351
+ /** 重置项目表单。 */
352
+ function resetForm() {
353
+ formDialogRef.value?.resetFields();
354
+ formDialogRef.value?.clearValidate();
355
+ formData.id = 0;
356
+ formData.tenant_id = undefined;
357
+ formData.name = "";
358
+ formData.code = "";
359
+ formData.sort = 1;
360
+ formData.status = Status.STATUS_ENABLE;
361
+ formData.remark = "";
362
+ }
363
+
364
+ /** 提交项目表单。 */
365
+ function handleSubmit() {
366
+ formDialogRef.value?.validate()?.then(valid => {
367
+ if (!valid) return;
368
+ const submitData = JSON.parse(JSON.stringify(formData)) as BaseTenantProjectForm;
369
+ const request = submitData.id
370
+ ? defBaseTenantProjectService.UpdateBaseTenantProject({ base_tenant_project: submitData })
371
+ : defBaseTenantProjectService.CreateBaseTenantProject({ base_tenant_project: submitData });
372
+ request.then(() => {
373
+ ElMessage.success(
374
+ t(submitData.id ? "common.message.update_success" : "common.message.create_success", {
375
+ resource: t("common.field.project")
376
+ })
377
+ );
378
+ handleCloseDialog();
379
+ void refreshTable();
380
+ });
381
+ });
382
+ }
383
+
384
+ /** 在项目状态切换前确认并提交。 */
385
+ async function handleBeforeSetStatus(row: BaseTenantProject) {
386
+ const nextStatus = row.status === Status.STATUS_ENABLE ? Status.STATUS_DISABLE : Status.STATUS_ENABLE;
387
+ const text = t(nextStatus === Status.STATUS_ENABLE ? "common.status.enabled" : "common.status.disabled");
388
+ try {
389
+ await ElMessageBox.confirm(
390
+ t("common.dialog.status_change", {
391
+ action: text,
392
+ resource: t("common.field.project"),
393
+ field: t("system.base.tenant_project.field.name"),
394
+ value: row.name || row.code || `ID:${row.id}`
395
+ }),
396
+ t("common.title.notice"),
397
+ {
398
+ confirmButtonText: t("common.action.confirm"),
399
+ cancelButtonText: t("common.action.cancel"),
400
+ type: "warning"
401
+ }
402
+ );
403
+ await defBaseTenantProjectService.SetBaseTenantProjectStatus({ id: row.id, status: nextStatus });
404
+ ElMessage.success(t("common.message.status_success", { action: text }));
405
+ await refreshTable();
406
+ return true;
407
+ } catch {
408
+ return false;
409
+ }
410
+ }
411
+
412
+ /** 删除项目,兼容单条删除与批量删除。 */
413
+ function handleDelete(selected?: number | string | Array<number | string> | BaseTenantProject | BaseTenantProject[]) {
414
+ const projectList = Array.isArray(selected)
415
+ ? (selected.filter(item => typeof item === "object") as BaseTenantProject[])
416
+ : selected && typeof selected === "object"
417
+ ? [selected as BaseTenantProject]
418
+ : [];
419
+ const projectIds = (
420
+ projectList.length ? projectList.map(item => item.id) : normalizeSelectedIds(selected as number | string | Array<number | string>)
421
+ ).join(",");
422
+ if (!projectIds) {
423
+ ElMessage.warning(t("common.message.select_delete_item"));
424
+ return;
425
+ }
426
+ const confirmMessage =
427
+ projectList.length === 1
428
+ ? `${t("common.dialog.delete_single", { resource: t("common.field.project") })}\n${t("common.dialog.resource_field", { field: t("system.base.tenant_project.field.name"), value: projectList[0].name || projectList[0].code || `ID:${projectList[0].id}` })}`
429
+ : projectList.length > 1
430
+ ? t("common.dialog.delete_batch", { count: projectList.length, unit: "", resource: t("common.field.project") })
431
+ : t("common.dialog.delete_selected", { resource: t("common.field.project") });
432
+ ElMessageBox.confirm(confirmMessage, t("common.title.warning"), {
433
+ confirmButtonText: t("common.action.confirm"),
434
+ cancelButtonText: t("common.action.cancel"),
435
+ type: "warning"
436
+ }).then(
437
+ () =>
438
+ defBaseTenantProjectService.DeleteBaseTenantProject({ id: projectIds }).then(() => {
439
+ ElMessage.success(t("common.message.delete_success", { resource: t("common.field.project") }));
440
+ void refreshTable();
441
+ }),
442
+ () => ElMessage.info(t("common.dialog.cancel_delete", { resource: t("common.field.project") }))
443
+ );
444
+ }
445
+
446
+ defineExpose({
447
+ /** 刷新项目列表。 */
448
+ refresh: refreshTable,
449
+ /** 兼容表格实例的刷新方法名称。 */
450
+ getTableList: refreshTable
451
+ });
452
+ </script>
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <el-tree-select
3
+ v-model="selectedValue"
4
+ :data="treeOptions"
5
+ :props="treeProps"
6
+ node-key="value"
7
+ check-strictly
8
+ filterable
9
+ clearable
10
+ render-after-expand
11
+ v-bind="$attrs"
12
+ />
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed, onMounted, ref } from "vue";
17
+ import { defBaseTenantProjectService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_tenant_project";
18
+ import type { TreeBaseTenantProjectResponse_Option } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_tenant_project";
19
+
20
+ defineOptions({ name: "TenantProjectSelect", inheritAttrs: false });
21
+
22
+ /** 租户项目选择值。 */
23
+ export interface TenantProjectSelection {
24
+ /** 节点唯一值。 */
25
+ value?: string;
26
+ /** 目标租户ID。 */
27
+ tenant_id?: number;
28
+ /** 目标项目ID。 */
29
+ project_id?: number;
30
+ /** 节点类型。 */
31
+ type?: "tenant" | "project";
32
+ }
33
+
34
+ /** 租户项目选择组件属性。 */
35
+ interface TenantProjectSelectProps {
36
+ /** 当前选中的租户项目。 */
37
+ modelValue?: TenantProjectSelection;
38
+ /** 外部传入的租户项目树。 */
39
+ options?: TreeBaseTenantProjectResponse_Option[];
40
+ }
41
+
42
+ const props = withDefaults(defineProps<TenantProjectSelectProps>(), {
43
+ modelValue: undefined,
44
+ options: undefined
45
+ });
46
+ const emit = defineEmits<{
47
+ /** 更新租户项目选择。 */
48
+ (event: "update:modelValue", value: TenantProjectSelection | undefined): void;
49
+ /** 租户项目选择变化。 */
50
+ (event: "change", value: TenantProjectSelection | undefined): void;
51
+ }>();
52
+
53
+ const loadedOptions = ref<TreeBaseTenantProjectResponse_Option[]>([]);
54
+ const treeOptions = computed(() => props.options ?? loadedOptions.value);
55
+ const treeProps = { label: "label", value: "value", children: "children" };
56
+
57
+ const selectedValue = computed<string | undefined>({
58
+ get: () => props.modelValue?.value,
59
+ set: value => {
60
+ const selection = parseSelection(value);
61
+ emit("update:modelValue", selection);
62
+ emit("change", selection);
63
+ }
64
+ });
65
+
66
+ onMounted(async () => {
67
+ if (props.options) return;
68
+ const response = await defBaseTenantProjectService.TreeBaseTenantProject({ keyword: "" });
69
+ loadedOptions.value = response.list ?? [];
70
+ });
71
+
72
+ function parseSelection(value?: string): TenantProjectSelection | undefined {
73
+ if (!value) return undefined;
74
+ const [type, tenantId, projectId] = value.split(":");
75
+ if (type === "tenant") {
76
+ return { value, type, tenant_id: Number(tenantId) };
77
+ }
78
+ if (type === "project") {
79
+ return { value, type, tenant_id: Number(tenantId), project_id: Number(projectId) };
80
+ }
81
+ return { value };
82
+ }
83
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <span>{{ label }}</span>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from "vue";
7
+ import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
8
+ import type { SelectOptionResponse_Option } from "@liujitcn/kratos-admin-core/rpc/common/v1/common";
9
+
10
+ defineOptions({ name: "TenantProjectText" });
11
+
12
+ /** 租户项目展示组件属性。 */
13
+ interface TenantProjectTextProps {
14
+ /** 包含租户和项目字段的列表行。 */
15
+ row: Record<string, any>;
16
+ /** 项目名称字段。 */
17
+ projectField?: string;
18
+ /** 租户名称字段。 */
19
+ tenantField?: string;
20
+ /** 租户选项,用于只有租户ID时解析名称。 */
21
+ tenantOptions?: SelectOptionResponse_Option[];
22
+ }
23
+
24
+ const props = withDefaults(defineProps<TenantProjectTextProps>(), {
25
+ projectField: "project_name",
26
+ tenantField: "tenant_name",
27
+ tenantOptions: () => []
28
+ });
29
+ const { isDefaultTenant, resolveTenantLabel } = useTenantScope();
30
+
31
+ const label = computed(() => {
32
+ const projectName = props.row[props.projectField] ?? props.row.project_name ?? props.row.name ?? props.row.project_id ?? "";
33
+ if (!isDefaultTenant.value) return String(projectName);
34
+ const tenantName = props.row[props.tenantField] ?? resolveTenantLabel(props.row, "tenant_id", props.tenantOptions);
35
+ return tenantName ? `${tenantName} / ${projectName}` : String(projectName);
36
+ });
37
+ </script>