create-windy 0.6.21 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/cli.js +11 -4
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/src/audit/policy.ts +6 -1
  5. package/template/apps/server/src/auth/bootstrap-admin.ts +10 -1
  6. package/template/apps/server/src/auth/composition.ts +5 -0
  7. package/template/apps/server/src/auth/credential-authorization.test.ts +257 -0
  8. package/template/apps/server/src/auth/credential-routes.test.ts +1 -0
  9. package/template/apps/server/src/auth/credential-service.ts +47 -12
  10. package/template/apps/server/src/auth/drizzle-repository.ts +13 -5
  11. package/template/apps/server/src/auth/drizzle-session-operations.ts +1 -0
  12. package/template/apps/server/src/auth/repository.ts +9 -6
  13. package/template/apps/server/src/auth/routes.test-support.ts +1 -0
  14. package/template/apps/server/src/auth/runtime.ts +4 -0
  15. package/template/apps/server/src/auth/service-options.ts +2 -0
  16. package/template/apps/server/src/auth/service.ts +2 -0
  17. package/template/apps/server/src/auth/user-credential-access.ts +37 -0
  18. package/template/apps/server/src/foundation-activation.test.ts +24 -1
  19. package/template/apps/server/src/foundation.ts +0 -2
  20. package/template/apps/server/src/index.ts +1 -0
  21. package/template/apps/server/src/runtime-development.ts +2 -0
  22. package/template/apps/server/src/runtime.test.ts +13 -4
  23. package/template/apps/server/src/system/department-organization.ts +58 -59
  24. package/template/apps/server/src/system/department-read-routes.ts +54 -0
  25. package/template/apps/server/src/system/department-routes.test.ts +27 -19
  26. package/template/apps/server/src/system/department-routes.ts +95 -33
  27. package/template/apps/server/src/system/department-transition-safety.test.ts +148 -0
  28. package/template/apps/server/src/system/department-validation.ts +79 -0
  29. package/template/apps/server/src/system/drizzle-repository.ts +14 -0
  30. package/template/apps/server/src/system/drizzle-system-configs.ts +2 -0
  31. package/template/apps/server/src/system/identity-read-model.ts +105 -0
  32. package/template/apps/server/src/system/identity-role-assignment.test.ts +199 -0
  33. package/template/apps/server/src/system/identity-role-assignment.ts +148 -0
  34. package/template/apps/server/src/system/identity-route-config.ts +29 -7
  35. package/template/apps/server/src/system/identity-write-security.test.ts +127 -0
  36. package/template/apps/server/src/system/identity-write-validation.ts +170 -0
  37. package/template/apps/server/src/system/organization-read-security.test.ts +221 -0
  38. package/template/apps/server/src/system/redis-health.test.ts +1 -1
  39. package/template/apps/server/src/system/repository.ts +29 -2
  40. package/template/apps/server/src/system/resource-routes.ts +116 -72
  41. package/template/apps/server/src/system/route-audit.test.ts +117 -0
  42. package/template/apps/server/src/system/route-helpers.ts +14 -0
  43. package/template/apps/server/src/system/route-types.ts +11 -2
  44. package/template/apps/server/src/system/routes.test.ts +0 -68
  45. package/template/apps/server/src/system/routes.ts +29 -2
  46. package/template/apps/server/src/system/user-account-policy.test.ts +3 -1
  47. package/template/apps/server/src/system/user-account-policy.ts +26 -9
  48. package/template/apps/web/src/pages/system/SystemResourceRoutePage.vue +27 -13
  49. package/template/apps/web/src/pages/system/SystemResourceRoutePage.webtest.ts +68 -0
  50. package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +131 -0
  51. package/template/apps/web/src/pages/system/components/PagedSelectionList.webtest.ts +32 -0
  52. package/template/apps/web/src/pages/system/components/SystemRecordDetailDialog.vue +67 -0
  53. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +114 -38
  54. package/template/apps/web/src/pages/system/components/SystemResourceEditor.webtest.ts +180 -0
  55. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +8 -0
  56. package/template/apps/web/src/pages/system/components/SystemResourceForm.vue +8 -2
  57. package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts +89 -0
  58. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +42 -46
  59. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +12 -55
  60. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +14 -2
  61. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +121 -85
  62. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.webtest.ts +57 -0
  63. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.vue +227 -0
  64. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.webtest.ts +62 -0
  65. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +72 -52
  66. package/template/apps/web/src/pages/system/components/role-policy/RolePermissionTree.vue +150 -79
  67. package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.vue +98 -8
  68. package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.webtest.ts +108 -0
  69. package/template/apps/web/src/pages/system/components/selection-options.ts +71 -0
  70. package/template/apps/web/src/pages/system/components/selection-options.webtest.ts +49 -0
  71. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +19 -27
  72. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +2 -6
  73. package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.vue +128 -0
  74. package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.webtest.ts +76 -0
  75. package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +61 -3
  76. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +144 -90
  77. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +95 -0
  78. package/template/apps/web/src/pages/system/components/user-account/UserListFilters.vue +168 -0
  79. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +33 -57
  80. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +4 -4
  81. package/template/apps/web/src/pages/system/components/user-account/save-user-account.ts +4 -1
  82. package/template/apps/web/src/pages/system/components/user-account/user-account-draft.ts +81 -0
  83. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +6 -10
  84. package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +72 -0
  85. package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +110 -0
  86. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +27 -1
  87. package/template/apps/web/src/pages/system/resource-config.ts +2 -0
  88. package/template/apps/web/src/services/system-api.ts +25 -0
  89. package/template/docs/architecture/foundation-infrastructure.md +12 -25
  90. package/template/docs/development/crud-generator.md +1 -1
  91. package/template/docs/operations/legacy-data-migration.md +73 -0
  92. package/template/docs/platform/bulk-data-jobs.md +1 -1
  93. package/template/docs/platform/module-configuration.md +2 -2
  94. package/template/docs/platform/organization-membership.md +37 -6
  95. package/template/docs/platform/scheduled-task-executor.md +1 -1
  96. package/template/package.json +2 -1
  97. package/template/packages/config/index.test.ts +1 -5
  98. package/template/packages/config/package.json +2 -2
  99. package/template/packages/config/src/platform-boundary.test.ts +39 -0
  100. package/template/packages/config/src/platform.ts +0 -28
  101. package/template/packages/database/drizzle/0063_license_project_lifecycle.sql +28 -0
  102. package/template/packages/database/drizzle/meta/0063_snapshot.json +15053 -0
  103. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  104. package/template/packages/database/package.json +2 -2
  105. package/template/packages/database/src/agent-runtime-schema.test.ts +5 -1
  106. package/template/packages/database/src/schema/index.ts +1 -0
  107. package/template/packages/database/src/schema/license-issuance-requests.ts +59 -0
  108. package/template/packages/database/src/schema/license-v1.ts +5 -0
  109. package/template/packages/jobs/package.json +2 -2
  110. package/template/packages/modules/package.json +1 -1
  111. package/template/packages/modules/src/system-api-permissions.ts +14 -0
  112. package/template/packages/modules/src/system-features.ts +0 -11
  113. package/template/packages/modules/src/system-module-catalog.test.ts +4 -0
  114. package/template/packages/modules/src/system-module-catalog.ts +0 -1
  115. package/template/packages/server-sdk/index.ts +1 -49
  116. package/template/packages/server-sdk/package.json +3 -2
  117. package/template/packages/server-sdk/src/ai-runtime-readiness-exports.ts +49 -0
  118. package/template/packages/shared/index.ts +2 -1
  119. package/template/packages/shared/package.json +1 -1
  120. package/template/packages/shared/src/license-console-capabilities.ts +163 -0
  121. package/template/packages/shared/src/license-console.ts +94 -0
  122. package/template/packages/shared/src/license.ts +5 -0
  123. package/template/docs/adapters/ruoyi-vue-compatibility.md +0 -93
  124. package/template/packages/shared/src/ruoyi.ts +0 -98
