@skopon-cool/form-sdk 0.1.4 → 0.1.6
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/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/{case_singleselect → case_multiselect}/adapter.d.ts +2 -2
- package/dist/blocks/case_multiselect/adapter.d.ts.map +1 -0
- package/dist/blocks/case_multiselect/index.d.ts +3 -0
- package/dist/blocks/case_multiselect/index.d.ts.map +1 -0
- package/dist/catalog/resumeSearchContext.d.ts +9 -0
- package/dist/catalog/resumeSearchContext.d.ts.map +1 -1
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts +4 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +901 -852
- package/dist/submit/buildCurlStatement.d.ts +3 -1
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +37 -14
- package/src/adapter/a2uiAdapter.ts +1 -4
- package/src/adapter/formSchema.ts +49 -29
- package/src/blocks/case_multiselect/adapter.ts +90 -0
- package/src/blocks/case_multiselect/index.ts +14 -0
- package/src/blocks/registry.ts +2 -2
- package/src/catalog/resumeSearchContext.tsx +10 -0
- package/src/catalog/skoponCaseSelect.tsx +38 -13
- package/src/catalog/skoponResumeSelect.tsx +76 -10
- package/src/components/AskUserFormCard.tsx +3 -1
- package/src/components/CurlSubmitBlock.tsx +28 -7
- package/src/components/SkoponA2uiStreamRenderer.tsx +11 -4
- package/src/index.ts +6 -1
- package/src/styles/a2ui-preview.css +4 -4
- package/src/styles/index.css +148 -17
- package/src/submit/buildCurlStatement.ts +18 -23
- package/src/submit/intersectPayloadBlocksWithForm.ts +14 -2
- package/src/submit/submit.test.ts +66 -3
- package/src/types/index.ts +10 -3
- package/dist/blocks/case_singleselect/adapter.d.ts.map +0 -1
- package/dist/blocks/case_singleselect/index.d.ts +0 -3
- package/dist/blocks/case_singleselect/index.d.ts.map +0 -1
- package/src/blocks/case_singleselect/adapter.ts +0 -74
- package/src/blocks/case_singleselect/index.ts +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { Avatar, Button, Spin, Typography } from 'antd'
|
|
3
|
-
import { ReloadOutlined, UserOutlined } from '@ant-design/icons'
|
|
2
|
+
import { Avatar, Button, Spin, Tag, Typography } from 'antd'
|
|
3
|
+
import { FileImageOutlined, ReloadOutlined, UserOutlined } from '@ant-design/icons'
|
|
4
4
|
import type { ComponentContext } from '@a2ui/web_core/v0_9'
|
|
5
5
|
import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
|
|
6
6
|
import { z } from 'zod'
|
|
@@ -9,6 +9,7 @@ import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
|
9
9
|
import { asStringArray, useSkoponBoundField } from './useSkoponBoundField'
|
|
10
10
|
import {
|
|
11
11
|
type ResumeSearchItem,
|
|
12
|
+
type ResumeSearchWorkItem,
|
|
12
13
|
useResumeSearch,
|
|
13
14
|
} from './resumeSearchContext'
|
|
14
15
|
|
|
@@ -54,6 +55,60 @@ const SkoponResumeSelectApi = {
|
|
|
54
55
|
.passthrough(),
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const MAX_TAG_COUNT = 3
|
|
59
|
+
const MAX_WORK_PREVIEW = 2
|
|
60
|
+
|
|
61
|
+
function renderTagRow(label: string, tags: string[] | undefined) {
|
|
62
|
+
const items = (tags ?? []).filter(Boolean)
|
|
63
|
+
const visible = items.slice(0, MAX_TAG_COUNT)
|
|
64
|
+
const overflow = items.length - visible.length
|
|
65
|
+
return (
|
|
66
|
+
<div className="skopon-resume-select-card-tags">
|
|
67
|
+
<span className="skopon-resume-select-card-tags-label">{label}</span>
|
|
68
|
+
{items.length > 0 ? (
|
|
69
|
+
<div className="skopon-resume-select-card-tags-list">
|
|
70
|
+
{visible.map((tag) => (
|
|
71
|
+
<Tag key={tag} className="skopon-resume-select-card-tag">
|
|
72
|
+
{tag}
|
|
73
|
+
</Tag>
|
|
74
|
+
))}
|
|
75
|
+
{overflow > 0 ? (
|
|
76
|
+
<Tag className="skopon-resume-select-card-tag">+{overflow}</Tag>
|
|
77
|
+
) : null}
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
<span className="skopon-resume-select-card-tags-empty">暂无</span>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderSatisfactionRow(satisfaction: number | undefined) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="skopon-resume-select-card-field">
|
|
89
|
+
<span className="skopon-resume-select-card-tags-label">满意度</span>
|
|
90
|
+
<span className="skopon-resume-select-card-field-value">
|
|
91
|
+
{typeof satisfaction === 'number' ? `${satisfaction}%` : '暂无'}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ResumeWorkPreview({ work }: { work: ResumeSearchWorkItem }) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="skopon-resume-select-card-work">
|
|
100
|
+
<div className="skopon-resume-select-card-work-cover">
|
|
101
|
+
{work.coverUrl ? (
|
|
102
|
+
<img src={work.coverUrl} alt="" className="skopon-resume-select-card-work-img" />
|
|
103
|
+
) : (
|
|
104
|
+
<FileImageOutlined className="skopon-resume-select-card-work-placeholder" />
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="skopon-resume-select-card-work-title">{work.title || '未命名作品'}</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
function ResumeCard({
|
|
58
113
|
item,
|
|
59
114
|
selected,
|
|
@@ -65,10 +120,12 @@ function ResumeCard({
|
|
|
65
120
|
disabled: boolean
|
|
66
121
|
onToggle: () => void
|
|
67
122
|
}) {
|
|
123
|
+
const works = (item.works ?? []).slice(0, MAX_WORK_PREVIEW)
|
|
124
|
+
|
|
68
125
|
return (
|
|
69
126
|
<button
|
|
70
127
|
type="button"
|
|
71
|
-
className={`skopon-resume-select-card${selected ? ' skopon-resume-select-card--selected' : ''}`}
|
|
128
|
+
className={`skopon-resume-select-card skopon-resume-select-card--profile${selected ? ' skopon-resume-select-card--selected' : ''}`}
|
|
72
129
|
disabled={disabled}
|
|
73
130
|
onClick={onToggle}
|
|
74
131
|
>
|
|
@@ -78,14 +135,23 @@ function ResumeCard({
|
|
|
78
135
|
icon={!item.avatarUrl ? <UserOutlined /> : undefined}
|
|
79
136
|
className="skopon-resume-select-card-avatar"
|
|
80
137
|
/>
|
|
138
|
+
<div className="skopon-resume-select-card-name">{item.name || '未命名简历'}</div>
|
|
81
139
|
<div className="skopon-resume-select-card-body">
|
|
82
|
-
|
|
83
|
-
{item.
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
<div className="skopon-resume-select-card-
|
|
87
|
-
|
|
88
|
-
{
|
|
140
|
+
{renderSatisfactionRow(item.satisfaction)}
|
|
141
|
+
{renderTagRow('性格', item.personality)}
|
|
142
|
+
{renderTagRow('专长', item.specialties)}
|
|
143
|
+
{renderTagRow('擅长领域', item.traits)}
|
|
144
|
+
<div className="skopon-resume-select-card-works">
|
|
145
|
+
<span className="skopon-resume-select-card-works-label">过往作品</span>
|
|
146
|
+
{works.length > 0 ? (
|
|
147
|
+
<div className="skopon-resume-select-card-works-list">
|
|
148
|
+
{works.map((work, index) => (
|
|
149
|
+
<ResumeWorkPreview key={`${work.title}-${index}`} work={work} />
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
<div className="skopon-resume-select-card-works-empty">暂无作品</div>
|
|
154
|
+
)}
|
|
89
155
|
</div>
|
|
90
156
|
</div>
|
|
91
157
|
</button>
|
|
@@ -120,10 +120,12 @@ export default function AskUserFormCard({
|
|
|
120
120
|
Boolean(payloadDef && payloadHasInputBlocks(payloadDef) && matchedBlocks.length === 0)
|
|
121
121
|
|
|
122
122
|
const renderBlocks = useMemo(() => {
|
|
123
|
+
if (usePayloadFallback && payloadDef) {
|
|
124
|
+
return getPayloadRenderableBlocks(payloadDef)
|
|
125
|
+
}
|
|
123
126
|
if (intersection && intersection.renderBlocks.length > 0) {
|
|
124
127
|
return intersection.renderBlocks
|
|
125
128
|
}
|
|
126
|
-
if (usePayloadFallback && payloadDef) return getPayloadRenderableBlocks(payloadDef)
|
|
127
129
|
return []
|
|
128
130
|
}, [intersection, usePayloadFallback, payloadDef])
|
|
129
131
|
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { Button, Tag, Tooltip, Typography } from 'antd'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildAskUserSubmitCurlFromPayload,
|
|
5
|
+
buildCurlStatement,
|
|
6
|
+
} from '../submit/buildCurlStatement'
|
|
4
7
|
import { copyTextToClipboard } from '../submit/submitFormJson'
|
|
5
8
|
|
|
9
|
+
export type CurlSubmitBlockCopyMode = 'display' | 'submitFields'
|
|
10
|
+
|
|
6
11
|
export interface CurlSubmitBlockProps {
|
|
7
12
|
payload: unknown
|
|
8
13
|
callbackUrl?: string | null
|
|
9
14
|
title?: string
|
|
10
15
|
unpublishedFormId?: string | null
|
|
11
16
|
incompleteFormId?: string | null
|
|
17
|
+
/** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
|
|
18
|
+
copyMode?: CurlSubmitBlockCopyMode
|
|
12
19
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -18,22 +25,34 @@ export default function CurlSubmitBlock({
|
|
|
18
25
|
title,
|
|
19
26
|
unpublishedFormId,
|
|
20
27
|
incompleteFormId,
|
|
28
|
+
copyMode = 'display',
|
|
21
29
|
onNotify,
|
|
22
30
|
}: CurlSubmitBlockProps) {
|
|
23
|
-
const
|
|
31
|
+
const displayCurl = useMemo(
|
|
24
32
|
() => buildCurlStatement(payload, callbackUrl),
|
|
25
33
|
[payload, callbackUrl],
|
|
26
34
|
)
|
|
35
|
+
const copyCurl = useMemo(() => {
|
|
36
|
+
if (copyMode === 'submitFields') {
|
|
37
|
+
return buildAskUserSubmitCurlFromPayload(payload, callbackUrl)
|
|
38
|
+
}
|
|
39
|
+
return displayCurl
|
|
40
|
+
}, [copyMode, payload, callbackUrl, displayCurl])
|
|
27
41
|
|
|
28
42
|
async function handleCopy() {
|
|
29
43
|
try {
|
|
30
|
-
await copyTextToClipboard(
|
|
44
|
+
await copyTextToClipboard(copyCurl)
|
|
31
45
|
onNotify?.('success', '已复制到剪贴板')
|
|
32
46
|
} catch {
|
|
33
47
|
onNotify?.('error', '复制失败')
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
|
|
51
|
+
const copyTooltip =
|
|
52
|
+
copyMode === 'submitFields'
|
|
53
|
+
? '复制提交字段 curl(向 callback 回传)'
|
|
54
|
+
: '复制 curl'
|
|
55
|
+
|
|
37
56
|
return (
|
|
38
57
|
<div className="ask-user-curl-card">
|
|
39
58
|
<div className="ask-user-curl-card-header">
|
|
@@ -50,11 +69,13 @@ export default function CurlSubmitBlock({
|
|
|
50
69
|
</Tooltip>
|
|
51
70
|
) : null}
|
|
52
71
|
</div>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
<Tooltip title={copyTooltip}>
|
|
73
|
+
<Button size="small" type="text" onClick={() => void handleCopy()}>
|
|
74
|
+
复制
|
|
75
|
+
</Button>
|
|
76
|
+
</Tooltip>
|
|
56
77
|
</div>
|
|
57
|
-
<pre className="skopon-form-curl-json">{
|
|
78
|
+
<pre className="skopon-form-curl-json">{displayCurl}</pre>
|
|
58
79
|
</div>
|
|
59
80
|
)
|
|
60
81
|
}
|
|
@@ -37,7 +37,10 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
37
37
|
injectBasicCatalogStyles()
|
|
38
38
|
}, [])
|
|
39
39
|
|
|
40
|
-
const processorRef = useRef<MessageProcessor<ReactComponentImplementation
|
|
40
|
+
const processorRef = useRef<MessageProcessor<ReactComponentImplementation> | null>(null)
|
|
41
|
+
if (processorRef.current === null) {
|
|
42
|
+
processorRef.current = createProcessor()
|
|
43
|
+
}
|
|
41
44
|
const processedCountRef = useRef(0)
|
|
42
45
|
const lastProcessedMessagesRef = useRef('')
|
|
43
46
|
const mountedRef = useRef(true)
|
|
@@ -68,9 +71,13 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
const [processor, setProcessor] = useState(
|
|
74
|
+
const [processor, setProcessor] = useState(
|
|
75
|
+
() => processorRef.current as MessageProcessor<ReactComponentImplementation>,
|
|
76
|
+
)
|
|
72
77
|
const [surfaces, setSurfaces] = useState(() =>
|
|
73
|
-
Array.from(
|
|
78
|
+
Array.from(
|
|
79
|
+
(processorRef.current as MessageProcessor<ReactComponentImplementation>).model.surfacesMap.values(),
|
|
80
|
+
),
|
|
74
81
|
)
|
|
75
82
|
|
|
76
83
|
useEffect(() => {
|
|
@@ -137,7 +144,7 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
const activeProcessor = processorRef.current
|
|
147
|
+
const activeProcessor = processorRef.current as MessageProcessor<ReactComponentImplementation>
|
|
141
148
|
const processedCount = processedCountRef.current
|
|
142
149
|
|
|
143
150
|
if (
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,12 @@ export { normalizeFormDefinition, syncFormDefinition } from './adapter/formSchem
|
|
|
41
41
|
|
|
42
42
|
export { extractSurfaceValues } from './adapter/extractSurfaceValues'
|
|
43
43
|
|
|
44
|
-
export {
|
|
44
|
+
export {
|
|
45
|
+
buildCurlStatement,
|
|
46
|
+
buildAskUserCurlStatement,
|
|
47
|
+
buildAskUserCurlBodyJson,
|
|
48
|
+
buildAskUserSubmitCurlFromPayload,
|
|
49
|
+
} from './submit/buildCurlStatement'
|
|
45
50
|
export {
|
|
46
51
|
extractExtraBlockValues,
|
|
47
52
|
getPayloadInputFieldNames,
|
|
@@ -254,8 +254,8 @@
|
|
|
254
254
|
box-shadow: var(--shadow-focus);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/* 原生 A2UI 按钮/chip;勿作用于 Ant Design(Switch、Select、Picker
|
|
258
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']) {
|
|
257
|
+
/* 原生 A2UI 按钮/chip;勿作用于 Ant Design(Switch、Select、Picker 等)及 Skopon 简历/案例卡片 */
|
|
258
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card) {
|
|
259
259
|
min-height: var(--button-height-md);
|
|
260
260
|
padding: 0 var(--button-padding-x-md);
|
|
261
261
|
font-family: var(--font-sans);
|
|
@@ -273,12 +273,12 @@
|
|
|
273
273
|
color var(--duration-fast) var(--ease-default);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']):hover:not(:disabled) {
|
|
276
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card):hover:not(:disabled) {
|
|
277
277
|
border-color: var(--color-border-strong);
|
|
278
278
|
background: var(--color-bg-subtle);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
.a2ui-surface.a2ui-container button:not([class*='ant-']):disabled {
|
|
281
|
+
.a2ui-surface.a2ui-container button:not([class*='ant-']):not(.skopon-resume-select-card):disabled {
|
|
282
282
|
cursor: not-allowed;
|
|
283
283
|
opacity: 0.45;
|
|
284
284
|
}
|
package/src/styles/index.css
CHANGED
|
@@ -196,21 +196,58 @@
|
|
|
196
196
|
|
|
197
197
|
.skopon-resume-select-list {
|
|
198
198
|
display: flex;
|
|
199
|
-
flex-direction:
|
|
200
|
-
|
|
199
|
+
flex-direction: row;
|
|
200
|
+
flex-wrap: nowrap;
|
|
201
|
+
gap: var(--space-3, 12px);
|
|
201
202
|
margin-top: var(--space-2, 8px);
|
|
203
|
+
overflow-x: auto;
|
|
204
|
+
padding-bottom: var(--space-1, 4px);
|
|
205
|
+
-webkit-overflow-scrolling: touch;
|
|
202
206
|
}
|
|
203
207
|
.skopon-resume-select-card {
|
|
204
208
|
display: flex;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
width: 100%;
|
|
209
|
+
flex: 0 0 auto;
|
|
210
|
+
box-sizing: border-box;
|
|
208
211
|
padding: 12px;
|
|
209
212
|
border: 1px solid var(--color-border, #d9d9d9);
|
|
210
|
-
border-radius:
|
|
213
|
+
border-radius: 4px;
|
|
211
214
|
background: var(--color-surface, #fff);
|
|
212
215
|
text-align: left;
|
|
213
216
|
cursor: pointer;
|
|
217
|
+
min-height: unset;
|
|
218
|
+
font-weight: inherit;
|
|
219
|
+
line-height: inherit;
|
|
220
|
+
appearance: none;
|
|
221
|
+
-webkit-appearance: none;
|
|
222
|
+
}
|
|
223
|
+
.a2ui-surface.a2ui-container button.skopon-resume-select-card {
|
|
224
|
+
border-radius: 4px;
|
|
225
|
+
min-height: unset;
|
|
226
|
+
padding: 12px;
|
|
227
|
+
font-weight: inherit;
|
|
228
|
+
}
|
|
229
|
+
.skopon-resume-select-card--profile {
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
align-items: center;
|
|
232
|
+
width: 200px;
|
|
233
|
+
min-width: 200px;
|
|
234
|
+
max-width: 200px;
|
|
235
|
+
height: 320px;
|
|
236
|
+
min-height: 320px;
|
|
237
|
+
max-height: 320px;
|
|
238
|
+
padding: 10px;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
}
|
|
241
|
+
.a2ui-surface.a2ui-container button.skopon-resume-select-card--profile {
|
|
242
|
+
padding: 10px;
|
|
243
|
+
min-height: 320px;
|
|
244
|
+
}
|
|
245
|
+
.skopon-resume-select-card:not(.skopon-resume-select-card--profile) {
|
|
246
|
+
flex-direction: row;
|
|
247
|
+
align-items: flex-start;
|
|
248
|
+
gap: 12px;
|
|
249
|
+
width: 220px;
|
|
250
|
+
min-width: 220px;
|
|
214
251
|
}
|
|
215
252
|
.skopon-resume-select-card:hover:not(:disabled) {
|
|
216
253
|
border-color: var(--color-primary, #1677ff);
|
|
@@ -223,27 +260,121 @@
|
|
|
223
260
|
cursor: not-allowed;
|
|
224
261
|
opacity: 0.72;
|
|
225
262
|
}
|
|
226
|
-
.skopon-resume-select-card-
|
|
227
|
-
flex:
|
|
228
|
-
|
|
263
|
+
.skopon-resume-select-card-avatar {
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
}
|
|
266
|
+
.skopon-resume-select-card--profile .skopon-resume-select-card-avatar {
|
|
267
|
+
margin-bottom: 6px;
|
|
229
268
|
}
|
|
230
269
|
.skopon-resume-select-card-name {
|
|
231
270
|
font-weight: 600;
|
|
271
|
+
font-size: 14px;
|
|
272
|
+
line-height: 1.3;
|
|
273
|
+
margin-bottom: 6px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
text-overflow: ellipsis;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
278
|
+
}
|
|
279
|
+
.skopon-resume-select-card--profile > .skopon-resume-select-card-name {
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
width: 100%;
|
|
282
|
+
text-align: center;
|
|
283
|
+
}
|
|
284
|
+
.skopon-resume-select-card-body {
|
|
285
|
+
width: 100%;
|
|
286
|
+
min-width: 0;
|
|
287
|
+
text-align: left;
|
|
288
|
+
}
|
|
289
|
+
.skopon-resume-select-card--profile .skopon-resume-select-card-body {
|
|
290
|
+
flex: 1;
|
|
291
|
+
min-height: 0;
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
overflow-x: hidden;
|
|
295
|
+
overflow-y: auto;
|
|
296
|
+
}
|
|
297
|
+
.skopon-resume-select-card-field {
|
|
232
298
|
margin-bottom: 4px;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
}
|
|
301
|
+
.skopon-resume-select-card-field-value {
|
|
302
|
+
font-size: 12px;
|
|
303
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
233
304
|
}
|
|
234
|
-
.skopon-resume-select-card-
|
|
235
|
-
|
|
236
|
-
|
|
305
|
+
.skopon-resume-select-card-tags-empty {
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
308
|
+
}
|
|
309
|
+
.skopon-resume-select-card-tags {
|
|
237
310
|
margin-bottom: 4px;
|
|
311
|
+
flex-shrink: 0;
|
|
312
|
+
}
|
|
313
|
+
.skopon-resume-select-card-tags-label,
|
|
314
|
+
.skopon-resume-select-card-works-label {
|
|
315
|
+
display: block;
|
|
316
|
+
margin-bottom: 2px;
|
|
317
|
+
font-size: 11px;
|
|
318
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
319
|
+
}
|
|
320
|
+
.skopon-resume-select-card-tags-list {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
gap: 4px;
|
|
324
|
+
}
|
|
325
|
+
.skopon-resume-select-card-tag {
|
|
326
|
+
margin: 0;
|
|
327
|
+
font-size: 11px;
|
|
328
|
+
line-height: 18px;
|
|
329
|
+
}
|
|
330
|
+
.skopon-resume-select-card-works {
|
|
331
|
+
margin-top: 4px;
|
|
332
|
+
flex-shrink: 0;
|
|
333
|
+
}
|
|
334
|
+
.skopon-resume-select-card-works-list {
|
|
335
|
+
display: flex;
|
|
336
|
+
flex-direction: column;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
}
|
|
339
|
+
.skopon-resume-select-card-works-empty {
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
342
|
+
}
|
|
343
|
+
.skopon-resume-select-card-work {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
gap: 6px;
|
|
347
|
+
min-width: 0;
|
|
348
|
+
}
|
|
349
|
+
.skopon-resume-select-card-work-cover {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
flex-shrink: 0;
|
|
354
|
+
width: 32px;
|
|
355
|
+
height: 32px;
|
|
356
|
+
border-radius: 2px;
|
|
357
|
+
border: 1px solid var(--color-border, #d9d9d9);
|
|
358
|
+
background: var(--color-bg-subtle, rgba(0, 0, 0, 0.02));
|
|
238
359
|
overflow: hidden;
|
|
239
|
-
text-overflow: ellipsis;
|
|
240
|
-
display: -webkit-box;
|
|
241
|
-
-webkit-line-clamp: 2;
|
|
242
|
-
-webkit-box-orient: vertical;
|
|
243
360
|
}
|
|
244
|
-
.skopon-resume-select-card-
|
|
361
|
+
.skopon-resume-select-card-work-img {
|
|
362
|
+
width: 100%;
|
|
363
|
+
height: 100%;
|
|
364
|
+
object-fit: cover;
|
|
365
|
+
}
|
|
366
|
+
.skopon-resume-select-card-work-placeholder {
|
|
367
|
+
font-size: 16px;
|
|
245
368
|
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
369
|
+
}
|
|
370
|
+
.skopon-resume-select-card-work-title {
|
|
371
|
+
flex: 1;
|
|
372
|
+
min-width: 0;
|
|
246
373
|
font-size: 12px;
|
|
374
|
+
color: var(--color-text, rgba(0, 0, 0, 0.88));
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
text-overflow: ellipsis;
|
|
377
|
+
white-space: nowrap;
|
|
247
378
|
}
|
|
248
379
|
.skopon-resume-select-actions {
|
|
249
380
|
margin-top: var(--space-2, 8px);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractExtraBlockValues,
|
|
3
|
+
parsePayloadBlocksJson,
|
|
4
|
+
} from './intersectPayloadBlocksWithForm'
|
|
5
|
+
|
|
1
6
|
function escapeSingleQuotes(text: string): string {
|
|
2
7
|
return text.replace(/'/g, `'\\''`)
|
|
3
8
|
}
|
|
@@ -12,33 +17,12 @@ export function buildCurlStatement(payload: unknown, callbackUrl?: string | null
|
|
|
12
17
|
].join('\n')
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
return ` ${JSON.stringify(key)}: ${JSON.stringify(value)}${trailingComma ? ',' : ''}`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
|
|
20
|
+
/** 构建合法 JSON body(card + extra 合并,便于 curl 直接执行) */
|
|
20
21
|
export function buildAskUserCurlBodyJson(
|
|
21
22
|
cardValues: Record<string, unknown>,
|
|
22
23
|
extraValues: Record<string, unknown>,
|
|
23
24
|
): string {
|
|
24
|
-
|
|
25
|
-
const cardEntries = Object.entries(cardValues)
|
|
26
|
-
const extraEntries = Object.entries(extraValues)
|
|
27
|
-
|
|
28
|
-
cardEntries.forEach(([key, value], index) => {
|
|
29
|
-
const hasMore = index < cardEntries.length - 1 || extraEntries.length > 0
|
|
30
|
-
lines.push(formatJsonBodyLine(key, value, hasMore))
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
if (extraEntries.length > 0) {
|
|
34
|
-
lines.push(' // 额外字段(未在卡片展示)')
|
|
35
|
-
extraEntries.forEach(([key, value], index) => {
|
|
36
|
-
lines.push(formatJsonBodyLine(key, value, index < extraEntries.length - 1))
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
lines.push('}')
|
|
41
|
-
return lines.join('\n')
|
|
25
|
+
return JSON.stringify({ ...cardValues, ...extraValues }, null, 2)
|
|
42
26
|
}
|
|
43
27
|
|
|
44
28
|
export interface BuildAskUserCurlStatementOptions {
|
|
@@ -60,3 +44,14 @@ export function buildAskUserCurlStatement({
|
|
|
60
44
|
` -d '${escapeSingleQuotes(body)}'`,
|
|
61
45
|
].join('\n')
|
|
62
46
|
}
|
|
47
|
+
|
|
48
|
+
/** 从 ask_user payload(blocksJson)生成向 callback 提交返回字段的 curl */
|
|
49
|
+
export function buildAskUserSubmitCurlFromPayload(
|
|
50
|
+
payload: unknown,
|
|
51
|
+
callbackUrl?: string | null,
|
|
52
|
+
): string {
|
|
53
|
+
const def = parsePayloadBlocksJson(payload)
|
|
54
|
+
if (!def) return buildCurlStatement(payload, callbackUrl)
|
|
55
|
+
const cardValues = extractExtraBlockValues(def.blocks)
|
|
56
|
+
return buildAskUserCurlStatement({ cardValues, callbackUrl })
|
|
57
|
+
}
|
|
@@ -127,7 +127,12 @@ function blockValueFromDefault(block: FormBlock): unknown {
|
|
|
127
127
|
const { type, defaultValue } = block
|
|
128
128
|
if (defaultValue !== undefined) {
|
|
129
129
|
if (type === 'toggle') return defaultValue === true || defaultValue === 'true'
|
|
130
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
type === 'multiselect' ||
|
|
132
|
+
type === 'checkbox' ||
|
|
133
|
+
type === 'resume_multiselect' ||
|
|
134
|
+
type === 'case_multiselect'
|
|
135
|
+
) {
|
|
131
136
|
if (Array.isArray(defaultValue)) return defaultValue.map(String)
|
|
132
137
|
if (typeof defaultValue === 'string' && defaultValue) return [defaultValue]
|
|
133
138
|
return []
|
|
@@ -135,7 +140,14 @@ function blockValueFromDefault(block: FormBlock): unknown {
|
|
|
135
140
|
return defaultValue
|
|
136
141
|
}
|
|
137
142
|
if (type === 'toggle') return false
|
|
138
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
type === 'multiselect' ||
|
|
145
|
+
type === 'checkbox' ||
|
|
146
|
+
type === 'resume_multiselect' ||
|
|
147
|
+
type === 'case_multiselect'
|
|
148
|
+
) {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
139
151
|
if (type === 'number') return null
|
|
140
152
|
return ''
|
|
141
153
|
}
|
|
@@ -5,6 +5,7 @@ import { describe, it, expect, vi } from 'vitest'
|
|
|
5
5
|
import {
|
|
6
6
|
buildAskUserCurlBodyJson,
|
|
7
7
|
buildAskUserCurlStatement,
|
|
8
|
+
buildAskUserSubmitCurlFromPayload,
|
|
8
9
|
buildCurlStatement,
|
|
9
10
|
} from '../submit/buildCurlStatement'
|
|
10
11
|
import {
|
|
@@ -31,21 +32,57 @@ describe('buildCurlStatement', () => {
|
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
describe('buildAskUserCurlStatement', () => {
|
|
34
|
-
it('
|
|
35
|
+
it('merges card and extra values into valid JSON', () => {
|
|
35
36
|
const body = buildAskUserCurlBodyJson(
|
|
36
37
|
{ video_title: 'hello' },
|
|
37
38
|
{ agent_hint: '请尽快确认' },
|
|
38
39
|
)
|
|
39
40
|
expect(body).toContain('"video_title": "hello"')
|
|
40
|
-
expect(body).toContain('// 额外字段(未在卡片展示)')
|
|
41
41
|
expect(body).toContain('"agent_hint": "请尽快确认"')
|
|
42
|
+
expect(body).not.toContain('// 额外字段')
|
|
43
|
+
expect(JSON.parse(body)).toEqual({
|
|
44
|
+
video_title: 'hello',
|
|
45
|
+
agent_hint: '请尽快确认',
|
|
46
|
+
})
|
|
42
47
|
|
|
43
48
|
const curl = buildAskUserCurlStatement({
|
|
44
49
|
cardValues: { video_title: 'hello' },
|
|
45
50
|
extraValues: { agent_hint: '请尽快确认' },
|
|
46
51
|
callbackUrl: 'https://example.com/hook',
|
|
47
52
|
})
|
|
48
|
-
expect(curl).toContain('
|
|
53
|
+
expect(curl).toContain('"agent_hint": "请尽快确认"')
|
|
54
|
+
expect(curl).not.toContain('// 额外字段')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('buildAskUserSubmitCurlFromPayload', () => {
|
|
59
|
+
it('builds submit-fields curl from blocksJson payload', () => {
|
|
60
|
+
const payload = {
|
|
61
|
+
title: 'Ask',
|
|
62
|
+
blocks: [
|
|
63
|
+
{ id: 'p1', type: 'text', name: 'video_title', label: '标题', defaultValue: 'hello' },
|
|
64
|
+
{ id: 'p2', type: 'text', name: 'agent_hint', label: '提示', defaultValue: 'hint' },
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
const curl = buildAskUserSubmitCurlFromPayload(payload, 'https://example.com/hook')
|
|
68
|
+
expect(curl).toContain("curl -X POST 'https://example.com/hook'")
|
|
69
|
+
expect(curl).toContain('"video_title": "hello"')
|
|
70
|
+
expect(curl).toContain('"agent_hint": "hint"')
|
|
71
|
+
const bodyMatch = curl.match(/-d '([\s\S]*)'$/)
|
|
72
|
+
expect(bodyMatch).toBeTruthy()
|
|
73
|
+
const bodyText = bodyMatch![1]!.replace(/'\\''/g, "'")
|
|
74
|
+
expect(JSON.parse(bodyText)).toEqual({
|
|
75
|
+
video_title: 'hello',
|
|
76
|
+
agent_hint: 'hint',
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('falls back to display curl when payload is not blocksJson', () => {
|
|
81
|
+
const payload = { video_title: { type: 'text' } }
|
|
82
|
+
const callbackUrl = 'https://example.com/hook'
|
|
83
|
+
expect(buildAskUserSubmitCurlFromPayload(payload, callbackUrl)).toBe(
|
|
84
|
+
buildCurlStatement(payload, callbackUrl),
|
|
85
|
+
)
|
|
49
86
|
})
|
|
50
87
|
})
|
|
51
88
|
|
|
@@ -96,6 +133,18 @@ describe('intersectPayloadBlocksWithForm', () => {
|
|
|
96
133
|
expect(extractExtraBlockValues(result.extraBlocks)).toEqual({ agent_hint: 'hint' })
|
|
97
134
|
})
|
|
98
135
|
|
|
136
|
+
it('defaults resume_multiselect extra blocks to empty array', () => {
|
|
137
|
+
const extraBlocks = [
|
|
138
|
+
{
|
|
139
|
+
id: '1',
|
|
140
|
+
type: 'resume_multiselect' as const,
|
|
141
|
+
name: 'resumes',
|
|
142
|
+
label: 'Resumes',
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
expect(extractExtraBlockValues(extraBlocks)).toEqual({ resumes: [] })
|
|
146
|
+
})
|
|
147
|
+
|
|
99
148
|
it('keeps form block type and schema when payload type differs', () => {
|
|
100
149
|
const selectForm: FormSchema = {
|
|
101
150
|
title: 'Form',
|
|
@@ -195,6 +244,20 @@ describe('payload fallback helpers', () => {
|
|
|
195
244
|
expect(payloadHasInputBlocks(surveyPayload)).toBe(true)
|
|
196
245
|
expect(getPayloadRenderableBlocks(surveyPayload).length).toBeGreaterThan(0)
|
|
197
246
|
})
|
|
247
|
+
|
|
248
|
+
it('prefers payload fallback blocks when intersection only has layout blocks', () => {
|
|
249
|
+
const result = intersectPayloadBlocksWithForm(surveyPayload, undefined)
|
|
250
|
+
const usePayloadFallback =
|
|
251
|
+
payloadHasInputBlocks(surveyPayload) && result.matchedBlocks.length === 0
|
|
252
|
+
expect(usePayloadFallback).toBe(true)
|
|
253
|
+
expect(result.renderBlocks.map((b) => b.type)).toEqual(['heading'])
|
|
254
|
+
|
|
255
|
+
const renderBlocks = usePayloadFallback
|
|
256
|
+
? getPayloadRenderableBlocks(surveyPayload)
|
|
257
|
+
: result.renderBlocks
|
|
258
|
+
expect(renderBlocks.map((b) => b.id)).toEqual(['title', 'q1'])
|
|
259
|
+
expect(getPayloadInputFieldNames(surveyPayload)).toEqual(['occupation'])
|
|
260
|
+
})
|
|
198
261
|
})
|
|
199
262
|
|
|
200
263
|
describe('submitFormJson', () => {
|