@skopon-cool/form-sdk 0.4.8 → 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.
Files changed (41) hide show
  1. package/README.md +26 -1
  2. package/dist/adapter/formSchema.d.ts.map +1 -1
  3. package/dist/adapter/formWeb.d.ts +14 -0
  4. package/dist/adapter/formWeb.d.ts.map +1 -0
  5. package/dist/blocks/registry.d.ts.map +1 -1
  6. package/dist/blocks/web/adapter.d.ts +8 -0
  7. package/dist/blocks/web/adapter.d.ts.map +1 -0
  8. package/dist/blocks/web/catalog.d.ts +9 -0
  9. package/dist/blocks/web/catalog.d.ts.map +1 -0
  10. package/dist/blocks/web/index.d.ts +3 -0
  11. package/dist/blocks/web/index.d.ts.map +1 -0
  12. package/dist/client/authStorage.d.ts +1 -1
  13. package/dist/client/authStorage.d.ts.map +1 -1
  14. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  15. package/dist/form-sdk.css +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1364 -1182
  19. package/dist/submit/intersectPayloadBlocksWithForm.d.ts +2 -2
  20. package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
  21. package/dist/types/index.d.ts +7 -2
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/adapter/a2uiAdapter.test.ts +39 -0
  25. package/src/adapter/formSchema.test.ts +38 -0
  26. package/src/adapter/formSchema.ts +16 -1
  27. package/src/adapter/formWeb.test.ts +38 -0
  28. package/src/adapter/formWeb.ts +41 -0
  29. package/src/blocks/registry.ts +6 -1
  30. package/src/blocks/web/adapter.ts +36 -0
  31. package/src/blocks/web/catalog.test.tsx +95 -0
  32. package/src/blocks/web/catalog.tsx +158 -0
  33. package/src/blocks/web/index.ts +11 -0
  34. package/src/client/authStorage.ts +1 -1
  35. package/src/components/AskUserFormCard.tsx +2 -1
  36. package/src/index.ts +9 -0
  37. package/src/styles/index.css +87 -0
  38. package/src/submit/intersectPayloadBlocksWithForm.ts +6 -5
  39. package/src/submit/submit.test.ts +14 -0
  40. package/src/submit/validateFormSubmit.ts +2 -2
  41. package/src/types/index.ts +12 -2
