create-windy 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +263 -121
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- 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/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -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 +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- 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/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- 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/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +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/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -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
|
|
118
|
+
const legacyWrapper = mount(GlobalWatermark, {
|
|
115
119
|
props: { scope: "business" },
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
expect(
|
|
119
|
-
|
|
120
|
-
).toBe(
|
|
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
|
|
|
@@ -25,7 +25,12 @@ const developmentAuth = developmentAuthStatus();
|
|
|
25
25
|
<div class="w-full max-w-md space-y-4">
|
|
26
26
|
<div class="flex items-center justify-center gap-3">
|
|
27
27
|
<span
|
|
28
|
-
class="flex size-10 items-center justify-center rounded-xl
|
|
28
|
+
class="flex size-10 items-center justify-center rounded-xl"
|
|
29
|
+
:class="
|
|
30
|
+
platform.branding.value.logoUrl
|
|
31
|
+
? 'bg-transparent text-foreground'
|
|
32
|
+
: 'bg-primary text-primary-foreground'
|
|
33
|
+
"
|
|
29
34
|
>
|
|
30
35
|
<img
|
|
31
36
|
v-if="platform.branding.value.logoUrl"
|
|
@@ -13,6 +13,12 @@ const access = useAccessSnapshot();
|
|
|
13
13
|
const toast = useAppToast();
|
|
14
14
|
const softwareName = shallowRef("");
|
|
15
15
|
const logoUrl = shallowRef("");
|
|
16
|
+
const logoUrlInput = computed({
|
|
17
|
+
get: () => (logoUrl.value.startsWith("data:") ? "" : logoUrl.value),
|
|
18
|
+
set: (value: string) => {
|
|
19
|
+
logoUrl.value = value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
16
22
|
const description = shallowRef("");
|
|
17
23
|
const readingLogo = shallowRef(false);
|
|
18
24
|
const loaded = shallowRef(false);
|
|
@@ -145,11 +151,17 @@ defineExpose({
|
|
|
145
151
|
</Button>
|
|
146
152
|
</div>
|
|
147
153
|
<Input
|
|
148
|
-
v-model="
|
|
154
|
+
v-model="logoUrlInput"
|
|
149
155
|
:disabled="!canManage"
|
|
150
|
-
maxlength="
|
|
156
|
+
maxlength="2048"
|
|
151
157
|
placeholder="或填写 https://example.com/logo.png"
|
|
152
158
|
/>
|
|
159
|
+
<span
|
|
160
|
+
v-if="logoUrl.startsWith('data:')"
|
|
161
|
+
class="text-xs font-normal text-muted-foreground"
|
|
162
|
+
>
|
|
163
|
+
当前使用本地图片,编码内容不在输入框中显示。
|
|
164
|
+
</span>
|
|
153
165
|
<span class="text-xs font-normal text-muted-foreground">
|
|
154
166
|
本地文件支持 PNG、JPEG、WebP,最大
|
|
155
167
|
{{ maxLocalLogoBytes / 1024 }} KiB;也可使用 HTTPS 或站内路径。
|
|
@@ -159,7 +171,12 @@ defineExpose({
|
|
|
159
171
|
<p class="mb-3 text-xs font-medium text-muted-foreground">预览</p>
|
|
160
172
|
<div class="flex items-center gap-3">
|
|
161
173
|
<div
|
|
162
|
-
class="flex size-12 items-center justify-center overflow-hidden rounded-lg
|
|
174
|
+
class="flex size-12 items-center justify-center overflow-hidden rounded-lg"
|
|
175
|
+
:class="
|
|
176
|
+
logoUrl
|
|
177
|
+
? 'bg-transparent text-foreground'
|
|
178
|
+
: 'bg-primary text-primary-foreground'
|
|
179
|
+
"
|
|
163
180
|
>
|
|
164
181
|
<img
|
|
165
182
|
v-if="logoUrl"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { shallowRef } from "vue";
|
|
4
|
+
import BrandingSettingsSection from "./BrandingSettingsSection.vue";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
refresh: vi.fn(),
|
|
8
|
+
save: vi.fn(),
|
|
9
|
+
success: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/composables/useAccessSnapshot", () => ({
|
|
14
|
+
useAccessSnapshot: () => ({
|
|
15
|
+
snapshot: shallowRef({
|
|
16
|
+
actor: { permissionKeys: ["system.settings.manage"] },
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/composables/useAppToast", () => ({
|
|
22
|
+
useAppToast: () => ({ success: mocks.success, error: mocks.error }),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@/composables/usePlatformSettings", () => ({
|
|
26
|
+
usePlatformSettings: () => ({
|
|
27
|
+
refresh: mocks.refresh,
|
|
28
|
+
save: mocks.save,
|
|
29
|
+
saving: shallowRef(false),
|
|
30
|
+
error: shallowRef(""),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe("品牌基本信息", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mocks.refresh.mockReset();
|
|
37
|
+
mocks.save.mockReset();
|
|
38
|
+
mocks.success.mockReset();
|
|
39
|
+
mocks.error.mockReset();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("本地 Logo 保留预览但不在地址输入框暴露 Base64,且不添加底色", async () => {
|
|
43
|
+
const localLogo = "data:image/png;base64,AA==";
|
|
44
|
+
mocks.refresh.mockResolvedValue({
|
|
45
|
+
softwareName: "利剑",
|
|
46
|
+
logoUrl: localLogo,
|
|
47
|
+
description: "企业级 Dashboard 产品基座",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const wrapper = mount(BrandingSettingsSection);
|
|
51
|
+
await flushPromises();
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
(wrapper.get('input[placeholder^="或填写"]').element as HTMLInputElement)
|
|
55
|
+
.value,
|
|
56
|
+
).toBe("");
|
|
57
|
+
expect(wrapper.get('img[alt="Logo 预览"]').attributes("src")).toBe(
|
|
58
|
+
localLogo,
|
|
59
|
+
);
|
|
60
|
+
expect(wrapper.text()).toContain("编码内容不在输入框中显示");
|
|
61
|
+
const preview = wrapper.get('img[alt="Logo 预览"]').element.parentElement;
|
|
62
|
+
expect(preview?.classList.contains("bg-transparent")).toBe(true);
|
|
63
|
+
expect(preview?.classList.contains("bg-primary")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("外部或站内 Logo 地址仍可编辑", async () => {
|
|
67
|
+
mocks.refresh.mockResolvedValue({
|
|
68
|
+
softwareName: "Windy",
|
|
69
|
+
logoUrl: "/assets/logo.png",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const wrapper = mount(BrandingSettingsSection);
|
|
73
|
+
await flushPromises();
|
|
74
|
+
|
|
75
|
+
expect(
|
|
76
|
+
(wrapper.get('input[placeholder^="或填写"]').element as HTMLInputElement)
|
|
77
|
+
.value,
|
|
78
|
+
).toBe("/assets/logo.png");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -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:
|
|
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"
|
|
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="
|
|
185
|
-
:disabled="formDisabled
|
|
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"
|
|
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
|
恢复默认
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import WatermarkSettingsSection from "./WatermarkSettingsSection.vue";
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => {
|
|
6
|
+
const actor = {
|
|
7
|
+
id: "user_settings",
|
|
8
|
+
type: "user" as const,
|
|
9
|
+
name: "张三",
|
|
10
|
+
username: "zhangsan",
|
|
11
|
+
displayName: "张三",
|
|
12
|
+
email: "zs@example.com",
|
|
13
|
+
roleCodes: ["admin"],
|
|
14
|
+
permissionKeys: ["system.watermark.read", "system.watermark.manage"],
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
actor: { value: actor },
|
|
18
|
+
access: {
|
|
19
|
+
value: {
|
|
20
|
+
actor,
|
|
21
|
+
menus: [],
|
|
22
|
+
permissions: [],
|
|
23
|
+
features: [
|
|
24
|
+
{
|
|
25
|
+
key: "system.watermark",
|
|
26
|
+
enabled: true,
|
|
27
|
+
visible: "visible" as const,
|
|
28
|
+
allowed: true,
|
|
29
|
+
reason: "enabled",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
policy: {
|
|
35
|
+
value: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
businessPagesEnabled: true,
|
|
38
|
+
density: "medium" as const,
|
|
39
|
+
contentFields: ["username", "displayName"] as const,
|
|
40
|
+
customContent: "",
|
|
41
|
+
opacityPercent: 8,
|
|
42
|
+
fontSize: 14,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
loadManaged: vi.fn(),
|
|
46
|
+
save: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
vi.mock("@/composables/useAccessSnapshot", () => ({
|
|
51
|
+
useAccessSnapshot: () => ({
|
|
52
|
+
snapshot: state.access,
|
|
53
|
+
ensureLoaded: vi.fn(async () => state.access.value),
|
|
54
|
+
}),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("@/composables/useAuthSession", () => ({
|
|
58
|
+
useAuthSession: () => ({ actor: state.actor }),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock("@/composables/useAppToast", () => ({
|
|
62
|
+
useAppToast: () => ({
|
|
63
|
+
success: vi.fn(),
|
|
64
|
+
error: vi.fn(),
|
|
65
|
+
}),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
vi.mock("@/composables/useWatermarkSettings", () => ({
|
|
69
|
+
useWatermarkSettings: () => ({
|
|
70
|
+
policy: state.policy,
|
|
71
|
+
loading: { value: false },
|
|
72
|
+
loadManaged: state.loadManaged,
|
|
73
|
+
save: state.save,
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
state.loadManaged.mockReset();
|
|
79
|
+
state.loadManaged.mockResolvedValue(state.policy.value);
|
|
80
|
+
state.save.mockReset();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("水印设置表单", () => {
|
|
84
|
+
test("只呈现一个开关且外观草稿立即更新预览", async () => {
|
|
85
|
+
const wrapper = mount(WatermarkSettingsSection);
|
|
86
|
+
await flushPromises();
|
|
87
|
+
|
|
88
|
+
expect(wrapper.text()).not.toContain("覆盖业务页面");
|
|
89
|
+
expect(wrapper.findAll('[aria-label="显示页面水印"]')).toHaveLength(1);
|
|
90
|
+
|
|
91
|
+
await wrapper.get('input[aria-label="水印不透明度"]').setValue("20");
|
|
92
|
+
await wrapper.get('input[aria-label="水印字体大小"]').setValue("24");
|
|
93
|
+
|
|
94
|
+
const previewStyle =
|
|
95
|
+
wrapper
|
|
96
|
+
.get('[data-testid="watermark-preview-pattern"]')
|
|
97
|
+
.attributes("style") ?? "";
|
|
98
|
+
expect(previewStyle).toContain("opacity: 0.2");
|
|
99
|
+
expect(decodeURIComponent(previewStyle)).toContain('font-size="24"');
|
|
100
|
+
expect(state.save).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("任意勾选身份字段后立即进入预览内容", async () => {
|
|
104
|
+
const wrapper = mount(WatermarkSettingsSection);
|
|
105
|
+
await flushPromises();
|
|
106
|
+
|
|
107
|
+
await wrapper.get('[aria-label="邮箱"]').trigger("click");
|
|
108
|
+
|
|
109
|
+
const previewStyle =
|
|
110
|
+
wrapper
|
|
111
|
+
.get('[data-testid="watermark-preview-pattern"]')
|
|
112
|
+
.attributes("style") ?? "";
|
|
113
|
+
expect(decodeURIComponent(previewStyle)).toContain("zs@example.com");
|
|
114
|
+
});
|
|
115
|
+
});
|