@@ -0,0 +1,180 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+ import { flushPromises, mount, type VueWrapper } from "@vue/test-utils";
3
+ import { defineComponent, h, ref } from "vue";
4
+ import { createMemoryHistory, createRouter, RouterView } from "vue-router";
5
+ import FormDialog from "@/components/common/FormDialog.vue";
6
+ import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
7
+ import SystemResourceEditor from "./SystemResourceEditor.vue";
8
+ import SystemResourceForm from "./SystemResourceForm.vue";
9
+ import { systemResourceConfigs } from "../resource-config";
10
+ const mounted: VueWrapper[] = [];
11
+ afterEach(() => {
12
+ for (const wrapper of mounted.splice(0)) wrapper.unmount();
13
+ });
14
+ async function setup(
15
+ save = vi
16
+ .fn<(_payload: Record<string, unknown>) => Promise<boolean>>()
17
+ .mockResolvedValue(true),
18
+ ) {
19
+ const EditorPage = defineComponent({
20
+ setup() {
21
+ const open = ref(true);
22
+ return () =>
23
+ h(SystemResourceEditor, {
24
+ open: open.value,
25
+ "onUpdate:open": (value: boolean) => {
26
+ open.value = value;
27
+ },
28
+ config: systemResourceConfigs.dictionaries,
29
+ defaults: { code: "original", name: "原名称" },
30
+ save,
31
+ });
32
+ },
33
+ });
34
+ const router = createRouter({
35
+ history: createMemoryHistory(),
36
+ routes: [
37
+ { path: "/edit/:tab?", component: EditorPage },
38
+ { path: "/other", component: { template: "<div>另一页面</div>" } },
39
+ ],
40
+ });
41
+ await router.push("/edit/one");
42
+ await router.isReady();
43
+ const wrapper = mount(
44
+ defineComponent(() => () => h(RouterView)),
45
+ { global: { plugins: [router] } },
46
+ );
47
+ mounted.push(wrapper);
48
+ await flushPromises();
49
+ const editor = wrapper.getComponent(SystemResourceEditor);
50
+ const input = editor
51
+ .getComponent(SystemResourceForm)
52
+ .get('input[placeholder="例如 invoice_status"]');
53
+ return { wrapper, editor, input, router, save };
54
+ }
55
+
56
+ describe("编辑容器未保存保护", () => {
57
+ test("关闭时取消保留输入,保存失败保留对话框和草稿", async () => {
58
+ const { editor, input, save } = await setup(
59
+ vi.fn().mockResolvedValue(false),
60
+ );
61
+ await input.setValue("draft");
62
+ const closing = editor.getComponent(FormDialog).props("beforeClose")!();
63
+ await flushPromises();
64
+ const confirmation = editor.getComponent(UnsavedChangesDialog);
65
+ expect(confirmation.props("open")).toBe(true);
66
+ confirmation.vm.$emit("save");
67
+ await flushPromises();
68
+ expect(save).toHaveBeenCalledWith(
69
+ expect.objectContaining({ code: "draft" }),
70
+ );
71
+ expect(confirmation.props("open")).toBe(true);
72
+ expect(editor.props("open")).toBe(true);
73
+ expect((input.element as HTMLInputElement).value).toBe("draft");
74
+ confirmation.vm.$emit("cancel");
75
+ expect(await closing).toBe(false);
76
+ expect(editor.props("open")).toBe(true);
77
+ });
78
+
79
+ test("恢复加载基线后不再拦截关闭和刷新", async () => {
80
+ const { editor, input } = await setup();
81
+ await input.setValue("draft");
82
+ const dirtyUnload = new Event("beforeunload", { cancelable: true });
83
+ window.dispatchEvent(dirtyUnload);
84
+ expect(dirtyUnload.defaultPrevented).toBe(true);
85
+ await input.setValue("original");
86
+ const cleanUnload = new Event("beforeunload", { cancelable: true });
87
+ window.dispatchEvent(cleanUnload);
88
+ expect(cleanUnload.defaultPrevented).toBe(false);
89
+ expect(await editor.getComponent(FormDialog).props("beforeClose")!()).toBe(
90
+ true,
91
+ );
92
+ });
93
+
94
+ test("路由更新或离开都先确认;保存成功才继续导航", async () => {
95
+ const { editor, input, router } = await setup();
96
+ await input.setValue("draft");
97
+ const switching = router.push("/edit/two");
98
+ await flushPromises();
99
+ editor.getComponent(UnsavedChangesDialog).vm.$emit("cancel");
100
+ await switching;
101
+ expect(router.currentRoute.value.path).toBe("/edit/one");
102
+ const leaving = router.push("/other");
103
+ await flushPromises();
104
+ editor.getComponent(UnsavedChangesDialog).vm.$emit("save");
105
+ await leaving;
106
+ expect(router.currentRoute.value.path).toBe("/other");
107
+ });
108
+
109
+ test("异步保存期间显示loading并阻止重复提交及关闭", async () => {
110
+ let finish: ((value: boolean) => void) | undefined;
111
+ const save = vi.fn(
112
+ () =>
113
+ new Promise<boolean>((resolve) => {
114
+ finish = resolve;
115
+ }),
116
+ );
117
+ const { editor, input } = await setup(save);
118
+ await input.setValue("draft");
119
+ const form = editor.getComponent(FormDialog);
120
+ form.vm.$emit("submit");
121
+ await flushPromises();
122
+ expect(form.props("loading")).toBe(true);
123
+ form.vm.$emit("submit");
124
+ expect(await form.props("beforeClose")!()).toBe(false);
125
+ expect(save).toHaveBeenCalledTimes(1);
126
+ finish?.(true);
127
+ await flushPromises();
128
+ expect(editor.props("open")).toBe(false);
129
+ });
130
+ test("先完成嵌套弹窗确认,再确认和保存包含新选择的账户草稿", async () => {
131
+ let finishChild: ((value: boolean) => void) | undefined;
132
+ const childBeforeClose = vi
133
+ .fn<() => Promise<boolean>>()
134
+ .mockImplementationOnce(
135
+ () =>
136
+ new Promise((resolve) => {
137
+ finishChild = resolve;
138
+ }),
139
+ )
140
+ .mockResolvedValue(true);
141
+ const payload: Record<string, unknown> = { departmentId: "old_department" };
142
+ const NestedForm = defineComponent({
143
+ emits: ["submit"],
144
+ setup(_, { expose, emit }) {
145
+ expose({
146
+ dirty: true,
147
+ beforeClose: childBeforeClose,
148
+ markSaved: vi.fn(),
149
+ submit: () => emit("submit", { ...payload }),
150
+ });
151
+ return () => h("div", "嵌套新建部门草稿");
152
+ },
153
+ });
154
+ const save = vi.fn().mockResolvedValue(true);
155
+ const wrapper = mount(SystemResourceEditor, {
156
+ props: {
157
+ open: true,
158
+ config: systemResourceConfigs.dictionaries,
159
+ defaults: {},
160
+ save,
161
+ },
162
+ global: { stubs: { SystemResourceForm: NestedForm } },
163
+ });
164
+ mounted.push(wrapper);
165
+ await flushPromises();
166
+ const closing = wrapper.getComponent(FormDialog).props("beforeClose")!();
167
+ await flushPromises();
168
+ expect(wrapper.getComponent(UnsavedChangesDialog).props("open")).toBe(
169
+ false,
170
+ );
171
+ expect(save).not.toHaveBeenCalled();
172
+ payload.departmentId = "created_department";
173
+ finishChild?.(true);
174
+ await flushPromises();
175
+ expect(wrapper.getComponent(UnsavedChangesDialog).props("open")).toBe(true);
176
+ wrapper.getComponent(UnsavedChangesDialog).vm.$emit("save");
177
+ expect(await closing).toBe(true);
178
+ expect(save).toHaveBeenCalledWith({ departmentId: "created_department" });
179
+ });
180
+ });
@@ -2,6 +2,7 @@
2
2
  import { Button } from "@/components/ui/button";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { SearchIcon } from "@lucide/vue";