@@ -9,7 +9,7 @@ export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' |
9
9
  export interface PayloadBlocksIntersection {
10
10
  matchedBlocks: FormBlock[];
11
11
  extraBlocks: FormBlock[];
12
- /** 按 payload 顺序:layout + matched input(不含 extra input) */
12
+ /** 按 payload 顺序:layout/media + matched input(不含 extra input) */
13
13
  renderBlocks: FormBlock[];
14
14
  title: string;
15
15
  description: string;
@@ -25,7 +25,7 @@ export declare function intersectPayloadBlocksWithForm(payloadDef: PayloadBlocks
25
25
  export declare function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown>;
26
26
  /** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
27
27
  export declare function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean;
28
- /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + 非 hidden input) */
28
+ /** 交集为空时 fallback:payload 中可上屏的 blocks(layout/media + 非 hidden input) */
29
29
  export declare function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[];
30
30
  /** payload 中所有 input 块 field name(fallback 提交用,含 hidden) */
31
31
  export declare function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,0DAA0D;IAC1D,YAAY,EAAE,SAAS,EAAE,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AA8ED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAmBvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CA+B3B;AA+BD,2CAA2C;AAC3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzF;AAED,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAIlF;AAED,mEAAmE;AACnE,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAO3F;AAED,4DAA4D;AAC5D,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF;AAED,6DAA6D;AAC7D,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,uBAAuB,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
1
+ {"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,gEAAgE;IAChE,YAAY,EAAE,SAAS,EAAE,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AA+ED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAmBvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CA+B3B;AA+BD,2CAA2C;AAC3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzF;AAED,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAIlF;AAED,yEAAyE;AACzE,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAO3F;AAED,4DAA4D;AAC5D,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF;AAED,6DAA6D;AAC7D,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,uBAAuB,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
@@ -4,7 +4,7 @@
4
4
  export type A2uiBinding<T> = T | {
5
5
  path: string;
6
6
  };
7
- export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
7
+ export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponWeb' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
8
8
  export interface A2uiComponentNode {
9
9
  id: string;
10
10
  component: A2uiComponentName | string;
@@ -23,7 +23,7 @@ export declare const A2UI_PROTOCOL_VERSION: "v0.9";
23
23
  export type FormJsonSchema = Record<string, unknown>;
24
24
  export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'resume_multiselect' | 'case_multiselect'
25
25
  /** @deprecated 加载时自动迁移为 case_multiselect */
26
- | 'case_singleselect' | 'image' | 'video' | 'audio';
26
+ | 'case_singleselect' | 'image' | 'video' | 'audio' | 'web';
27
27
  export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
28
28
  export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
29
29
  export declare const FORM_MEDIA_SIZES: FormMediaSize[];
@@ -72,6 +72,10 @@ export interface FormBlock {
72
72
  mediaUrl?: string;
73
73
  mediaUrls?: string[];
74
74
  mediaSize?: FormMediaSize;
75
+ /** 网页展示块:iframe 地址,仅支持 HTTP(S) 或同源相对地址 */
76
+ webUrl?: string;
77
+ /** 网页展示块:行内 iframe 高度(px) */
78
+ webHeight?: number;
75
79
  fileAcceptTypes?: string[];
76
80
  fileAcceptExtensions?: string[];
77
81
  filePlaceholderIcon?: FormFilePlaceholderIcon;
@@ -109,6 +113,7 @@ export interface FormSchema {
109
113
  jsonSchema: FormJsonSchema;
110
114
  }
111
115
  export declare function isMediaBlockType(type: FormBlockType): boolean;
116
+ export declare function isWebBlockType(type: FormBlockType): boolean;
112
117
  export declare function isLayoutBlockType(type: FormBlockType): boolean;
113
118
  export declare function isInputBlockType(type: FormBlockType): boolean;
114
119
  export declare function isDisplayBlockType(type: FormBlockType): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.8",
6
+ "version": "0.5.0",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -88,6 +88,45 @@ describe('a2uiAdapter blocks <-> surface', () => {
88
88
  expect(imgNode?.mediaSize).toBe('large')
89
89
  })
90
90
 
91
+ it('round-trips web blocks through SkoponWeb', () => {
92
+ const doc = blocksToA2ui(
93
+ {
94
+ title: '',
95
+ description: '',
96
+ blocks: [
97
+ {
98
+ id: 'b-web',
99
+ type: 'web',
100
+ label: '官网',
101
+ help: '可扩展查看',
102
+ webUrl: 'https://example.com/page',
103
+ webHeight: 480,
104
+ },
105
+ ],
106
+ },
107
+ { includeHeader: false },
108
+ )
109
+
110
+ const node = doc.components.find((component) => component.id === 'b-web')
111
+ expect(node).toMatchObject({
112
+ component: 'SkoponWeb',
113
+ url: 'https://example.com/page',
114
+ height: 480,
115
+ label: '官网',
116
+ help: '可扩展查看',
117
+ })
118
+
119
+ const back = a2uiToBlocks(doc)
120
+ expect(back.blocks[0]).toMatchObject({
121
+ id: 'b-web',
122
+ type: 'web',
123
+ label: '官网',
124
+ help: '可扩展查看',
125
+ webUrl: 'https://example.com/page',
126
+ webHeight: 480,
127
+ })
128
+ })
129
+
91
130
  it('blocksToA2ui omits hidden text controls but keeps dataModel defaults', () => {
92
131
  const doc = blocksToA2ui(
93
132
  {
@@ -4,10 +4,17 @@
4
4
  import { describe, it, expect } from 'vitest'
5
5
  import {
6
6
  coerceToggleValue,
7
+ createEmptyBlock,
7
8
  normalizeFormDefinition,
8
9
  blocksToJsonSchema,
9
10
  } from './formSchema'
10
11
  import { mapFormDefinitionFromDto } from './resolveSurface'
12
+ import {
13
+ isDisplayBlockType,
14
+ isInputBlockType,
15
+ isMediaBlockType,
16
+ isWebBlockType,
17
+ } from '../types/index'
11
18
 
12
19
  describe('coerceToggleValue', () => {
13
20
  it('accepts boolean, string, and numeric truthy/falsy values', () => {
@@ -128,6 +135,37 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
128
135
  })
129
136
  })
130
137
 
138
+ describe('web display block', () => {
139
+ it('creates and normalizes web defaults without treating web as input or media', () => {
140
+ const empty = createEmptyBlock('web')
141
+ expect(empty.webUrl).toBe('')
142
+ expect(empty.webHeight).toBe(360)
143
+ expect(isWebBlockType(empty.type)).toBe(true)
144
+ expect(isDisplayBlockType(empty.type)).toBe(true)
145
+ expect(isInputBlockType(empty.type)).toBe(false)
146
+ expect(isMediaBlockType(empty.type)).toBe(false)
147
+
148
+ const normalized = normalizeFormDefinition({
149
+ title: '',
150
+ description: '',
151
+ blocks: [
152
+ {
153
+ id: 'web-1',
154
+ type: 'web',
155
+ name: 'must_not_submit',
156
+ label: '官网',
157
+ webUrl: ' javascript:alert(1) ',
158
+ webHeight: 100,
159
+ },
160
+ ],
161
+ jsonSchema: {},
162
+ })
163
+ expect(normalized.blocks[0]?.webUrl).toBe('javascript:alert(1)')
164
+ expect(normalized.blocks[0]?.webHeight).toBe(200)
165
+ expect(normalized.jsonSchema).toMatchObject({ properties: {}, required: [] })
166
+ })
167
+ })
168
+
131
169
  describe('mapFormDefinitionFromDto jsonSchema', () => {
132
170
  it('regenerates jsonSchema from DTO blocks', () => {
133
171
  const def = mapFormDefinitionFromDto({
@@ -6,7 +6,12 @@ import type {
6
6
  FormJsonSchema,
7
7
  FormSchema,
8
8
  } from '../types/index'
9
- import { isInputBlockType, isMediaBlockType, isOptionsBlockType } from '../types/index'
9
+ import {
10
+ isInputBlockType,
11
+ isMediaBlockType,
12
+ isOptionsBlockType,
13
+ isWebBlockType,
14
+ } from '../types/index'
10
15
  import { generateId } from './id'
11
16
  import {
12
17
  filterFileAcceptExtensionsForTypes,
@@ -17,6 +22,7 @@ import {
17
22
  normalizeMediaSize,
18
23
  normalizeMediaUrls,
19
24
  } from './formMedia'
25
+ import { FORM_WEB_DEFAULT_HEIGHT, normalizeWebHeight } from './formWeb'
20
26
 
21
27
  const DEFAULT_MEDIA_URL_MAX = 10
22
28
 
@@ -162,6 +168,8 @@ function normalizeBlock(raw: FormBlock): FormBlock {
162
168
  ? normalizeMediaUrls(migrated, DEFAULT_MEDIA_URL_MAX)
163
169
  : undefined,
164
170
  mediaSize: isMediaBlockType(type) ? normalizeMediaSize(migrated.mediaSize) : undefined,
171
+ webUrl: isWebBlockType(type) ? migrated.webUrl?.trim() ?? '' : undefined,
172
+ webHeight: isWebBlockType(type) ? normalizeWebHeight(migrated.webHeight) : undefined,
165
173
  fileAcceptTypes,
166
174
  fileAcceptExtensions: filterFileAcceptExtensionsForTypes(
167
175
  fileAcceptTypes,
@@ -425,6 +433,13 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
425
433
  block.mediaSize = 'medium'
426
434
  }
427
435
 
436
+ if (isWebBlockType(type)) {
437
+ block.label = ''
438
+ block.help = ''
439
+ block.webUrl = ''
440
+ block.webHeight = FORM_WEB_DEFAULT_HEIGHT
441
+ }
442
+
428
443
  if (isOptionsBlockType(type)) {
429
444
  block.options = [
430
445
  { value: 'option_1', label: '选项 1' },
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, expect, it } from 'vitest'
5
+ import {
6
+ FORM_WEB_DEFAULT_HEIGHT,
7
+ FORM_WEB_MAX_HEIGHT,
8
+ FORM_WEB_MIN_HEIGHT,
9
+ isSupportedWebUrl,
10
+ normalizeWebHeight,
11
+ } from './formWeb'
12
+
13
+ describe('formWeb', () => {
14
+ it('normalizes iframe height to the supported integer range', () => {
15
+ expect(normalizeWebHeight(undefined)).toBe(FORM_WEB_DEFAULT_HEIGHT)
16
+ expect(normalizeWebHeight('bad')).toBe(FORM_WEB_DEFAULT_HEIGHT)
17
+ expect(normalizeWebHeight(199)).toBe(FORM_WEB_MIN_HEIGHT)
18
+ expect(normalizeWebHeight(480.9)).toBe(480)
19
+ expect(normalizeWebHeight(1201)).toBe(FORM_WEB_MAX_HEIGHT)
20
+ })
21
+
22
+ it('accepts HTTP(S) and explicit same-origin relative paths', () => {
23
+ expect(isSupportedWebUrl('https://example.com/page')).toBe(true)
24
+ expect(isSupportedWebUrl('http://localhost:3000/preview')).toBe(true)
25
+ expect(isSupportedWebUrl('/preview')).toBe(true)
26
+ expect(isSupportedWebUrl('./preview')).toBe(true)
27
+ expect(isSupportedWebUrl('../preview')).toBe(true)
28
+ })
29
+
30
+ it('rejects empty, dangerous, protocol-relative, and ambiguous paths', () => {
31
+ expect(isSupportedWebUrl('')).toBe(false)
32
+ expect(isSupportedWebUrl('javascript:alert(1)')).toBe(false)
33
+ expect(isSupportedWebUrl('data:text/html,test')).toBe(false)
34
+ expect(isSupportedWebUrl('//example.com/page')).toBe(false)
35
+ expect(isSupportedWebUrl('preview.html')).toBe(false)
36
+ expect(isSupportedWebUrl('https://')).toBe(false)
37
+ })
38
+ })
@@ -0,0 +1,41 @@
1
+ export const FORM_WEB_DEFAULT_HEIGHT = 360
2
+ export const FORM_WEB_MIN_HEIGHT = 200
3
+ export const FORM_WEB_MAX_HEIGHT = 1200
4
+
5
+ /**
6
+ * 网页展示高度统一持久化为整数像素。
7
+ * 空值或非数字回退默认值,越界值限制到允许范围。
8
+ */
9
+ export function normalizeWebHeight(raw: unknown): number {
10
+ if (raw === undefined || raw === null || raw === '') {
11
+ return FORM_WEB_DEFAULT_HEIGHT
12
+ }
13
+ const numeric = typeof raw === 'number' ? raw : Number(raw)
14
+ if (!Number.isFinite(numeric)) return FORM_WEB_DEFAULT_HEIGHT
15
+ return Math.min(
16
+ FORM_WEB_MAX_HEIGHT,
17
+ Math.max(FORM_WEB_MIN_HEIGHT, Math.floor(numeric)),
18
+ )
19
+ }
20
+
21
+ /**
22
+ * iframe 仅允许 HTTP(S) 绝对地址,或明确的同源相对路径。
23
+ * 不接受 protocol-relative、data、javascript 等地址。
24
+ */
25
+ export function isSupportedWebUrl(raw: unknown): raw is string {
26
+ if (typeof raw !== 'string') return false
27
+ const value = raw.trim()
28
+ if (!value) return false
29
+
30
+ if (/^\/(?!\/)/.test(value) || /^\.\.?\//.test(value)) {
31
+ return true
32
+ }
33
+ if (!/^https?:\/\//i.test(value)) return false
34
+
35
+ try {
36
+ const url = new URL(value)
37
+ return (url.protocol === 'http:' || url.protocol === 'https:') && Boolean(url.hostname)
38
+ } catch {
39
+ return false
40
+ }
41
+ }
@@ -2,8 +2,13 @@ import type { FormBlockType } from '../types/index'
2
2
  import type { BlockAdapterPlugin } from './types'
3
3
  import { caseMultiselectAdapter } from './case_multiselect'
4
4
  import { resumeMultiselectAdapter } from './resume_multiselect'
5
+ import { webAdapter } from './web'
5
6
 
6
- const adapters: BlockAdapterPlugin[] = [resumeMultiselectAdapter, caseMultiselectAdapter]
7
+ const adapters: BlockAdapterPlugin[] = [
8
+ resumeMultiselectAdapter,
9
+ caseMultiselectAdapter,
10
+ webAdapter,
11
+ ]
7
12
 
8
13
  const adapterByType = new Map<FormBlockType, BlockAdapterPlugin>(
9
14
  adapters.map((adapter) => [adapter.type, adapter]),
@@ -0,0 +1,36 @@
1
+ import type { A2uiComponentNode, FormBlock } from '../../types/index'
2
+ import { normalizeWebHeight } from '../../adapter/formWeb'
3
+ import type { BlockAdapterContext, BlockToComponentResult } from '../types'
4
+
5
+ export function webToComponent(
6
+ block: FormBlock,
7
+ ctx: BlockAdapterContext,
8
+ ): BlockToComponentResult {
9
+ return {
10
+ node: {
11
+ id: ctx.id,
12
+ component: 'SkoponWeb',
13
+ url: block.webUrl?.trim() ?? '',
14
+ height: normalizeWebHeight(block.webHeight),
15
+ label: ctx.label,
16
+ help: block.help ?? '',
17
+ },
18
+ }
19
+ }
20
+
21
+ export function webFromComponent(
22
+ node: A2uiComponentNode,
23
+ base: (type: 'web') => FormBlock,
24
+ helpers: {
25
+ asLiteral: (value: unknown) => string | undefined
26
+ nameFromValue: (value: unknown) => string
27
+ },
28
+ ): FormBlock {
29
+ const help = helpers.asLiteral(node.help)?.trim()
30
+ return {
31
+ ...base('web'),
32
+ webUrl: helpers.asLiteral(node.url)?.trim() ?? '',
33
+ webHeight: normalizeWebHeight(node.height),
34
+ ...(help ? { help } : {}),
35
+ }
36
+ }
@@ -0,0 +1,95 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
3
+ import { cleanup, fireEvent, render } from '@testing-library/react'
4
+ import { buildSkoponCatalog } from '../../catalog/a2uiCustomCatalog'
5
+ import { SkoponWebPreview } from './catalog'
6
+
7
+ beforeAll(() => {
8
+ const getComputedStyle = window.getComputedStyle.bind(window)
9
+ vi.spyOn(window, 'getComputedStyle').mockImplementation((element) =>
10
+ getComputedStyle(element),
11
+ )
12
+ class ResizeObserverMock {
13
+ observe() {}
14
+ unobserve() {}
15
+ disconnect() {}
16
+ }
17
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock)
18
+ vi.stubGlobal(
19
+ 'matchMedia',
20
+ vi.fn().mockImplementation((query: string) => ({
21
+ matches: false,
22
+ media: query,
23
+ onchange: null,
24
+ addListener: vi.fn(),
25
+ removeListener: vi.fn(),
26
+ addEventListener: vi.fn(),
27
+ removeEventListener: vi.fn(),
28
+ dispatchEvent: vi.fn(),
29
+ })),
30
+ )
31
+ })
32
+
33
+ afterEach(() => {
34
+ cleanup()
35
+ })
36
+
37
+ function activeFrames(url: string): HTMLIFrameElement[] {
38
+ return Array.from(document.querySelectorAll<HTMLIFrameElement>('iframe')).filter(
39
+ (frame) => frame.getAttribute('src') === url,
40
+ )
41
+ }
42
+
43
+ describe('SkoponWebPreview', () => {
44
+ it('is registered in the merged Skopon A2UI catalog', () => {
45
+ const catalog = buildSkoponCatalog()
46
+ expect(
47
+ [...catalog.components.values()].some((component) => component.name === 'SkoponWeb'),
48
+ ).toBe(true)
49
+ })
50
+
51
+ it('renders one inline iframe and moves the active URL into the modal', () => {
52
+ const url = 'https://example.com/page'
53
+ const view = render(
54
+ <SkoponWebPreview
55
+ url={url}
56
+ height={480}
57
+ label="官网"
58
+ help="可扩展查看"
59
+ />,
60
+ )
61
+
62
+ const inlineFrame = view.getByTitle('官网') as HTMLIFrameElement
63
+ expect(inlineFrame.style.height).toBe('480px')
64
+ expect(inlineFrame.getAttribute('loading')).toBe('lazy')
65
+ expect(activeFrames(url)).toHaveLength(1)
66
+ expect(view.getByText('可扩展查看')).toBeTruthy()
67
+
68
+ fireEvent.click(view.getByRole('button', { name: '扩展显示' }))
69
+
70
+ expect(document.querySelector('.skopon-web-modal')).toBeTruthy()
71
+ expect(view.queryByTitle('官网')).toBeNull()
72
+ expect(document.querySelector('iframe[title="官网(扩展显示)"]')).toBeTruthy()
73
+ expect(activeFrames(url)).toHaveLength(1)
74
+
75
+ const closeButton = document.querySelector<HTMLButtonElement>('.ant-modal-close')
76
+ expect(closeButton).toBeTruthy()
77
+ if (closeButton) fireEvent.click(closeButton)
78
+
79
+ expect(activeFrames(url)).toHaveLength(1)
80
+ expect(view.getByTitle('官网')).toBeTruthy()
81
+ })
82
+
83
+ it('shows distinct placeholders and never passes unsupported URLs to iframe', () => {
84
+ const emptyView = render(<SkoponWebPreview url="" />)
85
+ expect(emptyView.getByText('未设置网页 URL')).toBeTruthy()
86
+ expect(document.querySelector('iframe')).toBeNull()
87
+ expect(emptyView.queryByRole('button', { name: '扩展显示' })).toBeNull()
88
+ emptyView.unmount()
89
+
90
+ const invalidView = render(<SkoponWebPreview url="javascript:alert(1)" />)
91
+ expect(invalidView.getByText('网页 URL 无效')).toBeTruthy()
92
+ expect(document.querySelector('iframe')).toBeNull()
93
+ expect(invalidView.queryByRole('button', { name: '扩展显示' })).toBeNull()
94
+ })
95
+ })
@@ -0,0 +1,158 @@
1
+ import { FullscreenOutlined } from '@ant-design/icons'
2
+ import { Button, Modal, Tooltip, Typography } from 'antd'
3
+ import { useRef, useState, type Ref } from 'react'
4
+ import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
5
+ import { z } from 'zod'
6
+ import { isSupportedWebUrl, normalizeWebHeight } from '../../adapter/formWeb'
7
+
8
+ export interface SkoponWebPreviewProps {
9
+ url?: string
10
+ height?: number
11
+ label?: string
12
+ help?: string
13
+ }
14
+
15
+ function WebFrame({
16
+ url,
17
+ title,
18
+ className,
19
+ height,
20
+ frameRef,
21
+ }: {
22
+ url: string
23
+ title: string
24
+ className: string
25
+ height?: number
26
+ frameRef?: Ref<HTMLIFrameElement>
27
+ }) {
28
+ return (
29
+ <iframe
30
+ ref={frameRef}
31
+ src={url}
32
+ title={title}
33
+ className={className}
34
+ style={height === undefined ? undefined : { height }}
35
+ loading="lazy"
36
+ referrerPolicy="strict-origin-when-cross-origin"
37
+ allowFullScreen
38
+ />
39
+ )
40
+ }
41
+
42
+ export function SkoponWebPreview({
43
+ url = '',
44
+ height,
45
+ label,
46
+ help,
47
+ }: SkoponWebPreviewProps) {
48
+ const [expanded, setExpanded] = useState(false)
49
+ const [inlineActive, setInlineActive] = useState(true)
50
+ const modalFrameRef = useRef<HTMLIFrameElement>(null)
51
+ const trimmedUrl = url.trim()
52
+ const supportedUrl = isSupportedWebUrl(trimmedUrl) ? trimmedUrl : null
53
+ const resolvedHeight = normalizeWebHeight(height)
54
+ const title = label?.trim() || '网页展示'
55
+ const helpText = help?.trim()
56
+ const emptyText = trimmedUrl ? '网页 URL 无效' : '未设置网页 URL'
57
+
58
+ return (
59
+ <div className="form-block-preview skopon-web-preview">
60
+ {label?.trim() ? <div className="form-block-preview-label">{label.trim()}</div> : null}
61
+
62
+ <div
63
+ className={`skopon-web-frame-shell${supportedUrl ? '' : ' skopon-web-frame-shell--empty'}`}
64
+ style={{ height: resolvedHeight }}
65
+ >
66
+ {supportedUrl && inlineActive ? (
67
+ <WebFrame
68
+ url={supportedUrl}
69
+ title={title}
70
+ className="skopon-web-frame skopon-web-frame--inline"
71
+ height={resolvedHeight}
72
+ />
73
+ ) : null}
74
+
75
+ {!supportedUrl ? (
76
+ <div className="skopon-web-empty" role="status">
77
+ {emptyText}
78
+ </div>
79
+ ) : null}
80
+
81
+ {supportedUrl ? (
82
+ <Tooltip title="扩展显示">
83
+ <Button
84
+ type="default"
85
+ shape="circle"
86
+ className="skopon-web-expand-button"
87
+ icon={<FullscreenOutlined />}
88
+ aria-label="扩展显示"
89
+ onClick={() => {
90
+ setInlineActive(false)
91
+ setExpanded(true)
92
+ }}
93
+ />
94
+ </Tooltip>
95
+ ) : null}
96
+ </div>
97
+
98
+ {helpText ? (
99
+ <Typography.Text type="secondary" className="form-block-preview-help">
100
+ {helpText}
101
+ </Typography.Text>
102
+ ) : null}
103
+
104
+ <Modal
105
+ open={expanded}
106
+ title={title}
107
+ footer={null}
108
+ width="min(1200px, calc(100vw - 32px))"
109
+ className="skopon-web-modal"
110
+ destroyOnHidden
111
+ mask={{ closable: true }}
112
+ onCancel={() => {
113
+ // rc-dialog 在关闭动画期间会缓存子树;先清掉 src,避免行内 iframe
114
+ // 恢复后与缓存中的弹窗 iframe 同时继续运行。
115
+ modalFrameRef.current?.removeAttribute('src')
116
+ setExpanded(false)
117
+ setInlineActive(true)
118
+ }}
119
+ >
120
+ {supportedUrl && !inlineActive ? (
121
+ <WebFrame
122
+ url={supportedUrl}
123
+ title={`${title}(扩展显示)`}
124
+ className="skopon-web-frame skopon-web-frame--modal"
125
+ frameRef={modalFrameRef}
126
+ />
127
+ ) : null}
128
+ </Modal>
129
+ </div>
130
+ )
131
+ }
132
+
133
+ const SkoponWebApi = {
134
+ name: 'SkoponWeb',
135
+ schema: z
136
+ .object({
137
+ url: z.any().optional(),
138
+ height: z.any().optional(),
139
+ label: z.any().optional(),
140
+ help: z.any().optional(),
141
+ })
142
+ .passthrough(),
143
+ }
144
+
145
+ export const SkoponWebImpl = createBinderlessComponentImplementation(
146
+ SkoponWebApi,
147
+ ({ context }) => {
148
+ const props = context.componentModel.properties as Record<string, unknown>
149
+ return (
150
+ <SkoponWebPreview
151
+ url={typeof props.url === 'string' ? props.url : ''}
152
+ height={normalizeWebHeight(props.height)}
153
+ label={typeof props.label === 'string' ? props.label : ''}
154
+ help={typeof props.help === 'string' ? props.help : ''}
155
+ />
156
+ )
157
+ },
158
+ )
@@ -0,0 +1,11 @@
1
+ import type { BlockAdapterPlugin } from '../types'
2
+ import { webFromComponent, webToComponent } from './adapter'
3
+ import { SkoponWebImpl } from './catalog'
4
+
5
+ export const webAdapter: BlockAdapterPlugin = {
6
+ type: 'web',
7
+ componentName: 'SkoponWeb',
8
+ toComponent: webToComponent,
9
+ fromComponent: webFromComponent,
10
+ catalogComponents: [SkoponWebImpl],
11
+ }
@@ -1,5 +1,5 @@
1
1
  export interface SkoponAuthStorageOptions {
2
- /** 默认 sessionStorage(与 skopon-admin / HiAUTH 约定一致) */
2
+ /** 默认 sessionStorage(与 skopon-admin 会话约定一致) */
3
3
  storage?: 'sessionStorage' | 'localStorage'
4
4
  /** 默认 auth_user */
5
5
  storageKey?: string
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Button, Spin, Tag, Tooltip } from 'antd'
3
3
  import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
4
+ import { isInputBlockType } from '../types/index'
4
5
  import {
5
6
  resolveRealtimeWsUrl,
6
7
  resolveSubmitAuthHeaders,
@@ -213,7 +214,7 @@ function AskUserFormCardBody({
213
214
  const fieldNames = useMemo(
214
215
  () =>
215
216
  renderBlocks
216
- .filter((block) => block.name?.trim())
217
+ .filter((block) => isInputBlockType(block.type) && block.name?.trim())
217
218
  .map((block) => block.name!.trim()),
218
219
  [renderBlocks],
219
220
  )