@skopon-cool/form-sdk 0.1.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/README.md +82 -0
- package/dist/adapter/a2uiAdapter.d.ts +21 -0
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -0
- package/dist/adapter/extractSurfaceValues.d.ts +8 -0
- package/dist/adapter/extractSurfaceValues.d.ts.map +1 -0
- package/dist/adapter/formFileAccept.d.ts +17 -0
- package/dist/adapter/formFileAccept.d.ts.map +1 -0
- package/dist/adapter/formFilePlaceholderIcon.d.ts +5 -0
- package/dist/adapter/formFilePlaceholderIcon.d.ts.map +1 -0
- package/dist/adapter/formMedia.d.ts +7 -0
- package/dist/adapter/formMedia.d.ts.map +1 -0
- package/dist/adapter/formSchema.d.ts +6 -0
- package/dist/adapter/formSchema.d.ts.map +1 -0
- package/dist/adapter/id.d.ts +4 -0
- package/dist/adapter/id.d.ts.map +1 -0
- package/dist/adapter/resolveSurface.d.ts +6 -0
- package/dist/adapter/resolveSurface.d.ts.map +1 -0
- package/dist/catalog/a2uiCustomCatalog.d.ts +10 -0
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -0
- package/dist/catalog/a2uiPreviewContext.d.ts +11 -0
- package/dist/catalog/a2uiPreviewContext.d.ts.map +1 -0
- package/dist/catalog/useSkoponBoundField.d.ts +10 -0
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -0
- package/dist/client/formClient.d.ts +22 -0
- package/dist/client/formClient.d.ts.map +1 -0
- package/dist/components/AskUserFormCard.d.ts +13 -0
- package/dist/components/AskUserFormCard.d.ts.map +1 -0
- package/dist/components/CurlSubmitBlock.d.ts +10 -0
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -0
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +11 -0
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -0
- package/dist/components/SkoponFormRenderer.d.ts +16 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -0
- package/dist/form-sdk.css +1 -0
- package/dist/icons/FilePlaceholderIcon.d.ts +10 -0
- package/dist/icons/FilePlaceholderIcon.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1332 -0
- package/dist/submit/buildCurlStatement.d.ts +2 -0
- package/dist/submit/buildCurlStatement.d.ts.map +1 -0
- package/dist/submit/intersectPayloadWithForm.d.ts +17 -0
- package/dist/submit/intersectPayloadWithForm.d.ts.map +1 -0
- package/dist/submit/submitFormJson.d.ts +12 -0
- package/dist/submit/submitFormJson.d.ts.map +1 -0
- package/dist/types/index.d.ts +76 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/adapter/a2uiAdapter.test.ts +150 -0
- package/src/adapter/a2uiAdapter.ts +490 -0
- package/src/adapter/extractSurfaceValues.ts +25 -0
- package/src/adapter/formFileAccept.ts +198 -0
- package/src/adapter/formFilePlaceholderIcon.ts +33 -0
- package/src/adapter/formMedia.ts +50 -0
- package/src/adapter/formSchema.ts +139 -0
- package/src/adapter/id.ts +24 -0
- package/src/adapter/resolveSurface.ts +66 -0
- package/src/catalog/a2uiCustomCatalog.tsx +548 -0
- package/src/catalog/a2uiPreviewContext.tsx +26 -0
- package/src/catalog/useSkoponBoundField.ts +57 -0
- package/src/client/formClient.ts +72 -0
- package/src/components/AskUserFormCard.tsx +155 -0
- package/src/components/CurlSubmitBlock.tsx +60 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +70 -0
- package/src/components/SkoponFormRenderer.tsx +100 -0
- package/src/icons/FilePlaceholderIcon.tsx +40 -0
- package/src/index.ts +67 -0
- package/src/styles/a2ui-preview.css +345 -0
- package/src/styles/index.css +190 -0
- package/src/submit/buildCurlStatement.ts +13 -0
- package/src/submit/intersectPayloadWithForm.ts +54 -0
- package/src/submit/submit.test.ts +63 -0
- package/src/submit/submitFormJson.ts +50 -0
- package/src/types/index.ts +139 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Button, Spin, Tooltip } from 'antd'
|
|
3
|
+
import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
|
|
4
|
+
import { buildAskUserSurface } from '../adapter/resolveSurface'
|
|
5
|
+
import { buildCurlStatement } from '../submit/buildCurlStatement'
|
|
6
|
+
import { intersectPayloadWithForm } from '../submit/intersectPayloadWithForm'
|
|
7
|
+
import { copyTextToClipboard, submitFormJson } from '../submit/submitFormJson'
|
|
8
|
+
import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
|
|
9
|
+
import CurlSubmitBlock from './CurlSubmitBlock'
|
|
10
|
+
|
|
11
|
+
export interface AskUserFormCardProps {
|
|
12
|
+
payload: unknown
|
|
13
|
+
formUniqueId: string
|
|
14
|
+
callbackUrl?: string | null
|
|
15
|
+
submitMode?: SubmitMode
|
|
16
|
+
fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
|
|
17
|
+
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function AskUserFormCard({
|
|
21
|
+
payload,
|
|
22
|
+
formUniqueId,
|
|
23
|
+
callbackUrl,
|
|
24
|
+
submitMode = 'curl',
|
|
25
|
+
fetchFormDetail,
|
|
26
|
+
onNotify,
|
|
27
|
+
}: AskUserFormCardProps) {
|
|
28
|
+
const [formDef, setFormDef] = useState<FormSchema | null>(null)
|
|
29
|
+
const [formDisabled, setFormDisabled] = useState(false)
|
|
30
|
+
const [loading, setLoading] = useState(true)
|
|
31
|
+
const rendererRef = useRef<SkoponFormRendererRef>(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false
|
|
35
|
+
setLoading(true)
|
|
36
|
+
setFormDisabled(false)
|
|
37
|
+
fetchFormDetail({ formUniqueId })
|
|
38
|
+
.then((detail) => {
|
|
39
|
+
if (cancelled) return
|
|
40
|
+
setFormDisabled(Boolean(detail.disabled))
|
|
41
|
+
setFormDef(detail.disabled ? null : detail.formDefinition ?? null)
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
if (!cancelled) {
|
|
45
|
+
setFormDisabled(false)
|
|
46
|
+
setFormDef(null)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.finally(() => {
|
|
50
|
+
if (!cancelled) setLoading(false)
|
|
51
|
+
})
|
|
52
|
+
return () => {
|
|
53
|
+
cancelled = true
|
|
54
|
+
}
|
|
55
|
+
}, [formUniqueId, fetchFormDetail])
|
|
56
|
+
|
|
57
|
+
const { matchedBlocks, remainderPayload } = useMemo(
|
|
58
|
+
() => intersectPayloadWithForm(payload, formDef ?? undefined),
|
|
59
|
+
[payload, formDef],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const fieldNames = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
matchedBlocks
|
|
65
|
+
.map((block) => block.name?.trim())
|
|
66
|
+
.filter((name): name is string => Boolean(name)),
|
|
67
|
+
[matchedBlocks],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const surfaceDoc = useMemo(() => {
|
|
71
|
+
if (!formDef || matchedBlocks.length === 0) return null
|
|
72
|
+
return buildAskUserSurface(formDef, matchedBlocks)
|
|
73
|
+
}, [formDef, matchedBlocks])
|
|
74
|
+
|
|
75
|
+
if (loading) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="ask-user-form-card">
|
|
78
|
+
<Spin size="small" />
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (matchedBlocks.length === 0) {
|
|
84
|
+
return (
|
|
85
|
+
<CurlSubmitBlock
|
|
86
|
+
payload={payload}
|
|
87
|
+
callbackUrl={callbackUrl}
|
|
88
|
+
unpublishedFormId={formDisabled ? formUniqueId : undefined}
|
|
89
|
+
onNotify={onNotify}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hasRemainder = Object.keys(remainderPayload).length > 0
|
|
95
|
+
|
|
96
|
+
async function handleSubmit() {
|
|
97
|
+
const values = rendererRef.current?.getValues(fieldNames) ?? {}
|
|
98
|
+
if (submitMode === 'post') {
|
|
99
|
+
const url = (callbackUrl ?? '').trim()
|
|
100
|
+
if (!url) {
|
|
101
|
+
onNotify?.('error', 'callback_url 为空,无法提交')
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = await submitFormJson(url, values)
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
onNotify?.('success', '提交成功')
|
|
108
|
+
} else {
|
|
109
|
+
onNotify?.('error', `提交失败 (${result.status})`)
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const detail = error instanceof Error ? error.message : '提交失败'
|
|
113
|
+
onNotify?.('error', detail)
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await copyTextToClipboard(buildCurlStatement(values, callbackUrl))
|
|
120
|
+
onNotify?.('success', '已复制 curl 到剪贴板')
|
|
121
|
+
} catch {
|
|
122
|
+
onNotify?.('error', '复制失败')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="ask-user-form-card">
|
|
128
|
+
<SkoponFormRenderer
|
|
129
|
+
ref={rendererRef}
|
|
130
|
+
doc={surfaceDoc}
|
|
131
|
+
surfaceId={`ask-user-${formUniqueId}`}
|
|
132
|
+
fieldNames={fieldNames}
|
|
133
|
+
interactive
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
<div className="ask-user-form-actions">
|
|
137
|
+
<Tooltip title={submitMode === 'curl' ? '点击复制 curl' : '提交 JSON 到 callback_url'}>
|
|
138
|
+
<Button type="primary" size="small" onClick={() => void handleSubmit()}>
|
|
139
|
+
提交
|
|
140
|
+
</Button>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{hasRemainder ? (
|
|
145
|
+
<CurlSubmitBlock
|
|
146
|
+
payload={remainderPayload}
|
|
147
|
+
callbackUrl={callbackUrl}
|
|
148
|
+
title="以下字段不在表单中,请使用 curl 提交"
|
|
149
|
+
incompleteFormId={formUniqueId}
|
|
150
|
+
onNotify={onNotify}
|
|
151
|
+
/>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Button, Tag, Tooltip, Typography } from 'antd'
|
|
3
|
+
import { buildCurlStatement } from '../submit/buildCurlStatement'
|
|
4
|
+
import { copyTextToClipboard } from '../submit/submitFormJson'
|
|
5
|
+
|
|
6
|
+
export interface CurlSubmitBlockProps {
|
|
7
|
+
payload: unknown
|
|
8
|
+
callbackUrl?: string | null
|
|
9
|
+
title?: string
|
|
10
|
+
unpublishedFormId?: string | null
|
|
11
|
+
incompleteFormId?: string | null
|
|
12
|
+
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function CurlSubmitBlock({
|
|
16
|
+
payload,
|
|
17
|
+
callbackUrl,
|
|
18
|
+
title,
|
|
19
|
+
unpublishedFormId,
|
|
20
|
+
incompleteFormId,
|
|
21
|
+
onNotify,
|
|
22
|
+
}: CurlSubmitBlockProps) {
|
|
23
|
+
const curl = useMemo(
|
|
24
|
+
() => buildCurlStatement(payload, callbackUrl),
|
|
25
|
+
[payload, callbackUrl],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async function handleCopy() {
|
|
29
|
+
try {
|
|
30
|
+
await copyTextToClipboard(curl)
|
|
31
|
+
onNotify?.('success', '已复制到剪贴板')
|
|
32
|
+
} catch {
|
|
33
|
+
onNotify?.('error', '复制失败')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="ask-user-curl-card">
|
|
39
|
+
<div className="ask-user-curl-card-header">
|
|
40
|
+
<div className="ask-user-curl-card-header-title">
|
|
41
|
+
<Typography.Text type="secondary">{title ?? 'curl 命令'}</Typography.Text>
|
|
42
|
+
{unpublishedFormId ? (
|
|
43
|
+
<Tooltip title={unpublishedFormId}>
|
|
44
|
+
<Tag className="ask-user-curl-unpublished-tag">卡片 ID 未发布</Tag>
|
|
45
|
+
</Tooltip>
|
|
46
|
+
) : null}
|
|
47
|
+
{incompleteFormId ? (
|
|
48
|
+
<Tooltip title={incompleteFormId}>
|
|
49
|
+
<Tag className="ask-user-curl-incomplete-tag">卡片 ID 待补足</Tag>
|
|
50
|
+
</Tooltip>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
<Button size="small" type="text" onClick={() => void handleCopy()}>
|
|
54
|
+
复制
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
57
|
+
<pre className="skopon-form-curl-json">{curl}</pre>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
2
|
+
import { MessageProcessor, type A2uiMessage } from '@a2ui/web_core/v0_9'
|
|
3
|
+
import { injectBasicCatalogStyles } from '@a2ui/web_core/v0_9/basic_catalog'
|
|
4
|
+
import { A2uiSurface, type ReactComponentImplementation } from '@a2ui/react/v0_9'
|
|
5
|
+
import { injectStyles } from '@a2ui/react/styles'
|
|
6
|
+
import { buildSkoponCatalog } from '../catalog/a2uiCustomCatalog'
|
|
7
|
+
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
8
|
+
|
|
9
|
+
export interface SkoponA2uiStreamRendererProps {
|
|
10
|
+
/** 增量追加的 A2UI v0.9 消息 */
|
|
11
|
+
messages: A2uiMessage[]
|
|
12
|
+
surfaceId?: string
|
|
13
|
+
emptyHint?: ReactNode
|
|
14
|
+
interactive?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function SkoponA2uiStreamRenderer({
|
|
18
|
+
messages,
|
|
19
|
+
surfaceId = 'skopon-form-stream',
|
|
20
|
+
emptyHint = null,
|
|
21
|
+
interactive = true,
|
|
22
|
+
}: SkoponA2uiStreamRendererProps) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
injectStyles()
|
|
25
|
+
injectBasicCatalogStyles()
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
const processorRef = useRef<MessageProcessor<ReactComponentImplementation> | null>(null)
|
|
29
|
+
const processedCountRef = useRef(0)
|
|
30
|
+
|
|
31
|
+
const processor = useMemo(() => {
|
|
32
|
+
const p = new MessageProcessor([buildSkoponCatalog()])
|
|
33
|
+
processorRef.current = p
|
|
34
|
+
processedCountRef.current = 0
|
|
35
|
+
return p
|
|
36
|
+
}, [surfaceId])
|
|
37
|
+
|
|
38
|
+
const [surfaces, setSurfaces] = useState(() => Array.from(processor.model.surfacesMap.values()))
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const sync = () => setSurfaces(Array.from(processor.model.surfacesMap.values()))
|
|
42
|
+
const createdSub = processor.onSurfaceCreated(sync)
|
|
43
|
+
const deletedSub = processor.onSurfaceDeleted(sync)
|
|
44
|
+
return () => {
|
|
45
|
+
createdSub.unsubscribe()
|
|
46
|
+
deletedSub.unsubscribe()
|
|
47
|
+
}
|
|
48
|
+
}, [processor])
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!Array.isArray(messages) || messages.length === 0) return
|
|
52
|
+
const pending = messages.slice(processedCountRef.current)
|
|
53
|
+
if (pending.length === 0) return
|
|
54
|
+
processor.processMessages(pending)
|
|
55
|
+
processedCountRef.current = messages.length
|
|
56
|
+
setSurfaces(Array.from(processor.model.surfacesMap.values()))
|
|
57
|
+
}, [messages, processor])
|
|
58
|
+
|
|
59
|
+
if (surfaces.length === 0) return <>{emptyHint}</>
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<A2uiPreviewModeProvider interactive={interactive}>
|
|
63
|
+
<div className="a2ui-surface a2ui-container">
|
|
64
|
+
{surfaces.map((surface) => (
|
|
65
|
+
<A2uiSurface key={surface.id} surface={surface} />
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</A2uiPreviewModeProvider>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { MessageProcessor, type A2uiMessage } from '@a2ui/web_core/v0_9'
|
|
10
|
+
import { injectBasicCatalogStyles } from '@a2ui/web_core/v0_9/basic_catalog'
|
|
11
|
+
import { A2uiSurface, type ReactComponentImplementation } from '@a2ui/react/v0_9'
|
|
12
|
+
import { injectStyles } from '@a2ui/react/styles'
|
|
13
|
+
import type { A2uiSurfaceDoc } from '../types/index'
|
|
14
|
+
import { extractSurfaceValues } from '../adapter/extractSurfaceValues'
|
|
15
|
+
import { isA2uiSurfaceEmpty, surfaceDocToMessages } from '../adapter/a2uiAdapter'
|
|
16
|
+
import { SKOPON_CATALOG_ID, buildSkoponCatalog } from '../catalog/a2uiCustomCatalog'
|
|
17
|
+
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
18
|
+
|
|
19
|
+
export interface SkoponFormRendererRef {
|
|
20
|
+
getValues: (fieldNames?: string[]) => Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SkoponFormRendererProps {
|
|
24
|
+
doc: A2uiSurfaceDoc | null | undefined
|
|
25
|
+
surfaceId?: string
|
|
26
|
+
emptyHint?: ReactNode
|
|
27
|
+
interactive?: boolean
|
|
28
|
+
/** 默认用于 getValues() 的字段名列表 */
|
|
29
|
+
fieldNames?: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererProps>(
|
|
33
|
+
function SkoponFormRenderer(
|
|
34
|
+
{
|
|
35
|
+
doc,
|
|
36
|
+
surfaceId = 'skopon-form',
|
|
37
|
+
emptyHint = null,
|
|
38
|
+
interactive = true,
|
|
39
|
+
fieldNames = [],
|
|
40
|
+
},
|
|
41
|
+
ref,
|
|
42
|
+
) {
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
injectStyles()
|
|
45
|
+
injectBasicCatalogStyles()
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const processorRef = useRef<MessageProcessor<ReactComponentImplementation> | null>(null)
|
|
49
|
+
const fieldNamesRef = useRef(fieldNames)
|
|
50
|
+
fieldNamesRef.current = fieldNames
|
|
51
|
+
|
|
52
|
+
const surfaces = useMemo(() => {
|
|
53
|
+
if (isA2uiSurfaceEmpty(doc)) {
|
|
54
|
+
processorRef.current = null
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const processor = new MessageProcessor([buildSkoponCatalog()])
|
|
59
|
+
const messages = surfaceDocToMessages(doc!, {
|
|
60
|
+
surfaceId,
|
|
61
|
+
catalogId: SKOPON_CATALOG_ID,
|
|
62
|
+
}) as unknown as A2uiMessage[]
|
|
63
|
+
processor.processMessages(messages)
|
|
64
|
+
processorRef.current = processor
|
|
65
|
+
return Array.from(processor.model.surfacesMap.values())
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('[SkoponFormRenderer] 渲染 A2UI surface 失败', error)
|
|
68
|
+
processorRef.current = null
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
}, [doc, surfaceId])
|
|
72
|
+
|
|
73
|
+
useImperativeHandle(
|
|
74
|
+
ref,
|
|
75
|
+
() => ({
|
|
76
|
+
getValues(names) {
|
|
77
|
+
const processor = processorRef.current
|
|
78
|
+
if (!processor) return {}
|
|
79
|
+
const resolvedNames = (names ?? fieldNamesRef.current).map((n) => n.trim()).filter(Boolean)
|
|
80
|
+
return extractSurfaceValues(processor, surfaceId, resolvedNames)
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
[surfaceId],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if (surfaces.length === 0) return <>{emptyHint}</>
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<A2uiPreviewModeProvider interactive={interactive}>
|
|
90
|
+
<div className="a2ui-surface a2ui-container">
|
|
91
|
+
{surfaces.map((surface) => (
|
|
92
|
+
<A2uiSurface key={surface.id} surface={surface} />
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</A2uiPreviewModeProvider>
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
export default SkoponFormRenderer
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileExcelOutlined,
|
|
3
|
+
FileOutlined,
|
|
4
|
+
FileTextOutlined,
|
|
5
|
+
PictureOutlined,
|
|
6
|
+
SoundOutlined,
|
|
7
|
+
VideoCameraOutlined,
|
|
8
|
+
} from '@ant-design/icons'
|
|
9
|
+
import type { FormFilePlaceholderIcon } from '../types/index'
|
|
10
|
+
|
|
11
|
+
const PLACEHOLDER_ICON_COMPONENTS = {
|
|
12
|
+
video: VideoCameraOutlined,
|
|
13
|
+
audio: SoundOutlined,
|
|
14
|
+
image: PictureOutlined,
|
|
15
|
+
file: FileOutlined,
|
|
16
|
+
spreadsheet: FileExcelOutlined,
|
|
17
|
+
document: FileTextOutlined,
|
|
18
|
+
} satisfies Record<FormFilePlaceholderIcon, typeof FileOutlined>
|
|
19
|
+
|
|
20
|
+
interface FilePlaceholderIconProps {
|
|
21
|
+
type: FormFilePlaceholderIcon
|
|
22
|
+
size?: number
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 文件上传占位图标(@ant-design/icons,与 admin 编辑区语义对齐)。 */
|
|
27
|
+
export default function FilePlaceholderIcon({
|
|
28
|
+
type,
|
|
29
|
+
size = 40,
|
|
30
|
+
className,
|
|
31
|
+
}: FilePlaceholderIconProps) {
|
|
32
|
+
const Icon = PLACEHOLDER_ICON_COMPONENTS[type] ?? FileOutlined
|
|
33
|
+
return (
|
|
34
|
+
<Icon
|
|
35
|
+
className={className ?? 'skopon-form-file-placeholder-icon'}
|
|
36
|
+
style={{ fontSize: size, color: 'var(--color-primary, #1677ff)' }}
|
|
37
|
+
aria-hidden
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
A2uiBinding,
|
|
3
|
+
A2uiComponentName,
|
|
4
|
+
A2uiComponentNode,
|
|
5
|
+
A2uiSurfaceDoc,
|
|
6
|
+
FormBlock,
|
|
7
|
+
FormBlockOption,
|
|
8
|
+
FormBlockType,
|
|
9
|
+
FormDefinitionPayload,
|
|
10
|
+
FormDetailResult,
|
|
11
|
+
FormFilePlaceholderIcon,
|
|
12
|
+
FormJsonSchema,
|
|
13
|
+
FormMediaSize,
|
|
14
|
+
FormSchema,
|
|
15
|
+
SubmitMode,
|
|
16
|
+
} from './types/index'
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
A2UI_PROTOCOL_VERSION,
|
|
20
|
+
FORM_MEDIA_SIZES,
|
|
21
|
+
isInputBlockType,
|
|
22
|
+
isLayoutBlockType,
|
|
23
|
+
isMediaBlockType,
|
|
24
|
+
} from './types/index'
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
a2uiToBlocks,
|
|
28
|
+
blocksToA2ui,
|
|
29
|
+
isA2uiSurfaceEmpty,
|
|
30
|
+
surfaceDocToMessages,
|
|
31
|
+
type BlocksToA2uiOptions,
|
|
32
|
+
} from './adapter/a2uiAdapter'
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
buildAskUserSurface,
|
|
36
|
+
mapFormDefinitionFromDto,
|
|
37
|
+
resolveSurfaceFromFormDefinition,
|
|
38
|
+
} from './adapter/resolveSurface'
|
|
39
|
+
|
|
40
|
+
export { normalizeFormDefinition, syncFormDefinition } from './adapter/formSchema'
|
|
41
|
+
|
|
42
|
+
export { extractSurfaceValues } from './adapter/extractSurfaceValues'
|
|
43
|
+
|
|
44
|
+
export { buildCurlStatement } from './submit/buildCurlStatement'
|
|
45
|
+
export { intersectPayloadWithForm, type PayloadFormIntersection } from './submit/intersectPayloadWithForm'
|
|
46
|
+
export {
|
|
47
|
+
copyTextToClipboard,
|
|
48
|
+
submitFormJson,
|
|
49
|
+
type SubmitFormJsonOptions,
|
|
50
|
+
type SubmitFormJsonResult,
|
|
51
|
+
} from './submit/submitFormJson'
|
|
52
|
+
|
|
53
|
+
export { createFormClient, type FormClient, type FormClientOptions } from './client/formClient'
|
|
54
|
+
|
|
55
|
+
export { default as SkoponFormRenderer } from './components/SkoponFormRenderer'
|
|
56
|
+
export type { SkoponFormRendererProps, SkoponFormRendererRef } from './components/SkoponFormRenderer'
|
|
57
|
+
|
|
58
|
+
export { default as SkoponA2uiStreamRenderer } from './components/SkoponA2uiStreamRenderer'
|
|
59
|
+
export type { SkoponA2uiStreamRendererProps } from './components/SkoponA2uiStreamRenderer'
|
|
60
|
+
|
|
61
|
+
export { default as AskUserFormCard } from './components/AskUserFormCard'
|
|
62
|
+
export type { AskUserFormCardProps } from './components/AskUserFormCard'
|
|
63
|
+
|
|
64
|
+
export { default as CurlSubmitBlock } from './components/CurlSubmitBlock'
|
|
65
|
+
export type { CurlSubmitBlockProps } from './components/CurlSubmitBlock'
|
|
66
|
+
|
|
67
|
+
import './styles/index.css'
|