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.
- package/README.md +28 -13
- package/dist/cli.js +788 -280
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +18 -8
- package/template/README.md +32 -22
- package/template/apps/server/Dockerfile +1 -0
- package/template/apps/server/package.json +1 -0
- package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
- package/template/apps/server/scripts/validate-module-composition.ts +28 -0
- package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
- package/template/apps/server/src/agent/routes.test.ts +20 -7
- package/template/apps/server/src/agent/routes.ts +14 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
- package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
- package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
- package/template/apps/server/src/app/server-composition.ts +56 -0
- package/template/apps/server/src/application-services.ts +2 -0
- package/template/apps/server/src/bulk-data/service.ts +14 -0
- package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
- package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
- package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
- package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
- package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
- package/template/apps/server/src/feature/routes.ts +33 -15
- package/template/apps/server/src/foundation-activation.test.ts +60 -0
- package/template/apps/server/src/foundation.ts +64 -5
- package/template/apps/server/src/guards.test.ts +23 -0
- package/template/apps/server/src/guards.ts +12 -0
- package/template/apps/server/src/index.ts +28 -34
- package/template/apps/server/src/installed-business-modules.ts +13 -24
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
- package/template/apps/server/src/license/activation.test.ts +30 -0
- package/template/apps/server/src/license/activation.ts +26 -0
- package/template/apps/server/src/license/restriction.test.ts +16 -0
- package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
- package/template/apps/server/src/license/routes.ts +20 -4
- package/template/apps/server/src/license/runtime-service.test.ts +10 -2
- package/template/apps/server/src/module-bootstrap.ts +14 -23
- package/template/apps/server/src/module-composition.test.ts +43 -2
- package/template/apps/server/src/module-composition.ts +29 -0
- package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
- package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
- package/template/apps/server/src/module-host/durable-context.ts +42 -2
- package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
- package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +78 -2
- package/template/apps/server/src/module-host/initialize.ts +15 -4
- package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
- package/template/apps/server/src/module-host/project-attachments.ts +58 -0
- package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
- package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
- package/template/apps/server/src/module-host/project-composition.ts +311 -0
- package/template/apps/server/src/module-host/request-context.ts +3 -0
- package/template/apps/server/src/module-host/route-declaration.ts +15 -6
- package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
- package/template/apps/server/src/module-host/route-installer.ts +12 -2
- package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
- package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
- package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
- package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
- package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
- package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
- package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
- package/template/apps/server/src/module-host/task-definitions.ts +1 -0
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
- package/template/apps/server/src/module-host.test.ts +2 -0
- package/template/apps/server/src/module-preflight.test.ts +289 -0
- package/template/apps/server/src/module-preflight.ts +96 -0
- package/template/apps/server/src/module-runtime-validation.ts +1 -0
- package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
- package/template/apps/server/src/platform-http-runtime.ts +32 -7
- package/template/apps/server/src/route-guards.test.ts +22 -0
- package/template/apps/server/src/route-guards.ts +10 -3
- package/template/apps/server/src/runtime-audit-query.ts +50 -0
- package/template/apps/server/src/runtime-feature.test.ts +46 -0
- package/template/apps/server/src/runtime-feature.ts +5 -0
- package/template/apps/server/src/runtime.ts +10 -49
- package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
- package/template/apps/server/src/scheduler/audit.ts +1 -1
- package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
- package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
- package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
- package/template/apps/server/src/scheduler/service.test.ts +1 -0
- package/template/apps/server/src/scheduler/service.ts +18 -21
- package/template/apps/server/src/scheduler/types.ts +5 -1
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
- package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
- package/template/apps/server/src/settings/watermark-routes.ts +29 -1
- package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
- package/template/apps/server/src/startup-composition-validation.ts +54 -0
- package/template/apps/server/src/startup-preflight.test.ts +36 -0
- package/template/apps/server/src/startup-preflight.ts +37 -0
- package/template/apps/server/src/system/audit-routes.test.ts +9 -3
- package/template/apps/server/src/system/department-routes.test.ts +5 -3
- package/template/apps/server/src/system/feature-routes.test.ts +4 -3
- package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
- package/template/apps/server/src/system/interface-catalog.ts +3 -0
- package/template/apps/server/src/system/menu-routes.test.ts +5 -3
- package/template/apps/server/src/system/route-test-runtime.ts +24 -0
- package/template/apps/server/src/system/routes-validation.test.ts +7 -3
- package/template/apps/server/src/system/routes.test.ts +21 -11
- package/template/apps/server/src/system/routes.ts +11 -2
- package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
- package/template/apps/server/src/work-order/routes.test.ts +8 -0
- package/template/apps/server/src/work-order/task.test.ts +6 -1
- package/template/apps/web/Dockerfile +5 -0
- package/template/apps/web/package.json +1 -0
- package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
- package/template/apps/web/src/app/web-composition.ts +24 -0
- package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +40 -11
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
- package/template/apps/web/src/layout/AppSidebar.vue +8 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
- package/template/apps/web/src/layout/navigation.ts +29 -4
- package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
- package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
- package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
- package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
- package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
- package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
- package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
- package/template/apps/web/src/router/business-routes.ts +32 -45
- package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
- package/template/apps/web/src/router/index.ts +64 -54
- package/template/apps/web/src/router/manifest-routes.ts +27 -7
- package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
- package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
- package/template/apps/web/src/router/web-composition-host.ts +20 -0
- package/template/apps/web/src/services/work-order-api.ts +16 -0
- package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
- package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
- package/template/apps/web/vite.config.ts +22 -1
- package/template/docs/architecture/migration-bundle.md +10 -7
- package/template/docs/architecture/module-manifest-composition.md +85 -3
- package/template/docs/architecture/search-provider-protocol.md +6 -1
- package/template/docs/architecture/server-module-interface.md +70 -7
- package/template/docs/development/business-web-routes.md +53 -50
- package/template/docs/development/module-lifecycle.md +48 -54
- package/template/docs/platform/navigation-registry.md +5 -4
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/package.json +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/example-work-order/package.json +6 -0
- package/template/packages/example-work-order/src/admin-page.ts +78 -0
- package/template/packages/example-work-order/src/admin.ts +35 -0
- package/template/packages/example-work-order/src/manifest.test.ts +62 -5
- package/template/packages/example-work-order/src/manifest.ts +54 -7
- package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
- package/template/packages/example-work-order/src/server-module.ts +2 -2
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +4 -0
- package/template/packages/modules/package.json +3 -2
- package/template/packages/modules/src/admin-ui-entry.ts +8 -0
- package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
- package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
- package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
- package/template/packages/modules/src/capability-audit-validation.ts +38 -0
- package/template/packages/modules/src/capability-guard-validation.ts +148 -0
- package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
- package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
- package/template/packages/modules/src/composition.test.ts +74 -9
- package/template/packages/modules/src/composition.ts +8 -0
- package/template/packages/modules/src/manifest.ts +1 -0
- package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
- package/template/packages/modules/src/module-host-range.test.ts +57 -0
- package/template/packages/modules/src/module-host-range.ts +125 -0
- package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
- package/template/packages/modules/src/project-module-assembly.ts +126 -0
- package/template/packages/modules/src/system-admin-routes.ts +6 -5
- package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
- package/template/packages/modules/src/system-agent-tools.ts +2 -2
- package/template/packages/modules/src/system-api-binding.ts +43 -9
- package/template/packages/modules/src/system-api-control-plane.ts +202 -0
- package/template/packages/modules/src/system-api-data-governance.ts +4 -2
- package/template/packages/modules/src/system-api-operational.ts +57 -8
- package/template/packages/modules/src/system-api-permissions.ts +115 -21
- package/template/packages/modules/src/system-api-watermark.ts +3 -0
- package/template/packages/modules/src/system-audit-actions.ts +7 -0
- package/template/packages/modules/src/system-data-governance.ts +5 -2
- package/template/packages/modules/src/system-features.ts +31 -0
- package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
- package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
- package/template/packages/modules/src/system-module-catalog.ts +68 -7
- package/template/packages/modules/src/system-module-tasks.ts +8 -1
- package/template/packages/modules/src/system-modules-access.test.ts +4 -0
- package/template/packages/modules/src/system-modules.test.ts +10 -2
- package/template/packages/modules/src/system-modules.ts +6 -5
- package/template/packages/modules/src/system-permissions.ts +4 -0
- package/template/packages/modules/user/module.ts +19 -2
- package/template/packages/server-sdk/index.ts +8 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/durable-handler.ts +16 -1
- package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
- package/template/packages/server-sdk/src/module-adapter.ts +59 -0
- package/template/packages/server-sdk/src/route-definition.ts +3 -2
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.test.ts +7 -0
- package/template/packages/shared/src/audit.ts +16 -0
- package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/packages/shared/src/rbac.ts +2 -0
- package/template/apps/web/src/app/business-routes.ts +0 -22
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
2
|
+
import { createServerRuntime, type ServerRuntimeOptions } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
interface TestFeatureCatalog {
|
|
5
|
+
readonly features: readonly FeatureFlagDefinition[];
|
|
6
|
+
readonly disabledModuleNames?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type TestRuntimeOptions = Omit<
|
|
10
|
+
ServerRuntimeOptions,
|
|
11
|
+
"featureCatalog" | "disabledModuleNames"
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export function createRouteTestRuntime(
|
|
15
|
+
catalog: TestFeatureCatalog,
|
|
16
|
+
env: Record<string, string | undefined> = {},
|
|
17
|
+
options: TestRuntimeOptions = {},
|
|
18
|
+
) {
|
|
19
|
+
return createServerRuntime(env, {
|
|
20
|
+
...options,
|
|
21
|
+
featureCatalog: catalog.features,
|
|
22
|
+
disabledModuleNames: new Set(catalog.disabledModuleNames),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { createFoundationSnapshot } from "../foundation.js";
|
|
3
|
-
import { createServerRuntime } from "../runtime.js";
|
|
4
3
|
import { FakeRouteApp } from "./route-test-app.js";
|
|
4
|
+
import { createRouteTestRuntime } from "./route-test-runtime.js";
|
|
5
5
|
import { registerSystemRoutes } from "./routes.js";
|
|
6
6
|
import { createSystemRepositories } from "./seed.js";
|
|
7
7
|
|
|
@@ -71,12 +71,16 @@ describe("system route query validation", () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
async function createFixture(path: string) {
|
|
74
|
-
const
|
|
74
|
+
const snapshot = createFoundationSnapshot();
|
|
75
|
+
const runtime = createRouteTestRuntime(snapshot, {
|
|
76
|
+
WINDY_DEV_ADMIN_TOKEN: "token_1",
|
|
77
|
+
});
|
|
75
78
|
const app = new FakeRouteApp();
|
|
76
79
|
registerSystemRoutes(
|
|
77
80
|
app,
|
|
78
|
-
createSystemRepositories(
|
|
81
|
+
createSystemRepositories(snapshot),
|
|
79
82
|
runtime,
|
|
83
|
+
snapshot.features,
|
|
80
84
|
);
|
|
81
85
|
const guardContext = await runtime.createGuardContext(
|
|
82
86
|
new Request(`http://localhost${path}`, {
|
|
@@ -12,19 +12,21 @@ import { registerSystemRoutes } from "./routes.js";
|
|
|
12
12
|
import { createSystemRepositories } from "./seed.js";
|
|
13
13
|
|
|
14
14
|
async function routeFixture(
|
|
15
|
-
features
|
|
15
|
+
features?: FeatureFlagDefinition[],
|
|
16
16
|
auditWriter?: AuditEventWriter,
|
|
17
17
|
) {
|
|
18
|
+
const snapshot = createFoundationSnapshot();
|
|
19
|
+
const activeFeatures = features ?? snapshot.features;
|
|
18
20
|
const runtime = createServerRuntime(
|
|
19
21
|
{ WINDY_DEV_ADMIN_TOKEN: "token_1" },
|
|
20
|
-
{ auditWriter },
|
|
22
|
+
{ auditWriter, featureCatalog: activeFeatures },
|
|
21
23
|
);
|
|
22
24
|
const app = new FakeRouteApp();
|
|
23
25
|
registerSystemRoutes(
|
|
24
26
|
app,
|
|
25
|
-
createSystemRepositories(
|
|
27
|
+
createSystemRepositories(snapshot),
|
|
26
28
|
runtime,
|
|
27
|
-
|
|
29
|
+
activeFeatures,
|
|
28
30
|
);
|
|
29
31
|
const guardContext = await runtime.createGuardContext(
|
|
30
32
|
new Request("http://localhost/api/system/users", {
|
|
@@ -78,12 +80,17 @@ describe("system routes", () => {
|
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
test("缺少权限时返回 403 Response 并记录 security.denied", async () => {
|
|
81
|
-
const
|
|
83
|
+
const snapshot = createFoundationSnapshot();
|
|
84
|
+
const runtime = createServerRuntime(
|
|
85
|
+
{},
|
|
86
|
+
{ featureCatalog: snapshot.features },
|
|
87
|
+
);
|
|
82
88
|
const app = new FakeRouteApp();
|
|
83
89
|
registerSystemRoutes(
|
|
84
90
|
app,
|
|
85
|
-
createSystemRepositories(
|
|
91
|
+
createSystemRepositories(snapshot),
|
|
86
92
|
runtime,
|
|
93
|
+
snapshot.features,
|
|
87
94
|
);
|
|
88
95
|
const guardContext = await runtime.createGuardContext(
|
|
89
96
|
new Request("http://localhost/api/system/users"),
|
|
@@ -127,7 +134,7 @@ describe("system routes", () => {
|
|
|
127
134
|
|
|
128
135
|
expect(hiddenResponse.status).toBe(403);
|
|
129
136
|
await expect(hiddenResponse.json()).resolves.toMatchObject({
|
|
130
|
-
reason: "
|
|
137
|
+
reason: "dependency-missing",
|
|
131
138
|
});
|
|
132
139
|
|
|
133
140
|
// @windy-module system.license begin
|
|
@@ -209,7 +216,7 @@ describe("system routes", () => {
|
|
|
209
216
|
|
|
210
217
|
test("用户与角色变更使用关键策略,普通资源变更保持降级可用", async () => {
|
|
211
218
|
const policies: string[] = [];
|
|
212
|
-
const { app, guardContext } = await routeFixture(
|
|
219
|
+
const { app, guardContext } = await routeFixture(undefined, {
|
|
213
220
|
write: async (_event, policy) => void policies.push(policy || "ordinary"),
|
|
214
221
|
});
|
|
215
222
|
|
|
@@ -234,7 +241,7 @@ describe("system routes", () => {
|
|
|
234
241
|
});
|
|
235
242
|
|
|
236
243
|
test("关键资源审计失败会拒绝请求,普通资源仍完成并报告降级", async () => {
|
|
237
|
-
const { app, runtime, guardContext } = await routeFixture(
|
|
244
|
+
const { app, runtime, guardContext } = await routeFixture(undefined, {
|
|
238
245
|
write: async () => {
|
|
239
246
|
throw new Error("audit persistence unavailable");
|
|
240
247
|
},
|
|
@@ -281,7 +288,10 @@ describe("system routes", () => {
|
|
|
281
288
|
await repositories.features.syncManifest(snapshot.features);
|
|
282
289
|
const runtime = createServerRuntime(
|
|
283
290
|
{ WINDY_DEV_ADMIN_TOKEN: "token_1" },
|
|
284
|
-
{
|
|
291
|
+
{
|
|
292
|
+
featureResolver: repositories.features,
|
|
293
|
+
featureCatalog: snapshot.features,
|
|
294
|
+
},
|
|
285
295
|
);
|
|
286
296
|
const app = new FakeRouteApp();
|
|
287
297
|
registerSystemRoutes(app, repositories, runtime, snapshot.features);
|
|
@@ -315,7 +325,7 @@ describe("system routes", () => {
|
|
|
315
325
|
});
|
|
316
326
|
expect(denied.status).toBe(403);
|
|
317
327
|
await expect(denied.json()).resolves.toMatchObject({
|
|
318
|
-
reason: "
|
|
328
|
+
reason: "dependency-missing",
|
|
319
329
|
});
|
|
320
330
|
expect(app.handlers.has("POST /system/features")).toBe(false);
|
|
321
331
|
expect(app.handlers.has("POST /system/features/import")).toBe(false);
|
|
@@ -27,7 +27,7 @@ export function registerSystemRoutes(
|
|
|
27
27
|
app: RouteApp,
|
|
28
28
|
repositories: SystemRepositories,
|
|
29
29
|
runtime: ReturnType<typeof createServerRuntime>,
|
|
30
|
-
features: FeatureFlagDefinition[]
|
|
30
|
+
features: FeatureFlagDefinition[],
|
|
31
31
|
options: {
|
|
32
32
|
authRepository?: Pick<AuthRepository, "revokeUserSessions">;
|
|
33
33
|
} = {},
|
|
@@ -98,7 +98,16 @@ export function registerSystemRoutes(
|
|
|
98
98
|
),
|
|
99
99
|
runtime,
|
|
100
100
|
);
|
|
101
|
-
|
|
101
|
+
const featureManagement = findSystemFeature(features, "feature");
|
|
102
|
+
if (!featureManagement) {
|
|
103
|
+
throw new Error("功能开关管理缺少 system.feature Feature");
|
|
104
|
+
}
|
|
105
|
+
registerFeatureFlagRoutes(
|
|
106
|
+
app,
|
|
107
|
+
repositories.features,
|
|
108
|
+
runtime,
|
|
109
|
+
featureManagement,
|
|
110
|
+
);
|
|
102
111
|
// @windy-module system.license begin
|
|
103
112
|
registerResourceRoutes(
|
|
104
113
|
app,
|
|
@@ -43,7 +43,9 @@ describe("平台模块 composition root", () => {
|
|
|
43
43
|
"advanced",
|
|
44
44
|
]);
|
|
45
45
|
const workOrder = modules.find(({ name }) => name === "work-order");
|
|
46
|
-
expect(workOrder?.adminRoutes).toEqual([
|
|
46
|
+
expect(workOrder?.adminRoutes.map(({ key }) => key)).toEqual([
|
|
47
|
+
"work-order.ticket.list",
|
|
48
|
+
]);
|
|
47
49
|
expect(workOrder?.apiPermissions).toHaveLength(5);
|
|
48
50
|
expect(workOrder?.tasks.map(({ key }) => key)).toContain(
|
|
49
51
|
"work-order.ticket.snapshot",
|
|
@@ -88,9 +90,9 @@ describe("平台模块 composition root", () => {
|
|
|
88
90
|
"@southwind-ai/web",
|
|
89
91
|
);
|
|
90
92
|
expect(businessPackage.dependencies).not.toHaveProperty("vue");
|
|
91
|
-
expect(webPackage.dependencies).
|
|
92
|
-
"@southwind-ai/example-work-order",
|
|
93
|
-
);
|
|
93
|
+
expect(webPackage.dependencies).toMatchObject({
|
|
94
|
+
"@southwind-ai/example-work-order": "workspace:*",
|
|
95
|
+
});
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
test("示例模块经 Host 声明式装配提供搜索 Provider", async () => {
|
|
@@ -22,6 +22,9 @@ describe("示例工单 routes", () => {
|
|
|
22
22
|
items: WorkOrder[];
|
|
23
23
|
};
|
|
24
24
|
expect(listed.items.map(({ id }) => id)).toEqual(["ticket_self"]);
|
|
25
|
+
expect(fixture.runtime.auditSink.list()).toContainEqual(
|
|
26
|
+
expect.objectContaining({ action: "data.read" }),
|
|
27
|
+
);
|
|
25
28
|
|
|
26
29
|
const denied = (await fixture.invoke("GET", "/work-orders/:id", {
|
|
27
30
|
id: "ticket_other",
|
|
@@ -76,6 +79,11 @@ describe("示例工单 routes", () => {
|
|
|
76
79
|
target: expect.objectContaining({ id: created.id, type: "work-order" }),
|
|
77
80
|
}),
|
|
78
81
|
);
|
|
82
|
+
expect(
|
|
83
|
+
fixture.runtime.auditSink
|
|
84
|
+
.list()
|
|
85
|
+
.filter(({ action }) => action === "crud.create"),
|
|
86
|
+
).toHaveLength(1);
|
|
79
87
|
});
|
|
80
88
|
});
|
|
81
89
|
|
|
@@ -33,9 +33,14 @@ describe("示例工单后台任务", () => {
|
|
|
33
33
|
workOrderDataScopeBinding,
|
|
34
34
|
),
|
|
35
35
|
});
|
|
36
|
+
const definitions = createModuleTaskDefinitions(
|
|
37
|
+
hosts,
|
|
38
|
+
createFoundationSnapshot().features,
|
|
39
|
+
);
|
|
40
|
+
expect(definitions[0]?.auditAction).toBe("task.execute");
|
|
36
41
|
const scheduler = new SchedulerService(
|
|
37
42
|
new InMemorySchedulerRepository(),
|
|
38
|
-
|
|
43
|
+
definitions,
|
|
39
44
|
runtime,
|
|
40
45
|
);
|
|
41
46
|
await scheduler.initialize();
|
|
@@ -4,6 +4,7 @@ FROM oven/bun:1.3.14-alpine AS deps
|
|
|
4
4
|
WORKDIR /app
|
|
5
5
|
|
|
6
6
|
COPY package.json bun.lock ./
|
|
7
|
+
COPY windy.modules.json windy.modules.json
|
|
7
8
|
# windy-copy:deps begin
|
|
8
9
|
COPY apps/license/package.json apps/license/package.json
|
|
9
10
|
COPY apps/server/package.json apps/server/package.json
|
|
@@ -27,9 +28,13 @@ RUN --mount=type=cache,id=windy-bun-1.3.14,target=/root/.bun/install/cache,shari
|
|
|
27
28
|
FROM deps AS build
|
|
28
29
|
# windy-copy:build begin
|
|
29
30
|
COPY apps/web apps/web
|
|
31
|
+
COPY packages/example-work-order packages/example-work-order
|
|
32
|
+
COPY packages/jobs packages/jobs
|
|
30
33
|
COPY packages/license-sdk packages/license-sdk
|
|
31
34
|
COPY packages/modules packages/modules
|
|
35
|
+
COPY packages/server-sdk packages/server-sdk
|
|
32
36
|
COPY packages/shared packages/shared
|
|
37
|
+
COPY packages/storage packages/storage
|
|
33
38
|
# windy-copy:build end
|
|
34
39
|
RUN bun run --cwd apps/web build
|
|
35
40
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@tanstack/vue-form": "^1.33.0",
|
|
19
19
|
"@tanstack/vue-table": "^8.21.3",
|
|
20
20
|
"@vueuse/core": "^14.3.0",
|
|
21
|
+
"@southwind-ai/example-work-order": "workspace:*",
|
|
21
22
|
"@southwind-ai/license": "workspace:*",
|
|
22
23
|
"@southwind-ai/shared": "workspace:*",
|
|
23
24
|
"class-variance-authority": "^0.7.1",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import moduleAssembly from "../../../windy.modules.json";
|
|
2
|
+
import {
|
|
3
|
+
systemAdminUiManifests,
|
|
4
|
+
validateProjectModuleAssembly,
|
|
5
|
+
} from "@southwind-ai/modules/admin-ui";
|
|
6
|
+
import {
|
|
7
|
+
projectAdminUiManifest,
|
|
8
|
+
type AdminUiManifestSource,
|
|
9
|
+
} from "@southwind-ai/modules";
|
|
10
|
+
import { webComposition } from "../src/app/web-composition";
|
|
11
|
+
import {
|
|
12
|
+
resolveAdminUiComposition,
|
|
13
|
+
validateAdminRoutePathCollisions,
|
|
14
|
+
type AdminUiModule,
|
|
15
|
+
} from "../src/router/admin-ui-composition";
|
|
16
|
+
import { validatePlatformAdminRouteKeys } from "../src/router/manifest-routes";
|
|
17
|
+
import { platformAdminRouteBindings } from "../src/router/platform-admin-route-bindings";
|
|
18
|
+
import { reservedWebRoutePathList } from "../src/router/reserved-route-paths";
|
|
19
|
+
|
|
20
|
+
const assembly = validateProjectModuleAssembly(moduleAssembly);
|
|
21
|
+
const installedModules = await Promise.all(
|
|
22
|
+
assembly.modules
|
|
23
|
+
.filter(
|
|
24
|
+
(
|
|
25
|
+
entry,
|
|
26
|
+
): entry is typeof entry & {
|
|
27
|
+
readonly package: string;
|
|
28
|
+
} => entry.enabled && entry.package !== null,
|
|
29
|
+
)
|
|
30
|
+
.map(async (entry) => {
|
|
31
|
+
const [adminLoaded, manifestLoaded]: readonly [unknown, unknown] =
|
|
32
|
+
await Promise.all([
|
|
33
|
+
import(`${entry.package}/admin`),
|
|
34
|
+
import(`${entry.package}/manifest`),
|
|
35
|
+
]);
|
|
36
|
+
if (!isAdminUiModuleExport(adminLoaded)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Module ${entry.name} 缺少 windyAdminUiModule 标准导出`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!isModuleManifestExport(manifestLoaded)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Module ${entry.name} 缺少 windyModuleManifest 标准导出`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (adminLoaded.windyAdminUiModule.manifest.name !== entry.name) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Module package ${entry.package} 导出了错误 Admin UI:${adminLoaded.windyAdminUiModule.manifest.name}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const expectedAdminManifest = projectAdminUiManifest(
|
|
52
|
+
manifestLoaded.windyModuleManifest,
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
JSON.stringify(adminLoaded.windyAdminUiModule.manifest) !==
|
|
56
|
+
JSON.stringify(expectedAdminManifest)
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Module ${entry.name} 的 ./admin 与 ./manifest 投影不一致`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return adminLoaded.windyAdminUiModule;
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const composition = resolveAdminUiComposition({
|
|
67
|
+
declaration: webComposition,
|
|
68
|
+
assembly,
|
|
69
|
+
platformModules: systemAdminUiManifests(),
|
|
70
|
+
installedModules,
|
|
71
|
+
});
|
|
72
|
+
validateAdminRoutePathCollisions(composition, reservedWebRoutePathList);
|
|
73
|
+
validatePlatformAdminRouteKeys(
|
|
74
|
+
platformAdminRouteBindings.map(({ routeKey }) => routeKey),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
function isAdminUiModuleExport(
|
|
78
|
+
value: unknown,
|
|
79
|
+
): value is { readonly windyAdminUiModule: AdminUiModule } {
|
|
80
|
+
if (!isRecord(value) || !isRecord(value.windyAdminUiModule)) return false;
|
|
81
|
+
const module = value.windyAdminUiModule;
|
|
82
|
+
return (
|
|
83
|
+
isRecord(module.manifest) &&
|
|
84
|
+
typeof module.manifest.name === "string" &&
|
|
85
|
+
typeof module.manifest.version === "string" &&
|
|
86
|
+
Array.isArray(module.manifest.adminRoutes) &&
|
|
87
|
+
typeof module.hostRange === "string" &&
|
|
88
|
+
Array.isArray(module.pages)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isModuleManifestExport(
|
|
93
|
+
value: unknown,
|
|
94
|
+
): value is { readonly windyModuleManifest: AdminUiManifestSource } {
|
|
95
|
+
if (!isRecord(value) || !isRecord(value.windyModuleManifest)) return false;
|
|
96
|
+
const manifest = value.windyModuleManifest;
|
|
97
|
+
return (
|
|
98
|
+
typeof manifest.name === "string" &&
|
|
99
|
+
typeof manifest.version === "string" &&
|
|
100
|
+
Array.isArray(manifest.adminRoutes)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
105
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineWebComposition } from "../router/admin-ui-composition";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 客户拥有的 Admin UI 覆盖声明。
|
|
5
|
+
*
|
|
6
|
+
* 模块的窄 Manifest 与默认页面来自 package 标准 `./admin` 导出;这里只覆盖客户
|
|
7
|
+
* 自己接管的页面 loader 与呈现偏好,不复制路径、标题、Feature 或 Permission。
|
|
8
|
+
*/
|
|
9
|
+
export const webComposition = defineWebComposition({
|
|
10
|
+
overrides: [
|
|
11
|
+
// @windy-module work-order begin
|
|
12
|
+
{
|
|
13
|
+
moduleName: "work-order",
|
|
14
|
+
pages: [
|
|
15
|
+
{
|
|
16
|
+
routeKey: "work-order.ticket.list",
|
|
17
|
+
load: () => import("../pages/work-orders/WorkOrderListPage.vue"),
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
// @windy-module work-order end
|
|
22
|
+
],
|
|
23
|
+
presentation: [],
|
|
24
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { webComposition } from "./web-composition";
|
|
3
|
+
|
|
4
|
+
describe("project-owned Web 组合声明", () => {
|
|
5
|
+
test("示例工单只覆盖 moduleName 与 lazy page loader", () => {
|
|
6
|
+
const registration = webComposition.overrides.find(
|
|
7
|
+
({ moduleName }) => moduleName === "work-order",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
expect(registration?.moduleName).toBe("work-order");
|
|
11
|
+
expect(registration?.pages).toHaveLength(1);
|
|
12
|
+
expect(registration?.pages[0]?.routeKey).toBe("work-order.ticket.list");
|
|
13
|
+
expect(registration?.pages[0]?.load).toBeTypeOf("function");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -30,6 +30,8 @@ describe("全局水印策略状态", () => {
|
|
|
30
30
|
density: "high",
|
|
31
31
|
contentFields: ["custom"],
|
|
32
32
|
customContent: "全局审计",
|
|
33
|
+
opacityPercent: 12,
|
|
34
|
+
fontSize: 18,
|
|
33
35
|
});
|
|
34
36
|
const { useWatermarkSettings } = await import("./useWatermarkSettings");
|
|
35
37
|
const watermark = useWatermarkSettings(shallowRef(undefined));
|
|
@@ -42,6 +44,8 @@ describe("全局水印策略状态", () => {
|
|
|
42
44
|
density: "high",
|
|
43
45
|
contentFields: ["custom"],
|
|
44
46
|
customContent: "全局审计",
|
|
47
|
+
opacityPercent: 12,
|
|
48
|
+
fontSize: 18,
|
|
45
49
|
});
|
|
46
50
|
expect(localStorageRead).not.toHaveBeenCalled();
|
|
47
51
|
expect(localStorageWrite).not.toHaveBeenCalled();
|
|
@@ -54,6 +58,8 @@ describe("全局水印策略状态", () => {
|
|
|
54
58
|
density: "medium",
|
|
55
59
|
contentFields: ["username", "displayName"],
|
|
56
60
|
customContent: "",
|
|
61
|
+
opacityPercent: 8,
|
|
62
|
+
fontSize: 14,
|
|
57
63
|
});
|
|
58
64
|
api.update.mockResolvedValue({
|
|
59
65
|
enabled: true,
|
|
@@ -61,6 +67,8 @@ describe("全局水印策略状态", () => {
|
|
|
61
67
|
density: "high",
|
|
62
68
|
contentFields: ["username", "custom"],
|
|
63
69
|
customContent: " 内部资料 ",
|
|
70
|
+
opacityPercent: 16,
|
|
71
|
+
fontSize: 20,
|
|
64
72
|
});
|
|
65
73
|
const { useWatermarkSettings } = await import("./useWatermarkSettings");
|
|
66
74
|
const actor = shallowRef({
|
|
@@ -80,6 +88,8 @@ describe("全局水印策略状态", () => {
|
|
|
80
88
|
density: "high",
|
|
81
89
|
contentFields: ["username", "custom"],
|
|
82
90
|
customContent: "内部资料",
|
|
91
|
+
opacityPercent: 16,
|
|
92
|
+
fontSize: 20,
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
expect(api.update).toHaveBeenCalledWith({
|
|
@@ -88,6 +98,8 @@ describe("全局水印策略状态", () => {
|
|
|
88
98
|
density: "high",
|
|
89
99
|
contentFields: ["username", "custom"],
|
|
90
100
|
customContent: "内部资料",
|
|
101
|
+
opacityPercent: 16,
|
|
102
|
+
fontSize: 20,
|
|
91
103
|
});
|
|
92
104
|
expect(second.policy.value).toEqual({
|
|
93
105
|
enabled: true,
|
|
@@ -95,6 +107,8 @@ describe("全局水印策略状态", () => {
|
|
|
95
107
|
density: "high",
|
|
96
108
|
contentFields: ["username", "custom"],
|
|
97
109
|
customContent: "内部资料",
|
|
110
|
+
opacityPercent: 16,
|
|
111
|
+
fontSize: 20,
|
|
98
112
|
});
|
|
99
113
|
});
|
|
100
114
|
});
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
DEFAULT_PLATFORM_WATERMARK_POLICY,
|
|
4
4
|
WATERMARK_CONTENT_FIELDS,
|
|
5
5
|
WATERMARK_DENSITIES,
|
|
6
|
+
WATERMARK_FONT_SIZE_RANGE,
|
|
7
|
+
WATERMARK_OPACITY_RANGE,
|
|
6
8
|
type PlatformWatermarkPolicy,
|
|
7
9
|
type WatermarkContentField,
|
|
8
10
|
type WatermarkDensity,
|
|
@@ -16,8 +18,6 @@ const DENSITY_GAPS: Record<
|
|
|
16
18
|
medium: { horizontal: 72, vertical: 48 },
|
|
17
19
|
high: { horizontal: 36, vertical: 24 },
|
|
18
20
|
};
|
|
19
|
-
const FONT_SIZE = 14;
|
|
20
|
-
const LINE_HEIGHT = 22;
|
|
21
21
|
const ROTATION_RADIANS = (22 * Math.PI) / 180;
|
|
22
22
|
const MAX_LINE_WEIGHT = 32;
|
|
23
23
|
|
|
@@ -58,15 +58,26 @@ export function normalizeWatermarkPolicy(
|
|
|
58
58
|
),
|
|
59
59
|
),
|
|
60
60
|
];
|
|
61
|
+
const enabled = Boolean(value.enabled && value.businessPagesEnabled);
|
|
61
62
|
return {
|
|
62
|
-
enabled
|
|
63
|
-
businessPagesEnabled:
|
|
63
|
+
enabled,
|
|
64
|
+
businessPagesEnabled: enabled,
|
|
64
65
|
density,
|
|
65
66
|
contentFields:
|
|
66
67
|
contentFields.length > 0
|
|
67
68
|
? contentFields
|
|
68
69
|
: [...DEFAULT_PLATFORM_WATERMARK_POLICY.contentFields],
|
|
69
70
|
customContent: value.customContent.trim().slice(0, 80),
|
|
71
|
+
opacityPercent: boundedInteger(
|
|
72
|
+
value.opacityPercent,
|
|
73
|
+
WATERMARK_OPACITY_RANGE,
|
|
74
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY.opacityPercent,
|
|
75
|
+
),
|
|
76
|
+
fontSize: boundedInteger(
|
|
77
|
+
value.fontSize,
|
|
78
|
+
WATERMARK_FONT_SIZE_RANGE,
|
|
79
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
|
|
80
|
+
),
|
|
70
81
|
};
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -78,10 +89,19 @@ export interface WatermarkPattern {
|
|
|
78
89
|
export function createWatermarkPattern(
|
|
79
90
|
content: string,
|
|
80
91
|
density: WatermarkDensity,
|
|
92
|
+
fontSize: number,
|
|
81
93
|
): WatermarkPattern {
|
|
94
|
+
const safeFontSize = boundedInteger(
|
|
95
|
+
fontSize,
|
|
96
|
+
WATERMARK_FONT_SIZE_RANGE,
|
|
97
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY.fontSize,
|
|
98
|
+
);
|
|
99
|
+
const lineHeight = Math.ceil(safeFontSize * 1.57);
|
|
82
100
|
const lines = wrapWatermarkContent(content);
|
|
83
|
-
const textWidth = Math.max(
|
|
84
|
-
|
|
101
|
+
const textWidth = Math.max(
|
|
102
|
+
...lines.map((line) => estimatedTextWidth(line, safeFontSize)),
|
|
103
|
+
);
|
|
104
|
+
const textHeight = lines.length * lineHeight;
|
|
85
105
|
const rotatedWidth =
|
|
86
106
|
textWidth * Math.cos(ROTATION_RADIANS) +
|
|
87
107
|
textHeight * Math.sin(ROTATION_RADIANS);
|
|
@@ -91,17 +111,17 @@ export function createWatermarkPattern(
|
|
|
91
111
|
const gap = DENSITY_GAPS[density];
|
|
92
112
|
const width = Math.ceil(rotatedWidth + gap.horizontal);
|
|
93
113
|
const height = Math.ceil(rotatedHeight + gap.vertical);
|
|
94
|
-
const firstLineY = (height - textHeight) / 2 +
|
|
114
|
+
const firstLineY = (height - textHeight) / 2 + safeFontSize;
|
|
95
115
|
const tspans = lines
|
|
96
116
|
.map(
|
|
97
117
|
(line, index) =>
|
|
98
|
-
`<tspan x="${width / 2}" y="${firstLineY + index *
|
|
118
|
+
`<tspan x="${width / 2}" y="${firstLineY + index * lineHeight}">${escapeXml(line)}</tspan>`,
|
|
99
119
|
)
|
|
100
120
|
.join("");
|
|
101
121
|
const svg =
|
|
102
122
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
|
|
103
123
|
`<g transform="rotate(-22 ${width / 2} ${height / 2})">` +
|
|
104
|
-
`<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${
|
|
124
|
+
`<text text-anchor="middle" font-family="system-ui,sans-serif" font-size="${safeFontSize}" font-weight="500" fill="black">${tspans}</text>` +
|
|
105
125
|
"</g></svg>";
|
|
106
126
|
return {
|
|
107
127
|
image: `url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
|
|
@@ -125,8 +145,8 @@ function wrapWatermarkContent(content: string): string[] {
|
|
|
125
145
|
return lines.length > 0 ? lines : [""];
|
|
126
146
|
}
|
|
127
147
|
|
|
128
|
-
function estimatedTextWidth(value: string): number {
|
|
129
|
-
return Math.max(1, textWeight(value)) * (
|
|
148
|
+
function estimatedTextWidth(value: string, fontSize: number): number {
|
|
149
|
+
return Math.max(1, textWeight(value)) * (fontSize * 0.56);
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
function textWeight(value: string): number {
|
|
@@ -145,3 +165,12 @@ function escapeXml(value: string): string {
|
|
|
145
165
|
.replaceAll('"', """)
|
|
146
166
|
.replaceAll("'", "'");
|
|
147
167
|
}
|
|
168
|
+
|
|
169
|
+
function boundedInteger(
|
|
170
|
+
value: number,
|
|
171
|
+
range: { readonly min: number; readonly max: number },
|
|
172
|
+
fallback: number,
|
|
173
|
+
): number {
|
|
174
|
+
if (!Number.isFinite(value)) return fallback;
|
|
175
|
+
return Math.min(range.max, Math.max(range.min, Math.round(value)));
|
|
176
|
+
}
|
|
@@ -73,8 +73,8 @@ describe("水印设置", () => {
|
|
|
73
73
|
test("高密度缩小平铺单元且长内容自动换行以避免相邻水印重叠", () => {
|
|
74
74
|
const content =
|
|
75
75
|
"zhangsan · 张三 · 13800138000 · zs@example.com · 内部资料请勿外传";
|
|
76
|
-
const low = createWatermarkPattern(content, "low");
|
|
77
|
-
const high = createWatermarkPattern(content, "high");
|
|
76
|
+
const low = createWatermarkPattern(content, "low", 14);
|
|
77
|
+
const high = createWatermarkPattern(content, "high", 14);
|
|
78
78
|
const dimensions = (value: typeof low) =>
|
|
79
79
|
value.size.match(/\d+/g)!.map(Number);
|
|
80
80
|
|
|
@@ -86,21 +86,34 @@ describe("水印设置", () => {
|
|
|
86
86
|
).toBeGreaterThan(1);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test("
|
|
89
|
+
test("字体大小会改变平铺尺寸并写入实际 SVG", () => {
|
|
90
|
+
const small = createWatermarkPattern("zhangsan · 张三", "medium", 12);
|
|
91
|
+
const large = createWatermarkPattern("zhangsan · 张三", "medium", 32);
|
|
92
|
+
const width = (value: typeof small) => Number(value.size.match(/\d+/)?.[0]);
|
|
93
|
+
|
|
94
|
+
expect(width(large)).toBeGreaterThan(width(small));
|
|
95
|
+
expect(decodeURIComponent(large.image)).toContain('font-size="32"');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("服务端策略内容会被规范化后渲染且旧范围开关合并到总开关", () => {
|
|
90
99
|
expect(
|
|
91
100
|
normalizeWatermarkPolicy({
|
|
92
|
-
enabled:
|
|
93
|
-
businessPagesEnabled:
|
|
101
|
+
enabled: true,
|
|
102
|
+
businessPagesEnabled: false,
|
|
94
103
|
density: "high",
|
|
95
104
|
contentFields: ["phone", "phone"],
|
|
96
105
|
customContent: ` ${"x".repeat(100)} `,
|
|
106
|
+
opacityPercent: 99,
|
|
107
|
+
fontSize: 2,
|
|
97
108
|
}),
|
|
98
109
|
).toEqual({
|
|
99
110
|
enabled: false,
|
|
100
|
-
businessPagesEnabled:
|
|
111
|
+
businessPagesEnabled: false,
|
|
101
112
|
density: "high",
|
|
102
113
|
contentFields: ["phone"],
|
|
103
114
|
customContent: "x".repeat(80),
|
|
115
|
+
opacityPercent: 30,
|
|
116
|
+
fontSize: 12,
|
|
104
117
|
});
|
|
105
118
|
});
|
|
106
119
|
});
|
|
@@ -40,6 +40,7 @@ import { useAccessSnapshot } from "@/composables/useAccessSnapshot";
|
|
|
40
40
|
import { useLicenseRuntime } from "@/composables/useLicenseRuntime";
|
|
41
41
|
// @windy-module system.license end
|
|
42
42
|
import { usePlatformSettings } from "@/composables/usePlatformSettings";
|
|
43
|
+
import { webAdminUiComposition } from "@/router/web-composition-host";
|
|
43
44
|
import { authorizedSidebarMenus } from "./navigation";
|
|
44
45
|
|
|
45
46
|
interface NavigationGroup {
|
|
@@ -74,7 +75,13 @@ const license = useLicenseRuntime();
|
|
|
74
75
|
const platform = usePlatformSettings();
|
|
75
76
|
const openGroups = reactive<Record<string, boolean>>(loadOpenGroups());
|
|
76
77
|
const authorizedMenus = computed(() =>
|
|
77
|
-
access.snapshot.value
|
|
78
|
+
access.snapshot.value
|
|
79
|
+
? authorizedSidebarMenus(
|
|
80
|
+
access.snapshot.value,
|
|
81
|
+
webAdminUiComposition.presentation,
|
|
82
|
+
webAdminUiComposition,
|
|
83
|
+
)
|
|
84
|
+
: [],
|
|
78
85
|
);
|
|
79
86
|
const settingsMenu = computed(() =>
|
|
80
87
|
findMenu(authorizedMenus.value, "system.settings"),
|