create-windy 0.3.19 → 0.4.1

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 (234) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +788 -280
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +18 -8
  7. package/template/README.md +32 -22
  8. package/template/apps/server/Dockerfile +1 -0
  9. package/template/apps/server/package.json +1 -0
  10. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  11. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  12. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  13. package/template/apps/server/src/agent/routes.test.ts +20 -7
  14. package/template/apps/server/src/agent/routes.ts +14 -0
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  16. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  17. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  18. package/template/apps/server/src/app/server-composition.ts +56 -0
  19. package/template/apps/server/src/application-services.ts +2 -0
  20. package/template/apps/server/src/bulk-data/service.ts +14 -0
  21. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  22. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  23. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  24. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  25. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  26. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  27. package/template/apps/server/src/feature/routes.ts +33 -15
  28. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  29. package/template/apps/server/src/foundation.ts +64 -5
  30. package/template/apps/server/src/guards.test.ts +23 -0
  31. package/template/apps/server/src/guards.ts +12 -0
  32. package/template/apps/server/src/index.ts +28 -34
  33. package/template/apps/server/src/installed-business-modules.ts +13 -24
  34. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  35. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  36. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  37. package/template/apps/server/src/license/activation.test.ts +30 -0
  38. package/template/apps/server/src/license/activation.ts +26 -0
  39. package/template/apps/server/src/license/restriction.test.ts +16 -0
  40. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  41. package/template/apps/server/src/license/routes.ts +20 -4
  42. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  43. package/template/apps/server/src/module-bootstrap.ts +14 -23
  44. package/template/apps/server/src/module-composition.test.ts +43 -2
  45. package/template/apps/server/src/module-composition.ts +29 -0
  46. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  47. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  48. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  49. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  50. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  51. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  52. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  53. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  54. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  55. package/template/apps/server/src/module-host/initialize.ts +15 -4
  56. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  57. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  58. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  59. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  60. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  61. package/template/apps/server/src/module-host/request-context.ts +3 -0
  62. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  63. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  64. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  65. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  66. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  67. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  68. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  69. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  70. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  71. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  72. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  73. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  74. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  75. package/template/apps/server/src/module-host.test.ts +2 -0
  76. package/template/apps/server/src/module-preflight.test.ts +289 -0
  77. package/template/apps/server/src/module-preflight.ts +96 -0
  78. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  79. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  80. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  81. package/template/apps/server/src/route-guards.test.ts +22 -0
  82. package/template/apps/server/src/route-guards.ts +10 -3
  83. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  84. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  85. package/template/apps/server/src/runtime-feature.ts +5 -0
  86. package/template/apps/server/src/runtime.ts +10 -49
  87. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  88. package/template/apps/server/src/scheduler/audit.ts +1 -1
  89. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  90. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  91. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  92. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  93. package/template/apps/server/src/scheduler/service.ts +18 -21
  94. package/template/apps/server/src/scheduler/types.ts +5 -1
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  100. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  101. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  102. package/template/apps/server/src/startup-preflight.ts +37 -0
  103. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  104. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  105. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  106. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  107. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  108. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  109. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  110. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  111. package/template/apps/server/src/system/routes.test.ts +21 -11
  112. package/template/apps/server/src/system/routes.ts +11 -2
  113. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  114. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  115. package/template/apps/server/src/work-order/task.test.ts +6 -1
  116. package/template/apps/web/Dockerfile +5 -0
  117. package/template/apps/web/package.json +1 -0
  118. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  119. package/template/apps/web/src/app/web-composition.ts +24 -0
  120. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  121. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  122. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  123. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  124. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  125. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  126. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  127. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  128. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  129. package/template/apps/web/src/layout/navigation.ts +29 -4
  130. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  131. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  132. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  133. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  134. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  135. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  136. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  137. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  138. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  139. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  140. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  141. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  142. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  143. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  144. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  145. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  146. package/template/apps/web/src/router/business-routes.ts +32 -45
  147. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  148. package/template/apps/web/src/router/index.ts +64 -54
  149. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  150. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  151. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  152. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  153. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  154. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  155. package/template/apps/web/src/services/work-order-api.ts +16 -0
  156. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  157. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  158. package/template/apps/web/vite.config.ts +22 -1
  159. package/template/docs/architecture/migration-bundle.md +10 -7
  160. package/template/docs/architecture/module-manifest-composition.md +85 -3
  161. package/template/docs/architecture/search-provider-protocol.md +6 -1
  162. package/template/docs/architecture/server-module-interface.md +70 -7
  163. package/template/docs/development/business-web-routes.md +53 -50
  164. package/template/docs/development/module-lifecycle.md +48 -54
  165. package/template/docs/platform/navigation-registry.md +5 -4
  166. package/template/docs/platform/platform-shell-settings.md +15 -10
  167. package/template/docs/platform/web-system-crud.md +1 -1
  168. package/template/package.json +1 -1
  169. package/template/packages/config/package.json +1 -1
  170. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  171. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  172. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  173. package/template/packages/database/package.json +1 -1
  174. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  175. package/template/packages/example-work-order/package.json +6 -0
  176. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  177. package/template/packages/example-work-order/src/admin.ts +35 -0
  178. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  179. package/template/packages/example-work-order/src/manifest.ts +54 -7
  180. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  181. package/template/packages/example-work-order/src/server-module.ts +2 -2
  182. package/template/packages/jobs/package.json +1 -1
  183. package/template/packages/modules/index.ts +4 -0
  184. package/template/packages/modules/package.json +3 -2
  185. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  186. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  187. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  188. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  189. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  190. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  191. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  192. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  193. package/template/packages/modules/src/composition.test.ts +74 -9
  194. package/template/packages/modules/src/composition.ts +8 -0
  195. package/template/packages/modules/src/manifest.ts +1 -0
  196. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  197. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  198. package/template/packages/modules/src/module-host-range.ts +125 -0
  199. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  200. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  201. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  202. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  203. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  204. package/template/packages/modules/src/system-api-binding.ts +43 -9
  205. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  206. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  207. package/template/packages/modules/src/system-api-operational.ts +57 -8
  208. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  209. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  210. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  211. package/template/packages/modules/src/system-data-governance.ts +5 -2
  212. package/template/packages/modules/src/system-features.ts +31 -0
  213. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  214. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  215. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  216. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  217. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  218. package/template/packages/modules/src/system-modules.test.ts +10 -2
  219. package/template/packages/modules/src/system-modules.ts +6 -5
  220. package/template/packages/modules/src/system-permissions.ts +4 -0
  221. package/template/packages/modules/user/module.ts +19 -2
  222. package/template/packages/server-sdk/index.ts +8 -0
  223. package/template/packages/server-sdk/package.json +2 -1
  224. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  225. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  226. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  227. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.test.ts +7 -0
  230. package/template/packages/shared/src/audit.ts +16 -0
  231. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  232. package/template/packages/shared/src/platform-settings.ts +17 -0
  233. package/template/packages/shared/src/rbac.ts +2 -0
  234. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -31,17 +31,16 @@ const allowed = computed(() => {
31
31
  props.scope === "admin" ? "system.watermark.read" : undefined,
32
32
  }).allowed;