5
+ import UserListFilters from "./user-account/UserListFilters.vue";
5
6
  import type { SystemResourceConfig } from "../resource-config";
6
7
  // @windy-module system.audit begin
7
8
  import AuditLogFilters from "./AuditLogFilters.vue";
@@ -23,6 +24,9 @@ defineProps<{ config: SystemResourceConfig }>();
23
24
  const keyword = defineModel<string>("keyword", { required: true });
24
25
  const emit = defineEmits<{
25
26
  search: [];
27
+ identityChange: [
28
+ filters: { departmentId?: string; roleId?: string; status?: string },
29
+ ];
26
30
  preset: [value: string];
27
31
  auditChange: [filters: AuditLogFilterValue];
28
32
  auditExportOptions: [options: AuditExportOptions];
@@ -30,6 +34,10 @@ const emit = defineEmits<{
30
34
  </script>
31
35
 
32
36
  <template>
37
+ <UserListFilters
38
+ v-if="config.key === 'users'"
39
+ @change="emit('identityChange', $event)"
40
+ />
33
41
  <!-- @windy-module system.audit begin -->
34
42
  <AuditLogFilters
35
43
  v-if="config.usesCustomFilters"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { watch, shallowRef } from "vue";
2
+ import { computed, watch, shallowRef } from "vue";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import type { ResourceField } from "../resource-config";
5
5
  import type { SystemRecord } from "@/services/system-api";
@@ -15,11 +15,17 @@ const emit = defineEmits<{
15
15
  }>();
16
16
 
17
17
  const form = shallowRef<Record<string, unknown>>({});
18
+ const baseline = shallowRef("");
19
+ const dirty = computed(() => JSON.stringify(form.value) !== baseline.value);
20
+ function markSaved() {
21
+ baseline.value = JSON.stringify(form.value);
22
+ }
18
23
 
19
24
  watch(
20
25
  () => [props.item, props.defaults] as const,
21
26
  () => {
22
27
  form.value = { ...props.defaults, ...(props.item || {}) };
28
+ markSaved();
23
29
  },
24
30
  { immediate: true },
25
31
  );
@@ -42,7 +48,7 @@ function submit() {
42
48
  emit("submit", form.value);
43
49
  }
44
50
 
45
- defineExpose({ submit });
51
+ defineExpose({ submit, dirty, markSaved });
46
52
  </script>
47
53
 
48
54
  <template>
@@ -0,0 +1,89 @@
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import { flushPromises, mount } from "@vue/test-utils";
3
+ import { systemResourceConfigs } from "../resource-config";
4
+ import SystemResourcePage from "./SystemResourcePage.vue";
5
+ import DepartmentOrganizationTable from "./department/DepartmentOrganizationTable.vue";
6
+ import DepartmentOrganizationForm from "./department/DepartmentOrganizationForm.vue";
7
+ import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
8
+ const api = vi.hoisted(() => ({
9
+ list: vi.fn(),
10
+ organization: vi.fn(),
11
+ preview: vi.fn(),
12
+ transitionDepartment: vi.fn(),
13
+ }));
14
+ vi.mock("@/services/system-api", async () => ({
15
+ ...(await vi.importActual<typeof import("@/services/system-api")>(
16
+ "@/services/system-api",
17
+ )),
18
+ listSystemResource: api.list,
19
+ listDepartmentOrganization: api.organization,
20
+ previewDepartmentTransition: api.preview,
21
+ transitionDepartment: api.transitionDepartment,
22
+ }));
23
+ afterEach(() => vi.clearAllMocks());
24
+ test("部门停用和删除使用显式迁移目标", async () => {
25
+ const department = {
26
+ id: "dept_sales",
27
+ code: "sales",
28
+ name: "销售部",
29
+ parentId: "dept_platform",
30
+ status: "active",
31
+ };
32
+ api.organization.mockResolvedValue({
33
+ items: [department],
34
+ total: 1,
35
+ page: 1,
36
+ pageSize: 10,
37
+ });
38
+ api.list.mockResolvedValue({
39
+ items: [department],
40
+ total: 1,
41
+ page: 1,
42
+ pageSize: 10,
43
+ });
44
+ api.preview.mockResolvedValue({
45
+ childDepartmentCount: 0,
46
+ userCount: 1,
47
+ roleCount: 0,
48
+ });
49
+ api.transitionDepartment.mockResolvedValue({
50
+ department: { ...department, status: "disabled" },
51
+ movedChildren: 0,
52
+ movedUsers: 1,
53
+ updatedRoles: 0,
54
+ });
55
+ const wrapper = mount(SystemResourcePage, {
56
+ props: { config: systemResourceConfigs.departments },
57
+ });
58
+ await flushPromises();
59
+
60
+ wrapper
61
+ .getComponent(DepartmentOrganizationTable)
62
+ .vm.$emit("edit", department);
63
+ await flushPromises();
64
+ wrapper.getComponent(DepartmentOrganizationForm).vm.$emit("submit", {
65
+ ...department,
66
+ status: "disabled",
67
+ _transitionTargetDepartmentId: "dept_platform",
68
+ });
69
+ await flushPromises();
70
+ expect(api.transitionDepartment).toHaveBeenCalledWith("dept_sales", {
71
+ action: "disable",
72
+ targetDepartmentId: "dept_platform",
73
+ updates: { name: "销售部", code: "sales", parentId: "dept_platform" },
74
+ });
75
+
76
+ wrapper
77
+ .getComponent(DepartmentOrganizationTable)
78
+ .vm.$emit("remove", department);
79
+ await flushPromises();
80
+ wrapper
81
+ .getComponent(DepartmentTransitionDialog)
82
+ .vm.$emit("confirm", "dept_platform");
83
+ await flushPromises();
84
+ expect(api.transitionDepartment).toHaveBeenLastCalledWith("dept_sales", {
85
+ action: "delete",
86
+ targetDepartmentId: "dept_platform",
87
+ });
88
+ wrapper.unmount();
89
+ });
@@ -15,7 +15,9 @@ import DictionaryBulkPanel from "./dictionary-bulk/DictionaryBulkPanel.vue";
15
15
  import FeatureLicensePolicyDialog from "./FeatureLicensePolicyDialog.vue";
16
16
  import SystemResourceFilters from "./SystemResourceFilters.vue";
17
17
  import { useFeatureLicenseEditor } from "../composables/useFeatureLicenseEditor";
18
- import { saveUserAccount } from "./user-account/save-user-account";
18
+ import { useResourceEditor } from "../composables/useResourceEditor";
19
+ import DepartmentOrganizationTable from "./department/DepartmentOrganizationTable.vue";
20
+ import SystemRecordDetailDialog from "./SystemRecordDetailDialog.vue";
19
21
  import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
20
22
  import { useDepartmentTransitions } from "../composables/useDepartmentTransitions";
21
23
  import SystemResourceActions from "./SystemResourceActions.vue";
@@ -57,9 +59,7 @@ const resource = useSystemResource({
57
59
  });
58
60
  const toast = useAppToast();
59
61
 
60
- const editorOpen = shallowRef(false);
61
62
  const confirmOpen = shallowRef(false);
62
- const editingItem = shallowRef<SystemRecord | undefined>();
63
63
  const viewingItem = shallowRef<SystemRecord | undefined>();
64
64
  const detailOpen = shallowRef(false);
65
65
  const pendingDelete = shallowRef<SystemRecord | undefined>();
@@ -70,6 +70,13 @@ const departmentTransitions = useDepartmentTransitions({
70
70
  reload: resource.reload,
71
71
  mutated: () => emit("mutated"),
72
72
  });
73
+ const editor = useResourceEditor({
74
+ key: props.config.key,
75
+ save: resource.save,
76
+ disable: departmentTransitions.disable,
77
+ mutated: () => emit("mutated"),
78
+ });
79
+ const { open: editorOpen, item: editingItem } = editor;
73
80
  const {
74
81
  open: licenseEditorOpen,
75
82
  item: licenseEditorItem,
@@ -85,7 +92,7 @@ const {
85
92
  onFailed: (description) =>
86
93
  toast.error("License 范围更新失败", { description }),
87
94
  });
88
- const formDefaults = computed(() => props.config.createDefaults());
95
+ const formDefaults = shallowRef(props.config.createDefaults());
89
96
  const displayedItems = computed(() =>
90
97
  resource.items.value.map((item) => {
91
98
  const inlineToggle = props.config.inlineToggle;
@@ -100,13 +107,11 @@ onMounted(() => {
100
107
  });
101
108
 
102
109
  function openCreate() {
103
- editingItem.value = undefined;
104
- editorOpen.value = true;
110
+ formDefaults.value = props.config.createDefaults();
111
+ editor.edit();
105
112
  }
106
-
107
113
  function openEdit(item: SystemRecord) {
108
- editingItem.value = item;
109
- editorOpen.value = true;
114
+ editor.edit(item);
110
115
  }
111
116
 
112
117
  function openDelete(item: SystemRecord) {
@@ -119,41 +124,6 @@ function openDetail(item: SystemRecord) {
119
124
  detailOpen.value = true;
120
125
  }
121
126
 
122
- async function submit(payload: Record<string, unknown>) {
123
- if (
124
- props.config.key === "departments" &&
125
- editingItem.value &&
126
- editingItem.value.status === "active" &&
127
- payload.status === "disabled"
128
- ) {
129
- const targetDepartmentId =
130
- typeof payload._transitionTargetDepartmentId === "string"
131
- ? payload._transitionTargetDepartmentId
132
- : undefined;
133
- const disabled = await departmentTransitions.disable(
134
- editingItem.value,
135
- targetDepartmentId,
136
- );
137
- if (disabled) editorOpen.value = false;
138
- return;
139
- }
140
- const normalizedPayload = { ...payload };
141
- delete normalizedPayload._transitionTargetDepartmentId;
142
- const saved =
143
- props.config.key === "users"
144
- ? await saveUserAccount(
145
- normalizedPayload,
146
- editingItem.value?.id,
147
- resource.save,
148
- toast,
149
- )
150
- : await resource.save(normalizedPayload, editingItem.value?.id);
151
- if (saved) {
152
- editorOpen.value = false;
153
- emit("mutated");
154
- }
155
- }
156
-
157
127
  async function toggleInline(item: SystemRecord, checked: boolean) {
158
128
  const inlineToggle = props.config.inlineToggle;
159
129
  if (!inlineToggle || pendingToggleId.value) return;
@@ -245,6 +215,7 @@ async function importData(file: File) {
245
215
  @preset="applySearchPreset"
246
216
  @audit-change="resource.setAuditFilters"
247
217
  @audit-export-options="resource.setExportOptions"
218
+ @identity-change="resource.setIdentityFilters"
248
219
  />
249
220
  </CardHeader>
250
221
  <CardContent class="space-y-4">
@@ -253,6 +224,17 @@ async function importData(file: File) {
253
224
  :error="resource.error.value"
254
225
  @retry="resource.reload"
255
226
  />
227
+ <DepartmentOrganizationTable
228
+ v-else-if="config.key === 'departments'"
229
+ :items="displayedItems"
230
+ :keyword="resource.q.value"
231
+ :loading="resource.loading.value"
232
+ :editable="resource.canUpdate.value"
233
+ :deletable="resource.canDelete.value"
234
+ @edit="openEdit"
235
+ @remove="openDelete"
236
+ @view="openDetail"
237
+ />
256
238
  <SystemResourceTable
257
239
  v-else
258
240
  :columns="config.columns"
@@ -262,7 +244,9 @@ async function importData(file: File) {
262
244
  :inline-toggle="config.inlineToggle"
263
245
  :toggle-pending-id="pendingToggleId"
264
246
  :loading="resource.loading.value"
265
- :viewable="auditViewable"
247
+ :viewable="
248
+ auditViewable || config.key === 'users' || config.key === 'roles'
249
+ "
266
250
  :page="resource.page.value"
267
251
  :page-size="resource.pageSize.value"
268
252
  @edit="openEdit"
@@ -272,6 +256,7 @@ async function importData(file: File) {
272
256
  @view="openDetail"
273
257
  />
274
258
  <DataPagination
259
+ v-if="config.key !== 'departments'"
275
260
  :page="resource.page.value"
276
261
  :page-size="resource.pageSize.value"
277
262
  :total="resource.total.value"
@@ -308,7 +293,8 @@ async function importData(file: File) {
308
293
  :permissions="permissions"
309
294
  :department-create-mode="departmentCreateMode"
310
295
  :loading="resource.saving.value || departmentTransitions.loading.value"
311
- @submit="submit"
296
+ :save="editor.submit"
297
+ :error="resource.error.value"
312
298
  />
313
299
 
314
300
  <DepartmentTransitionDialog
@@ -337,6 +323,16 @@ async function importData(file: File) {
337
323
  @save="saveLicensePolicy"
338
324
  />
339
325
 
326
+ <SystemRecordDetailDialog
327
+ v-if="
328
+ config.key === 'users' ||
329
+ config.key === 'departments' ||
330
+ config.key === 'roles'
331
+ "
332
+ v-model:open="detailOpen"
333
+ :config="config"
334
+ :item="viewingItem"
335
+ />
340
336
  <!-- @windy-module system.audit begin -->
341
337
  <AuditLogDetailDialog
342
338
  v-if="config.key === 'auditLogs'"
@@ -4,14 +4,14 @@ import { systemResourceConfigs } from "../resource-config";
4
4
  import SystemResourcePage from "./SystemResourcePage.vue";
5
5
  import FeatureLicensePolicyDialog from "./FeatureLicensePolicyDialog.vue";
6
6
  import SystemResourceTable from "./SystemResourceTable.vue";
7
- import DepartmentOrganizationForm from "./department/DepartmentOrganizationForm.vue";
8
- import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
9
7
 
10
8
  const api = vi.hoisted(() => ({
11
9
  list: vi.fn(),
12
10
  update: vi.fn(),
13
11
  quickCreateDepartment: vi.fn(),
14
12
  transitionDepartment: vi.fn(),
13
+ organization: vi.fn(),
14
+ preview: vi.fn(),
15
15
  loadPasswordPolicy: vi.fn(),
16
16
  }));
17
17
 
@@ -25,6 +25,8 @@ vi.mock("@/services/system-api", async () => {
25
25
  updateSystemResource: api.update,
26
26
  quickCreateDepartment: api.quickCreateDepartment,
27
27
  transitionDepartment: api.transitionDepartment,
28
+ listDepartmentOrganization: api.organization,
29
+ previewDepartmentTransition: api.preview,
28
30
  };
29
31
  });
30
32
 
@@ -255,7 +257,7 @@ describe("SystemResourcePage", () => {
255
257
  });
256
258
  // @windy-module system.audit end
257
259
 
258
- test("用户新增和编辑使用部门下拉,有创建权限时显示快捷新建", async () => {
260
+ test("用户编辑完整加载组织并分页选择部门,有创建权限时显示快捷新建", async () => {
259
261
  api.list.mockImplementation((resource: string) =>
260
262
  Promise.resolve(
261
263
  resource === "departments"
@@ -284,61 +286,16 @@ describe("SystemResourcePage", () => {
284
286
 
285
287
  expect(document.body.textContent).toContain("所属部门");
286
288
  expect(document.body.textContent).toContain("新建部门");
289
+ expect(document.body.textContent).toContain("每页 10 条");
290
+ expect(document.body.textContent).toContain("销售部 · sales");
287
291
  expect(api.list).toHaveBeenCalledWith(
288
292
  "departments",
289
- expect.objectContaining({ status: "active", sort: "order,name" }),
293
+ expect.objectContaining({
294
+ pageSize: 10,
295
+ status: undefined,
296
+ sort: "order,name,id",
297
+ }),
290
298
  );
291
299
  wrapper.unmount();
292
300
  });
293
-
294
- test("部门停用和删除使用显式迁移目标", async () => {
295
- const department = {
296
- id: "dept_sales",
297
- code: "sales",
298
- name: "销售部",
299
- parentId: "dept_platform",
300
- status: "active",
301
- };
302
- api.list.mockResolvedValue({
303
- items: [department],
304
- total: 1,
305
- page: 1,
306
- pageSize: 10,
307
- });
308
- api.transitionDepartment.mockResolvedValue({
309
- department: { ...department, status: "disabled" },
310
- movedChildren: 0,
311
- movedUsers: 1,
312
- updatedRoles: 0,
313
- });
314
- const wrapper = mount(SystemResourcePage, {
315
- props: { config: systemResourceConfigs.departments },
316
- });
317
- await flushPromises();
318
-
319
- wrapper.getComponent(SystemResourceTable).vm.$emit("edit", department);
320
- await flushPromises();
321
- wrapper.getComponent(DepartmentOrganizationForm).vm.$emit("submit", {
322
- ...department,
323
- status: "disabled",
324
- _transitionTargetDepartmentId: "dept_platform",
325
- });
326
- await flushPromises();
327
- expect(api.transitionDepartment).toHaveBeenCalledWith("dept_sales", {
328
- action: "disable",
329
- targetDepartmentId: "dept_platform",
330
- });
331
-
332
- wrapper.getComponent(SystemResourceTable).vm.$emit("remove", department);
333
- await flushPromises();
334
- wrapper
335
- .getComponent(DepartmentTransitionDialog)
336
- .vm.$emit("confirm", "dept_platform");
337
- await flushPromises();
338
- expect(api.transitionDepartment).toHaveBeenLastCalledWith("dept_sales", {
339
- action: "delete",
340
- targetDepartmentId: "dept_platform",
341
- });
342
- wrapper.unmount();
343
- });
344
301
  });
@@ -69,6 +69,12 @@ function formatValue(item: SystemRecord, column: ResourceColumn): string {
69
69
  return auditAction(value)?.label || String(value || "-");
70
70
  if (column.format === "date-time") return formatDateTime(value);
71
71
  if (column.format === "governed-value") return formatGovernedValue(value);
72
+ if (column.key === "status")
73
+ return value === "active"
74
+ ? "启用"
75
+ : value === "disabled"
76
+ ? "停用"
77
+ : String(value || "—");
72
78
  if (typeof value === "boolean") return value ? "是" : "否";
73
79
  if (Array.isArray(value)) return value.join(", ");
74
80
  if (value && typeof value === "object") {
@@ -80,7 +86,9 @@ function formatValue(item: SystemRecord, column: ResourceColumn): string {
80
86
  JSON.stringify(value),
81
87
  );
82
88
  }
83
- return value ? String(value) : "-";
89
+ return value === undefined || value === null || value === ""
90
+ ? "-"
91
+ : String(value);
84
92
  }
85
93
 
86
94
  function formatGovernedValue(value: unknown): string {
@@ -232,7 +240,11 @@ function licenseVersionLabel(version: string): string {
232
240
  v-if="viewable"
233
241
  variant="outline"
234
242
  size="sm"
235
- :aria-label="`查看审计日志 ${String(item.id)} 详情`"
243
+ :aria-label="
244
+ columns.some((column) => column.format === 'audit-action')
245
+ ? `查看审计日志 ${item.id} 详情`
246
+ : `查看 ${String(item.displayName || item.name || item.id)} 详情`
247
+ "
236
248
  @click="emit('view', item)"
237
249
  >
238
250
  查看详情