@skopon-cool/form-sdk 0.1.4 → 0.1.5

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 (42) hide show
  1. package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
  2. package/dist/adapter/formSchema.d.ts.map +1 -1
  3. package/dist/blocks/{case_singleselect → case_multiselect}/adapter.d.ts +2 -2
  4. package/dist/blocks/case_multiselect/adapter.d.ts.map +1 -0
  5. package/dist/blocks/case_multiselect/index.d.ts +3 -0
  6. package/dist/blocks/case_multiselect/index.d.ts.map +1 -0
  7. package/dist/catalog/resumeSearchContext.d.ts +9 -0
  8. package/dist/catalog/resumeSearchContext.d.ts.map +1 -1
  9. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
  10. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
  11. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  12. package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
  13. package/dist/form-sdk.css +1 -1
  14. package/dist/index.js +932 -892
  15. package/dist/submit/buildCurlStatement.d.ts +1 -1
  16. package/dist/submit/buildCurlStatement.d.ts.map +1 -1
  17. package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
  18. package/dist/types/index.d.ts +11 -4
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/adapter/a2uiAdapter.test.ts +37 -14
  22. package/src/adapter/a2uiAdapter.ts +1 -4
  23. package/src/adapter/formSchema.ts +49 -29
  24. package/src/blocks/case_multiselect/adapter.ts +90 -0
  25. package/src/blocks/case_multiselect/index.ts +14 -0
  26. package/src/blocks/registry.ts +2 -2
  27. package/src/catalog/resumeSearchContext.tsx +10 -0
  28. package/src/catalog/skoponCaseSelect.tsx +38 -13
  29. package/src/catalog/skoponResumeSelect.tsx +76 -10
  30. package/src/components/AskUserFormCard.tsx +3 -1
  31. package/src/components/SkoponA2uiStreamRenderer.tsx +11 -4
  32. package/src/styles/a2ui-preview.css +4 -4
  33. package/src/styles/index.css +148 -17
  34. package/src/submit/buildCurlStatement.ts +2 -23
  35. package/src/submit/intersectPayloadBlocksWithForm.ts +14 -2
  36. package/src/submit/submit.test.ts +34 -3
  37. package/src/types/index.ts +10 -3
  38. package/dist/blocks/case_singleselect/adapter.d.ts.map +0 -1
  39. package/dist/blocks/case_singleselect/index.d.ts +0 -3
  40. package/dist/blocks/case_singleselect/index.d.ts.map +0 -1
  41. package/src/blocks/case_singleselect/adapter.ts +0 -74
  42. 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
- <div className="skopon-resume-select-card-name">{item.name}</div>
83
- {item.description ? (
84
- <div className="skopon-resume-select-card-desc">{item.description}</div>
85
- ) : null}
86
- <div className="skopon-resume-select-card-meta">
87
- {item.resumeUniqueId}
88
- {typeof item.satisfaction === 'number' ? ` · 满意度 ${item.satisfaction}%` : ''}
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
 
@@ -37,7 +37,10 @@ export default function SkoponA2uiStreamRenderer({
37
37
  injectBasicCatalogStyles()
38
38
  }, [])
39
39
 
