create-windy 0.2.33 → 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.
Files changed (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -1,7 +1,7 @@
1
1
  import { flushPromises, mount } from "@vue/test-utils";
2
2
  import { describe, expect, test, vi } from "vitest";
3
- import { Select } from "@/components/ui/select";
4
3
  import { Checkbox } from "@/components/ui/checkbox";
4
+ import { Select } from "@/components/ui/select";
5
5
  import AuditLogFilters from "./AuditLogFilters.vue";
6
6
 
7
7
  const api = vi.hoisted(() => ({ list: vi.fn() }));
@@ -13,7 +13,7 @@ vi.mock("@/services/system-api", async () => ({
13
13
  }));
14
14
 
15
15
  describe("审计日志筛选", () => {
16
- test("操作者、动作和时间都是选择控件,动作使用稳定中文目录", async () => {
16
+ test("提交关键字、操作者、动作和本地分钟范围,并原样设置导出选项", async () => {
17
17
  api.list.mockResolvedValue({
18
18
  items: [
19
19
  {
@@ -30,30 +30,67 @@ describe("审计日志筛选", () => {
30
30
  await flushPromises();
31
31
 
32
32
  const selects = wrapper.findAllComponents(Select);
33
- expect(selects).toHaveLength(3);
34
- expect(wrapper.get('[aria-label="筛选操作者"]')).toBeTruthy();
35
- expect(wrapper.get('[aria-label="筛选动作"]')).toBeTruthy();
36
- expect(wrapper.get('[aria-label="筛选时间"]')).toBeTruthy();
37
-
33
+ expect(selects).toHaveLength(2);
34
+ await wrapper.get('[aria-label="审计关键字"]').setValue(" 案件甲 ");
35
+ await wrapper
36
+ .get('[aria-label="审计开始时间"]')
37
+ .setValue("2026-07-20T09:15");
38
+ await wrapper
39
+ .get('[aria-label="审计结束时间"]')
40
+ .setValue("2026-07-20T10:20");
38
41
  selects[0]?.vm.$emit("update:modelValue", "user_1");
39
42
  selects[1]?.vm.$emit("update:modelValue", "feature.update");
40
- selects[2]?.vm.$emit("update:modelValue", "week");
41
- await flushPromises();
43
+ await wrapper.get("form").trigger("submit");
42
44
 
43
- const lastChange = wrapper.emitted("change")?.at(-1)?.[0] as
44
- | { actorId?: string; action?: string; occurredAfter?: string }
45
- | undefined;
46
- expect(lastChange).toMatchObject({
45
+ expect(wrapper.emitted("change")?.at(-1)?.[0]).toEqual({
46
+ q: "案件甲",
47
47
  actorId: "user_1",
48
48
  action: "feature.update",
49
+ occurredAfter: new Date(2026, 6, 20, 9, 15, 0, 0).toISOString(),
50
+ occurredBefore: new Date(2026, 6, 20, 10, 20, 59, 999).toISOString(),
49
51
  });
50
- expect(lastChange?.occurredAfter).toMatch(/^\d{4}-\d{2}-\d{2}T/);
51
52
 
52
53
  wrapper.getComponent(Checkbox).vm.$emit("update:modelValue", true);
53
54
  await flushPromises();
54
55
  expect(wrapper.emitted("export-options")?.at(-1)?.[0]).toEqual({
55
56
  includeScheduler: true,
56
57
  });
57
- expect(wrapper.text()).toContain("页面列表始终只展示用户与业务操作");
58
+ });
59
+
60
+ test("可单独清空搜索,并可重置全部筛选", async () => {
61
+ api.list.mockResolvedValue({
62
+ items: [],
63
+ total: 0,
64
+ page: 1,
65
+ pageSize: 100,
66
+ });
67
+ const wrapper = mount(AuditLogFilters);
68
+ await flushPromises();
69
+ await wrapper.get('[aria-label="审计关键字"]').setValue("case_1");
70
+ await wrapper
71
+ .get('[aria-label="审计开始时间"]')
72
+ .setValue("2026-07-20T09:15");
73
+ await wrapper.get('[aria-label="清空审计关键字"]').trigger("click");
74
+ expect(wrapper.emitted("change")?.at(-1)?.[0]).toMatchObject({
75
+ q: undefined,
76
+ occurredAfter: new Date(2026, 6, 20, 9, 15, 0, 0).toISOString(),
77
+ });
78
+
79
+ await wrapper
80
+ .findAll("button")
81
+ .find((button) => button.text().includes("重置"))!
82
+ .trigger("click");
83
+
84
+ expect(wrapper.emitted("change")?.at(-1)?.[0]).toEqual({
85
+ q: undefined,
86
+ actorId: undefined,
87
+ action: undefined,
88
+ occurredAfter: undefined,
89
+ occurredBefore: undefined,
90
+ });
91
+ expect(
92
+ (wrapper.get('[aria-label="审计关键字"]').element as HTMLInputElement)
93
+ .value,
94
+ ).toBe("");
58
95
  });
59
96
  });
@@ -0,0 +1,129 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ NotificationRevision,
4
+ PageResult,
5
+ PlatformNotification,
6
+ } from "@southwind-ai/shared";
7
+ import { DEFAULT_PAGE_SIZE } from "@southwind-ai/shared";
8
+ import { shallowRef, watch } from "vue";
9
+ import DataPagination from "@/components/common/DataPagination.vue";
10
+ import ModalBody from "@/components/common/ModalBody.vue";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ } from "@/components/ui/dialog";
19
+ import { formatDateTime } from "@/lib/date-time";
20
+ import { listNotificationRevisions } from "@/services/notification-api";
21
+
22
+ const open = defineModel<boolean>("open", { default: false });
23
+
24
+ const props = defineProps<{
25
+ notification?: PlatformNotification;
26
+ }>();
27
+
28
+ const history = shallowRef<PageResult<NotificationRevision>>();
29
+ const loading = shallowRef(false);
30
+ const error = shallowRef("");
31
+ const page = shallowRef(1);
32
+
33
+ watch(
34
+ [open, () => props.notification?.id],
35
+ ([isOpen, notificationId], [, previousId]) => {
36
+ if (!isOpen || !notificationId) return;
37
+ if (notificationId !== previousId) page.value = 1;
38
+ void loadHistory();
39
+ },
40
+ );
41
+
42
+ async function loadHistory() {
43
+ const notification = props.notification;
44
+ if (!notification) return;
45
+ loading.value = true;
46
+ error.value = "";
47
+ try {
48
+ history.value = await listNotificationRevisions(
49
+ notification.id,
50
+ page.value,
51
+ DEFAULT_PAGE_SIZE,
52
+ );
53
+ } catch (currentError) {
54
+ error.value =
55
+ currentError instanceof Error
56
+ ? currentError.message
57
+ : String(currentError);
58
+ } finally {
59
+ loading.value = false;
60
+ }
61
+ }
62
+
63
+ function setPage(value: number) {
64
+ page.value = value;
65
+ void loadHistory();
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <Dialog v-model:open="open">
71
+ <DialogContent class="sm:max-w-3xl">
72
+ <DialogHeader>
73
+ <DialogTitle>公告修改历史</DialogTitle>
74
+ <DialogDescription>
75
+ {{ notification?.title || "公告" }}的版本内容、修改人和修改时间。
76
+ </DialogDescription>
77
+ </DialogHeader>
78
+ <ModalBody class="space-y-3">
79
+ <p v-if="error" class="text-sm text-destructive">{{ error }}</p>
80
+ <p
81
+ v-if="loading"
82
+ class="py-8 text-center text-sm text-muted-foreground"
83
+ >
84
+ 正在加载修改历史…
85
+ </p>
86
+ <template v-else>
87
+ <article
88
+ v-for="revision in history?.items"
89
+ :key="revision.revision"
90
+ class="space-y-3 rounded-md border p-4"
91
+ >
92
+ <div class="flex flex-wrap items-center justify-between gap-2">
93
+ <div class="flex items-center gap-2">
94
+ <Badge>v{{ revision.revision }}</Badge>
95
+ <Badge variant="outline">{{ revision.category }}</Badge>
96
+ </div>
97
+ <span class="text-xs text-muted-foreground">
98
+ {{ formatDateTime(revision.editedAt) }} ·
99
+ {{ revision.editedBy }}
100
+ </span>
101
+ </div>
102
+ <div>
103
+ <h3 class="font-medium">{{ revision.title }}</h3>
104
+ <p class="mt-1 text-sm text-muted-foreground">
105
+ {{ revision.summary }}
106
+ </p>
107
+ </div>
108
+ <p class="whitespace-pre-wrap text-sm leading-6">
109
+ {{ revision.content }}
110
+ </p>
111
+ </article>
112
+ </template>
113
+ <p
114
+ v-if="!loading && !history?.items.length"
115
+ class="py-8 text-center text-sm text-muted-foreground"
116
+ >
117
+ 暂无版本历史。
118
+ </p>
119
+ <DataPagination
120
+ v-if="history"
121
+ :page="history.page"
122
+ :page-size="history.pageSize"
123
+ :total="history.total"
124
+ @update:page="setPage"
125
+ />
126
+ </ModalBody>
127
+ </DialogContent>
128
+ </Dialog>
129
+ </template>
@@ -0,0 +1,91 @@
1
+ <script setup lang="ts">
2
+ import type { PlatformNotification } from "@southwind-ai/shared";
3
+ import { ArchiveIcon, HistoryIcon, PencilIcon } from "@lucide/vue";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { formatDateTime } from "@/lib/date-time";
7
+
8
+ defineProps<{
9
+ notification: PlatformNotification;
10
+ sequence: number;
11
+ canEdit: boolean;
12
+ }>();
13
+
14
+ defineEmits<{
15
+ archive: [notification: PlatformNotification];
16
+ edit: [notification: PlatformNotification];
17
+ history: [notification: PlatformNotification];
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <article
23
+ class="flex flex-col gap-3 rounded-md border p-4 md:flex-row md:items-start md:justify-between"
24
+ >
25
+ <div class="min-w-0 space-y-2">
26
+ <div class="flex flex-wrap items-center gap-2">
27
+ <h2 class="font-medium text-foreground">
28
+ {{ sequence }}. {{ notification.title }}
29
+ </h2>
30
+ <Badge
31
+ :variant="
32
+ notification.status === 'published' ? 'default' : 'secondary'
33
+ "
34
+ >
35
+ {{
36
+ notification.status === "published"
37
+ ? "已发布"
38
+ : notification.status === "scheduled"
39
+ ? "已定时"
40
+ : "已归档"
41
+ }}
42
+ </Badge>
43
+ <Badge variant="outline">{{ notification.category }}</Badge>
44
+ <Badge variant="outline">v{{ notification.revision }}</Badge>
45
+ </div>
46
+ <p class="text-sm text-muted-foreground">
47
+ {{ notification.summary }}
48
+ </p>
49
+ <p class="text-xs text-muted-foreground">
50
+ {{ notification.status === "scheduled" ? "计划发布" : "发布时间" }}
51
+ {{
52
+ formatDateTime(
53
+ notification.status === "scheduled"
54
+ ? notification.scheduledFor
55
+ : notification.publishedAt,
56
+ )
57
+ }}
58
+ · 最近修改
59
+ {{ formatDateTime(notification.updatedAt) }}
60
+ </p>
61
+ </div>
62
+ <div class="flex shrink-0 flex-wrap gap-2">
63
+ <Button
64
+ variant="outline"
65
+ size="sm"
66
+ @click="$emit('history', notification)"
67
+ >
68
+ <HistoryIcon data-icon="inline-start" />
69
+ 历史
70
+ </Button>
71
+ <Button
72
+ v-if="canEdit && notification.status === 'published'"
73
+ variant="outline"
74
+ size="sm"
75
+ @click="$emit('edit', notification)"
76
+ >
77
+ <PencilIcon data-icon="inline-start" />
78
+ 编辑
79
+ </Button>
80
+ <Button
81
+ v-if="notification.status !== 'archived'"
82
+ variant="outline"
83
+ size="sm"
84
+ @click="$emit('archive', notification)"
85
+ >
86
+ <ArchiveIcon data-icon="inline-start" />
87
+ 归档
88
+ </Button>
89
+ </div>
90
+ </article>
91
+ </template>
@@ -0,0 +1,121 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ NotificationAudienceOptions,
4
+ PlatformNotification,
5
+ PublishNotificationInput,
6
+ } from "@southwind-ai/shared";
7
+ import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
8
+ import { onBeforeRouteLeave } from "vue-router";
9
+ import FormDialog from "@/components/common/FormDialog.vue";
10
+ import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
11
+ import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
12
+ import {
13
+ getNotificationAudienceOptions,
14
+ publishNotification,
15
+ } from "@/services/notification-api";
16
+ import NotificationPublishForm from "./NotificationPublishForm.vue";
17
+
18
+ const open = defineModel<boolean>("open", { default: false });
19
+ const emit = defineEmits<{
20
+ published: [notification: PlatformNotification];
21
+ error: [message: string];
22
+ }>();
23
+ const form = shallowRef(emptyForm());
24
+ const baseline = shallowRef(emptyForm());
25
+ const audienceOptions = shallowRef<NotificationAudienceOptions>();
26
+ const publishing = shallowRef(false);
27
+ const confirmation = useUnsavedChangesConfirmation();
28
+ const dirty = computed(
29
+ () =>
30
+ open.value && JSON.stringify(form.value) !== JSON.stringify(baseline.value),
31
+ );
32
+
33
+ watch(open, async (value) => {
34
+ if (!value) return;
35
+ if (!audienceOptions.value) {
36
+ try {
37
+ audienceOptions.value = await getNotificationAudienceOptions();
38
+ } catch (error) {
39
+ emit("error", message(error));
40
+ }
41
+ }
42
+ });
43
+ onMounted(() => window.addEventListener("beforeunload", preventDirtyUnload));
44
+ onBeforeUnmount(() =>
45
+ window.removeEventListener("beforeunload", preventDirtyUnload),
46
+ );
47
+ onBeforeRouteLeave(() => (dirty.value ? confirmation.request(publish) : true));
48
+
49
+ async function publish(): Promise<boolean> {
50
+ if (publishing.value) return false;
51
+ publishing.value = true;
52
+ try {
53
+ const notification = await publishNotification({ ...form.value });
54
+ form.value = emptyForm();
55
+ baseline.value = emptyForm();
56
+ open.value = false;
57
+ emit("published", notification);
58
+ return true;
59
+ } catch (error) {
60
+ emit("error", message(error));
61
+ return false;
62
+ } finally {
63
+ publishing.value = false;
64
+ }
65
+ }
66
+
67
+ function beforeClose() {
68
+ return dirty.value ? confirmation.request(publish) : true;
69
+ }
70
+
71
+ function preventDirtyUnload(event: BeforeUnloadEvent) {
72
+ if (!dirty.value) return;
73
+ event.preventDefault();
74
+ event.returnValue = "";
75
+ }
76
+
77
+ function emptyForm(): PublishNotificationInput {
78
+ return {
79
+ title: "",
80
+ summary: "",
81
+ content: "",
82
+ category: "系统",
83
+ recipientUserIds: [],
84
+ recipientRoleCodes: [],
85
+ recipientDepartmentIds: [],
86
+ deliveryChannels: [],
87
+ includeChildDepartments: false,
88
+ };
89
+ }
90
+
91
+ function message(error: unknown) {
92
+ return error instanceof Error ? error.message : String(error);
93
+ }
94
+ </script>
95
+
96
+ <template>
97
+ <FormDialog
98
+ v-model:open="open"
99
+ title="发布新通知"
100
+ description="可立即或定时发布,并按用户、角色、部门和偏好合并投递范围。"
101
+ :submit-text="form.scheduledFor ? '确认定时发布' : '确认发布'"
102
+ :loading="publishing"
103
+ :before-close="beforeClose"
104
+ prevent-outside-close
105
+ content-class="sm:max-w-4xl"
106
+ @submit="publish"
107
+ >
108
+ <NotificationPublishForm
109
+ v-model="form"
110
+ :audience-options="audienceOptions"
111
+ />
112
+ </FormDialog>
113
+
114
+ <UnsavedChangesDialog
115
+ :open="confirmation.open.value"
116
+ :saving="confirmation.saving.value"
117
+ description="新通知尚未发布。取消会留在编辑状态;发布成功后才会继续关闭或离开。"
118
+ @cancel="confirmation.cancel"
119
+ @save="confirmation.save"
120
+ />
121
+ </template>
@@ -1,12 +1,54 @@
1
1
  <script setup lang="ts">
2
- import type { PublishNotificationInput } from "@southwind-ai/shared";
2
+ import type {
3
+ ExternalNotificationChannel,
4
+ NotificationAudienceOptions,
5
+ PublishNotificationInput,
6
+ } from "@southwind-ai/shared";
7
+ import { Checkbox } from "@/components/ui/checkbox";
3
8
  import { Input } from "@/components/ui/input";
4
9
 
5
10
  const model = defineModel<PublishNotificationInput>({ required: true });
11
+ defineProps<{
12
+ audienceOptions?: NotificationAudienceOptions;
13
+ }>();
6
14
 
7
15
  function update(key: keyof PublishNotificationInput, value: string | number) {
8
16
  model.value = { ...model.value, [key]: String(value) };
9
17
  }
18
+
19
+ function updateArray(
20
+ key:
21
+ | "recipientUserIds"
22
+ | "recipientRoleCodes"
23
+ | "recipientDepartmentIds"
24
+ | "deliveryChannels",
25
+ value: string,
26
+ checked: boolean | "indeterminate",
27
+ ) {
28
+ const current = model.value[key] || [];
29
+ model.value = {
30
+ ...model.value,
31
+ [key]:
32
+ checked === true
33
+ ? [...new Set([...current, value])]
34
+ : current.filter((item) => item !== value),
35
+ };
36
+ }
37
+
38
+ function updateSchedule(value: string | number) {
39
+ const local = String(value);
40
+ model.value = {
41
+ ...model.value,
42
+ scheduledFor: local ? new Date(local).toISOString() : undefined,
43
+ };
44
+ }
45
+
46
+ function localSchedule(value?: string) {
47
+ if (!value) return "";
48
+ const date = new Date(value);
49
+ const offset = date.getTimezoneOffset() * 60_000;
50
+ return new Date(date.getTime() - offset).toISOString().slice(0, 16);
51
+ }
10
52
  </script>
11
53
 
12
54
  <template>
@@ -54,4 +96,123 @@ function update(key: keyof PublishNotificationInput, value: string | number) {
54
96
  @input="update('content', ($event.target as HTMLTextAreaElement).value)"
55
97
  />
56
98
  </label>
99
+
100
+ <template v-if="audienceOptions">
101
+ <div class="grid gap-4 md:grid-cols-2">
102
+ <label class="space-y-2 text-sm font-medium">
103
+ <span>发布时间</span>
104
+ <Input
105
+ type="datetime-local"
106
+ :model-value="localSchedule(model.scheduledFor)"
107
+ @update:model-value="updateSchedule"
108
+ />
109
+ <span class="block text-xs font-normal text-muted-foreground">
110
+ 留空立即发布;选择未来时间后由调度器发布。
111
+ </span>
112
+ </label>
113
+ <fieldset class="space-y-2">
114
+ <legend class="text-sm font-medium">外部渠道</legend>
115
+ <div class="flex gap-4 pt-2">
116
+ <label
117
+ v-for="channel in ['email', 'sms'] as ExternalNotificationChannel[]"
118
+ :key="channel"
119
+ class="flex items-center gap-2 text-sm"
120
+ >
121
+ <Checkbox
122
+ :model-value="model.deliveryChannels?.includes(channel)"
123
+ @update:model-value="
124
+ updateArray('deliveryChannels', channel, $event)
125
+ "
126
+ />
127
+ {{ channel === "email" ? "邮件" : "短信" }}
128
+ </label>
129
+ </div>
130
+ <p class="text-xs text-muted-foreground">
131
+ 短信仅在业务项目注入供应商 Adapter 后可投递。
132
+ </p>
133
+ </fieldset>
134
+ </div>
135
+
136
+ <div class="space-y-3 rounded-md border p-4">
137
+ <div>
138
+ <p class="text-sm font-medium">定向受众</p>
139
+ <p class="text-xs text-muted-foreground">
140
+ 用户、角色和部门按“或”合并;全部留空即广播。
141
+ </p>
142
+ </div>
143
+ <div class="grid gap-4 md:grid-cols-3">
144
+ <fieldset class="space-y-2">
145
+ <legend class="text-xs font-medium text-muted-foreground">
146
+ 用户
147
+ </legend>
148
+ <label
149
+ v-for="option in audienceOptions.users"
150
+ :key="option.id"
151
+ class="flex items-center gap-2 text-sm"
152
+ >
153
+ <Checkbox
154
+ :model-value="model.recipientUserIds?.includes(option.id)"
155
+ @update:model-value="
156
+ updateArray('recipientUserIds', option.id, $event)
157
+ "
158
+ />
159
+ {{ option.label }}
160
+ </label>
161
+ </fieldset>
162
+ <fieldset class="space-y-2">
163
+ <legend class="text-xs font-medium text-muted-foreground">
164
+ 角色
165
+ </legend>
166
+ <label
167
+ v-for="option in audienceOptions.roles"
168
+ :key="option.id"
169
+ class="flex items-center gap-2 text-sm"
170
+ >
171
+ <Checkbox
172
+ :model-value="
173
+ option.code
174
+ ? model.recipientRoleCodes?.includes(option.code)
175
+ : false
176
+ "
177
+ @update:model-value="
178
+ option.code &&
179
+ updateArray('recipientRoleCodes', option.code, $event)
180
+ "
181
+ />
182
+ {{ option.label }}
183
+ </label>
184
+ </fieldset>
185
+ <fieldset class="space-y-2">
186
+ <legend class="text-xs font-medium text-muted-foreground">
187
+ 部门
188
+ </legend>
189
+ <label
190
+ v-for="option in audienceOptions.departments"
191
+ :key="option.id"
192
+ class="flex items-center gap-2 text-sm"
193
+ >
194
+ <Checkbox
195
+ :model-value="model.recipientDepartmentIds?.includes(option.id)"
196
+ @update:model-value="
197
+ updateArray('recipientDepartmentIds', option.id, $event)
198
+ "
199
+ />
200
+ {{ option.label }}
201
+ </label>
202
+ <label class="flex items-center gap-2 pt-2 text-sm">
203
+ <Checkbox
204
+ :model-value="model.includeChildDepartments"
205
+ @update:model-value="
206
+ model = {
207
+ ...model,
208
+ includeChildDepartments: $event === true,
209
+ }
210
+ "
211
+ />
212
+ 包含子部门
213
+ </label>
214
+ </fieldset>
215
+ </div>
216
+ </div>
217
+ </template>
57
218
  </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { useTemplateRef } from "vue";
3
+ import { Button } from "@/components/ui/button";
4
+ import { DownloadIcon, PlusIcon, RefreshCwIcon, UploadIcon } from "@lucide/vue";
5
+
6
+ defineProps<{
7
+ canCreate: boolean;
8
+ canImport: boolean;
9
+ replacesNativeTransfer?: boolean;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ refresh: [];
14
+ export: [];
15
+ import: [file: File];
16
+ create: [];
17
+ }>();
18
+
19
+ const importFileInput = useTemplateRef<HTMLInputElement>("importFileInput");
20
+
21
+ function openImport() {
22
+ importFileInput.value?.click();
23
+ }
24
+
25
+ function selectImport(event: Event) {
26
+ const input = event.target as HTMLInputElement;
27
+ const file = input.files?.[0];
28
+ if (file) emit("import", file);
29
+ input.value = "";
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div class="flex flex-wrap gap-2">
35
+ <Button variant="outline" @click="emit('refresh')">
36
+ <RefreshCwIcon data-icon="inline-start" />
37
+ 刷新
38
+ </Button>
39
+ <Button
40
+ v-if="!replacesNativeTransfer"
41
+ variant="outline"
42
+ @click="emit('export')"
43
+ >
44
+ <DownloadIcon data-icon="inline-start" />
45
+ 导出
46
+ </Button>
47
+ <Button
48
+ v-if="canImport && !replacesNativeTransfer"
49
+ variant="outline"
50
+ @click="openImport"
51
+ >
52
+ <UploadIcon data-icon="inline-start" />
53
+ 导入
54
+ </Button>
55
+ <Button v-if="canCreate" @click="emit('create')">
56
+ <PlusIcon data-icon="inline-start" />
57
+ 新增
58
+ </Button>
59
+ <input
60
+ v-if="!replacesNativeTransfer"
61
+ ref="importFileInput"
62
+ type="file"
63
+ accept="application/json,.json"
64
+ class="hidden"
65
+ @change="selectImport"
66
+ />
67
+ </div>
68
+ </template>