create-windy 0.2.34 → 0.3.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.
- package/dist/cli.js +167 -17
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +5 -0
- package/template/README.md +3 -1
- package/template/apps/agent-server/Dockerfile +1 -0
- package/template/apps/agent-server/README.md +18 -5
- package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
- package/template/apps/agent-server/pyproject.toml +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
- package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
- package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
- package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
- package/template/apps/server/package.json +4 -1
- package/template/apps/server/src/agent/run-facade.ts +2 -7
- package/template/apps/server/src/agent/tool-registry.ts +2 -6
- package/template/apps/server/src/application-services.ts +50 -0
- package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
- package/template/apps/server/src/audit/search-summary.ts +2 -6
- package/template/apps/server/src/auth/credential-restriction.ts +1 -0
- package/template/apps/server/src/auth/password.ts +30 -21
- package/template/apps/server/src/auth/routes.test.ts +44 -3
- package/template/apps/server/src/auth/routes.ts +6 -1
- package/template/apps/server/src/auth/runtime.ts +4 -1
- package/template/apps/server/src/auth/service.ts +25 -7
- package/template/apps/server/src/configuration/bootstrap.ts +20 -1
- package/template/apps/server/src/configuration/definition.test.ts +71 -0
- package/template/apps/server/src/configuration/definition.ts +242 -12
- package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
- package/template/apps/server/src/configuration/repository.ts +27 -2
- package/template/apps/server/src/configuration/routes.ts +4 -1
- package/template/apps/server/src/configuration/service.test.ts +100 -0
- package/template/apps/server/src/configuration/service.ts +50 -10
- package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
- package/template/apps/server/src/data-governance/export/service.ts +2 -6
- package/template/apps/server/src/example-modules.ts +4 -1
- package/template/apps/server/src/guards.ts +18 -5
- package/template/apps/server/src/http/request-source.test.ts +85 -0
- package/template/apps/server/src/http/request-source.ts +90 -0
- package/template/apps/server/src/index.ts +23 -6
- package/template/apps/server/src/license/routes.ts +2 -0
- package/template/apps/server/src/license/runtime-service.ts +10 -11
- package/template/apps/server/src/module-bootstrap.ts +2 -2
- package/template/apps/server/src/module-host/request-context.ts +6 -6
- package/template/apps/server/src/module-host/storage-scope.ts +2 -9
- package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
- package/template/apps/server/src/notification/channel-adapter.ts +46 -0
- package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
- package/template/apps/server/src/notification/delivery-policy.ts +37 -0
- package/template/apps/server/src/notification/delivery-repository.ts +198 -0
- package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
- package/template/apps/server/src/notification/delivery-worker.ts +88 -0
- package/template/apps/server/src/notification/drizzle-management.ts +180 -0
- package/template/apps/server/src/notification/drizzle-query.ts +160 -0
- package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
- package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
- package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
- package/template/apps/server/src/notification/extension-routes.ts +111 -0
- package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
- package/template/apps/server/src/notification/memory-support.ts +72 -0
- package/template/apps/server/src/notification/preference-repository.ts +95 -0
- package/template/apps/server/src/notification/recipient-directory.ts +104 -0
- package/template/apps/server/src/notification/repository.test.ts +171 -0
- package/template/apps/server/src/notification/repository.ts +179 -32
- package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
- package/template/apps/server/src/notification/route-support.ts +225 -0
- package/template/apps/server/src/notification/routes.test.ts +182 -0
- package/template/apps/server/src/notification/routes.ts +225 -132
- package/template/apps/server/src/notification/service.test.ts +104 -0
- package/template/apps/server/src/notification/service.ts +140 -0
- package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
- package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
- package/template/apps/server/src/persistence.integration.test.ts +41 -2
- package/template/apps/server/src/route-guards.ts +1 -0
- package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/runtime.test.ts +23 -9
- package/template/apps/server/src/runtime.ts +39 -2
- package/template/apps/server/src/scheduler/audit.ts +2 -11
- package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
- package/template/apps/server/src/scheduler/definitions.ts +7 -0
- package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
- package/template/apps/server/src/scheduler/routes.ts +2 -7
- package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
- package/template/apps/server/src/search/opensearch-client.ts +145 -0
- package/template/apps/server/src/search/service.ts +2 -6
- package/template/apps/server/src/settings/routes.ts +15 -3
- package/template/apps/server/src/settings/runtime.ts +10 -1
- package/template/apps/server/src/storage/runtime.ts +13 -0
- package/template/apps/server/src/system/audit-query.ts +33 -4
- package/template/apps/server/src/system/audit-routes.test.ts +58 -4
- package/template/apps/server/src/system/audit-routes.ts +6 -5
- package/template/apps/server/src/system/department-organization.ts +320 -0
- package/template/apps/server/src/system/department-routes.test.ts +207 -48
- package/template/apps/server/src/system/department-routes.ts +114 -33
- package/template/apps/server/src/system/drizzle-repository.ts +15 -7
- package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
- package/template/apps/server/src/system/drizzle-system.ts +37 -9
- package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
- package/template/apps/server/src/system/identity-route-config.ts +7 -2
- package/template/apps/server/src/system/mutation-runner.ts +23 -0
- package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
- package/template/apps/server/src/system/resource-access-response.ts +12 -6
- package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
- package/template/apps/server/src/system/route-helpers.ts +2 -5
- package/template/apps/server/src/system/route-types.ts +12 -3
- package/template/apps/server/src/system/routes-validation.test.ts +26 -0
- package/template/apps/server/src/system/routes.test.ts +10 -2
- package/template/apps/server/src/system/routes.ts +70 -36
- package/template/apps/server/src/system/seed.ts +7 -0
- package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
- package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
- package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
- package/template/apps/server/src/work-order/search-provider.ts +60 -1
- package/template/apps/server/src/work-order/service.ts +42 -10
- package/template/apps/web/src/app/error-pages.ts +9 -0
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
- package/template/apps/web/src/components/common/FormDialog.vue +16 -2
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
- package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
- package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
- package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
- package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
- package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
- package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
- package/template/apps/web/src/lib/date-time.ts +22 -10
- package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
- package/template/apps/web/src/main.ts +14 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
- package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
- package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
- package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
- package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
- package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
- package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
- package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
- package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
- package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
- package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
- package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
- package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
- package/template/apps/web/src/pages/system/resource-config.ts +12 -4
- package/template/apps/web/src/router/error-navigation.ts +50 -0
- package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
- package/template/apps/web/src/router/index.ts +71 -0
- package/template/apps/web/src/router/index.webtest.ts +29 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/auth-api.ts +5 -0
- package/template/apps/web/src/services/http.ts +43 -6
- package/template/apps/web/src/services/http.webtest.ts +70 -0
- package/template/apps/web/src/services/notification-api.ts +85 -2
- package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
- package/template/apps/web/src/services/search-api.ts +39 -0
- package/template/apps/web/src/services/system-api.ts +18 -0
- package/template/apps/web/vitest.config.ts +1 -0
- package/template/docker-compose.yml +66 -0
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/search-provider-protocol.md +38 -2
- package/template/docs/development/custom-error-pages.md +78 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +32 -6
- package/template/docs/platform/audit-reliability.md +46 -2
- package/template/docs/platform/module-configuration.md +13 -4
- package/template/docs/platform/notifications.md +67 -11
- package/template/docs/platform/organization-membership.md +17 -3
- package/template/docs/platform/system-crud-api.md +16 -0
- package/template/docs/platform/web-system-crud.md +6 -2
- package/template/package.json +4 -0
- package/template/packages/config/index.test.ts +45 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +169 -48
- package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
- package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
- package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
- package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
- package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
- package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
- package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
- package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/identity.ts +7 -0
- package/template/packages/database/src/schema/notifications.ts +135 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
- package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +54 -0
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-module-catalog.ts +2 -1
- package/template/packages/modules/src/system-modules.test.ts +13 -1
- package/template/packages/modules/src/system-modules.ts +12 -0
- package/template/packages/modules/src/system-permissions.ts +9 -5
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.ts +25 -0
- package/template/packages/shared/src/module-configuration.ts +5 -0
- package/template/packages/shared/src/notification.ts +92 -1
- package/template/packages/shared/src/password.ts +7 -2
- package/template/packages/shared/src/platform-settings.ts +2 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/upload-service.test.ts +16 -0
- package/template/packages/storage/src/upload-service.ts +5 -1
- package/template/scripts/notification-postgres-regression.ts +27 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useTemplateRef } from "vue";
|
|
3
|
+
import type { PermissionDefinition } from "@southwind-ai/shared";
|
|
4
|
+
import FormDialog from "@/components/common/FormDialog.vue";
|
|
5
|
+
import type { SystemRecord } from "@/services/system-api";
|
|
6
|
+
import type { SystemResourceConfig } from "../resource-config";
|
|
7
|
+
import DepartmentOrganizationForm from "./department/DepartmentOrganizationForm.vue";
|
|
8
|
+
import RolePolicyForm from "./role-policy/RolePolicyForm.vue";
|
|
9
|
+
import SystemResourceForm from "./SystemResourceForm.vue";
|
|
10
|
+
import UserAccountForm from "./user-account/UserAccountForm.vue";
|
|
11
|
+
|
|
12
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
config: SystemResourceConfig;
|
|
15
|
+
item?: SystemRecord;
|
|
16
|
+
defaults: Record<string, unknown>;
|
|
17
|
+
permissions?: PermissionDefinition[];
|
|
18
|
+
departmentCreateMode?: "create" | "manage";
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
}>();
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
submit: [payload: Record<string, unknown>];
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
interface ResourceFormHandle {
|
|
26
|
+
submit(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resourceForm = useTemplateRef<ResourceFormHandle>("resourceForm");
|
|
30
|
+
const title = computed(() => (props.item ? "编辑记录" : "新增记录"));
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<FormDialog
|
|
35
|
+
v-model:open="open"
|
|
36
|
+
:title="title"
|
|
37
|
+
:description="config.description"
|
|
38
|
+
:loading="loading"
|
|
39
|
+
:content-class="config.key === 'roles' ? 'sm:max-w-4xl' : undefined"
|
|
40
|
+
@submit="resourceForm?.submit()"
|
|
41
|
+
>
|
|
42
|
+
<RolePolicyForm
|
|
43
|
+
v-if="config.key === 'roles'"
|
|
44
|
+
ref="resourceForm"
|
|
45
|
+
:item="item"
|
|
46
|
+
:defaults="defaults"
|
|
47
|
+
:permissions="permissions || []"
|
|
48
|
+
@submit="emit('submit', $event)"
|
|
49
|
+
/>
|
|
50
|
+
<UserAccountForm
|
|
51
|
+
v-else-if="config.key === 'users'"
|
|
52
|
+
ref="resourceForm"
|
|
53
|
+
:item="item"
|
|
54
|
+
:defaults="defaults"
|
|
55
|
+
:department-create-mode="departmentCreateMode"
|
|
56
|
+
@submit="emit('submit', $event)"
|
|
57
|
+
/>
|
|
58
|
+
<DepartmentOrganizationForm
|
|
59
|
+
v-else-if="config.key === 'departments'"
|
|
60
|
+
ref="resourceForm"
|
|
61
|
+
:item="item"
|
|
62
|
+
:defaults="defaults"
|
|
63
|
+
@submit="emit('submit', $event)"
|
|
64
|
+
/>
|
|
65
|
+
<SystemResourceForm
|
|
66
|
+
v-else
|
|
67
|
+
ref="resourceForm"
|
|
68
|
+
:fields="config.fields"
|
|
69
|
+
:item="item"
|
|
70
|
+
:defaults="defaults"
|
|
71
|
+
@submit="emit('submit', $event)"
|
|
72
|
+
/>
|
|
73
|
+
</FormDialog>
|
|
74
|
+
</template>
|
|
@@ -8,9 +8,11 @@ import AuditLogFilters from "./AuditLogFilters.vue";
|
|
|
8
8
|
// @windy-module system.audit end
|
|
9
9
|
|
|
10
10
|
interface AuditLogFilterValue {
|
|
11
|
+
q?: string;
|
|
11
12
|
actorId?: string;
|
|
12
13
|
action?: string;
|
|
13
14
|
occurredAfter?: string;
|
|
15
|
+
occurredBefore?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
interface AuditExportOptions {
|
|
@@ -35,10 +37,7 @@ const emit = defineEmits<{
|
|
|
35
37
|
@export-options="emit('auditExportOptions', $event)"
|
|
36
38
|
/>
|
|
37
39
|
<!-- @windy-module system.audit end -->
|
|
38
|
-
<div
|
|
39
|
-
v-if="!config.usesCustomFilters"
|
|
40
|
-
class="flex flex-col gap-2 md:flex-row"
|
|
41
|
-
>
|
|
40
|
+
<div v-if="!config.usesCustomFilters" class="flex flex-col gap-2 md:flex-row">
|
|
42
41
|
<Input
|
|
43
42
|
v-model="keyword"
|
|
44
43
|
:placeholder="config.searchPlaceholder"
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, shallowRef
|
|
2
|
+
import { computed, onMounted, shallowRef } from "vue";
|
|
3
3
|
import type { PermissionDefinition } from "@southwind-ai/shared";
|
|
4
4
|
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
|
|
5
5
|
import DataPagination from "@/components/common/DataPagination.vue";
|
|
6
|
-
import FormDialog from "@/components/common/FormDialog.vue";
|
|
7
6
|
import { Button } from "@/components/ui/button";
|
|
8
7
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
9
|
-
import { DownloadIcon, PlusIcon, RefreshCwIcon, UploadIcon } from "@lucide/vue";
|
|
10
8
|
import type { SystemRecord } from "@/services/system-api";
|
|
11
9
|
import { useSystemResource } from "../composables/useSystemResource";
|
|
12
10
|
import { useAppToast } from "@/composables/useAppToast";
|
|
13
11
|
import type { SystemResourceConfig } from "../resource-config";
|
|
14
|
-
import SystemResourceForm from "./SystemResourceForm.vue";
|
|
15
12
|
import SystemResourceTable from "./SystemResourceTable.vue";
|
|
16
13
|
// @windy-module system.bulk-data begin
|
|
17
14
|
import DictionaryBulkPanel from "./dictionary-bulk/DictionaryBulkPanel.vue";
|
|
18
15
|
// @windy-module system.bulk-data end
|
|
19
|
-
import RolePolicyForm from "./role-policy/RolePolicyForm.vue";
|
|
20
|
-
import UserAccountForm from "./user-account/UserAccountForm.vue";
|
|
21
16
|
import FeatureLicensePolicyDialog from "./FeatureLicensePolicyDialog.vue";
|
|
22
17
|
import SystemResourceFilters from "./SystemResourceFilters.vue";
|
|
23
18
|
import { useFeatureLicenseEditor } from "../composables/useFeatureLicenseEditor";
|
|
24
19
|
import { saveUserAccount } from "./user-account/save-user-account";
|
|
20
|
+
import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.vue";
|
|
21
|
+
import { useDepartmentTransitions } from "../composables/useDepartmentTransitions";
|
|
22
|
+
import SystemResourceActions from "./SystemResourceActions.vue";
|
|
23
|
+
import SystemResourceEditor from "./SystemResourceEditor.vue";
|
|
24
|
+
// @windy-module system.audit begin
|
|
25
|
+
import AuditLogDetailDialog from "./AuditLogDetailDialog.vue";
|
|
26
|
+
// @windy-module system.audit end
|
|
25
27
|
|
|
26
28
|
const props = withDefaults(
|
|
27
29
|
defineProps<{
|
|
@@ -39,6 +41,7 @@ const props = withDefaults(
|
|
|
39
41
|
// @windy-module system.bulk-data end
|
|
40
42
|
},
|
|
41
43
|
);
|
|
44
|
+
const auditViewable = props.config.apiResource === "audit-logs";
|
|
42
45
|
|
|
43
46
|
const emit = defineEmits<{
|
|
44
47
|
mutated: [];
|
|
@@ -57,10 +60,16 @@ const toast = useAppToast();
|
|
|
57
60
|
const editorOpen = shallowRef(false);
|
|
58
61
|
const confirmOpen = shallowRef(false);
|
|
59
62
|
const editingItem = shallowRef<SystemRecord | undefined>();
|
|
63
|
+
const viewingItem = shallowRef<SystemRecord | undefined>();
|
|
64
|
+
const detailOpen = shallowRef(false);
|
|
60
65
|
const pendingDelete = shallowRef<SystemRecord | undefined>();
|
|
61
66
|
const keyword = shallowRef("");
|
|
62
67
|
const pendingToggleId = shallowRef("");
|
|
63
68
|
const toggleOverrides = shallowRef<Record<string, boolean>>({});
|
|
69
|
+
const departmentTransitions = useDepartmentTransitions({
|
|
70
|
+
reload: resource.reload,
|
|
71
|
+
mutated: () => emit("mutated"),
|
|
72
|
+
});
|
|
64
73
|
const {
|
|
65
74
|
open: licenseEditorOpen,
|
|
66
75
|
item: licenseEditorItem,
|
|
@@ -76,17 +85,7 @@ const {
|
|
|
76
85
|
onFailed: (description) =>
|
|
77
86
|
toast.error("License 范围更新失败", { description }),
|
|
78
87
|
});
|
|
79
|
-
interface ResourceFormHandle {
|
|
80
|
-
submit(): void;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const resourceForm = useTemplateRef<ResourceFormHandle>("resourceForm");
|
|
84
|
-
const importFileInput = useTemplateRef<HTMLInputElement>("importFileInput");
|
|
85
|
-
|
|
86
88
|
const formDefaults = computed(() => props.config.createDefaults());
|
|
87
|
-
const dialogTitle = computed(() =>
|
|
88
|
-
editingItem.value ? "编辑记录" : "新增记录",
|
|
89
|
-
);
|
|
90
89
|
const displayedItems = computed(() =>
|
|
91
90
|
resource.items.value.map((item) => {
|
|
92
91
|
const inlineToggle = props.config.inlineToggle;
|
|
@@ -115,16 +114,40 @@ function openDelete(item: SystemRecord) {
|
|
|
115
114
|
confirmOpen.value = true;
|
|
116
115
|
}
|
|
117
116
|
|
|
117
|
+
function openDetail(item: SystemRecord) {
|
|
118
|
+
viewingItem.value = item;
|
|
119
|
+
detailOpen.value = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
118
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;
|
|
119
142
|
const saved =
|
|
120
143
|
props.config.key === "users"
|
|
121
144
|
? await saveUserAccount(
|
|
122
|
-
|
|
145
|
+
normalizedPayload,
|
|
123
146
|
editingItem.value?.id,
|
|
124
147
|
resource.save,
|
|
125
148
|
toast,
|
|
126
149
|
)
|
|
127
|
-
: await resource.save(
|
|
150
|
+
: await resource.save(normalizedPayload, editingItem.value?.id);
|
|
128
151
|
if (saved) {
|
|
129
152
|
editorOpen.value = false;
|
|
130
153
|
emit("mutated");
|
|
@@ -151,8 +174,19 @@ async function toggleInline(item: SystemRecord, checked: boolean) {
|
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
176
|
|
|
154
|
-
async function confirmDelete() {
|
|
177
|
+
async function confirmDelete(targetDepartmentId?: string) {
|
|
155
178
|
if (!pendingDelete.value) return;
|
|
179
|
+
if (props.config.key === "departments") {
|
|
180
|
+
const deleted = await departmentTransitions.remove(
|
|
181
|
+
pendingDelete.value,
|
|
182
|
+
targetDepartmentId,
|
|
183
|
+
);
|
|
184
|
+
if (deleted) {
|
|
185
|
+
confirmOpen.value = false;
|
|
186
|
+
pendingDelete.value = undefined;
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
156
190
|
await resource.remove(pendingDelete.value.id);
|
|
157
191
|
if (!resource.error.value) {
|
|
158
192
|
confirmOpen.value = false;
|
|
@@ -170,24 +204,12 @@ function applySearchPreset(value: string) {
|
|
|
170
204
|
applySearch();
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
function submitForm() {
|
|
174
|
-
resourceForm.value?.submit();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
207
|
function exportData() {
|
|
178
208
|
void resource.exportCurrent(`${props.config.apiResource}.json`);
|
|
179
209
|
}
|
|
180
210
|
|
|
181
|
-
function
|
|
182
|
-
importFileInput.value?.click();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function importData(event: Event) {
|
|
186
|
-
const input = event.target as HTMLInputElement;
|
|
187
|
-
const file = input.files?.[0];
|
|
188
|
-
if (!file) return;
|
|
211
|
+
async function importData(file: File) {
|
|
189
212
|
await resource.importFile(file);
|
|
190
|
-
input.value = "";
|
|
191
213
|
}
|
|
192
214
|
</script>
|
|
193
215
|
|
|
@@ -206,40 +228,15 @@ async function importData(event: Event) {
|
|
|
206
228
|
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
|
|
207
229
|
>
|
|
208
230
|
<CardTitle class="text-base">数据列表</CardTitle>
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
<DownloadIcon data-icon="inline-start" />
|
|
220
|
-
导出
|
|
221
|
-
</Button>
|
|
222
|
-
<Button
|
|
223
|
-
v-if="resource.canImport.value && !config.replacesNativeTransfer"
|
|
224
|
-
variant="outline"
|
|
225
|
-
@click="openImport"
|
|
226
|
-
>
|
|
227
|
-
<UploadIcon data-icon="inline-start" />
|
|
228
|
-
导入
|
|
229
|
-
</Button>
|
|
230
|
-
<Button v-if="resource.canCreate.value" @click="openCreate">
|
|
231
|
-
<PlusIcon data-icon="inline-start" />
|
|
232
|
-
新增
|
|
233
|
-
</Button>
|
|
234
|
-
<input
|
|
235
|
-
v-if="!config.replacesNativeTransfer"
|
|
236
|
-
ref="importFileInput"
|
|
237
|
-
type="file"
|
|
238
|
-
accept="application/json,.json"
|
|
239
|
-
class="hidden"
|
|
240
|
-
@change="importData"
|
|
241
|
-
/>
|
|
242
|
-
</div>
|
|
231
|
+
<SystemResourceActions
|
|
232
|
+
:can-create="resource.canCreate.value"
|
|
233
|
+
:can-import="resource.canImport.value"
|
|
234
|
+
:replaces-native-transfer="config.replacesNativeTransfer"
|
|
235
|
+
@refresh="resource.reload"
|
|
236
|
+
@export="exportData"
|
|
237
|
+
@import="importData"
|
|
238
|
+
@create="openCreate"
|
|
239
|
+
/>
|
|
243
240
|
</div>
|
|
244
241
|
<SystemResourceFilters
|
|
245
242
|
v-model:keyword="keyword"
|
|
@@ -270,12 +267,14 @@ async function importData(event: Event) {
|
|
|
270
267
|
:inline-toggle="config.inlineToggle"
|
|
271
268
|
:toggle-pending-id="pendingToggleId"
|
|
272
269
|
:loading="resource.loading.value"
|
|
270
|
+
:viewable="auditViewable"
|
|
273
271
|
:page="resource.page.value"
|
|
274
272
|
:page-size="resource.pageSize.value"
|
|
275
273
|
@edit="openEdit"
|
|
276
274
|
@edit-license="openLicenseEditor"
|
|
277
275
|
@remove="openDelete"
|
|
278
276
|
@toggle="toggleInline"
|
|
277
|
+
@view="openDetail"
|
|
279
278
|
/>
|
|
280
279
|
<DataPagination
|
|
281
280
|
:page="resource.page.value"
|
|
@@ -305,42 +304,28 @@ async function importData(event: Event) {
|
|
|
305
304
|
</p>
|
|
306
305
|
</section>
|
|
307
306
|
<!-- @windy-module system.bulk-data end -->
|
|
308
|
-
<
|
|
307
|
+
<SystemResourceEditor
|
|
309
308
|
v-if="!config.inlineToggle"
|
|
310
309
|
v-model:open="editorOpen"
|
|
311
|
-
:
|
|
312
|
-
:
|
|
313
|
-
:
|
|
314
|
-
:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
ref="resourceForm"
|
|
328
|
-
:item="editingItem"
|
|
329
|
-
:defaults="formDefaults"
|
|
330
|
-
:department-create-mode="departmentCreateMode"
|
|
331
|
-
@submit="submit"
|
|
332
|
-
/>
|
|
333
|
-
<SystemResourceForm
|
|
334
|
-
v-else
|
|
335
|
-
ref="resourceForm"
|
|
336
|
-
:fields="config.fields"
|
|
337
|
-
:item="editingItem"
|
|
338
|
-
:defaults="formDefaults"
|
|
339
|
-
@submit="submit"
|
|
340
|
-
/>
|
|
341
|
-
</FormDialog>
|
|
310
|
+
:config="config"
|
|
311
|
+
:item="editingItem"
|
|
312
|
+
:defaults="formDefaults"
|
|
313
|
+
:permissions="permissions"
|
|
314
|
+
:department-create-mode="departmentCreateMode"
|
|
315
|
+
:loading="resource.saving.value || departmentTransitions.loading.value"
|
|
316
|
+
@submit="submit"
|
|
317
|
+
/>
|
|
318
|
+
|
|
319
|
+
<DepartmentTransitionDialog
|
|
320
|
+
v-if="config.key === 'departments'"
|
|
321
|
+
v-model:open="confirmOpen"
|
|
322
|
+
:department="pendingDelete"
|
|
323
|
+
:loading="departmentTransitions.loading.value"
|
|
324
|
+
@confirm="confirmDelete"
|
|
325
|
+
/>
|
|
342
326
|
|
|
343
327
|
<ConfirmDialog
|
|
328
|
+
v-else
|
|
344
329
|
v-model:open="confirmOpen"
|
|
345
330
|
title="删除记录"
|
|
346
331
|
description="删除后会进入软删除状态,并写入审计事件。"
|
|
@@ -356,5 +341,13 @@ async function importData(event: Event) {
|
|
|
356
341
|
:loading="resource.saving.value"
|
|
357
342
|
@save="saveLicensePolicy"
|
|
358
343
|
/>
|
|
344
|
+
|
|
345
|
+
<!-- @windy-module system.audit begin -->
|
|
346
|
+
<AuditLogDetailDialog
|
|
347
|
+
v-if="config.key === 'auditLogs'"
|
|
348
|
+
v-model:open="detailOpen"
|
|
349
|
+
:item="viewingItem"
|
|
350
|
+
/>
|
|
351
|
+
<!-- @windy-module system.audit end -->
|
|
359
352
|
</div>
|
|
360
353
|
</template>
|
|
@@ -4,11 +4,15 @@ 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";
|
|
7
9
|
|
|
8
10
|
const api = vi.hoisted(() => ({
|
|
9
11
|
list: vi.fn(),
|
|
10
12
|
update: vi.fn(),
|
|
11
13
|
quickCreateDepartment: vi.fn(),
|
|
14
|
+
transitionDepartment: vi.fn(),
|
|
15
|
+
loadPasswordPolicy: vi.fn(),
|
|
12
16
|
}));
|
|
13
17
|
|
|
14
18
|
vi.mock("@/services/system-api", async () => {
|
|
@@ -20,12 +24,25 @@ vi.mock("@/services/system-api", async () => {
|
|
|
20
24
|
listSystemResource: api.list,
|
|
21
25
|
updateSystemResource: api.update,
|
|
22
26
|
quickCreateDepartment: api.quickCreateDepartment,
|
|
27
|
+
transitionDepartment: api.transitionDepartment,
|
|
23
28
|
};
|
|
24
29
|
});
|
|
25
30
|
|
|
31
|
+
vi.mock("@/services/auth-api", () => ({
|
|
32
|
+
loadPasswordPolicy: api.loadPasswordPolicy,
|
|
33
|
+
}));
|
|
34
|
+
|
|
26
35
|
afterEach(() => vi.clearAllMocks());
|
|
27
36
|
|
|
28
37
|
describe("SystemResourcePage", () => {
|
|
38
|
+
api.loadPasswordPolicy.mockResolvedValue({
|
|
39
|
+
minLength: 8,
|
|
40
|
+
requireUppercase: true,
|
|
41
|
+
requireLowercase: true,
|
|
42
|
+
requireNumber: true,
|
|
43
|
+
requireSpecial: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
29
46
|
test("权限策略使用 Manifest 目录级联勾选而不是手输权限 key", async () => {
|
|
30
47
|
api.list.mockResolvedValue({
|
|
31
48
|
items: [
|
|
@@ -260,4 +277,55 @@ describe("SystemResourcePage", () => {
|
|
|
260
277
|
);
|
|
261
278
|
wrapper.unmount();
|
|
262
279
|
});
|
|
280
|
+
|
|
281
|
+
test("部门停用和删除使用显式迁移目标", async () => {
|
|
282
|
+
const department = {
|
|
283
|
+
id: "dept_sales",
|
|
284
|
+
code: "sales",
|
|
285
|
+
name: "销售部",
|
|
286
|
+
parentId: "dept_platform",
|
|
287
|
+
status: "active",
|
|
288
|
+
};
|
|
289
|
+
api.list.mockResolvedValue({
|
|
290
|
+
items: [department],
|
|
291
|
+
total: 1,
|
|
292
|
+
page: 1,
|
|
293
|
+
pageSize: 10,
|
|
294
|
+
});
|
|
295
|
+
api.transitionDepartment.mockResolvedValue({
|
|
296
|
+
department: { ...department, status: "disabled" },
|
|
297
|
+
movedChildren: 0,
|
|
298
|
+
movedUsers: 1,
|
|
299
|
+
updatedRoles: 0,
|
|
300
|
+
});
|
|
301
|
+
const wrapper = mount(SystemResourcePage, {
|
|
302
|
+
props: { config: systemResourceConfigs.departments },
|
|
303
|
+
});
|
|
304
|
+
await flushPromises();
|
|
305
|
+
|
|
306
|
+
wrapper.getComponent(SystemResourceTable).vm.$emit("edit", department);
|
|
307
|
+
await flushPromises();
|
|
308
|
+
wrapper.getComponent(DepartmentOrganizationForm).vm.$emit("submit", {
|
|
309
|
+
...department,
|
|
310
|
+
status: "disabled",
|
|
311
|
+
_transitionTargetDepartmentId: "dept_platform",
|
|
312
|
+
});
|
|
313
|
+
await flushPromises();
|
|
314
|
+
expect(api.transitionDepartment).toHaveBeenCalledWith("dept_sales", {
|
|
315
|
+
action: "disable",
|
|
316
|
+
targetDepartmentId: "dept_platform",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
wrapper.getComponent(SystemResourceTable).vm.$emit("remove", department);
|
|
320
|
+
await flushPromises();
|
|
321
|
+
wrapper
|
|
322
|
+
.getComponent(DepartmentTransitionDialog)
|
|
323
|
+
.vm.$emit("confirm", "dept_platform");
|
|
324
|
+
await flushPromises();
|
|
325
|
+
expect(api.transitionDepartment).toHaveBeenLastCalledWith("dept_sales", {
|
|
326
|
+
action: "delete",
|
|
327
|
+
targetDepartmentId: "dept_platform",
|
|
328
|
+
});
|
|
329
|
+
wrapper.unmount();
|
|
330
|
+
});
|
|
263
331
|
});
|
|
@@ -38,6 +38,7 @@ const props = defineProps<{
|
|
|
38
38
|
inlineToggle?: ResourceInlineToggle;
|
|
39
39
|
togglePendingId?: string;
|
|
40
40
|
loading?: boolean;
|
|
41
|
+
viewable?: boolean;
|
|
41
42
|
page?: number;
|
|
42
43
|
pageSize?: number;
|
|
43
44
|
}>();
|
|
@@ -47,6 +48,7 @@ const emit = defineEmits<{
|
|
|
47
48
|
editLicense: [item: SystemRecord];
|
|
48
49
|
remove: [item: SystemRecord];
|
|
49
50
|
toggle: [item: SystemRecord, checked: boolean];
|
|
51
|
+
view: [item: SystemRecord];
|
|
50
52
|
}>();
|
|
51
53
|
|
|
52
54
|
const hasRows = computed(() => props.items.length > 0);
|
|
@@ -54,6 +56,14 @@ const access = useAccessSnapshot();
|
|
|
54
56
|
|
|
55
57
|
function formatValue(item: SystemRecord, column: ResourceColumn): string {
|
|
56
58
|
if (column.format === "license-policy") return formatLicensePolicy(item);
|
|
59
|
+
if (column.format === "audit-ip") {
|
|
60
|
+
const actor = item.actor;
|
|
61
|
+
if (!actor || typeof actor !== "object" || Array.isArray(actor)) {
|
|
62
|
+
return "未记录";
|
|
63
|
+
}
|
|
64
|
+
const ip = (actor as Record<string, unknown>).ip;
|
|
65
|
+
return typeof ip === "string" && ip.trim() ? ip : "未记录";
|
|
66
|
+
}
|
|
57
67
|
const value = item[column.key];
|
|
58
68
|
if (column.format === "audit-action")
|
|
59
69
|
return auditAction(value)?.label || String(value || "-");
|
|
@@ -218,6 +228,15 @@ function licenseVersionLabel(version: string): string {
|
|
|
218
228
|
/>
|
|
219
229
|
</div>
|
|
220
230
|
<div v-else class="flex justify-end gap-2">
|
|
231
|
+
<Button
|
|
232
|
+
v-if="viewable"
|
|
233
|
+
variant="outline"
|
|
234
|
+
size="sm"
|
|
235
|
+
:aria-label="`查看审计日志 ${String(item.id)} 详情`"
|
|
236
|
+
@click="emit('view', item)"
|
|
237
|
+
>
|
|
238
|
+
查看详情
|
|
239
|
+
</Button>
|
|
221
240
|
<Button
|
|
222
241
|
v-if="editable"
|
|
223
242
|
variant="outline"
|
|
@@ -235,7 +254,7 @@ function licenseVersionLabel(version: string): string {
|
|
|
235
254
|
删除
|
|
236
255
|
</Button>
|
|
237
256
|
<span
|
|
238
|
-
v-if="!editable && !deletable"
|
|
257
|
+
v-if="!viewable && !editable && !deletable"
|
|
239
258
|
class="text-sm text-muted-foreground"
|
|
240
259
|
>
|
|
241
260
|
只读
|
|
@@ -157,19 +157,40 @@ describe("SystemResourceTable", () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
// @windy-module system.audit begin
|
|
160
|
-
test("
|
|
160
|
+
test("审计表直接显示动作摘要、IP,并从操作列进入详情", async () => {
|
|
161
|
+
const item = {
|
|
162
|
+
id: "audit_1",
|
|
163
|
+
action: "feature.update",
|
|
164
|
+
actor: { id: "user_1", name: "管理员", ip: "203.0.113.8" },
|
|
165
|
+
target: { type: "features", id: "system.audit" },
|
|
166
|
+
};
|
|
161
167
|
const wrapper = mount(SystemResourceTable, {
|
|
162
168
|
props: {
|
|
163
|
-
columns:
|
|
164
|
-
items: [
|
|
169
|
+
columns: systemResourceConfigs.auditLogs.columns,
|
|
170
|
+
items: [item],
|
|
171
|
+
viewable: true,
|
|
165
172
|
},
|
|
166
173
|
});
|
|
167
174
|
|
|
168
175
|
expect(wrapper.text()).toContain("功能开关更新");
|
|
169
|
-
expect(wrapper.text()).
|
|
170
|
-
expect(
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
expect(wrapper.text()).toContain("更新 features(system.audit)");
|
|
177
|
+
expect(wrapper.text()).toContain("203.0.113.8");
|
|
178
|
+
expect(wrapper.text()).toContain("查看详情");
|
|
179
|
+
await wrapper
|
|
180
|
+
.get('[aria-label="查看审计日志 audit_1 详情"]')
|
|
181
|
+
.trigger("click");
|
|
182
|
+
expect(wrapper.emitted("view")).toEqual([[item]]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("历史审计没有 IP 时显示未记录", () => {
|
|
186
|
+
const wrapper = mount(SystemResourceTable, {
|
|
187
|
+
props: {
|
|
188
|
+
columns: [{ key: "ip", label: "IP", format: "audit-ip" }],
|
|
189
|
+
items: [{ id: "audit_legacy", actor: { id: "user_1" } }],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(wrapper.text()).toContain("未记录");
|
|
173
194
|
});
|
|
174
195
|
// @windy-module system.audit end
|
|
175
196
|
|