@skopon-cool/form-sdk 0.6.0 → 0.6.2
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 +30 -2
- package/dist/adapter/formSchema.d.ts +3 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -1
- package/dist/catalog/contentSearchContext.d.ts +35 -0
- package/dist/catalog/contentSearchContext.d.ts.map +1 -0
- package/dist/catalog/pairResumeContent.d.ts +20 -0
- package/dist/catalog/pairResumeContent.d.ts.map +1 -0
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/client/authStorage.d.ts +9 -0
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/client/contentSearchClient.d.ts +36 -0
- package/dist/client/contentSearchClient.d.ts.map +1 -0
- package/dist/client/resumeSearchClient.d.ts.map +1 -1
- package/dist/client/skoponSdkClients.d.ts +4 -0
- package/dist/client/skoponSdkClients.d.ts.map +1 -1
- package/dist/client/uploadClient.d.ts +1 -1
- package/dist/client/uploadClient.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +3 -0
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +4 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +3 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1963 -1625
- package/dist/types/index.d.ts +25 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/formSchema.test.ts +8 -0
- package/src/adapter/formSchema.ts +49 -0
- package/src/blocks/resume_multiselect/adapter.ts +31 -0
- package/src/catalog/contentSearchContext.tsx +53 -0
- package/src/catalog/pairResumeContent.test.ts +34 -0
- package/src/catalog/pairResumeContent.ts +55 -0
- package/src/catalog/skoponResumeSelect.tsx +348 -153
- package/src/catalog/skoponSelectLoading.test.tsx +84 -5
- package/src/client/authStorage.test.ts +67 -1
- package/src/client/authStorage.ts +67 -0
- package/src/client/contentSearchClient.test.ts +123 -0
- package/src/client/contentSearchClient.ts +156 -0
- package/src/client/resumeSearchClient.test.ts +5 -0
- package/src/client/resumeSearchClient.ts +5 -0
- package/src/client/skoponSdkClients.ts +9 -0
- package/src/client/uploadClient.test.ts +41 -2
- package/src/client/uploadClient.ts +7 -3
- package/src/components/AskUserFormCard.tsx +5 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +15 -9
- package/src/components/SkoponFormRenderer.tsx +20 -12
- package/src/index.ts +19 -0
- package/src/styles/a2ui-preview.css +4 -4
- package/src/styles/index.css +158 -13
- package/src/submit/submit.test.ts +1 -0
- package/src/types/index.ts +26 -1
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Avatar, Button, Spin, Tag, Typography } from 'antd'
|
|
3
|
-
import { FileImageOutlined, ReloadOutlined, UserOutlined } from '@ant-design/icons'
|
|
3
|
+
import { CaretRightOutlined, 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'
|
|
7
|
-
import type { FormResumeFilter } from '../types/index'
|
|
7
|
+
import type { FormContentFilter, FormResumeFilter } from '../types/index'
|
|
8
|
+
import type { ContentSearchItem } from './contentSearchContext'
|
|
9
|
+
import { useContentSearch } from './contentSearchContext'
|
|
10
|
+
import {
|
|
11
|
+
advanceCursor,
|
|
12
|
+
nextPageForWindow,
|
|
13
|
+
normalizeDisplayCount,
|
|
14
|
+
pairKey,
|
|
15
|
+
takeWindow,
|
|
16
|
+
windowIndexes,
|
|
17
|
+
type PageCacheSnapshot,
|
|
18
|
+
} from './pairResumeContent'
|
|
8
19
|
import { useFormLiveOptions } from '../runtime/formLiveOptionsContext'
|
|
9
20
|
import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
10
21
|
import { asStringArray, clampStringArraySelection, isCardSelectionDisabled, readOptionalSelectCount, toggleStringArraySelection, useSkoponBoundField } from './useSkoponBoundField'
|
|
11
22
|
import {
|
|
12
23
|
type ResumeSearchItem,
|
|
13
|
-
type ResumeSearchWorkItem,
|
|
14
24
|
useResumeSearch,
|
|
15
25
|
} from './resumeSearchContext'
|
|
16
26
|
|
|
@@ -18,24 +28,29 @@ function readString(value: unknown): string {
|
|
|
18
28
|
return typeof value === 'string' ? value : ''
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
function readStringList(raw: unknown): string[] {
|
|
32
|
+
return Array.isArray(raw) ? raw.map(String).filter(Boolean) : []
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
function readResumeFilter(raw: unknown): FormResumeFilter {
|
|
22
36
|
if (!raw || typeof raw !== 'object') {
|
|
23
|
-
return { names: [], agentUniqueIds: [], resumeUniqueIds: [], pageSize: 20 }
|
|
37
|
+
return { names: [], agentUniqueIds: [], tagUniqueIds: [], resumeUniqueIds: [], pageSize: 20 }
|
|
24
38
|
}
|
|
25
39
|
const rec = raw as Record<string, unknown>
|
|
26
|
-
const
|
|
27
|
-
const agentUniqueIds = Array.isArray(rec.agentUniqueIds)
|
|
28
|
-
? rec.agentUniqueIds.map(String).filter(Boolean)
|
|
29
|
-
: []
|
|
30
|
-
const resumeUniqueIds = Array.isArray(rec.resumeUniqueIds)
|
|
31
|
-
? rec.resumeUniqueIds.map(String).filter(Boolean)
|
|
32
|
-
: []
|
|
40
|
+
const agentKind = typeof rec.agentKind === 'string' ? rec.agentKind.trim() : ''
|
|
33
41
|
const pageSizeRaw = typeof rec.pageSize === 'number' ? rec.pageSize : Number(rec.pageSize)
|
|
34
42
|
const pageSize =
|
|
35
43
|
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
36
44
|
? Math.floor(pageSizeRaw)
|
|
37
45
|
: 20
|
|
38
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
names: readStringList(rec.names),
|
|
48
|
+
...(agentKind ? { agentKind } : {}),
|
|
49
|
+
agentUniqueIds: readStringList(rec.agentUniqueIds),
|
|
50
|
+
tagUniqueIds: readStringList(rec.tagUniqueIds),
|
|
51
|
+
resumeUniqueIds: readStringList(rec.resumeUniqueIds),
|
|
52
|
+
pageSize,
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
function filterKey(filter: FormResumeFilter): string {
|
|
@@ -51,6 +66,9 @@ const SkoponResumeSelectApi = {
|
|
|
51
66
|
help: z.any().optional(),
|
|
52
67
|
enableRefresh: z.any().optional(),
|
|
53
68
|
resumeFilter: z.any().optional(),
|
|
69
|
+
resumeDisplayCount: z.any().optional(),
|
|
70
|
+
resumeDisplayMode: z.any().optional(),
|
|
71
|
+
contentFilter: z.any().optional(),
|
|
54
72
|
minCount: z.any().optional(),
|
|
55
73
|
maxCount: z.any().optional(),
|
|
56
74
|
value: z.any().optional(),
|
|
@@ -58,112 +76,210 @@ const SkoponResumeSelectApi = {
|
|
|
58
76
|
.passthrough(),
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
const
|
|
62
|
-
|
|
79
|
+
const DEFAULT_CONTENT_FILTER: FormContentFilter = {
|
|
80
|
+
flowType: 'content-production',
|
|
81
|
+
tagUniqueIds: [],
|
|
82
|
+
pageSize: 20,
|
|
83
|
+
}
|
|
63
84
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
</div>
|
|
82
|
-
) : (
|
|
83
|
-
<span className="skopon-resume-select-card-tags-empty">暂无</span>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
)
|
|
85
|
+
function readContentFilter(raw: unknown): FormContentFilter {
|
|
86
|
+
if (!raw || typeof raw !== 'object') return DEFAULT_CONTENT_FILTER
|
|
87
|
+
const rec = raw as Record<string, unknown>
|
|
88
|
+
const flowType = typeof rec.flowType === 'string' ? rec.flowType.trim() : ''
|
|
89
|
+
const category = typeof rec.category === 'string' ? rec.category.trim() : ''
|
|
90
|
+
const pageSizeRaw = typeof rec.pageSize === 'number' ? rec.pageSize : Number(rec.pageSize)
|
|
91
|
+
const pageSize =
|
|
92
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
93
|
+
? Math.floor(pageSizeRaw)
|
|
94
|
+
: 20
|
|
95
|
+
return {
|
|
96
|
+
flowType: flowType || 'content-production',
|
|
97
|
+
...(category ? { category } : {}),
|
|
98
|
+
tagUniqueIds: readStringList(rec.tagUniqueIds),
|
|
99
|
+
tagMatch: rec.tagMatch === 'any' ? 'any' : 'all',
|
|
100
|
+
pageSize,
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
interface RingCache<T> {
|
|
105
|
+
items: T[]
|
|
106
|
+
total: number
|
|
107
|
+
fetchedPage: number
|
|
108
|
+
pageSize: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface PageResult<T> {
|
|
112
|
+
list: T[]
|
|
113
|
+
total: number
|
|
114
|
+
page: number
|
|
115
|
+
pageSize: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function emptyRing<T>(pageSize: number): RingCache<T> {
|
|
119
|
+
return { items: [], total: 0, fetchedPage: 0, pageSize }
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
function
|
|
122
|
+
function snapshotOf<T>(cache: RingCache<T>): PageCacheSnapshot {
|
|
123
|
+
return {
|
|
124
|
+
itemCount: cache.items.length,
|
|
125
|
+
total: cache.total,
|
|
126
|
+
fetchedPage: cache.fetchedPage,
|
|
127
|
+
pageSize: cache.pageSize,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadThroughWindow<T>(
|
|
132
|
+
cache: RingCache<T>,
|
|
133
|
+
cursor: number,
|
|
134
|
+
displayCount: number,
|
|
135
|
+
fetchPage: (page: number) => Promise<PageResult<T>>,
|
|
136
|
+
): Promise<RingCache<T>> {
|
|
137
|
+
let next = cache
|
|
138
|
+
for (let guard = 0; guard < 20; guard += 1) {
|
|
139
|
+
const page = nextPageForWindow(
|
|
140
|
+
snapshotOf(next),
|
|
141
|
+
windowIndexes(cursor, displayCount, next.total),
|
|
142
|
+
)
|
|
143
|
+
if (page == null) return next
|
|
144
|
+
const result = await fetchPage(page)
|
|
145
|
+
const fetchedPage = result.page || page
|
|
146
|
+
next = {
|
|
147
|
+
items: fetchedPage <= 1 ? result.list : next.items.concat(result.list),
|
|
148
|
+
total: result.total,
|
|
149
|
+
fetchedPage,
|
|
150
|
+
pageSize: result.pageSize || next.pageSize,
|
|
151
|
+
}
|
|
152
|
+
if (result.list.length === 0) return next
|
|
153
|
+
}
|
|
154
|
+
return next
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ContentStage({ content }: { content: ContentSearchItem }) {
|
|
158
|
+
const [playing, setPlaying] = useState(false)
|
|
159
|
+
const canPlay = Boolean(content.mediaUrl || content.link || content.coverUrl)
|
|
160
|
+
|
|
161
|
+
const play = (event: { stopPropagation: () => void; preventDefault: () => void }) => {
|
|
162
|
+
event.stopPropagation()
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
if (content.mediaKind === 'video' && content.mediaUrl) {
|
|
165
|
+
setPlaying(true)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
const href = content.link || content.mediaUrl || content.coverUrl
|
|
169
|
+
if (href) window.open(href, '_blank', 'noopener,noreferrer')
|
|
170
|
+
}
|
|
171
|
+
|
|
101
172
|
return (
|
|
102
|
-
<div className="skopon-resume-select-card-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
<div className="skopon-resume-select-card-stage">
|
|
174
|
+
{playing && content.mediaUrl ? (
|
|
175
|
+
<video
|
|
176
|
+
className="skopon-resume-select-card-stage-video"
|
|
177
|
+
src={content.mediaUrl}
|
|
178
|
+
poster={content.coverUrl ?? undefined}
|
|
179
|
+
controls
|
|
180
|
+
autoPlay
|
|
181
|
+
playsInline
|
|
182
|
+
onClick={(event) => event.stopPropagation()}
|
|
183
|
+
/>
|
|
184
|
+
) : (
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="skopon-resume-select-card-stage-button"
|
|
188
|
+
aria-label="播放内容"
|
|
189
|
+
disabled={!canPlay}
|
|
190
|
+
onClick={play}
|
|
191
|
+
>
|
|
192
|
+
{content.coverUrl ? (
|
|
193
|
+
<img src={content.coverUrl} alt="" className="skopon-resume-select-card-cover-img" />
|
|
194
|
+
) : (
|
|
195
|
+
<FileImageOutlined className="skopon-resume-select-card-cover-placeholder" />
|
|
196
|
+
)}
|
|
197
|
+
{canPlay ? <CaretRightOutlined className="skopon-resume-select-card-play" /> : null}
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
111
200
|
</div>
|
|
112
201
|
)
|
|
113
202
|
}
|
|
114
203
|
|
|
115
204
|
function ResumeCard({
|
|
116
205
|
item,
|
|
206
|
+
content,
|
|
117
207
|
selected,
|
|
118
208
|
disabled,
|
|
119
209
|
onToggle,
|
|
120
210
|
}: {
|
|
121
|
-
item
|
|
211
|
+
item?: ResumeSearchItem
|
|
212
|
+
content: ContentSearchItem
|
|
122
213
|
selected: boolean
|
|
123
214
|
disabled: boolean
|
|
124
215
|
onToggle: () => void
|
|
125
216
|
}) {
|
|
126
|
-
const
|
|
127
|
-
|
|
217
|
+
const specialties = (item?.specialties ?? []).filter(Boolean).slice(0, 3)
|
|
128
218
|
return (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
disabled={disabled}
|
|
133
|
-
|
|
219
|
+
<div
|
|
220
|
+
role="button"
|
|
221
|
+
tabIndex={disabled ? -1 : 0}
|
|
222
|
+
aria-disabled={disabled || undefined}
|
|
223
|
+
aria-pressed={selected}
|
|
224
|
+
className={`skopon-resume-select-card skopon-resume-select-card--profile${selected ? ' skopon-resume-select-card--selected' : ''}${disabled ? ' skopon-resume-select-card--disabled' : ''}`}
|
|
225
|
+
onClick={() => {
|
|
226
|
+
if (!disabled) onToggle()
|
|
227
|
+
}}
|
|
228
|
+
onKeyDown={(event) => {
|
|
229
|
+
if (disabled) return
|
|
230
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
231
|
+
event.preventDefault()
|
|
232
|
+
onToggle()
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
134
235
|
>
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<ResumeWorkPreview key={`${work.title}-${index}`} work={work} />
|
|
236
|
+
{item ? <div className="skopon-resume-select-card-person">
|
|
237
|
+
<Avatar
|
|
238
|
+
size={36}
|
|
239
|
+
src={item.avatarUrl || undefined}
|
|
240
|
+
icon={!item.avatarUrl ? <UserOutlined /> : undefined}
|
|
241
|
+
className="skopon-resume-select-card-avatar"
|
|
242
|
+
/>
|
|
243
|
+
<div className="skopon-resume-select-card-person-text">
|
|
244
|
+
<div className="skopon-resume-select-card-person-name">
|
|
245
|
+
{item.name || '未命名简历'}
|
|
246
|
+
</div>
|
|
247
|
+
{specialties.length > 0 ? (
|
|
248
|
+
<div className="skopon-resume-select-card-specialties">
|
|
249
|
+
{specialties.map((tag) => (
|
|
250
|
+
<Tag key={tag} className="skopon-resume-select-card-tag">
|
|
251
|
+
{tag}
|
|
252
|
+
</Tag>
|
|
153
253
|
))}
|
|
154
254
|
</div>
|
|
155
|
-
) :
|
|
156
|
-
<div className="skopon-resume-select-card-works-empty">暂无作品</div>
|
|
157
|
-
)}
|
|
255
|
+
) : null}
|
|
158
256
|
</div>
|
|
159
257
|
</div>
|
|
160
|
-
|
|
258
|
+
: null}
|
|
259
|
+
<div className="skopon-resume-select-card-content-title">
|
|
260
|
+
{content.title || '未命名内容'}
|
|
261
|
+
</div>
|
|
262
|
+
{(content.tagLabels ?? []).length > 0 ? (
|
|
263
|
+
<div className="skopon-resume-select-card-content-tags" aria-label="内容标签">
|
|
264
|
+
<span className="skopon-resume-select-card-content-tags-label">内容标签</span>
|
|
265
|
+
<div className="skopon-resume-select-card-content-tags-list">
|
|
266
|
+
{(content.tagLabels ?? []).map((tag) => (
|
|
267
|
+
<Tag key={tag} className="skopon-resume-select-card-content-tag">
|
|
268
|
+
{tag}
|
|
269
|
+
</Tag>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
) : null}
|
|
274
|
+
<ContentStage content={content} />
|
|
275
|
+
</div>
|
|
161
276
|
)
|
|
162
277
|
}
|
|
163
278
|
|
|
164
279
|
function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
165
280
|
const { interactive } = useA2uiPreviewMode()
|
|
166
281
|
const resumeSearch = useResumeSearch()
|
|
282
|
+
const contentSearch = useContentSearch()
|
|
167
283
|
const { publishLiveOptions } = useFormLiveOptions()
|
|
168
284
|
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
169
285
|
component: 'SkoponResumeSelect',
|
|
@@ -177,16 +293,25 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
177
293
|
const enableRefresh = props.enableRefresh !== false
|
|
178
294
|
const minCount = readOptionalSelectCount(props.minCount)
|
|
179
295
|
const maxCount = readOptionalSelectCount(props.maxCount)
|
|
296
|
+
const contentOnly = props.resumeDisplayMode === 'content-only'
|
|
297
|
+
const displayCount = normalizeDisplayCount(props.resumeDisplayCount)
|
|
180
298
|
const resumeFilter = useMemo(() => readResumeFilter(props.resumeFilter), [props.resumeFilter])
|
|
181
|
-
const
|
|
299
|
+
const contentFilter = useMemo(() => readContentFilter(props.contentFilter), [props.contentFilter])
|
|
300
|
+
const resumeSig = filterKey(resumeFilter)
|
|
301
|
+
const contentSig = filterKey(contentFilter)
|
|
182
302
|
|
|
183
|
-
const [
|
|
303
|
+
const [resumeCache, setResumeCache] = useState<RingCache<ResumeSearchItem>>(() => emptyRing(20))
|
|
304
|
+
const [contentCache, setContentCache] = useState<RingCache<ContentSearchItem>>(() => emptyRing(20))
|
|
305
|
+
const [resumeCursor, setResumeCursor] = useState(0)
|
|
306
|
+
const [contentCursor, setContentCursor] = useState(0)
|
|
184
307
|
const [loading, setLoading] = useState(false)
|
|
185
308
|
const [error, setError] = useState('')
|
|
186
|
-
const [items, setItems] = useState<ResumeSearchItem[]>([])
|
|
187
|
-
const [total, setTotal] = useState(0)
|
|
188
|
-
const [pageSize, setPageSize] = useState(resumeFilter.pageSize ?? 20)
|
|
189
309
|
const requestSeqRef = useRef(0)
|
|
310
|
+
const refreshInFlightRef = useRef(false)
|
|
311
|
+
const resumeCacheRef = useRef(resumeCache)
|
|
312
|
+
const contentCacheRef = useRef(contentCache)
|
|
313
|
+
resumeCacheRef.current = resumeCache
|
|
314
|
+
contentCacheRef.current = contentCache
|
|
190
315
|
|
|
191
316
|
const selectedIds = asStringArray(value)
|
|
192
317
|
|
|
@@ -195,73 +320,144 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
195
320
|
if (clamped !== null) setValueSilent(clamped)
|
|
196
321
|
}, [maxCount, selectedIds, setValueSilent])
|
|
197
322
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const filter = readResumeFilter(JSON.parse(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
323
|
+
const fetchResumePage = useCallback(
|
|
324
|
+
(page: number) => {
|
|
325
|
+
const filter = readResumeFilter(JSON.parse(resumeSig) as FormResumeFilter)
|
|
326
|
+
if (!resumeSearch) return Promise.reject(new Error('未配置简历搜索服务'))
|
|
327
|
+
return resumeSearch({ filter, page })
|
|
328
|
+
},
|
|
329
|
+
[resumeSearch, resumeSig],
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
const fetchContentPage = useCallback(
|
|
333
|
+
(page: number) => {
|
|
334
|
+
const filter = readContentFilter(JSON.parse(contentSig) as FormContentFilter)
|
|
335
|
+
if (!contentSearch) return Promise.reject(new Error('未配置内容搜索服务'))
|
|
336
|
+
return contentSearch({ filter, page })
|
|
337
|
+
},
|
|
338
|
+
[contentSearch, contentSig],
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const seq = ++requestSeqRef.current
|
|
343
|
+
if ((!contentOnly && !resumeSearch) || !contentSearch) {
|
|
344
|
+
setResumeCache(emptyRing(resumeFilter.pageSize ?? 20))
|
|
345
|
+
setContentCache(emptyRing(contentFilter.pageSize ?? 20))
|
|
346
|
+
setResumeCursor(0)
|
|
347
|
+
setContentCursor(0)
|
|
348
|
+
setError(!contentOnly && !resumeSearch ? '未配置简历搜索服务' : '未配置内容搜索服务')
|
|
349
|
+
setLoading(false)
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
setLoading(true)
|
|
353
|
+
setError('')
|
|
354
|
+
setResumeCursor(0)
|
|
355
|
+
setContentCursor(0)
|
|
356
|
+
void (async () => {
|
|
211
357
|
try {
|
|
212
|
-
const
|
|
358
|
+
const [resumeFirst, contentFirst] = await Promise.all([
|
|
359
|
+
contentOnly ? Promise.resolve({ list: [], total: 0, page: 1, pageSize: 20 }) : fetchResumePage(1),
|
|
360
|
+
fetchContentPage(1),
|
|
361
|
+
])
|
|
213
362
|
if (seq !== requestSeqRef.current) return
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
publishLiveOptions(
|
|
220
|
-
fieldName,
|
|
221
|
-
result.list.map((item) => ({
|
|
222
|
-
value: item.resumeUniqueId,
|
|
223
|
-
label: item.name || item.resumeUniqueId,
|
|
224
|
-
})),
|
|
225
|
-
)
|
|
363
|
+
let resumes: RingCache<ResumeSearchItem> = {
|
|
364
|
+
items: resumeFirst.list,
|
|
365
|
+
total: resumeFirst.total,
|
|
366
|
+
fetchedPage: resumeFirst.page || 1,
|
|
367
|
+
pageSize: resumeFirst.pageSize,
|
|
226
368
|
}
|
|
369
|
+
let contents: RingCache<ContentSearchItem> = {
|
|
370
|
+
items: contentFirst.list,
|
|
371
|
+
total: contentFirst.total,
|
|
372
|
+
fetchedPage: contentFirst.page || 1,
|
|
373
|
+
pageSize: contentFirst.pageSize,
|
|
374
|
+
}
|
|
375
|
+
if (!contentOnly) resumes = await loadThroughWindow(resumes, 0, displayCount, fetchResumePage)
|
|
376
|
+
contents = await loadThroughWindow(contents, 0, displayCount, fetchContentPage)
|
|
377
|
+
if (seq !== requestSeqRef.current) return
|
|
378
|
+
setResumeCache(resumes)
|
|
379
|
+
setContentCache(contents)
|
|
227
380
|
} catch (err) {
|
|
228
381
|
if (seq !== requestSeqRef.current) return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
setError(err instanceof Error ? err.message : '
|
|
382
|
+
setResumeCache(emptyRing(20))
|
|
383
|
+
setContentCache(emptyRing(20))
|
|
384
|
+
setError(err instanceof Error ? err.message : (contentOnly ? '加载内容失败' : '加载简历和内容失败'))
|
|
232
385
|
} finally {
|
|
233
386
|
if (seq === requestSeqRef.current) setLoading(false)
|
|
234
387
|
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
388
|
+
})()
|
|
389
|
+
}, [
|
|
390
|
+
contentOnly,
|
|
391
|
+
contentFilter.pageSize,
|
|
392
|
+
contentSearch,
|
|
393
|
+
displayCount,
|
|
394
|
+
fetchContentPage,
|
|
395
|
+
fetchResumePage,
|
|
396
|
+
resumeFilter.pageSize,
|
|
397
|
+
resumeSearch,
|
|
398
|
+
])
|
|
238
399
|
|
|
400
|
+
const resumeWindow = takeWindow(resumeCache.items, resumeCursor, displayCount, resumeCache.total)
|
|
401
|
+
const contentWindow = takeWindow(contentCache.items, contentCursor, displayCount, contentCache.total)
|
|
402
|
+
const pairs: { item?: ResumeSearchItem; content: ContentSearchItem; key: string }[] = contentOnly
|
|
403
|
+
? contentWindow.map(content => ({ content, key: content.flowUniqueId }))
|
|
404
|
+
: resumeWindow.flatMap((item, index) => {
|
|
405
|
+
const content = contentWindow[index]
|
|
406
|
+
if (!content) return []
|
|
407
|
+
return [{ item, content, key: pairKey(item.resumeUniqueId, content.flowUniqueId) }]
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
const pairSig = pairs.map((pair) => `${pair.key}\t${pair.item?.name ?? ''}\t${pair.content.title}`).join('\n')
|
|
239
411
|
useEffect(() => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
412
|
+
if (!fieldName) return
|
|
413
|
+
publishLiveOptions(
|
|
414
|
+
fieldName,
|
|
415
|
+
pairs.map((pair) => ({
|
|
416
|
+
value: pair.key,
|
|
417
|
+
label: pair.item ? `${pair.item.name || pair.item.resumeUniqueId} / ${pair.content.title}` : pair.content.title,
|
|
418
|
+
})),
|
|
419
|
+
)
|
|
420
|
+
}, [fieldName, pairSig, publishLiveOptions])
|
|
243
421
|
|
|
244
|
-
const toggleSelect = (
|
|
422
|
+
const toggleSelect = (key: string) => {
|
|
245
423
|
if (!interactive) return
|
|
246
|
-
const next = toggleStringArraySelection(
|
|
247
|
-
selectedIds,
|
|
248
|
-
resumeUniqueId,
|
|
249
|
-
minCount,
|
|
250
|
-
maxCount,
|
|
251
|
-
)
|
|
424
|
+
const next = toggleStringArraySelection(selectedIds, key, minCount, maxCount)
|
|
252
425
|
if (next === null) return
|
|
253
426
|
setValue(next)
|
|
254
427
|
}
|
|
255
428
|
|
|
256
429
|
const handleRefresh = () => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
430
|
+
if (refreshInFlightRef.current) return
|
|
431
|
+
if ((!contentOnly && resumeCache.total <= 0) || contentCache.total <= 0) return
|
|
432
|
+
refreshInFlightRef.current = true
|
|
433
|
+
const seq = ++requestSeqRef.current
|
|
434
|
+
const nextResumeCursor = advanceCursor(resumeCursor, displayCount, resumeCache.total)
|
|
435
|
+
const nextContentCursor = advanceCursor(contentCursor, displayCount, contentCache.total)
|
|
436
|
+
setLoading(true)
|
|
437
|
+
setError('')
|
|
438
|
+
void (async () => {
|
|
439
|
+
try {
|
|
440
|
+
const [resumes, contents] = await Promise.all([
|
|
441
|
+
contentOnly ? Promise.resolve(resumeCacheRef.current) : loadThroughWindow(resumeCacheRef.current, nextResumeCursor, displayCount, fetchResumePage),
|
|
442
|
+
loadThroughWindow(contentCacheRef.current, nextContentCursor, displayCount, fetchContentPage),
|
|
443
|
+
])
|
|
444
|
+
if (seq !== requestSeqRef.current) return
|
|
445
|
+
setResumeCache(resumes)
|
|
446
|
+
setContentCache(contents)
|
|
447
|
+
setResumeCursor(nextResumeCursor)
|
|
448
|
+
setContentCursor(nextContentCursor)
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (seq !== requestSeqRef.current) return
|
|
451
|
+
setError(err instanceof Error ? err.message : '换一批失败')
|
|
452
|
+
} finally {
|
|
453
|
+
refreshInFlightRef.current = false
|
|
454
|
+
if (seq === requestSeqRef.current) setLoading(false)
|
|
455
|
+
}
|
|
456
|
+
})()
|
|
262
457
|
}
|
|
263
458
|
|
|
264
|
-
const emptyLabel = placeholder || '
|
|
459
|
+
const emptyLabel = placeholder || (contentOnly ? '暂无匹配的内容' : '暂无匹配的简历或内容')
|
|
460
|
+
const showEmpty = !loading && !error && pairs.length === 0
|
|
265
461
|
|
|
266
462
|
return (
|
|
267
463
|
<div
|
|
@@ -269,9 +465,9 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
269
465
|
data-skopon-field={fieldName || undefined}
|
|
270
466
|
>
|
|
271
467
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
272
|
-
{loading &&
|
|
468
|
+
{loading && pairs.length === 0 ? (
|
|
273
469
|
<div className="skopon-resume-select-status">
|
|
274
|
-
<Spin size="small" />
|
|
470
|
+
<Spin size="small" /> {contentOnly ? '加载内容…' : '加载简历和内容…'}
|
|
275
471
|
</div>
|
|
276
472
|
) : null}
|
|
277
473
|
{!loading && error ? (
|
|
@@ -279,36 +475,35 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
279
475
|
{error}
|
|
280
476
|
</Typography.Text>
|
|
281
477
|
) : null}
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
) : null}
|
|
285
|
-
{items.length > 0 ? (
|
|
478
|
+
{showEmpty ? <div className="skopon-resume-select-empty">{emptyLabel}</div> : null}
|
|
479
|
+
{pairs.length > 0 ? (
|
|
286
480
|
<div className="skopon-resume-select-list">
|
|
287
|
-
{
|
|
481
|
+
{pairs.map((pair) => (
|
|
288
482
|
<ResumeCard
|
|
289
|
-
key={
|
|
290
|
-
item={item}
|
|
291
|
-
|
|
483
|
+
key={pair.key}
|
|
484
|
+
item={pair.item}
|
|
485
|
+
content={pair.content}
|
|
486
|
+
selected={selectedIds.includes(pair.key)}
|
|
292
487
|
disabled={isCardSelectionDisabled(
|
|
293
488
|
selectedIds,
|
|
294
|
-
|
|
489
|
+
pair.key,
|
|
295
490
|
interactive,
|
|
296
491
|
minCount,
|
|
297
492
|
maxCount,
|
|
298
493
|
)}
|
|
299
|
-
onToggle={() => toggleSelect(
|
|
494
|
+
onToggle={() => toggleSelect(pair.key)}
|
|
300
495
|
/>
|
|
301
496
|
))}
|
|
302
497
|
</div>
|
|
303
498
|
) : null}
|
|
304
|
-
{enableRefresh && resumeSearch ? (
|
|
499
|
+
{enableRefresh && (contentOnly || resumeSearch) && contentSearch ? (
|
|
305
500
|
<div className="skopon-resume-select-actions">
|
|
306
501
|
<Button
|
|
307
502
|
type="default"
|
|
308
503
|
size="small"
|
|
309
504
|
icon={<ReloadOutlined />}
|
|
310
505
|
loading={loading}
|
|
311
|
-
disabled={!interactive}
|
|
506
|
+
disabled={!interactive || (!contentOnly && resumeCache.total <= 0) || contentCache.total <= 0}
|
|
312
507
|
onClick={handleRefresh}
|
|
313
508
|
>
|
|
314
509
|
换一批
|