40
- const processorRef = useRef<MessageProcessor<ReactComponentImplementation>>(createProcessor())
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(() => processorRef.current)
74
+ const [processor, setProcessor] = useState(
75
+ () => processorRef.current as MessageProcessor<ReactComponentImplementation>,
76
+ )
72
77
  const [surfaces, setSurfaces] = useState(() =>
73
- Array.from(processorRef.current.model.surfacesMap.values()),
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 (
@@ -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
  }
@@ -196,21 +196,58 @@
196
196
 
197
197
  .skopon-resume-select-list {
198
198
  display: flex;
199
- flex-direction: column;
200
- gap: var(--space-2, 8px);
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
- align-items: flex-start;
206
- gap: 12px;
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: var(--radius-md, 8px);
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-body {
227
- flex: 1;
228
- min-width: 0;
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-desc {
235
- color: var(--color-text-secondary, rgba(0, 0, 0, 0.65));
236
- font-size: 13px;
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-meta {
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);
@@ -12,33 +12,12 @@ export function buildCurlStatement(payload: unknown, callbackUrl?: string | null
12
12
  ].join('\n')
13
13
  }
14
14
 
15
- function formatJsonBodyLine(key: string, value: unknown, trailingComma: boolean): string {
16
- return ` ${JSON.stringify(key)}: ${JSON.stringify(value)}${trailingComma ? ',' : ''}`
17
- }
18
-
19
- /** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
15
+ /** 构建合法 JSON body(card + extra 合并,便于 curl 直接执行) */
20
16
  export function buildAskUserCurlBodyJson(
21
17
  cardValues: Record<string, unknown>,
22
18
  extraValues: Record<string, unknown>,
23
19
  ): string {
24
- const lines: string[] = ['{']
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')
20
+ return JSON.stringify({ ...cardValues, ...extraValues }, null, 2)
42
21
  }
43
22
 
44
23
  export interface BuildAskUserCurlStatementOptions {
@@ -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 (type === 'multiselect' || type === 'checkbox') {
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 (type === 'multiselect' || type === 'checkbox') return []
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
  }
@@ -31,21 +31,26 @@ describe('buildCurlStatement', () => {
31
31
  })
32
32
 
33
33
  describe('buildAskUserCurlStatement', () => {
34
- it('includes extra fields with comment after card values', () => {
34
+ it('merges card and extra values into valid JSON', () => {
35
35
  const body = buildAskUserCurlBodyJson(
36
36
  { video_title: 'hello' },
37
37
  { agent_hint: '请尽快确认' },
38
38
  )
39
39
  expect(body).toContain('"video_title": "hello"')
40
- expect(body).toContain('// 额外字段(未在卡片展示)')
41
40
  expect(body).toContain('"agent_hint": "请尽快确认"')
41
+ expect(body).not.toContain('// 额外字段')
42
+ expect(JSON.parse(body)).toEqual({
43
+ video_title: 'hello',
44
+ agent_hint: '请尽快确认',
45
+ })
42
46
 
43
47
  const curl = buildAskUserCurlStatement({
44
48
  cardValues: { video_title: 'hello' },
45
49
  extraValues: { agent_hint: '请尽快确认' },
46
50
  callbackUrl: 'https://example.com/hook',
47
51
  })
48
- expect(curl).toContain('// 额外字段(未在卡片展示)')
52
+ expect(curl).toContain('"agent_hint": "请尽快确认"')
53
+ expect(curl).not.toContain('// 额外字段')
49
54
  })
50
55
  })
51
56
 
@@ -96,6 +101,18 @@ describe('intersectPayloadBlocksWithForm', () => {
96
101
  expect(extractExtraBlockValues(result.extraBlocks)).toEqual({ agent_hint: 'hint' })
97
102
  })
98
103
 
104
+ it('defaults resume_multiselect extra blocks to empty array', () => {
105
+ const extraBlocks = [
106
+ {
107
+ id: '1',
108
+ type: 'resume_multiselect' as const,
109
+ name: 'resumes',
110
+ label: 'Resumes',
111
+ },
112
+ ]
113
+ expect(extractExtraBlockValues(extraBlocks)).toEqual({ resumes: [] })
114
+ })
115
+
99
116
  it('keeps form block type and schema when payload type differs', () => {
100
117
  const selectForm: FormSchema = {
101
118
  title: 'Form',
@@ -195,6 +212,20 @@ describe('payload fallback helpers', () => {
195
212
  expect(payloadHasInputBlocks(surveyPayload)).toBe(true)
196
213
  expect(getPayloadRenderableBlocks(surveyPayload).length).toBeGreaterThan(0)
197
214
  })
215
+
216
+ it('prefers payload fallback blocks when intersection only has layout blocks', () => {
217
+ const result = intersectPayloadBlocksWithForm(surveyPayload, undefined)
218
+ const usePayloadFallback =
219
+ payloadHasInputBlocks(surveyPayload) && result.matchedBlocks.length === 0
220
+ expect(usePayloadFallback).toBe(true)
221
+ expect(result.renderBlocks.map((b) => b.type)).toEqual(['heading'])
222
+
223
+ const renderBlocks = usePayloadFallback
224
+ ? getPayloadRenderableBlocks(surveyPayload)
225
+ : result.renderBlocks
226
+ expect(renderBlocks.map((b) => b.id)).toEqual(['title', 'q1'])
227
+ expect(getPayloadInputFieldNames(surveyPayload)).toEqual(['occupation'])
228
+ })
198
229
  })
199
230
 
200
231
  describe('submitFormJson', () => {
@@ -56,6 +56,8 @@ export type FormBlockType =
56
56
  | 'time'
57
57
  | 'file'
58
58
  | 'resume_multiselect'
59
+ | 'case_multiselect'
60
+ /** @deprecated 加载时自动迁移为 case_multiselect */
59
61
  | 'case_singleselect'
60
62
  | 'image'
61
63
  | 'video'
@@ -92,13 +94,18 @@ export interface FormResumeFilter {
92
94
  pageSize?: number
93
95
  }
94
96
 
95
- /** 案例单选组件:渲染时用于 /univ/case/list 的筛选条件 */
97
+ /** 案例多选组件:渲染时用于 /univ/case/list 的筛选条件 */
96
98
  export interface FormCaseFilter {
97
99
  /** Agent 类型(编辑器辅助,运行时不用) */
98
100
  agentKind?: string
99
- /** Agent unique_id(编辑器辅助,运行时不用) */
101
+ /** Agent unique_id(运行时用于限定所属 Agent) */
100
102
  agentUniqueId?: string
101
- flowId?: number
103
+ /** 流类型 unique_id */
104
+ flowType?: string
105
+ /** 流类别 unique_id */
106
+ category?: string
107
+ /** 流程编号(flow_unique_id)多选 */
108
+ flowUniqueIds?: string[]
102
109
  pageSize?: number
103
110
  }
104
111
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/blocks/case_singleselect/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAE3E,iBAAS,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAErD;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,mBAAmB,GACvB,sBAAsB,CAiBxB;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iBAAiB,EACvB,IAAI,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,SAAS,EAC9C,OAAO,EAAE;IACP,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;CAC1C,GACA,SAAS,CAmCX;AAED,OAAO,EAAE,SAAS,EAAE,CAAA"}
@@ -1,3 +0,0 @@
1
- import type { BlockAdapterPlugin } from '../types';
2
- export declare const caseSingleselectAdapter: BlockAdapterPlugin;
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/blocks/case_singleselect/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAOlD,eAAO,MAAM,uBAAuB,EAAE,kBAMrC,CAAA"}
@@ -1,74 +0,0 @@
1
- import type { A2uiComponentNode, FormBlock } from '../../types/index'
2
- import type { BlockAdapterContext, BlockToComponentResult } from '../types'
3
-
4
- function asLiteral(value: unknown): string | undefined {
5
- return typeof value === 'string' ? value : undefined
6
- }
7
-
8
- export function caseSingleselectToComponent(
9
- block: FormBlock,
10
- ctx: BlockAdapterContext,
11
- ): BlockToComponentResult {
12
- const { id, label, path, withData } = ctx
13
- return withData({
14
- id,
15
- component: 'SkoponCaseSelect',
16
- label,
17
- placeholder: block.placeholder ?? '',
18
- help: block.help ?? '',
19
- enableRefresh: block.caseEnableRefresh !== false,
20
- caseFilter: {
21
- agentKind: block.caseFilter?.agentKind,
22
- agentUniqueId: block.caseFilter?.agentUniqueId,
23
- flowId: block.caseFilter?.flowId,
24
- pageSize: block.caseFilter?.pageSize ?? 20,
25
- },
26
- value: { path },
27
- })
28
- }
29
-
30
- export function caseSingleselectFromComponent(
31
- node: A2uiComponentNode,
32
- base: (type: 'case_singleselect') => FormBlock,
33
- helpers: {
34
- asLiteral: (value: unknown) => string | undefined
35
- nameFromValue: (value: unknown) => string
36
- },
37
- ): FormBlock {
38
- const name = helpers.nameFromValue(node.value)
39
- const block: FormBlock = {
40
- ...base('case_singleselect'),
41
- name,
42
- caseEnableRefresh: node.enableRefresh !== false,
43
- }
44
- const placeholder = helpers.asLiteral(node.placeholder)
45
- if (placeholder) block.placeholder = placeholder
46
- const help = helpers.asLiteral(node.help)
47
- if (help) block.help = help
48
- if (node.caseFilter && typeof node.caseFilter === 'object') {
49
- const cf = node.caseFilter as Record<string, unknown>
50
- const flowIdRaw = typeof cf.flowId === 'number' ? cf.flowId : Number(cf.flowId)
51
- const pageSizeRaw = typeof cf.pageSize === 'number' ? cf.pageSize : Number(cf.pageSize)
52
- const agentKind =
53
- typeof cf.agentKind === 'string' && cf.agentKind.trim()
54
- ? cf.agentKind.trim()
55
- : undefined
56
- const agentUniqueId =
57
- typeof cf.agentUniqueId === 'string' && cf.agentUniqueId.trim()
58
- ? cf.agentUniqueId.trim()
59
- : undefined
60
- block.caseFilter = {
61
- agentKind,
62
- agentUniqueId,
63
- flowId:
64
- Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined,
65
- pageSize:
66
- Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
67
- ? Math.floor(pageSizeRaw)
68
- : 20,
69
- }
70
- }
71
- return block
72
- }
73
-
74
- export { asLiteral }
@@ -1,14 +0,0 @@
1
- import type { BlockAdapterPlugin } from '../types'
2
- import {
3
- caseSingleselectFromComponent,
4
- caseSingleselectToComponent,
5
- } from './adapter'
6
- import { SkoponCaseSelectImpl } from './catalog'
7
-
8
- export const caseSingleselectAdapter: BlockAdapterPlugin = {
9
- type: 'case_singleselect',
10
- componentName: 'SkoponCaseSelect',
11
- toComponent: caseSingleselectToComponent,
12
- fromComponent: caseSingleselectFromComponent,
13
- catalogComponents: [SkoponCaseSelectImpl],
14
- }