@skopon-cool/form-sdk 0.6.3 → 0.6.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.
- package/dist/adapter/a2uiAdapter.d.ts +1 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formAssetOptions.d.ts +3 -0
- package/dist/adapter/formAssetOptions.d.ts.map +1 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/adapter/resolveSurface.d.ts +1 -1
- package/dist/adapter/resolveSurface.d.ts.map +1 -1
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/materialUploadContext.d.ts +24 -0
- package/dist/catalog/materialUploadContext.d.ts.map +1 -0
- package/dist/components/AskUserFormCard.d.ts +5 -0
- package/dist/components/AskUserFormCard.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2170 -1928
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts +11 -2
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
- package/dist/types/index.d.ts +35 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +4 -0
- package/src/adapter/a2uiAdapter.ts +58 -2
- package/src/adapter/formAssetOptions.ts +19 -0
- package/src/adapter/formSchema.ts +54 -0
- package/src/adapter/resolveSurface.ts +4 -1
- package/src/catalog/a2uiCustomCatalog.tsx +296 -46
- package/src/catalog/fileUploadPreview.test.tsx +77 -11
- package/src/catalog/materialUploadContext.tsx +43 -0
- package/src/catalog/moduleTabs.test.tsx +59 -0
- package/src/components/AskUserFormCard.tsx +33 -9
- package/src/components/AskUserFormCard.upload.test.tsx +66 -0
- package/src/components/SkoponFormRenderer.tsx +6 -0
- package/src/icons/FilePlaceholderIcon.tsx +2 -2
- package/src/index.ts +2 -0
- package/src/styles/index.css +295 -0
- package/src/submit/buildAnswerAgentBody.ts +2 -0
- package/src/submit/intersectPayloadBlocksWithForm.ts +52 -3
- package/src/submit/submit.test.ts +54 -0
- package/src/types/index.ts +37 -0
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
} from '@a2ui/react/v0_9'
|
|
9
9
|
import {
|
|
10
10
|
Checkbox,
|
|
11
|
+
Button,
|
|
11
12
|
DatePicker,
|
|
12
13
|
Input,
|
|
13
14
|
InputNumber,
|
|
14
15
|
Image,
|
|
16
|
+
Modal,
|
|
15
17
|
Radio,
|
|
16
18
|
Select,
|
|
17
19
|
Switch,
|
|
@@ -30,17 +32,16 @@ import {
|
|
|
30
32
|
} from '../adapter/formFileAccept'
|
|
31
33
|
import { buildMediaListClassName, normalizeMediaSize } from '../adapter/formMedia'
|
|
32
34
|
import { normalizeFormOptions } from '../adapter/formOptions'
|
|
35
|
+
import { normalizeAssetOptions } from '../adapter/formAssetOptions'
|
|
33
36
|
import {
|
|
34
37
|
getRegisteredCatalogComponentNames,
|
|
35
38
|
getRegisteredCatalogComponents,
|
|
36
39
|
} from '../blocks/registry'
|
|
37
40
|
import FilePlaceholderIcon from '../icons/FilePlaceholderIcon'
|
|
38
|
-
import {
|
|
39
|
-
isUploadCancelledError,
|
|
40
|
-
uploadFileToCdn,
|
|
41
|
-
} from '../client/uploadClient'
|
|
41
|
+
import { isUploadCancelledError } from '../client/uploadClient'
|
|
42
42
|
import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
43
43
|
import { useFileUploadStatus } from './fileUploadStatusContext'
|
|
44
|
+
import { useMaterialUpload } from './materialUploadContext'
|
|
44
45
|
import {
|
|
45
46
|
asOptionalString,
|
|
46
47
|
asStringArray,
|
|
@@ -57,6 +58,7 @@ export const SKOPON_CATALOG_ID = 'skopon-form-v0_9'
|
|
|
57
58
|
const SKOPON_COMPONENT_NAMES = new Set([
|
|
58
59
|
'SkoponMedia',
|
|
59
60
|
'SkoponSelect',
|
|
61
|
+
'SkoponModulePanel',
|
|
60
62
|
'Image',
|
|
61
63
|
'Video',
|
|
62
64
|
'AudioPlayer',
|
|
@@ -280,6 +282,58 @@ const SkoponSelectImpl = createBinderlessComponentImplementation(
|
|
|
280
282
|
SkoponSelectPreview,
|
|
281
283
|
)
|
|
282
284
|
|
|
285
|
+
const SkoponModulePanelImpl = createBinderlessComponentImplementation(
|
|
286
|
+
{
|
|
287
|
+
name: 'SkoponModulePanel',
|
|
288
|
+
schema: z.object({
|
|
289
|
+
children: z.array(z.string()).optional(),
|
|
290
|
+
description: z.any().optional(),
|
|
291
|
+
reviewCriteria: z.any().optional(),
|
|
292
|
+
exampleUrls: z.any().optional(),
|
|
293
|
+
}).passthrough(),
|
|
294
|
+
},
|
|
295
|
+
({ context, buildChild }) => {
|
|
296
|
+
const props = context.componentModel.properties as Record<string, unknown>
|
|
297
|
+
const children = Array.isArray(props.children) ? props.children.map(String) : []
|
|
298
|
+
const description = readString(props.description).trim()
|
|
299
|
+
const requirementsTitle = readString(props.requirementsTitle).trim() || '本类素材要求'
|
|
300
|
+
const reviewCriteria = readString(props.reviewCriteria).trim()
|
|
301
|
+
const exampleUrls = Array.isArray(props.exampleUrls)
|
|
302
|
+
? props.exampleUrls.map(String).map((url) => url.trim()).filter(Boolean)
|
|
303
|
+
: []
|
|
304
|
+
return (
|
|
305
|
+
<section className="skopon-module-panel">
|
|
306
|
+
{description ? <Typography.Paragraph type="secondary" className="skopon-module-panel__description">{description}</Typography.Paragraph> : null}
|
|
307
|
+
<div className="skopon-module-panel__grid">
|
|
308
|
+
{children.map((id) => <div className="skopon-module-panel__field" key={id}>{buildChild(id)}</div>)}
|
|
309
|
+
</div>
|
|
310
|
+
{reviewCriteria || exampleUrls.length > 0 ? (
|
|
311
|
+
<aside className="skopon-module-panel__guide">
|
|
312
|
+
{reviewCriteria ? (
|
|
313
|
+
<div className="skopon-module-panel__requirements">
|
|
314
|
+
<Typography.Text strong>{requirementsTitle}</Typography.Text>
|
|
315
|
+
<Typography.Paragraph type="secondary">{reviewCriteria}</Typography.Paragraph>
|
|
316
|
+
</div>
|
|
317
|
+
) : null}
|
|
318
|
+
{exampleUrls.length > 0 ? (
|
|
319
|
+
<div className="skopon-module-panel__examples">
|
|
320
|
+
<Typography.Text strong>参考示例</Typography.Text>
|
|
321
|
+
<div className="skopon-module-panel__example-list">
|
|
322
|
+
<Image.PreviewGroup>
|
|
323
|
+
{exampleUrls.map((url, index) => (
|
|
324
|
+
<Image key={`${url}-${index}`} src={url} alt={`参考示例 ${index + 1}`} width={88} height={88} style={{ objectFit: 'cover' }} />
|
|
325
|
+
))}
|
|
326
|
+
</Image.PreviewGroup>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
) : null}
|
|
330
|
+
</aside>
|
|
331
|
+
) : null}
|
|
332
|
+
</section>
|
|
333
|
+
)
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
|
|
283
337
|
function ChoicePickerPreview({ context }: { context: ComponentContext }) {
|
|
284
338
|
const { interactive } = useA2uiPreviewMode()
|
|
285
339
|
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
@@ -517,11 +571,34 @@ interface FileUploadListItem {
|
|
|
517
571
|
name: string
|
|
518
572
|
status: 'uploading' | 'done' | 'error'
|
|
519
573
|
url?: string
|
|
574
|
+
assetId?: string
|
|
575
|
+
source?: 'upload' | 'library_select'
|
|
520
576
|
error?: string
|
|
521
577
|
}
|
|
522
578
|
|
|
523
|
-
|
|
524
|
-
|
|
579
|
+
interface FileValueRef {
|
|
580
|
+
assetId?: string
|
|
581
|
+
source?: 'upload' | 'library_select'
|
|
582
|
+
url?: string
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function normalizeFileValues(value: unknown): FileValueRef[] {
|
|
586
|
+
const values = Array.isArray(value) ? value : value == null || value === '' ? [] : [value]
|
|
587
|
+
return values.reduce<FileValueRef[]>((result, entry) => {
|
|
588
|
+
if (typeof entry === 'string' && entry.trim()) {
|
|
589
|
+
result.push({ url: entry.trim() })
|
|
590
|
+
return result
|
|
591
|
+
}
|
|
592
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return result
|
|
593
|
+
const obj = entry as Record<string, unknown>
|
|
594
|
+
const assetId = String(obj.asset_id ?? obj.assetId ?? '').trim()
|
|
595
|
+
if (!assetId) return result
|
|
596
|
+
result.push({
|
|
597
|
+
assetId,
|
|
598
|
+
source: obj.source === 'library_select' ? 'library_select' as const : 'upload' as const,
|
|
599
|
+
})
|
|
600
|
+
return result
|
|
601
|
+
}, [])
|
|
525
602
|
}
|
|
526
603
|
|
|
527
604
|
function fileNameFromUrl(url: string): string {
|
|
@@ -541,15 +618,25 @@ function fileItemFromUrl(url: string, uid: string): FileUploadListItem {
|
|
|
541
618
|
function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
542
619
|
const { interactive } = useA2uiPreviewMode()
|
|
543
620
|
const { beginUpload } = useFileUploadStatus()
|
|
621
|
+
const uploadMaterial = useMaterialUpload()
|
|
544
622
|
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
545
623
|
component: 'FileUpload',
|
|
546
624
|
changeAction: 'change',
|
|
547
625
|
})
|
|
548
626
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
627
|
+
const sourceMode = props.fileSourceMode === 'asset_only' || props.fileSourceMode === 'upload_or_asset' ? props.fileSourceMode : 'upload_required'
|
|
628
|
+
const assetOptions = normalizeAssetOptions(props.fileAssetOptions)
|
|
629
|
+
const [assetPickerOpen, setAssetPickerOpen] = useState(false)
|
|
549
630
|
const label = readString(props.label)
|
|
550
631
|
const accept = readString(props.accept)
|
|
551
632
|
const help = readString(props.help)
|
|
552
633
|
const reviewCriteria = readString(props.fileReviewCriteria)
|
|
634
|
+
const reviewStatus = props.fileReviewStatus === 'passed' || props.fileReviewStatus === 'failed'
|
|
635
|
+
? props.fileReviewStatus
|
|
636
|
+
: undefined
|
|
637
|
+
const reviewMessage = readString(props.fileReviewMessage)
|
|
638
|
+
const rejectedUrls = asStringArray(props.fileRejectedUrls)
|
|
639
|
+
.filter((url) => /^https?:\/\//i.test(url))
|
|
553
640
|
const exampleUrls = asStringArray(props.fileExampleUrls)
|
|
554
641
|
.filter((url) => /^https?:\/\//i.test(url))
|
|
555
642
|
const fileAcceptTypes = Array.isArray(props.fileAcceptTypes)
|
|
@@ -566,34 +653,58 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
566
653
|
)
|
|
567
654
|
? (props.filePlaceholderIcon as FormFilePlaceholderIcon)
|
|
568
655
|
: 'document'
|
|
656
|
+
const compact = props.compact === true
|
|
569
657
|
const minCount = typeof props.minCount === 'number' ? Math.max(0, props.minCount) : 0
|
|
570
658
|
const maxCount = typeof props.maxCount === 'number' ? props.maxCount : 1
|
|
571
659
|
const hasMaxLimit = maxCount > 0
|
|
572
|
-
const
|
|
660
|
+
const sourceLabel = sourceMode === 'asset_only'
|
|
661
|
+
? '素材 · 仅选择'
|
|
662
|
+
: sourceMode === 'upload_or_asset'
|
|
663
|
+
? '物料或素材'
|
|
664
|
+
: '物料 · 仅上传'
|
|
665
|
+
const initialValues = normalizeFileValues(value)
|
|
573
666
|
const externalUidRef = useRef(0)
|
|
574
667
|
const mountedRef = useRef(true)
|
|
575
668
|
const controllersRef = useRef(new Map<string, AbortController>())
|
|
576
669
|
const setValueRef = useRef(setValue)
|
|
577
670
|
setValueRef.current = setValue
|
|
578
|
-
const [items, setItems] = useState<FileUploadListItem[]>(() =>
|
|
579
|
-
|
|
580
|
-
|
|
671
|
+
const [items, setItems] = useState<FileUploadListItem[]>(() => [
|
|
672
|
+
...initialValues.map((entry) => {
|
|
673
|
+
if (entry.url) return fileItemFromUrl(entry.url, `external-${externalUidRef.current++}`)
|
|
674
|
+
const option = assetOptions.find((item) => item.assetId === entry.assetId)
|
|
675
|
+
return {
|
|
676
|
+
uid: `asset-${entry.assetId}-${externalUidRef.current++}`,
|
|
677
|
+
name: option?.label || `素材 ${entry.assetId}`,
|
|
678
|
+
status: 'done' as const,
|
|
679
|
+
url: option?.previewUrl || option?.url,
|
|
680
|
+
assetId: entry.assetId,
|
|
681
|
+
source: entry.source,
|
|
682
|
+
}
|
|
683
|
+
}),
|
|
684
|
+
...rejectedUrls.map((url) => ({
|
|
685
|
+
...fileItemFromUrl(url, `rejected-${externalUidRef.current++}`),
|
|
686
|
+
status: 'error' as const,
|
|
687
|
+
error: reviewMessage || '审核不合格,请重新上传',
|
|
688
|
+
})),
|
|
689
|
+
])
|
|
581
690
|
const itemsRef = useRef(items)
|
|
582
691
|
itemsRef.current = items
|
|
583
692
|
|
|
584
693
|
const commitItems = useCallback(
|
|
585
694
|
(
|
|
586
695
|
updater: (current: FileUploadListItem[]) => FileUploadListItem[],
|
|
587
|
-
|
|
696
|
+
writeValues: boolean,
|
|
588
697
|
) => {
|
|
589
698
|
const next = updater(itemsRef.current)
|
|
590
699
|
itemsRef.current = next
|
|
591
700
|
if (mountedRef.current) setItems(next)
|
|
592
|
-
if (
|
|
701
|
+
if (writeValues) {
|
|
593
702
|
setValueRef.current(
|
|
594
703
|
next
|
|
595
|
-
.filter((item) => item.status === 'done' && item.url)
|
|
596
|
-
.map((item) => item.
|
|
704
|
+
.filter((item) => item.status === 'done' && (item.assetId || item.url))
|
|
705
|
+
.map((item) => item.assetId
|
|
706
|
+
? { asset_id: item.assetId, source: item.source || 'upload' }
|
|
707
|
+
: item.url as string),
|
|
597
708
|
)
|
|
598
709
|
}
|
|
599
710
|
return next
|
|
@@ -611,27 +722,34 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
611
722
|
}, [])
|
|
612
723
|
|
|
613
724
|
useEffect(() => {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const consumed = new Set<number>()
|
|
725
|
+
const normalized = normalizeFileValues(value)
|
|
726
|
+
if (!Array.isArray(value)) {
|
|
727
|
+
setValueSilent(normalized.map((entry) => entry.assetId
|
|
728
|
+
? { asset_id: entry.assetId, source: entry.source || 'upload' }
|
|
729
|
+
: entry.url))
|
|
730
|
+
}
|
|
731
|
+
const expected = new Set(normalized.map((entry) => entry.assetId ? `asset:${entry.assetId}` : `url:${entry.url}`))
|
|
622
732
|
commitItems(
|
|
623
733
|
(current) => {
|
|
624
|
-
const next = current.filter((item) =>
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
return
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
next.push(fileItemFromUrl(url, `external-${externalUidRef.current++}`))
|
|
734
|
+
const next = current.filter((item) =>
|
|
735
|
+
item.status !== 'done' || expected.has(item.assetId ? `asset:${item.assetId}` : `url:${item.url}`))
|
|
736
|
+
const present = new Set(next.map((item) => item.assetId ? `asset:${item.assetId}` : `url:${item.url}`))
|
|
737
|
+
normalized.forEach((entry) => {
|
|
738
|
+
const key = entry.assetId ? `asset:${entry.assetId}` : `url:${entry.url}`
|
|
739
|
+
if (present.has(key)) return
|
|
740
|
+
if (entry.url) {
|
|
741
|
+
next.push(fileItemFromUrl(entry.url, `external-${externalUidRef.current++}`))
|
|
742
|
+
return
|
|
634
743
|
}
|
|
744
|
+
const option = assetOptions.find((item) => item.assetId === entry.assetId)
|
|
745
|
+
next.push({
|
|
746
|
+
uid: `asset-${entry.assetId}-${externalUidRef.current++}`,
|
|
747
|
+
name: option?.label || `素材 ${entry.assetId}`,
|
|
748
|
+
status: 'done',
|
|
749
|
+
url: option?.previewUrl || option?.url,
|
|
750
|
+
assetId: entry.assetId,
|
|
751
|
+
source: entry.source,
|
|
752
|
+
})
|
|
635
753
|
})
|
|
636
754
|
return next
|
|
637
755
|
},
|
|
@@ -641,26 +759,44 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
641
759
|
|
|
642
760
|
const startUpload = useCallback(
|
|
643
761
|
(file: File & { uid?: string }) => {
|
|
644
|
-
if (!interactive) return
|
|
645
|
-
|
|
762
|
+
if (!interactive || sourceMode === 'asset_only') return
|
|
763
|
+
const activeCount = itemsRef.current.filter((item) => item.status !== 'error').length
|
|
764
|
+
if (hasMaxLimit && activeCount >= maxCount) return
|
|
646
765
|
|
|
647
766
|
const uid = `${file.uid ?? 'file'}-${externalUidRef.current++}`
|
|
648
767
|
const controller = new AbortController()
|
|
649
768
|
controllersRef.current.set(uid, controller)
|
|
650
769
|
commitItems(
|
|
651
|
-
(current) => [...current, { uid, name: file.name, status: 'uploading' }],
|
|
770
|
+
(current) => [...current.filter((item) => item.status !== 'error'), { uid, name: file.name, status: 'uploading' }],
|
|
652
771
|
false,
|
|
653
772
|
)
|
|
654
773
|
const endUpload = beginUpload()
|
|
655
774
|
|
|
656
|
-
|
|
775
|
+
const upload = uploadMaterial
|
|
776
|
+
? uploadMaterial(file, {
|
|
777
|
+
fieldName,
|
|
778
|
+
moduleId: readString(props.moduleId) || undefined,
|
|
779
|
+
positionKey: fieldName || undefined,
|
|
780
|
+
assetSlotKey: readString(props.assetSlotKey) || undefined,
|
|
781
|
+
assetType: readString(props.assetType) || undefined,
|
|
782
|
+
}, { signal: controller.signal })
|
|
783
|
+
: Promise.reject(new Error('当前宿主未配置私有物料上传,请联系管理员'))
|
|
784
|
+
void upload
|
|
657
785
|
.then((result) => {
|
|
658
786
|
if (!mountedRef.current || controller.signal.aborted) return
|
|
659
787
|
commitItems(
|
|
660
788
|
(current) =>
|
|
661
789
|
current.map((item) =>
|
|
662
790
|
item.uid === uid
|
|
663
|
-
? {
|
|
791
|
+
? {
|
|
792
|
+
...item,
|
|
793
|
+
status: 'done',
|
|
794
|
+
name: result.name || item.name,
|
|
795
|
+
url: result.previewUrl,
|
|
796
|
+
assetId: result.assetId || undefined,
|
|
797
|
+
source: result.assetId ? 'upload' : undefined,
|
|
798
|
+
error: undefined,
|
|
799
|
+
}
|
|
664
800
|
: item,
|
|
665
801
|
),
|
|
666
802
|
true,
|
|
@@ -682,7 +818,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
682
818
|
endUpload()
|
|
683
819
|
})
|
|
684
820
|
},
|
|
685
|
-
[beginUpload, commitItems, hasMaxLimit, interactive, maxCount],
|
|
821
|
+
[beginUpload, commitItems, fieldName, hasMaxLimit, interactive, maxCount, props, sourceMode, uploadMaterial],
|
|
686
822
|
)
|
|
687
823
|
|
|
688
824
|
const handleRemove = useCallback(
|
|
@@ -695,7 +831,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
695
831
|
)
|
|
696
832
|
|
|
697
833
|
const uploadedCount = items.filter((item) => item.status === 'done').length
|
|
698
|
-
const atMaxLimit = hasMaxLimit && items.length >= maxCount
|
|
834
|
+
const atMaxLimit = hasMaxLimit && items.filter((item) => item.status !== 'error').length >= maxCount
|
|
699
835
|
const belowMinCount = uploadedCount < minCount
|
|
700
836
|
const latestError = [...items].reverse().find((item) => item.error)?.error
|
|
701
837
|
const fileList: UploadFile[] = items.map((item) => ({
|
|
@@ -705,12 +841,117 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
705
841
|
url: item.url,
|
|
706
842
|
response: item.error,
|
|
707
843
|
}))
|
|
844
|
+
const assetPicker = <Modal title={`选择推荐素材 · ${label || '素材位置'}`} open={assetPickerOpen} footer={null} width={860} onCancel={() => setAssetPickerOpen(false)}>
|
|
845
|
+
{assetOptions.length ? <div className="skopon-asset-picker">
|
|
846
|
+
{assetOptions.map((asset) => {
|
|
847
|
+
const isImage = asset.assetType === 'image' || Boolean(asset.previewUrl && asset.previewUrl !== asset.url)
|
|
848
|
+
const chooseAsset = () => {
|
|
849
|
+
if (!interactive || reviewStatus === 'passed') return
|
|
850
|
+
if (hasMaxLimit && maxCount > 1 && atMaxLimit) return
|
|
851
|
+
if (maxCount === 1) {
|
|
852
|
+
for (const controller of controllersRef.current.values()) controller.abort()
|
|
853
|
+
controllersRef.current.clear()
|
|
854
|
+
}
|
|
855
|
+
commitItems((current) => {
|
|
856
|
+
const retained = maxCount === 1 ? [] : current.filter((item) => item.status !== 'error' && item.url !== asset.url)
|
|
857
|
+
return [...retained, {
|
|
858
|
+
uid: `asset-${asset.assetId}-${externalUidRef.current++}`,
|
|
859
|
+
name: asset.label,
|
|
860
|
+
status: 'done',
|
|
861
|
+
url: asset.previewUrl || asset.url,
|
|
862
|
+
assetId: asset.assetId,
|
|
863
|
+
source: 'library_select',
|
|
864
|
+
}]
|
|
865
|
+
}, true)
|
|
866
|
+
setAssetPickerOpen(false)
|
|
867
|
+
}
|
|
868
|
+
return <article key={asset.assetId} className="skopon-asset-picker__card">
|
|
869
|
+
<div className="skopon-asset-picker__media">
|
|
870
|
+
{isImage
|
|
871
|
+
? <Image src={asset.previewUrl || asset.url} alt={asset.label} preview={{ mask: '放大预览' }} />
|
|
872
|
+
: <div className="skopon-asset-picker__placeholder"><FilePlaceholderIcon type={asset.assetType === 'video' ? 'video' : asset.assetType === 'audio' ? 'audio' : 'document'} size={36} /></div>}
|
|
873
|
+
</div>
|
|
874
|
+
<Typography.Text className="skopon-asset-picker__name" title={asset.label}>{asset.label}</Typography.Text>
|
|
875
|
+
<Button type="primary" size="small" block aria-label={`选择素材 ${asset.label}`} disabled={!interactive || reviewStatus === 'passed' || (hasMaxLimit && maxCount > 1 && atMaxLimit)} onClick={chooseAsset}>使用此素材</Button>
|
|
876
|
+
</article>
|
|
877
|
+
})}
|
|
878
|
+
</div> : <Typography.Paragraph type="secondary">此位置暂无可选素材,请先在素材库为该位置打标。</Typography.Paragraph>}
|
|
879
|
+
</Modal>
|
|
880
|
+
|
|
881
|
+
if (compact) {
|
|
882
|
+
const displayItem = items.find((item) => item.status === 'error') ?? items[0]
|
|
883
|
+
const displayUrl = displayItem?.url
|
|
884
|
+
const isImage = fileAcceptTypes?.includes('image/*') ?? placeholderIcon === 'image'
|
|
885
|
+
const state = reviewStatus === 'passed'
|
|
886
|
+
? 'passed'
|
|
887
|
+
: reviewStatus === 'failed'
|
|
888
|
+
? 'failed'
|
|
889
|
+
: displayItem?.status === 'uploading'
|
|
890
|
+
? 'uploading'
|
|
891
|
+
: displayItem?.status === 'done'
|
|
892
|
+
? 'pending'
|
|
893
|
+
: 'empty'
|
|
894
|
+
const stateText = state === 'passed'
|
|
895
|
+
? '审核通过'
|
|
896
|
+
: state === 'failed'
|
|
897
|
+
? (reviewMessage || '审核不合格,请替换')
|
|
898
|
+
: state === 'uploading'
|
|
899
|
+
? '正在上传…'
|
|
900
|
+
: state === 'pending'
|
|
901
|
+
? '等待审核'
|
|
902
|
+
: `${acceptSummary ?? '文件'}${maxCount > 1 ? `,最多 ${maxCount} 个` : ''}`
|
|
903
|
+
const canChoose = interactive && state !== 'passed' && state !== 'uploading'
|
|
904
|
+
|
|
905
|
+
return (
|
|
906
|
+
<><article className={`skopon-compact-upload skopon-compact-upload--${state}`} data-skopon-field={fieldName || undefined}>
|
|
907
|
+
<div className="skopon-compact-upload__thumb">
|
|
908
|
+
{displayUrl && isImage ? (
|
|
909
|
+
<img src={displayUrl} alt={label || '已上传素材'} />
|
|
910
|
+
) : (
|
|
911
|
+
<FilePlaceholderIcon type={placeholderIcon} size={30} />
|
|
912
|
+
)}
|
|
913
|
+
{state !== 'empty' ? <span className="skopon-compact-upload__state-mark" aria-hidden>
|
|
914
|
+
{state === 'passed' ? '✓' : state === 'failed' ? '!' : state === 'uploading' ? '…' : '•'}
|
|
915
|
+
</span> : null}
|
|
916
|
+
</div>
|
|
917
|
+
<div className="skopon-compact-upload__meta">
|
|
918
|
+
<strong>{label || '上传素材'}</strong>
|
|
919
|
+
<span className={`skopon-compact-upload__source skopon-compact-upload__source--${sourceMode}`}>{sourceLabel}</span>
|
|
920
|
+
<span className="skopon-compact-upload__status">{stateText}</span>
|
|
921
|
+
<div className="skopon-file-source-actions">
|
|
922
|
+
{canChoose && sourceMode !== 'asset_only' ? (
|
|
923
|
+
<Upload
|
|
924
|
+
accept={buildFileAcceptAttribute(fileAcceptTypes, fileAcceptExtensions)}
|
|
925
|
+
multiple={!hasMaxLimit || maxCount > 1}
|
|
926
|
+
showUploadList={false}
|
|
927
|
+
beforeUpload={(file) => {
|
|
928
|
+
if (displayItem?.status === 'done') handleRemove(displayItem.uid)
|
|
929
|
+
startUpload(file)
|
|
930
|
+
return Upload.LIST_IGNORE
|
|
931
|
+
}}
|
|
932
|
+
>
|
|
933
|
+
<Button size="small">上传</Button>
|
|
934
|
+
</Upload>
|
|
935
|
+
) : null}
|
|
936
|
+
{canChoose && sourceMode !== 'upload_required' ? <Button size="small" onClick={() => setAssetPickerOpen(true)}>选择素材</Button> : null}
|
|
937
|
+
</div>
|
|
938
|
+
{latestError && state !== 'failed' ? <span className="skopon-compact-upload__error">{latestError}</span> : null}
|
|
939
|
+
</div>
|
|
940
|
+
</article>{assetPicker}</>
|
|
941
|
+
)
|
|
942
|
+
}
|
|
708
943
|
|
|
709
944
|
return (
|
|
710
945
|
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
711
946
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
712
|
-
{
|
|
713
|
-
|
|
947
|
+
{reviewStatus === 'passed' ? (
|
|
948
|
+
<Typography.Paragraph style={{ color: '#389e0d', marginBottom: 8 }}>✅ 审核通过,已保留,无需重新上传</Typography.Paragraph>
|
|
949
|
+
) : null}
|
|
950
|
+
{reviewStatus === 'failed' ? (
|
|
951
|
+
<Typography.Paragraph type="danger" style={{ marginBottom: 8 }}>❌ 审核不合格:{reviewMessage || '请重新上传'}</Typography.Paragraph>
|
|
952
|
+
) : null}
|
|
953
|
+
{help ? <Typography.Paragraph type="secondary" style={{ whiteSpace: 'pre-line' }}>{help}</Typography.Paragraph> : null}
|
|
954
|
+
{reviewCriteria ? <Typography.Paragraph type="secondary" style={{ whiteSpace: 'pre-line' }}>素材要求:{reviewCriteria}</Typography.Paragraph> : null}
|
|
714
955
|
{exampleUrls.length ? (
|
|
715
956
|
<div className="form-file-examples">
|
|
716
957
|
<Typography.Text type="secondary">参考示例(不作为上传素材提交)</Typography.Text>
|
|
@@ -723,13 +964,15 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
723
964
|
</div>
|
|
724
965
|
</div>
|
|
725
966
|
) : null}
|
|
967
|
+
{sourceMode !== 'upload_required' ? <div className="skopon-file-source-actions"><Button disabled={!interactive || reviewStatus === 'passed'} onClick={() => setAssetPickerOpen(true)}>选择素材</Button></div> : null}
|
|
968
|
+
{sourceMode !== 'asset_only' ?
|
|
726
969
|
<Upload.Dragger
|
|
727
|
-
disabled={!interactive}
|
|
728
|
-
openFileDialogOnClick={!atMaxLimit}
|
|
970
|
+
disabled={!interactive || reviewStatus === 'passed'}
|
|
971
|
+
openFileDialogOnClick={reviewStatus !== 'passed' && !atMaxLimit}
|
|
729
972
|
accept={buildFileAcceptAttribute(fileAcceptTypes, fileAcceptExtensions)}
|
|
730
973
|
className="form-file-upload-preview"
|
|
731
974
|
multiple={!hasMaxLimit || maxCount > 1}
|
|
732
|
-
showUploadList={interactive && items.length > 0}
|
|
975
|
+
showUploadList={interactive && items.length > 0 ? { showRemoveIcon: reviewStatus !== 'passed' } : false}
|
|
733
976
|
fileList={interactive ? fileList : []}
|
|
734
977
|
beforeUpload={(file) => {
|
|
735
978
|
if (!interactive) return Upload.LIST_IGNORE
|
|
@@ -737,6 +980,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
737
980
|
return Upload.LIST_IGNORE
|
|
738
981
|
}}
|
|
739
982
|
onRemove={(file) => {
|
|
983
|
+
if (reviewStatus === 'passed') return false
|
|
740
984
|
handleRemove(file.uid)
|
|
741
985
|
return true
|
|
742
986
|
}}
|
|
@@ -746,11 +990,15 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
746
990
|
</p>
|
|
747
991
|
<p className="form-file-upload-preview-text">点击或拖拽文件到此处上传</p>
|
|
748
992
|
</Upload.Dragger>
|
|
993
|
+
: <div className="skopon-selected-assets">{items.map((item) => <div key={item.uid} className="skopon-selected-assets__row">
|
|
994
|
+
{item.url ? <a href={item.url} target="_blank" rel="noreferrer">{item.name}</a> : <span>{item.name}</span>}
|
|
995
|
+
{interactive && reviewStatus !== 'passed' ? <Button size="small" type="text" onClick={() => handleRemove(item.uid)}>移除</Button> : null}
|
|
996
|
+
</div>)}</div>}
|
|
749
997
|
<Typography.Text type="secondary" className="form-block-preview-help">
|
|
750
998
|
允许:{acceptSummary ?? '全部类型'}
|
|
751
999
|
{minCount > 0 ? `;至少 ${minCount} 个文件` : ''}
|
|
752
|
-
{
|
|
753
|
-
|
|
1000
|
+
{maxCount > 1 ? `;最多 ${maxCount} 个文件` : ''}
|
|
1001
|
+
{sourceMode === 'asset_only' ? ';从推荐素材中选择' : ';选择文件后自动上传至 CDN'}
|
|
754
1002
|
{belowMinCount && interactive ? `(当前 ${uploadedCount} 个,未达下限)` : ''}
|
|
755
1003
|
</Typography.Text>
|
|
756
1004
|
{latestError ? (
|
|
@@ -758,6 +1006,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
758
1006
|
{latestError}
|
|
759
1007
|
</Typography.Text>
|
|
760
1008
|
) : null}
|
|
1009
|
+
{assetPicker}
|
|
761
1010
|
</div>
|
|
762
1011
|
)
|
|
763
1012
|
}
|
|
@@ -925,6 +1174,7 @@ export function buildSkoponCatalog(): Catalog<ReactComponentImplementation> {
|
|
|
925
1174
|
...baseComponents,
|
|
926
1175
|
SkoponMediaImpl,
|
|
927
1176
|
SkoponSelectImpl,
|
|
1177
|
+
SkoponModulePanelImpl,
|
|
928
1178
|
ImageImpl,
|
|
929
1179
|
VideoImpl,
|
|
930
1180
|
AudioPlayerImpl,
|