33
33
  });
34
- const scopeEnabled = computed(
35
- () => props.scope === "admin" || watermark.policy.value.businessPagesEnabled,
36
- );
37
34
  const patternStyle = computed<StyleValue>(() => {
38
35
  const pattern = createWatermarkPattern(
39
36
  watermark.content.value,
40
37
  watermark.policy.value.density,
38
+ watermark.policy.value.fontSize,
41
39
  );
42
40
  return {
43
41
  "--watermark-image": pattern.image,
44
42
  "--watermark-size": pattern.size,
43
+ opacity: watermark.policy.value.opacityPercent / 100,
45
44
  };
46
45
  });
47
46
  </script>
@@ -50,12 +49,9 @@ const patternStyle = computed<StyleValue>(() => {
50
49
  <Teleport to="body">
51
50
  <div
52
51
  v-if="
53
- allowed &&
54
- scopeEnabled &&
55
- watermark.policy.value.enabled &&
56
- watermark.content.value
52
+ allowed && watermark.policy.value.enabled && watermark.content.value
57
53
  "
58
- class="global-watermark pointer-events-none fixed inset-0 isolate z-[60] transform-gpu bg-foreground opacity-[0.075] dark:opacity-[0.11]"
54
+ class="global-watermark pointer-events-none fixed inset-0 isolate z-[60] transform-gpu bg-foreground"
59
55
  :style="patternStyle"
60
56
  aria-hidden="true"
61
57
  data-testid="global-watermark"
@@ -41,6 +41,8 @@ const state = vi.hoisted(() => ({
41
41
  density: "medium" as const,
42
42
  contentFields: ["username", "displayName"] as const,
43
43
  customContent: "",
44
+ opacityPercent: 8,
45
+ fontSize: 14,
44
46
  },
45
47
  },
46
48
  content: { value: "zhangsan · 张三" },
@@ -77,11 +79,13 @@ beforeEach(() => {
77
79
  density: "medium" as const,
78
80
  contentFields: ["username", "displayName"] as const,
79
81
  customContent: "",
82
+ opacityPercent: 8,
83
+ fontSize: 14,
80
84
  };
81
85
  });
82
86
 
83
87
  describe("GlobalWatermark", () => {
84
- test("Admin 水印沿用原有总开关且不受业务页面范围影响", () => {
88
+ test("总开关统一控制 Admin 与业务页面水印", () => {
85
89
  const wrapper = mount(GlobalWatermark);
86
90
 
87
91
  expect(wrapper.find('[data-testid="global-watermark"]').exists()).toBe(
@@ -97,7 +101,7 @@ describe("GlobalWatermark", () => {
97
101
  ).toBe(false);
98
102
  });
99
103
 
100
- test("业务页面水印默认开启并可由持久化范围开关关闭", () => {
104
+ test("旧业务范围字段不再形成独立开关", () => {
101
105
  const wrapper = mount(GlobalWatermark, {
102
106
  props: { scope: "business" },
103
107
  });
@@ -111,13 +115,13 @@ describe("GlobalWatermark", () => {
111
115
  ...state.policy.value,
112
116
  businessPagesEnabled: false,
113
117
  };
114
- const disabledWrapper = mount(GlobalWatermark, {
118
+ const legacyWrapper = mount(GlobalWatermark, {
115
119
  props: { scope: "business" },
116
120
  });
117
121
 
118
122
  expect(
119
- disabledWrapper.find('[data-testid="global-watermark"]').exists(),
120
- ).toBe(false);
123
+ legacyWrapper.find('[data-testid="global-watermark"]').exists(),
124
+ ).toBe(true);
121
125
  });
122
126
 
123
127
  test("使用按内容和密度计算的平铺蒙版并保持审计层级", () => {
@@ -128,6 +132,7 @@ describe("GlobalWatermark", () => {
128
132
 
129
133
  expect(element.attributes("style")).toContain("--watermark-image:");
130
134
  expect(element.attributes("style")).toContain("--watermark-size:");
135
+ expect(element.attributes("style")).toContain("opacity: 0.08");
131
136
  expect(element.classes()).toEqual(
132
137
  expect.arrayContaining([
133
138
  "isolate",
@@ -145,6 +150,8 @@ describe("GlobalWatermark", () => {
145
150
  density: "medium",
146
151
  contentFields: ["username", "displayName"],
147
152
  customContent: "",
153
+ opacityPercent: 8,
154
+ fontSize: 14,
148
155
  };
149
156
  state.access.value.features[0]!.allowed = false;
150
157
 
@@ -23,6 +23,7 @@ import {
23
23
  import { Input } from "@/components/ui/input";
24
24
  import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
25
25
  import { useGlobalSearch } from "@/composables/useGlobalSearch";
26
+ import { webAdminUiComposition } from "@/router/web-composition-host";
26
27
  import GlobalSearchResults from "./GlobalSearchResults.vue";
27
28
  import { authorizedSidebarMenus } from "./navigation";
28
29
 
@@ -42,7 +43,13 @@ const businessSearch = useGlobalSearch(query);
42
43
 
43
44
  const items = computed(() =>
44
45
  access.snapshot.value
45
- ? flattenMenus(authorizedSidebarMenus(access.snapshot.value))
46
+ ? flattenMenus(
47
+ authorizedSidebarMenus(
48
+ access.snapshot.value,
49
+ webAdminUiComposition.presentation,
50
+ webAdminUiComposition,
51
+ ),
52
+ )
46
53
  : [],
47
54
  );
48
55
  const results = computed(() => {
@@ -5,6 +5,7 @@ import type {
5
5
  MenuNode,
6
6
  PermissionDefinition,
7
7
  } from "@southwind-ai/shared";
8
+ import type { AdminRoutePresentation } from "../router/admin-ui-composition";
8
9
 
9
10
  export interface WebAccessActor {
10
11
  id: string;
@@ -47,6 +48,11 @@ export interface WebAccessDecision {
47
48
  reason: "allowed" | "missing-permission" | "feature-disabled";
48
49
  }
49
50
 
51
+ export interface AdminRouteActivation {
52
+ readonly knownRouteKeys: ReadonlySet<string>;
53
+ readonly visibleRouteKeys: ReadonlySet<string>;
54
+ }
55
+
50
56
  export const dashboardMenu: MenuNode = {
51
57
  key: "dashboard",
52
58
  title: "工作台",
@@ -80,9 +86,11 @@ export function evaluateWebAccess(
80
86
 
81
87
  export function authorizedSidebarMenus(
82
88
  snapshot: WebAccessSnapshot,
89
+ presentation: ReadonlyMap<string, AdminRoutePresentation> = new Map(),
90
+ activation?: AdminRouteActivation,
83
91
  ): MenuNode[] {
84
92
  return [dashboardMenu, ...snapshot.menus]
85
- .map((menu) => authorizeMenu(menu, snapshot))
93
+ .map((menu) => authorizeMenu(menu, snapshot, presentation, activation))
86
94
  .filter((menu): menu is MenuNode => Boolean(menu))
87
95
  .sort((left, right) => left.order - right.order);
88
96
  }
@@ -94,14 +102,31 @@ export function canToggleFeatureFlags(snapshot: WebAccessSnapshot): boolean {
94
102
  function authorizeMenu(
95
103
  menu: MenuNode,
96
104
  snapshot: WebAccessSnapshot,
105
+ presentation: ReadonlyMap<string, AdminRoutePresentation>,
106
+ activation?: AdminRouteActivation,
97
107
  ): MenuNode | undefined {
98
108
  if (!menu.visible) return undefined;
109
+ if (
110
+ activation?.knownRouteKeys.has(menu.key) &&
111
+ !activation.visibleRouteKeys.has(menu.key)
112
+ ) {
113
+ return undefined;
114
+ }
115
+ const override = presentation.get(menu.key);
116
+ if (override?.hidden) return undefined;
99
117
  const children = menu.children
100
- ?.map((child) => authorizeMenu(child, snapshot))
101
- .filter((child): child is MenuNode => Boolean(child));
118
+ ?.map((child) => authorizeMenu(child, snapshot, presentation, activation))
119
+ .filter((child): child is MenuNode => Boolean(child))
120
+ .sort((left, right) => left.order - right.order);
102
121
  const access = evaluateWebAccess(snapshot, menu);
103
122
  if (!access.allowed || (menu.type === "catalog" && children?.length === 0)) {
104
123
  return undefined;
105
124
  }
106
- return { ...menu, children };
125
+ return {
126
+ ...menu,
127
+ ...(override?.title === undefined ? {} : { title: override.title }),
128
+ ...(override?.icon === undefined ? {} : { icon: override.icon }),
129
+ ...(override?.order === undefined ? {} : { order: override.order }),
130
+ children,
131
+ };
107
132
  }
@@ -6,6 +6,7 @@ import {
6
6
  evaluateWebAccess,
7
7
  type WebAccessSnapshot,
8
8
  } from "./navigation";
9
+ import type { AdminRoutePresentation } from "../router/admin-ui-composition";
9
10
 
10
11
  describe("Web 访问快照", () => {
11
12
  test("菜单同时受服务端权限和 Feature 有效态约束", () => {
@@ -45,6 +46,56 @@ describe("Web 访问快照", () => {
45
46
  "system.audit",
46
47
  );
47
48
  });
49
+
50
+ test("客户可按稳定 Route Key 隐藏、排序或替换菜单呈现", () => {
51
+ const snapshot = accessSnapshot();
52
+ snapshot.menus[0]!.children!.push({
53
+ key: "system.role",
54
+ title: "角色管理",
55
+ type: "menu",
56
+ order: 20,
57
+ visible: true,
58
+ path: "/admin/system/roles",
59
+ });
60
+ const presentation = new Map<string, AdminRoutePresentation>([
61
+ [
62
+ "system.audit",
63
+ {
64
+ routeKey: "system.audit",
65
+ title: "操作留痕",
66
+ icon: "scroll-text",
67
+ order: 30,
68
+ },
69
+ ],
70
+ [
71
+ "system.role",
72
+ {
73
+ routeKey: "system.role",
74
+ hidden: true,
75
+ },
76
+ ],
77
+ ]);
78
+
79
+ const menus = authorizedSidebarMenus(snapshot, presentation);
80
+ const audit = menus[1]?.children?.[0];
81
+
82
+ expect(audit).toMatchObject({
83
+ key: "system.audit",
84
+ title: "操作留痕",
85
+ icon: "scroll-text",
86
+ order: 30,
87
+ });
88
+ expect(flattenKeys(menus)).not.toContain("system.role");
89
+ });
90
+
91
+ test("根装配清单关闭 Admin 呈现后不保留失效菜单", () => {
92
+ const menus = authorizedSidebarMenus(accessSnapshot(), new Map(), {
93
+ knownRouteKeys: new Set(["system.audit"]),
94
+ visibleRouteKeys: new Set(),
95
+ });
96
+
97
+ expect(flattenKeys(menus)).not.toContain("system.audit");
98
+ });
48
99
  });
49
100
 
50
101
  function accessSnapshot(): WebAccessSnapshot {
@@ -5,6 +5,7 @@ import {
5
5
  authorizedSidebarMenus,
6
6
  type WebAccessSnapshot,
7
7
  } from "@/layout/navigation";
8
+ import { webAdminUiComposition } from "@/router/web-composition-host";
8
9
 
9
10
  export interface DashboardMetric {
10
11
  label: string;
@@ -32,7 +33,11 @@ export function buildDashboardModel(
32
33
  ): DashboardModel {
33
34
  const features = snapshot?.features || [];
34
35
  const availableMenuCount = snapshot
35
- ? authorizedSidebarMenus(snapshot)
36
+ ? authorizedSidebarMenus(
37
+ snapshot,
38
+ webAdminUiComposition.presentation,
39
+ webAdminUiComposition,
40
+ )
36
41
  .flatMap((menu) => (menu.children?.length ? menu.children : [menu]))
37
42
  .filter((menu) => menu.key !== "dashboard" && Boolean(menu.path)).length
38
43
  : 0;
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { computed, type StyleValue } from "vue";
3
+ import { createWatermarkPattern } from "@/composables/watermark-settings";
4
+ import type { WatermarkDensity } from "@southwind-ai/shared";
5
+
6
+ const props = defineProps<{
7
+ enabled: boolean;
8
+ content: string;
9
+ density: WatermarkDensity;
10
+ opacityPercent: number;
11
+ fontSize: number;
12
+ }>();
13
+
14
+ const patternStyle = computed<StyleValue>(() => {
15
+ const pattern = createWatermarkPattern(
16
+ props.content,
17
+ props.density,
18
+ props.fontSize,
19
+ );
20
+ return {
21
+ "--watermark-image": pattern.image,
22
+ "--watermark-size": pattern.size,
23
+ opacity: props.opacityPercent / 100,
24
+ };
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <section class="grid gap-3" aria-label="水印即时预览">
30
+ <div class="flex items-center justify-between gap-3">
31
+ <h3 class="text-sm font-medium">即时预览</h3>
32
+ <span class="text-xs text-muted-foreground">
33
+ {{ opacityPercent }}% · {{ fontSize }}px
34
+ </span>
35
+ </div>
36
+ <div
37
+ class="relative h-56 overflow-hidden rounded-xl border bg-muted/30"
38
+ data-testid="watermark-preview"
39
+ >
40
+ <div
41
+ v-if="enabled && content"
42
+ class="watermark-preview pointer-events-none absolute inset-0 bg-foreground"
43
+ :style="patternStyle"
44
+ aria-hidden="true"
45
+ data-testid="watermark-preview-pattern"
46
+ />
47
+ <div
48
+ v-else
49
+ class="absolute inset-0 grid place-items-center text-sm text-muted-foreground"
50
+ >
51
+ 水印已关闭
52
+ </div>
53
+ <div
54
+ class="absolute inset-x-6 bottom-6 rounded-lg border bg-background/90 p-4 shadow-sm"
55
+ >
56
+ <p class="text-sm font-medium">业务页面内容示意</p>
57
+ <p class="mt-1 text-xs text-muted-foreground">
58
+ 修改字段、密度、不透明度或字体大小后,此处立即呈现实际效果。
59
+ </p>
60
+ </div>
61
+ </div>
62
+ </section>
63
+ </template>
64
+
65
+ <style scoped>
66
+ .watermark-preview {
67
+ mask-image: var(--watermark-image);
68
+ mask-size: var(--watermark-size);
69
+ -webkit-mask-image: var(--watermark-image);
70
+ -webkit-mask-size: var(--watermark-size);
71
+ }
72
+ </style>
@@ -0,0 +1,55 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import WatermarkPreview from "./WatermarkPreview.vue";
4
+
5
+ describe("水印即时预览", () => {
6
+ test("外观草稿变化后立即复用实际平铺算法更新", async () => {
7
+ const wrapper = mount(WatermarkPreview, {
8
+ props: {
9
+ enabled: true,
10
+ content: "zhangsan · 张三",
11
+ density: "medium",
12
+ opacityPercent: 8,
13
+ fontSize: 14,
14
+ },
15
+ });
16
+ const initialStyle =
17
+ wrapper
18
+ .get('[data-testid="watermark-preview-pattern"]')
19
+ .attributes("style") ?? "";
20
+
21
+ expect(initialStyle).toContain("opacity: 0.08");
22
+ expect(decodeURIComponent(initialStyle)).toContain('font-size="14"');
23
+
24
+ await wrapper.setProps({
25
+ content: "张三 · 内部资料",
26
+ opacityPercent: 20,
27
+ fontSize: 24,
28
+ });
29
+ const updatedStyle =
30
+ wrapper
31
+ .get('[data-testid="watermark-preview-pattern"]')
32
+ .attributes("style") ?? "";
33
+
34
+ expect(updatedStyle).toContain("opacity: 0.2");
35
+ expect(decodeURIComponent(updatedStyle)).toContain('font-size="24"');
36
+ expect(decodeURIComponent(updatedStyle)).toContain("内部资料");
37
+ });
38
+
39
+ test("关闭唯一开关时即时显示停用状态", () => {
40
+ const wrapper = mount(WatermarkPreview, {
41
+ props: {
42
+ enabled: false,
43
+ content: "zhangsan",
44
+ density: "medium",
45
+ opacityPercent: 8,
46
+ fontSize: 14,
47
+ },
48
+ });
49
+
50
+ expect(wrapper.text()).toContain("水印已关闭");
51
+ expect(
52
+ wrapper.find('[data-testid="watermark-preview-pattern"]').exists(),
53
+ ).toBe(false);
54
+ });
55
+ });
@@ -15,12 +15,16 @@ import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
15
15
  import { useAppToast } from "@/composables/useAppToast";
16
16
  import { useAuthSession } from "@/composables/useAuthSession";
17
17
  import { useWatermarkSettings } from "@/composables/useWatermarkSettings";
18
+ import { watermarkContent } from "@/composables/watermark-settings";
18
19
  import { evaluateWebAccess } from "@/layout/navigation";
19
20
  import {
20
21
  DEFAULT_PLATFORM_WATERMARK_POLICY,
22
+ WATERMARK_FONT_SIZE_RANGE,
23
+ WATERMARK_OPACITY_RANGE,
21
24
  type WatermarkContentField,
22
25
  type WatermarkDensity,
23
26
  } from "@southwind-ai/shared";
27
+ import WatermarkPreview from "./WatermarkPreview.vue";
24
28
 
25
29
  const densityOptions: { value: WatermarkDensity; label: string }[] = [
26
30
  { value: "low", label: "低" },
@@ -44,14 +48,13 @@ const access = useAccessSnapshot();
44
48
  const toast = useAppToast();
45
49
  const watermark = useWatermarkSettings(auth.actor);
46
50
  const enabled = shallowRef(watermark.policy.value.enabled);
47
- const businessPagesEnabled = shallowRef(
48
- watermark.policy.value.businessPagesEnabled,
49
- );
50
51
  const density = shallowRef<WatermarkDensity>(watermark.policy.value.density);
51
52
  const contentFields = shallowRef<WatermarkContentField[]>([
52
53
  ...watermark.policy.value.contentFields,
53
54
  ]);
54
55
  const customContent = shallowRef(watermark.policy.value.customContent);
56
+ const opacityPercent = shallowRef(watermark.policy.value.opacityPercent);
57
+ const fontSize = shallowRef(watermark.policy.value.fontSize);
55
58
  const saving = shallowRef(false);
56
59
  const baseline = shallowRef(serializeForm());
57
60
  const canManage = computed(() => {
@@ -70,6 +73,14 @@ const isDirty = computed(
70
73
  const formDisabled = computed(
71
74
  () => !canManage.value || saving.value || watermark.loading.value,
72
75
  );
76
+ const previewContent = computed(
77
+ () =>
78
+ watermarkContent(
79
+ auth.actor.value,
80
+ contentFields.value,
81
+ customContent.value,
82
+ ) || "当前用户",
83
+ );
73
84
 
74
85
  onMounted(async () => {
75
86
  try {
@@ -87,10 +98,12 @@ async function save(): Promise<boolean> {
87
98
  try {
88
99
  await watermark.save({
89
100
  enabled: enabled.value,
90
- businessPagesEnabled: businessPagesEnabled.value,
101
+ businessPagesEnabled: enabled.value,
91
102
  density: density.value,
92
103
  contentFields: contentFields.value,
93
104
  customContent: customContent.value,
105
+ opacityPercent: opacityPercent.value,
106
+ fontSize: fontSize.value,
94
107
  });
95
108
  applyPolicyToForm();
96
109
  toast.success("水印设置已保存");
@@ -107,30 +120,32 @@ async function save(): Promise<boolean> {
107
120
 
108
121
  function reset() {
109
122
  enabled.value = DEFAULT_PLATFORM_WATERMARK_POLICY.enabled;
110
- businessPagesEnabled.value =
111
- DEFAULT_PLATFORM_WATERMARK_POLICY.businessPagesEnabled;
112
123
  density.value = DEFAULT_PLATFORM_WATERMARK_POLICY.density;
113
124
  contentFields.value = [...DEFAULT_PLATFORM_WATERMARK_POLICY.contentFields];
114
125
  customContent.value = DEFAULT_PLATFORM_WATERMARK_POLICY.customContent;
126
+ opacityPercent.value = DEFAULT_PLATFORM_WATERMARK_POLICY.opacityPercent;
127
+ fontSize.value = DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize;
115
128
  toast.success("已恢复为默认值,保存后生效");
116
129
  }
117
130
 
118
131
  function applyPolicyToForm() {
119
132
  enabled.value = watermark.policy.value.enabled;
120
- businessPagesEnabled.value = watermark.policy.value.businessPagesEnabled;
121
133
  density.value = watermark.policy.value.density;
122
134
  contentFields.value = [...watermark.policy.value.contentFields];
123
135
  customContent.value = watermark.policy.value.customContent;
136
+ opacityPercent.value = watermark.policy.value.opacityPercent;
137
+ fontSize.value = watermark.policy.value.fontSize;
124
138
  baseline.value = serializeForm();
125
139
  }
126
140
 
127
141
  function serializeForm() {
128
142
  return JSON.stringify({
129
143
  enabled: enabled.value,
130
- businessPagesEnabled: businessPagesEnabled.value,
131
144
  density: density.value,
132
145
  contentFields: [...contentFields.value].sort(),
133
146
  customContent: customContent.value.trim(),
147
+ opacityPercent: opacityPercent.value,
148
+ fontSize: fontSize.value,
134
149
  });
135
150
  }
136
151
 
@@ -161,29 +176,19 @@ defineExpose({
161
176
  </p>
162
177
  </div>
163
178
  <div class="grid gap-5">
164
- <label
165
- class="flex items-center justify-between gap-4 text-sm font-medium"
166
- >
167
- <span>显示页面水印</span>
168
- <Switch
169
- v-model="enabled"
170
- :disabled="formDisabled"
171
- aria-label="显示页面水印"
172
- />
173
- </label>
174
179
  <label
175
180
  class="flex items-center justify-between gap-4 text-sm font-medium"
176
181
  >
177
182
  <span>
178
- <span class="block">覆盖业务页面</span>
183
+ <span class="block">显示页面水印</span>
179
184
  <span class="mt-1 block text-xs font-normal text-muted-foreground">
180
- 开启后,首页及使用业务布局的页面也会显示水印。
185
+ 开启后,管理页面和业务页面统一显示水印。
181
186
  </span>
182
187
  </span>
183
188
  <Switch
184
- v-model="businessPagesEnabled"
185
- :disabled="formDisabled || !enabled"
186
- aria-label="在业务页面显示水印"
189
+ v-model="enabled"
190
+ :disabled="formDisabled"
191
+ aria-label="显示页面水印"
187
192
  />
188
193
  </label>
189
194
  <label class="grid gap-2 text-sm font-medium">
@@ -207,7 +212,7 @@ defineExpose({
207
212
  </span>
208
213
  </label>
209
214
  <fieldset class="grid gap-3" :disabled="formDisabled || !enabled">
210
- <legend class="mb-1 text-sm font-medium">水印内容</legend>
215
+ <legend class="mb-1 text-sm font-medium">水印字段(可任意组合)</legend>
211
216
  <label
212
217
  v-for="option in contentOptions"
213
218
  :key="option.value"
@@ -243,6 +248,57 @@ defineExpose({
243
248
  最多 80 个字符,与勾选的用户身份内容一同显示。
244
249
  </span>
245
250
  </label>
251
+ <div class="grid gap-5 sm:grid-cols-2">
252
+ <label class="grid gap-2 text-sm font-medium">
253
+ <span class="flex items-center justify-between gap-3">
254
+ <span>不透明度</span>
255
+ <output for="watermark-opacity">{{ opacityPercent }}%</output>
256
+ </span>
257
+ <input
258
+ id="watermark-opacity"
259
+ v-model.number="opacityPercent"
260
+ type="range"
261
+ :min="WATERMARK_OPACITY_RANGE.min"
262
+ :max="WATERMARK_OPACITY_RANGE.max"
263
+ :step="WATERMARK_OPACITY_RANGE.step"
264
+ :disabled="formDisabled || !enabled"
265
+ aria-label="水印不透明度"
266
+ class="w-full accent-primary"
267
+ />
268
+ <span class="text-xs font-normal text-muted-foreground">
269
+ 可设置 {{ WATERMARK_OPACITY_RANGE.min }}% 至
270
+ {{ WATERMARK_OPACITY_RANGE.max }}%。
271
+ </span>
272
+ </label>
273
+ <label class="grid gap-2 text-sm font-medium">
274
+ <span class="flex items-center justify-between gap-3">
275
+ <span>字体大小</span>
276
+ <output for="watermark-font-size">{{ fontSize }}px</output>
277
+ </span>
278
+ <input
279
+ id="watermark-font-size"
280
+ v-model.number="fontSize"
281
+ type="range"
282
+ :min="WATERMARK_FONT_SIZE_RANGE.min"
283
+ :max="WATERMARK_FONT_SIZE_RANGE.max"
284
+ :step="WATERMARK_FONT_SIZE_RANGE.step"
285
+ :disabled="formDisabled || !enabled"
286
+ aria-label="水印字体大小"
287
+ class="w-full accent-primary"
288
+ />
289
+ <span class="text-xs font-normal text-muted-foreground">
290
+ 可设置 {{ WATERMARK_FONT_SIZE_RANGE.min }}px 至
291
+ {{ WATERMARK_FONT_SIZE_RANGE.max }}px。
292
+ </span>
293
+ </label>
294
+ </div>
295
+ <WatermarkPreview
296
+ :enabled="enabled"
297
+ :content="previewContent"
298
+ :density="density"
299
+ :opacity-percent="opacityPercent"
300
+ :font-size="fontSize"
301
+ />
246
302
  <div v-if="canManage" class="flex justify-end gap-3 border-t pt-5">
247
303
  <Button variant="outline" :disabled="formDisabled" @click="reset">
248
304
  恢复默认