@skopon-cool/form-sdk 0.6.3 → 0.6.4
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/components/AskUserFormCard.d.ts +2 -0
- package/dist/components/AskUserFormCard.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 +2002 -1797
- 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 +199 -15
- package/src/catalog/fileUploadPreview.test.tsx +56 -0
- package/src/catalog/moduleTabs.test.tsx +59 -0
- package/src/components/AskUserFormCard.tsx +28 -9
- package/src/components/AskUserFormCard.upload.test.tsx +66 -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,6 +32,7 @@ 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,
|
|
@@ -57,6 +60,7 @@ export const SKOPON_CATALOG_ID = 'skopon-form-v0_9'
|
|
|
57
60
|
const SKOPON_COMPONENT_NAMES = new Set([
|
|
58
61
|
'SkoponMedia',
|
|
59
62
|
'SkoponSelect',
|
|
63
|
+
'SkoponModulePanel',
|
|
60
64
|
'Image',
|
|
61
65
|
'Video',
|
|
62
66
|
'AudioPlayer',
|
|
@@ -280,6 +284,58 @@ const SkoponSelectImpl = createBinderlessComponentImplementation(
|
|
|
280
284
|
SkoponSelectPreview,
|
|
281
285
|
)
|
|
282
286
|
|
|
287
|
+
const SkoponModulePanelImpl = createBinderlessComponentImplementation(
|
|
288
|
+
{
|
|
289
|
+
name: 'SkoponModulePanel',
|
|
290
|
+
schema: z.object({
|
|
291
|
+
children: z.array(z.string()).optional(),
|
|
292
|
+
description: z.any().optional(),
|
|
293
|
+
reviewCriteria: z.any().optional(),
|
|
294
|
+
exampleUrls: z.any().optional(),
|
|
295
|
+
}).passthrough(),
|
|
296
|
+
},
|
|
297
|
+
({ context, buildChild }) => {
|
|
298
|
+
const props = context.componentModel.properties as Record<string, unknown>
|
|
299
|
+
const children = Array.isArray(props.children) ? props.children.map(String) : []
|
|
300
|
+
const description = readString(props.description).trim()
|
|
301
|
+
const requirementsTitle = readString(props.requirementsTitle).trim() || '本类素材要求'
|
|
302
|
+
const reviewCriteria = readString(props.reviewCriteria).trim()
|
|
303
|
+
const exampleUrls = Array.isArray(props.exampleUrls)
|
|
304
|
+
? props.exampleUrls.map(String).map((url) => url.trim()).filter(Boolean)
|
|
305
|
+
: []
|
|
306
|
+
return (
|
|
307
|
+
<section className="skopon-module-panel">
|
|
308
|
+
{description ? <Typography.Paragraph type="secondary" className="skopon-module-panel__description">{description}</Typography.Paragraph> : null}
|
|
309
|
+
<div className="skopon-module-panel__grid">
|
|
310
|
+
{children.map((id) => <div className="skopon-module-panel__field" key={id}>{buildChild(id)}</div>)}
|
|
311
|
+
</div>
|
|
312
|
+
{reviewCriteria || exampleUrls.length > 0 ? (
|
|
313
|
+
<aside className="skopon-module-panel__guide">
|
|
314
|
+
{reviewCriteria ? (
|
|
315
|
+
<div className="skopon-module-panel__requirements">
|
|
316
|
+
<Typography.Text strong>{requirementsTitle}</Typography.Text>
|
|
317
|
+
<Typography.Paragraph type="secondary">{reviewCriteria}</Typography.Paragraph>
|
|
318
|
+
</div>
|
|
319
|
+
) : null}
|
|
320
|
+
{exampleUrls.length > 0 ? (
|
|
321
|
+
<div className="skopon-module-panel__examples">
|
|
322
|
+
<Typography.Text strong>参考示例</Typography.Text>
|
|
323
|
+
<div className="skopon-module-panel__example-list">
|
|
324
|
+
<Image.PreviewGroup>
|
|
325
|
+
{exampleUrls.map((url, index) => (
|
|
326
|
+
<Image key={`${url}-${index}`} src={url} alt={`参考示例 ${index + 1}`} width={88} height={88} style={{ objectFit: 'cover' }} />
|
|
327
|
+
))}
|
|
328
|
+
</Image.PreviewGroup>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
) : null}
|
|
332
|
+
</aside>
|
|
333
|
+
) : null}
|
|
334
|
+
</section>
|
|
335
|
+
)
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
283
339
|
function ChoicePickerPreview({ context }: { context: ComponentContext }) {
|
|
284
340
|
const { interactive } = useA2uiPreviewMode()
|
|
285
341
|
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
@@ -546,10 +602,19 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
546
602
|
changeAction: 'change',
|
|
547
603
|
})
|
|
548
604
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
605
|
+
const sourceMode = props.fileSourceMode === 'asset_only' || props.fileSourceMode === 'upload_or_asset' ? props.fileSourceMode : 'upload_required'
|
|
606
|
+
const assetOptions = normalizeAssetOptions(props.fileAssetOptions)
|
|
607
|
+
const [assetPickerOpen, setAssetPickerOpen] = useState(false)
|
|
549
608
|
const label = readString(props.label)
|
|
550
609
|
const accept = readString(props.accept)
|
|
551
610
|
const help = readString(props.help)
|
|
552
611
|
const reviewCriteria = readString(props.fileReviewCriteria)
|
|
612
|
+
const reviewStatus = props.fileReviewStatus === 'passed' || props.fileReviewStatus === 'failed'
|
|
613
|
+
? props.fileReviewStatus
|
|
614
|
+
: undefined
|
|
615
|
+
const reviewMessage = readString(props.fileReviewMessage)
|
|
616
|
+
const rejectedUrls = asStringArray(props.fileRejectedUrls)
|
|
617
|
+
.filter((url) => /^https?:\/\//i.test(url))
|
|
553
618
|
const exampleUrls = asStringArray(props.fileExampleUrls)
|
|
554
619
|
.filter((url) => /^https?:\/\//i.test(url))
|
|
555
620
|
const fileAcceptTypes = Array.isArray(props.fileAcceptTypes)
|
|
@@ -566,18 +631,29 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
566
631
|
)
|
|
567
632
|
? (props.filePlaceholderIcon as FormFilePlaceholderIcon)
|
|
568
633
|
: 'document'
|
|
634
|
+
const compact = props.compact === true
|
|
569
635
|
const minCount = typeof props.minCount === 'number' ? Math.max(0, props.minCount) : 0
|
|
570
636
|
const maxCount = typeof props.maxCount === 'number' ? props.maxCount : 1
|
|
571
637
|
const hasMaxLimit = maxCount > 0
|
|
638
|
+
const sourceLabel = sourceMode === 'asset_only'
|
|
639
|
+
? '素材 · 仅选择'
|
|
640
|
+
: sourceMode === 'upload_or_asset'
|
|
641
|
+
? '物料或素材'
|
|
642
|
+
: '物料 · 仅上传'
|
|
572
643
|
const initialUrls = normalizeFileUrls(value)
|
|
573
644
|
const externalUidRef = useRef(0)
|
|
574
645
|
const mountedRef = useRef(true)
|
|
575
646
|
const controllersRef = useRef(new Map<string, AbortController>())
|
|
576
647
|
const setValueRef = useRef(setValue)
|
|
577
648
|
setValueRef.current = setValue
|
|
578
|
-
const [items, setItems] = useState<FileUploadListItem[]>(() =>
|
|
579
|
-
initialUrls.map((url) => fileItemFromUrl(url, `external-${externalUidRef.current++}`)),
|
|
580
|
-
|
|
649
|
+
const [items, setItems] = useState<FileUploadListItem[]>(() => [
|
|
650
|
+
...initialUrls.map((url) => fileItemFromUrl(url, `external-${externalUidRef.current++}`)),
|
|
651
|
+
...rejectedUrls.map((url) => ({
|
|
652
|
+
...fileItemFromUrl(url, `rejected-${externalUidRef.current++}`),
|
|
653
|
+
status: 'error' as const,
|
|
654
|
+
error: reviewMessage || '审核不合格,请重新上传',
|
|
655
|
+
})),
|
|
656
|
+
])
|
|
581
657
|
const itemsRef = useRef(items)
|
|
582
658
|
itemsRef.current = items
|
|
583
659
|
|
|
@@ -641,14 +717,15 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
641
717
|
|
|
642
718
|
const startUpload = useCallback(
|
|
643
719
|
(file: File & { uid?: string }) => {
|
|
644
|
-
if (!interactive) return
|
|
645
|
-
|
|
720
|
+
if (!interactive || sourceMode === 'asset_only') return
|
|
721
|
+
const activeCount = itemsRef.current.filter((item) => item.status !== 'error').length
|
|
722
|
+
if (hasMaxLimit && activeCount >= maxCount) return
|
|
646
723
|
|
|
647
724
|
const uid = `${file.uid ?? 'file'}-${externalUidRef.current++}`
|
|
648
725
|
const controller = new AbortController()
|
|
649
726
|
controllersRef.current.set(uid, controller)
|
|
650
727
|
commitItems(
|
|
651
|
-
(current) => [...current, { uid, name: file.name, status: 'uploading' }],
|
|
728
|
+
(current) => [...current.filter((item) => item.status !== 'error'), { uid, name: file.name, status: 'uploading' }],
|
|
652
729
|
false,
|
|
653
730
|
)
|
|
654
731
|
const endUpload = beginUpload()
|
|
@@ -682,7 +759,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
682
759
|
endUpload()
|
|
683
760
|
})
|
|
684
761
|
},
|
|
685
|
-
[beginUpload, commitItems, hasMaxLimit, interactive, maxCount],
|
|
762
|
+
[beginUpload, commitItems, hasMaxLimit, interactive, maxCount, sourceMode],
|
|
686
763
|
)
|
|
687
764
|
|
|
688
765
|
const handleRemove = useCallback(
|
|
@@ -695,7 +772,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
695
772
|
)
|
|
696
773
|
|
|
697
774
|
const uploadedCount = items.filter((item) => item.status === 'done').length
|
|
698
|
-
const atMaxLimit = hasMaxLimit && items.length >= maxCount
|
|
775
|
+
const atMaxLimit = hasMaxLimit && items.filter((item) => item.status !== 'error').length >= maxCount
|
|
699
776
|
const belowMinCount = uploadedCount < minCount
|
|
700
777
|
const latestError = [...items].reverse().find((item) => item.error)?.error
|
|
701
778
|
const fileList: UploadFile[] = items.map((item) => ({
|
|
@@ -705,12 +782,110 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
705
782
|
url: item.url,
|
|
706
783
|
response: item.error,
|
|
707
784
|
}))
|
|
785
|
+
const assetPicker = <Modal title={`选择推荐素材 · ${label || '素材位置'}`} open={assetPickerOpen} footer={null} width={860} onCancel={() => setAssetPickerOpen(false)}>
|
|
786
|
+
{assetOptions.length ? <div className="skopon-asset-picker">
|
|
787
|
+
{assetOptions.map((asset) => {
|
|
788
|
+
const isImage = asset.assetType === 'image' || Boolean(asset.previewUrl && asset.previewUrl !== asset.url)
|
|
789
|
+
const chooseAsset = () => {
|
|
790
|
+
if (!interactive || reviewStatus === 'passed') return
|
|
791
|
+
if (hasMaxLimit && maxCount > 1 && atMaxLimit) return
|
|
792
|
+
if (maxCount === 1) {
|
|
793
|
+
for (const controller of controllersRef.current.values()) controller.abort()
|
|
794
|
+
controllersRef.current.clear()
|
|
795
|
+
}
|
|
796
|
+
commitItems((current) => {
|
|
797
|
+
const retained = maxCount === 1 ? [] : current.filter((item) => item.status !== 'error' && item.url !== asset.url)
|
|
798
|
+
return [...retained, { uid: `asset-${asset.assetId}-${externalUidRef.current++}`, name: asset.label, status: 'done', url: asset.url }]
|
|
799
|
+
}, true)
|
|
800
|
+
setAssetPickerOpen(false)
|
|
801
|
+
}
|
|
802
|
+
return <article key={asset.assetId} className="skopon-asset-picker__card">
|
|
803
|
+
<div className="skopon-asset-picker__media">
|
|
804
|
+
{isImage
|
|
805
|
+
? <Image src={asset.previewUrl || asset.url} alt={asset.label} preview={{ mask: '放大预览' }} />
|
|
806
|
+
: <div className="skopon-asset-picker__placeholder"><FilePlaceholderIcon type={asset.assetType === 'video' ? 'video' : asset.assetType === 'audio' ? 'audio' : 'document'} size={36} /></div>}
|
|
807
|
+
</div>
|
|
808
|
+
<Typography.Text className="skopon-asset-picker__name" title={asset.label}>{asset.label}</Typography.Text>
|
|
809
|
+
<Button type="primary" size="small" block aria-label={`选择素材 ${asset.label}`} disabled={!interactive || reviewStatus === 'passed' || (hasMaxLimit && maxCount > 1 && atMaxLimit)} onClick={chooseAsset}>使用此素材</Button>
|
|
810
|
+
</article>
|
|
811
|
+
})}
|
|
812
|
+
</div> : <Typography.Paragraph type="secondary">此位置暂无可选素材,请先在素材库为该位置打标。</Typography.Paragraph>}
|
|
813
|
+
</Modal>
|
|
814
|
+
|
|
815
|
+
if (compact) {
|
|
816
|
+
const displayItem = items.find((item) => item.status === 'error') ?? items[0]
|
|
817
|
+
const displayUrl = displayItem?.url
|
|
818
|
+
const isImage = fileAcceptTypes?.includes('image/*') ?? placeholderIcon === 'image'
|
|
819
|
+
const state = reviewStatus === 'passed'
|
|
820
|
+
? 'passed'
|
|
821
|
+
: reviewStatus === 'failed'
|
|
822
|
+
? 'failed'
|
|
823
|
+
: displayItem?.status === 'uploading'
|
|
824
|
+
? 'uploading'
|
|
825
|
+
: displayItem?.status === 'done'
|
|
826
|
+
? 'pending'
|
|
827
|
+
: 'empty'
|
|
828
|
+
const stateText = state === 'passed'
|
|
829
|
+
? '审核通过'
|
|
830
|
+
: state === 'failed'
|
|
831
|
+
? (reviewMessage || '审核不合格,请替换')
|
|
832
|
+
: state === 'uploading'
|
|
833
|
+
? '正在上传…'
|
|
834
|
+
: state === 'pending'
|
|
835
|
+
? '等待审核'
|
|
836
|
+
: `${acceptSummary ?? '文件'}${maxCount > 1 ? `,最多 ${maxCount} 个` : ''}`
|
|
837
|
+
const canChoose = interactive && state !== 'passed' && state !== 'uploading'
|
|
838
|
+
|
|
839
|
+
return (
|
|
840
|
+
<><article className={`skopon-compact-upload skopon-compact-upload--${state}`} data-skopon-field={fieldName || undefined}>
|
|
841
|
+
<div className="skopon-compact-upload__thumb">
|
|
842
|
+
{displayUrl && isImage ? (
|
|
843
|
+
<img src={displayUrl} alt={label || '已上传素材'} />
|
|
844
|
+
) : (
|
|
845
|
+
<FilePlaceholderIcon type={placeholderIcon} size={30} />
|
|
846
|
+
)}
|
|
847
|
+
{state !== 'empty' ? <span className="skopon-compact-upload__state-mark" aria-hidden>
|
|
848
|
+
{state === 'passed' ? '✓' : state === 'failed' ? '!' : state === 'uploading' ? '…' : '•'}
|
|
849
|
+
</span> : null}
|
|
850
|
+
</div>
|
|
851
|
+
<div className="skopon-compact-upload__meta">
|
|
852
|
+
<strong>{label || '上传素材'}</strong>
|
|
853
|
+
<span className={`skopon-compact-upload__source skopon-compact-upload__source--${sourceMode}`}>{sourceLabel}</span>
|
|
854
|
+
<span className="skopon-compact-upload__status">{stateText}</span>
|
|
855
|
+
<div className="skopon-file-source-actions">
|
|
856
|
+
{canChoose && sourceMode !== 'asset_only' ? (
|
|
857
|
+
<Upload
|
|
858
|
+
accept={buildFileAcceptAttribute(fileAcceptTypes, fileAcceptExtensions)}
|
|
859
|
+
multiple={!hasMaxLimit || maxCount > 1}
|
|
860
|
+
showUploadList={false}
|
|
861
|
+
beforeUpload={(file) => {
|
|
862
|
+
if (displayItem?.status === 'done') handleRemove(displayItem.uid)
|
|
863
|
+
startUpload(file)
|
|
864
|
+
return Upload.LIST_IGNORE
|
|
865
|
+
}}
|
|
866
|
+
>
|
|
867
|
+
<Button size="small">上传</Button>
|
|
868
|
+
</Upload>
|
|
869
|
+
) : null}
|
|
870
|
+
{canChoose && sourceMode !== 'upload_required' ? <Button size="small" onClick={() => setAssetPickerOpen(true)}>选择素材</Button> : null}
|
|
871
|
+
</div>
|
|
872
|
+
{latestError && state !== 'failed' ? <span className="skopon-compact-upload__error">{latestError}</span> : null}
|
|
873
|
+
</div>
|
|
874
|
+
</article>{assetPicker}</>
|
|
875
|
+
)
|
|
876
|
+
}
|
|
708
877
|
|
|
709
878
|
return (
|
|
710
879
|
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
711
880
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
712
|
-
{
|
|
713
|
-
|
|
881
|
+
{reviewStatus === 'passed' ? (
|
|
882
|
+
<Typography.Paragraph style={{ color: '#389e0d', marginBottom: 8 }}>✅ 审核通过,已保留,无需重新上传</Typography.Paragraph>
|
|
883
|
+
) : null}
|
|
884
|
+
{reviewStatus === 'failed' ? (
|
|
885
|
+
<Typography.Paragraph type="danger" style={{ marginBottom: 8 }}>❌ 审核不合格:{reviewMessage || '请重新上传'}</Typography.Paragraph>
|
|
886
|
+
) : null}
|
|
887
|
+
{help ? <Typography.Paragraph type="secondary" style={{ whiteSpace: 'pre-line' }}>{help}</Typography.Paragraph> : null}
|
|
888
|
+
{reviewCriteria ? <Typography.Paragraph type="secondary" style={{ whiteSpace: 'pre-line' }}>素材要求:{reviewCriteria}</Typography.Paragraph> : null}
|
|
714
889
|
{exampleUrls.length ? (
|
|
715
890
|
<div className="form-file-examples">
|
|
716
891
|
<Typography.Text type="secondary">参考示例(不作为上传素材提交)</Typography.Text>
|
|
@@ -723,13 +898,15 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
723
898
|
</div>
|
|
724
899
|
</div>
|
|
725
900
|
) : null}
|
|
901
|
+
{sourceMode !== 'upload_required' ? <div className="skopon-file-source-actions"><Button disabled={!interactive || reviewStatus === 'passed'} onClick={() => setAssetPickerOpen(true)}>选择素材</Button></div> : null}
|
|
902
|
+
{sourceMode !== 'asset_only' ?
|
|
726
903
|
<Upload.Dragger
|
|
727
|
-
disabled={!interactive}
|
|
728
|
-
openFileDialogOnClick={!atMaxLimit}
|
|
904
|
+
disabled={!interactive || reviewStatus === 'passed'}
|
|
905
|
+
openFileDialogOnClick={reviewStatus !== 'passed' && !atMaxLimit}
|
|
729
906
|
accept={buildFileAcceptAttribute(fileAcceptTypes, fileAcceptExtensions)}
|
|
730
907
|
className="form-file-upload-preview"
|
|
731
908
|
multiple={!hasMaxLimit || maxCount > 1}
|
|
732
|
-
showUploadList={interactive && items.length > 0}
|
|
909
|
+
showUploadList={interactive && items.length > 0 ? { showRemoveIcon: reviewStatus !== 'passed' } : false}
|
|
733
910
|
fileList={interactive ? fileList : []}
|
|
734
911
|
beforeUpload={(file) => {
|
|
735
912
|
if (!interactive) return Upload.LIST_IGNORE
|
|
@@ -737,6 +914,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
737
914
|
return Upload.LIST_IGNORE
|
|
738
915
|
}}
|
|
739
916
|
onRemove={(file) => {
|
|
917
|
+
if (reviewStatus === 'passed') return false
|
|
740
918
|
handleRemove(file.uid)
|
|
741
919
|
return true
|
|
742
920
|
}}
|
|
@@ -746,11 +924,15 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
746
924
|
</p>
|
|
747
925
|
<p className="form-file-upload-preview-text">点击或拖拽文件到此处上传</p>
|
|
748
926
|
</Upload.Dragger>
|
|
927
|
+
: <div className="skopon-selected-assets">{items.map((item) => <div key={item.uid} className="skopon-selected-assets__row">
|
|
928
|
+
{item.url ? <a href={item.url} target="_blank" rel="noreferrer">{item.name}</a> : <span>{item.name}</span>}
|
|
929
|
+
{interactive && reviewStatus !== 'passed' ? <Button size="small" type="text" onClick={() => handleRemove(item.uid)}>移除</Button> : null}
|
|
930
|
+
</div>)}</div>}
|
|
749
931
|
<Typography.Text type="secondary" className="form-block-preview-help">
|
|
750
932
|
允许:{acceptSummary ?? '全部类型'}
|
|
751
933
|
{minCount > 0 ? `;至少 ${minCount} 个文件` : ''}
|
|
752
|
-
{
|
|
753
|
-
|
|
934
|
+
{maxCount > 1 ? `;最多 ${maxCount} 个文件` : ''}
|
|
935
|
+
{sourceMode === 'asset_only' ? ';从推荐素材中选择' : ';选择文件后自动上传至 CDN'}
|
|
754
936
|
{belowMinCount && interactive ? `(当前 ${uploadedCount} 个,未达下限)` : ''}
|
|
755
937
|
</Typography.Text>
|
|
756
938
|
{latestError ? (
|
|
@@ -758,6 +940,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
758
940
|
{latestError}
|
|
759
941
|
</Typography.Text>
|
|
760
942
|
) : null}
|
|
943
|
+
{assetPicker}
|
|
761
944
|
</div>
|
|
762
945
|
)
|
|
763
946
|
}
|
|
@@ -925,6 +1108,7 @@ export function buildSkoponCatalog(): Catalog<ReactComponentImplementation> {
|
|
|
925
1108
|
...baseComponents,
|
|
926
1109
|
SkoponMediaImpl,
|
|
927
1110
|
SkoponSelectImpl,
|
|
1111
|
+
SkoponModulePanelImpl,
|
|
928
1112
|
ImageImpl,
|
|
929
1113
|
VideoImpl,
|
|
930
1114
|
AudioPlayerImpl,
|
|
@@ -58,6 +58,9 @@ function renderFileSurface(options: {
|
|
|
58
58
|
onUploadingChange?: (uploading: boolean) => void
|
|
59
59
|
fileExampleUrls?: string[]
|
|
60
60
|
fileReviewCriteria?: string
|
|
61
|
+
fileReviewStatus?: 'passed' | 'failed'
|
|
62
|
+
fileReviewMessage?: string
|
|
63
|
+
fileRejectedUrls?: string[]
|
|
61
64
|
help?: string
|
|
62
65
|
} = {}) {
|
|
63
66
|
const doc = blocksToA2ui(
|
|
@@ -75,6 +78,9 @@ function renderFileSurface(options: {
|
|
|
75
78
|
defaultValue: options.defaultValue,
|
|
76
79
|
fileExampleUrls: options.fileExampleUrls,
|
|
77
80
|
fileReviewCriteria: options.fileReviewCriteria,
|
|
81
|
+
fileReviewStatus: options.fileReviewStatus,
|
|
82
|
+
fileReviewMessage: options.fileReviewMessage,
|
|
83
|
+
fileRejectedUrls: options.fileRejectedUrls,
|
|
78
84
|
help: options.help,
|
|
79
85
|
},
|
|
80
86
|
],
|
|
@@ -111,6 +117,28 @@ describe('FileUploadPreview', () => {
|
|
|
111
117
|
expect(getByAltText('附件示例 1').getAttribute('src')).toBe('https://cdn.example.com/example.jpg')
|
|
112
118
|
expect(ref.current?.getValues()).toEqual({ attachments: [] })
|
|
113
119
|
})
|
|
120
|
+
it('shows a rejected uploaded file and reason without treating its old URL as a valid answer', () => {
|
|
121
|
+
const { getByText, container, ref } = renderFileSurface({
|
|
122
|
+
minCount: 1,
|
|
123
|
+
fileReviewStatus: 'failed',
|
|
124
|
+
fileReviewMessage: '场景中检测到人物',
|
|
125
|
+
fileRejectedUrls: ['https://cdn.example.com/rejected-scene.jpg'],
|
|
126
|
+
})
|
|
127
|
+
expect(getByText('❌ 审核不合格:场景中检测到人物')).toBeTruthy()
|
|
128
|
+
expect(container.textContent).toContain('rejected-scene.jpg')
|
|
129
|
+
expect(ref.current?.getValues()).toEqual({ attachments: [] })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('shows and locks a retained approved upload', () => {
|
|
133
|
+
const { getByText, container, ref } = renderFileSurface({
|
|
134
|
+
defaultValue: ['https://cdn.example.com/approved.jpg'],
|
|
135
|
+
fileReviewStatus: 'passed',
|
|
136
|
+
})
|
|
137
|
+
expect(getByText('✅ 审核通过,已保留,无需重新上传')).toBeTruthy()
|
|
138
|
+
expect(container.textContent).toContain('approved.jpg')
|
|
139
|
+
expect(container.querySelector('input[type="file"]')?.hasAttribute('disabled')).toBe(true)
|
|
140
|
+
expect(ref.current?.getValues()).toEqual({ attachments: ['https://cdn.example.com/approved.jpg'] })
|
|
141
|
+
})
|
|
114
142
|
it('stores successful CDN URLs in user selection order', async () => {
|
|
115
143
|
const first = deferred<{ url: string }>()
|
|
116
144
|
const second = deferred<{ url: string }>()
|
|
@@ -192,3 +220,31 @@ describe('FileUploadPreview', () => {
|
|
|
192
220
|
expect(getByText('legacy file.pdf')).toBeTruthy()
|
|
193
221
|
})
|
|
194
222
|
})
|
|
223
|
+
|
|
224
|
+
describe('Footage selection in generic material forms', () => {
|
|
225
|
+
function renderAssetField(mode: 'asset_only' | 'upload_or_asset') {
|
|
226
|
+
const ref = createRef<SkoponFormRendererRef>()
|
|
227
|
+
const doc = blocksToA2ui({ blocks: [{
|
|
228
|
+
id: 'asset-field', type: 'file', name: 'scene', label: '场景', fileSourceMode: mode,
|
|
229
|
+
fileMaxCount: 1, fileAssetOptions: [{ assetId: '17', label: '山景', assetType: 'image', url: 'https://cdn.test/scene.jpg' }],
|
|
230
|
+
}] }, { includeHeader: false })
|
|
231
|
+
return { ref, ...render(createElement(SkoponFormRenderer, { ref, doc, surfaceId: 'asset-test', fieldNames: ['scene'] })) }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it('asset-only fields choose from Footage and submit the URL without uploading', async () => {
|
|
235
|
+
const view = renderAssetField('asset_only')
|
|
236
|
+
expect(view.container.querySelector('input[type="file"]')).toBeNull()
|
|
237
|
+
fireEvent.click(view.getByRole('button', { name: '选择素材' }))
|
|
238
|
+
fireEvent.click(await view.findByRole('button', { name: '选择素材 山景' }))
|
|
239
|
+
await waitFor(() => expect(view.ref.current?.getValues().scene).toEqual(['https://cdn.test/scene.jpg']))
|
|
240
|
+
expect(uploadMock).not.toHaveBeenCalled()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('mixed fields retain both upload and library selection actions', async () => {
|
|
244
|
+
const view = renderAssetField('upload_or_asset')
|
|
245
|
+
expect(view.container.querySelector('input[type="file"]')).not.toBeNull()
|
|
246
|
+
fireEvent.click(view.getByRole('button', { name: '选择素材' }))
|
|
247
|
+
fireEvent.click(await view.findByRole('button', { name: '选择素材 山景' }))
|
|
248
|
+
await waitFor(() => expect(view.ref.current?.getValues().scene).toEqual(['https://cdn.test/scene.jpg']))
|
|
249
|
+
})
|
|
250
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('@a2ui/web_core/v0_9/basic_catalog', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import('@a2ui/web_core/v0_9/basic_catalog')>()
|
|
6
|
+
return { ...actual, injectBasicCatalogStyles: vi.fn() }
|
|
7
|
+
})
|
|
8
|
+
vi.mock('@a2ui/react/styles', () => ({ injectStyles: vi.fn(), removeStyles: vi.fn() }))
|
|
9
|
+
|
|
10
|
+
import { createElement } from 'react'
|
|
11
|
+
import { cleanup, fireEvent, render } from '@testing-library/react'
|
|
12
|
+
import { blocksToA2ui } from '../adapter/a2uiAdapter'
|
|
13
|
+
import SkoponFormRenderer from '../components/SkoponFormRenderer'
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} }
|
|
17
|
+
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
|
18
|
+
})
|
|
19
|
+
afterEach(cleanup)
|
|
20
|
+
|
|
21
|
+
describe('form module tabs', () => {
|
|
22
|
+
it('renders grouped upload fields and module guidance inside tabs', () => {
|
|
23
|
+
const doc = blocksToA2ui({
|
|
24
|
+
title: '',
|
|
25
|
+
description: '',
|
|
26
|
+
modules: [
|
|
27
|
+
{ id: 'person', label: '人物', reviewCriteria: '人物五官清晰' },
|
|
28
|
+
{ id: 'scene', label: '场景', reviewCriteria: '场景中不能有人物' },
|
|
29
|
+
],
|
|
30
|
+
blocks: [
|
|
31
|
+
{ id: 'p', type: 'file', name: 'person', label: '人物图', moduleId: 'person', fileSourceMode: 'upload_required', fileReviewCriteria: '人物五官清晰' },
|
|
32
|
+
{ id: 'p2', type: 'file', name: 'person2', label: '人物备选图', moduleId: 'person', fileSourceMode: 'upload_or_asset', fileReviewCriteria: '人物五官清晰' },
|
|
33
|
+
{ id: 's', type: 'file', name: 'scene', label: '场景图', moduleId: 'scene', fileSourceMode: 'asset_only', fileReviewCriteria: '场景中不能有人物' },
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
const view = render(createElement(SkoponFormRenderer, { doc, surfaceId: 'modules-test' }))
|
|
37
|
+
expect(view.container.querySelector('.skopon-compact-upload')).toBeTruthy()
|
|
38
|
+
expect(view.container.querySelector('.ant-upload-drag')).toBeNull()
|
|
39
|
+
expect(view.getByText('人物图')).toBeTruthy()
|
|
40
|
+
expect(view.getByText('物料 · 仅上传')).toBeTruthy()
|
|
41
|
+
expect(view.container.querySelectorAll('.skopon-compact-upload__state-mark')).toHaveLength(0)
|
|
42
|
+
expect(view.container.textContent).not.toContain('最多 1')
|
|
43
|
+
const cards = view.container.querySelectorAll('.skopon-compact-upload')
|
|
44
|
+
expect(cards).toHaveLength(2)
|
|
45
|
+
expect(cards[0].querySelector('button')?.textContent?.replace(/\s/g, '')).toContain('上传')
|
|
46
|
+
expect(cards[0].textContent).not.toContain('选择素材')
|
|
47
|
+
expect(cards[1].textContent).toContain('物料或素材')
|
|
48
|
+
expect(cards[1].textContent?.replace(/\s/g, '')).toContain('上传')
|
|
49
|
+
expect(cards[1].textContent).toContain('选择素材')
|
|
50
|
+
expect(view.getByText('人物素材要求')).toBeTruthy()
|
|
51
|
+
expect(view.getByText('人物五官清晰')).toBeTruthy()
|
|
52
|
+
fireEvent.click(view.getByRole('button', { name: '场景' }))
|
|
53
|
+
expect(view.getByText('场景图')).toBeTruthy()
|
|
54
|
+
expect(view.getByText('素材 · 仅选择')).toBeTruthy()
|
|
55
|
+
expect(view.queryByRole('button', { name: '上传' })).toBeNull()
|
|
56
|
+
expect(view.getByRole('button', { name: '选择素材' })).toBeTruthy()
|
|
57
|
+
expect(view.getByText('场景中不能有人物')).toBeTruthy()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -61,6 +61,8 @@ export interface AskUserFormCardProps {
|
|
|
61
61
|
payload: unknown
|
|
62
62
|
/** 可选;无则跳过拉取 vt_forms,仅用 payload blocks fallback */
|
|
63
63
|
formUniqueId?: string | null
|
|
64
|
+
/** 开放卡使用 latest;已提交/失效卡使用 snapshot,冻结发卡时外观。 */
|
|
65
|
+
formDefinitionPolicy?: 'latest' | 'snapshot'
|
|
64
66
|
/**
|
|
65
67
|
* ask_source_unique_id(Flow/QA 业务 id)。
|
|
66
68
|
* 有值且 sessionStorage.auth_user.realtime_ws_url 存在、且渲染出表单控件时按卡建连。
|
|
@@ -90,6 +92,7 @@ interface AskUserFormCardBodyProps extends AskUserFormCardProps {
|
|
|
90
92
|
function AskUserFormCardBody({
|
|
91
93
|
payload,
|
|
92
94
|
formUniqueId,
|
|
95
|
+
formDefinitionPolicy = 'latest',
|
|
93
96
|
askUserMessageId,
|
|
94
97
|
callbackUrl,
|
|
95
98
|
submitMode = 'curl',
|
|
@@ -147,7 +150,7 @@ function AskUserFormCardBody({
|
|
|
147
150
|
|
|
148
151
|
useEffect(() => {
|
|
149
152
|
let cancelled = false
|
|
150
|
-
if (!resolvedFormUniqueId) {
|
|
153
|
+
if (!resolvedFormUniqueId || formDefinitionPolicy === 'snapshot') {
|
|
151
154
|
setLoading(false)
|
|
152
155
|
setFormDisabled(false)
|
|
153
156
|
setFormDef(null)
|
|
@@ -178,18 +181,30 @@ function AskUserFormCardBody({
|
|
|
178
181
|
return () => {
|
|
179
182
|
cancelled = true
|
|
180
183
|
}
|
|
181
|
-
}, [resolvedFormUniqueId, fetchFormDetail])
|
|
184
|
+
}, [resolvedFormUniqueId, fetchFormDetail, formDefinitionPolicy])
|
|
182
185
|
|
|
183
186
|
const payloadDef = useMemo(() => parsePayloadBlocksJson(payload), [payload])
|
|
184
187
|
|
|
188
|
+
// Older hibot servers omitted schema when persisting card_data.payload.
|
|
189
|
+
// Runtime source mode is unique to the dynamic material collection payload,
|
|
190
|
+
// so retain its snapshot blocks in that compatibility case as well.
|
|
191
|
+
const dynamicMaterialForm = payloadDef?.schema === 'skopon.material-form/v2' || Boolean(
|
|
192
|
+
payloadDef?.rawBlocks?.some((block) =>
|
|
193
|
+
block.type === 'file' &&
|
|
194
|
+
['upload_required', 'upload_or_asset', 'asset_only'].includes(String(block.fileSourceMode)),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
185
198
|
const intersection = useMemo(() => {
|
|
186
199
|
if (!payloadDef) return null
|
|
187
|
-
return intersectPayloadBlocksWithForm(payloadDef, formDef ?? undefined
|
|
188
|
-
|
|
200
|
+
return intersectPayloadBlocksWithForm(payloadDef, formDef ?? undefined, {
|
|
201
|
+
definitionPolicy: formDefinitionPolicy,
|
|
202
|
+
})
|
|
203
|
+
}, [payloadDef, formDef, formDefinitionPolicy])
|
|
189
204
|
|
|
190
205
|
const matchedBlocks = intersection?.matchedBlocks ?? []
|
|
191
206
|
const usePayloadFallback =
|
|
192
|
-
Boolean(payloadDef && payloadHasInputBlocks(payloadDef) && matchedBlocks.length === 0)
|
|
207
|
+
Boolean(payloadDef && payloadHasInputBlocks(payloadDef) && (dynamicMaterialForm || matchedBlocks.length === 0))
|
|
193
208
|
|
|
194
209
|
const renderBlocks = useMemo(() => {
|
|
195
210
|
if (usePayloadFallback && payloadDef) {
|
|
@@ -224,16 +239,17 @@ function AskUserFormCardBody({
|
|
|
224
239
|
[renderBlocks],
|
|
225
240
|
)
|
|
226
241
|
|
|
227
|
-
const surfaceTitle = intersection?.title ?? payloadDef?.title ?? ''
|
|
228
|
-
const surfaceDescription = intersection?.description ?? payloadDef?.description ?? ''
|
|
242
|
+
const surfaceTitle = dynamicMaterialForm ? payloadDef?.title ?? '' : intersection?.title ?? payloadDef?.title ?? ''
|
|
243
|
+
const surfaceDescription = dynamicMaterialForm ? payloadDef?.description ?? '' : intersection?.description ?? payloadDef?.description ?? ''
|
|
244
|
+
const surfaceModules = dynamicMaterialForm ? payloadDef?.modules : intersection?.modules ?? payloadDef?.modules
|
|
229
245
|
|
|
230
246
|
const surfaceDoc = useMemo(() => {
|
|
231
247
|
if (renderBlocks.length === 0) return null
|
|
232
248
|
return buildAskUserSurface(
|
|
233
|
-
{ title: surfaceTitle, description: surfaceDescription },
|
|
249
|
+
{ title: surfaceTitle, description: surfaceDescription, modules: surfaceModules },
|
|
234
250
|
renderBlocks,
|
|
235
251
|
)
|
|
236
|
-
}, [surfaceTitle, surfaceDescription, renderBlocks])
|
|
252
|
+
}, [surfaceTitle, surfaceDescription, surfaceModules, renderBlocks])
|
|
237
253
|
|
|
238
254
|
const docKey = useMemo(() => {
|
|
239
255
|
if (!surfaceDoc) return ''
|
|
@@ -370,8 +386,10 @@ function AskUserFormCardBody({
|
|
|
370
386
|
const effectiveForm =
|
|
371
387
|
renderBlocks.length > 0
|
|
372
388
|
? {
|
|
389
|
+
...(dynamicMaterialForm ? { schema: 'skopon.material-form/v2' as const } : {}),
|
|
373
390
|
title: surfaceTitle,
|
|
374
391
|
description: surfaceDescription,
|
|
392
|
+
modules: surfaceModules,
|
|
375
393
|
blocks: renderBlocks,
|
|
376
394
|
}
|
|
377
395
|
: (payloadDef ?? undefined)
|
|
@@ -411,6 +429,7 @@ function AskUserFormCardBody({
|
|
|
411
429
|
renderBlocks,
|
|
412
430
|
surfaceTitle,
|
|
413
431
|
surfaceDescription,
|
|
432
|
+
surfaceModules,
|
|
414
433
|
extraValues,
|
|
415
434
|
reportSnapshot,
|
|
416
435
|
runtime,
|
|
@@ -39,6 +39,29 @@ beforeEach(() => {
|
|
|
39
39
|
afterEach(cleanup)
|
|
40
40
|
|
|
41
41
|
describe('AskUserFormCard file upload guard', () => {
|
|
42
|
+
it('freezes settled cards on the message snapshot without fetching latest definition', async () => {
|
|
43
|
+
const fetchFormDetail = vi.fn()
|
|
44
|
+
const snapshotBlock = {
|
|
45
|
+
id: 'snapshot-title',
|
|
46
|
+
type: 'text' as const,
|
|
47
|
+
name: 'title',
|
|
48
|
+
label: '历史标题',
|
|
49
|
+
defaultValue: '已提交内容',
|
|
50
|
+
}
|
|
51
|
+
const view = render(
|
|
52
|
+
createElement(AskUserFormCard, {
|
|
53
|
+
payload: { title: '历史表单', description: '', blocks: [snapshotBlock] },
|
|
54
|
+
formUniqueId: 'form-versioned',
|
|
55
|
+
formDefinitionPolicy: 'snapshot',
|
|
56
|
+
callbackUrl: null,
|
|
57
|
+
fetchFormDetail,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await waitFor(() => expect(view.getByText('历史标题')).toBeTruthy())
|
|
62
|
+
expect(fetchFormDetail).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
42
65
|
it('disables submit until every file upload settles', async () => {
|
|
43
66
|
let resolveUpload!: (value: { url: string }) => void
|
|
44
67
|
uploadMock.mockReturnValue(
|
|
@@ -89,3 +112,46 @@ describe('AskUserFormCard file upload guard', () => {
|
|
|
89
112
|
await waitFor(() => expect(submit.disabled).toBe(false))
|
|
90
113
|
})
|
|
91
114
|
})
|
|
115
|
+
|
|
116
|
+
it('generic material schemas render every runtime position even when only some keys match the stored form', async () => {
|
|
117
|
+
const field = { id: 'portrait', type: 'file' as const, name: 'portrait', label: '人物', moduleId: 'people' }
|
|
118
|
+
const view = render(createElement(AskUserFormCard, {
|
|
119
|
+
payload: { schema: 'skopon.material-form/v2', title: '通用素材收集', modules: [{ id: 'people', label: '人物 Tab' }], blocks: [field, { ...field, id: 'outfit', name: 'outfit', label: '服装' }] },
|
|
120
|
+
formUniqueId: 'generic-form', callbackUrl: '/callback',
|
|
121
|
+
fetchFormDetail: async () => ({ formUniqueId: 'generic-form', disabled: false, formDefinition: normalizeFormDefinition({ title: '旧标题', blocks: [field], jsonSchema: {} }) }),
|
|
122
|
+
}))
|
|
123
|
+
await waitFor(() => expect(view.getByText('服装')).toBeTruthy())
|
|
124
|
+
expect(view.getByText('人物')).toBeTruthy()
|
|
125
|
+
expect(view.getByText('人物 Tab')).toBeTruthy()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('keeps runtime material source modes when hibot snapshot omits the schema marker', async () => {
|
|
129
|
+
const module = { id: 'materials', label: '素材' }
|
|
130
|
+
const payloadBlocks = [
|
|
131
|
+
{ id: 'upload', name: 'upload', type: 'file' as const, label: '物料', moduleId: 'materials', fileSourceMode: 'upload_required' as const },
|
|
132
|
+
{ id: 'asset', name: 'asset', type: 'file' as const, label: '素材', moduleId: 'materials', fileSourceMode: 'asset_only' as const },
|
|
133
|
+
{ id: 'mixed', name: 'mixed', type: 'file' as const, label: '物料或素材', moduleId: 'materials', fileSourceMode: 'upload_or_asset' as const },
|
|
134
|
+
]
|
|
135
|
+
const storedBlocks = payloadBlocks.map(({ fileSourceMode: _mode, ...block }) => block)
|
|
136
|
+
const formDefinition = normalizeFormDefinition({
|
|
137
|
+
title: '存量表单', modules: [module], blocks: storedBlocks, jsonSchema: {},
|
|
138
|
+
})
|
|
139
|
+
const view = render(createElement(AskUserFormCard, {
|
|
140
|
+
payload: { title: '本次素材', modules: [module], blocks: payloadBlocks },
|
|
141
|
+
formUniqueId: 'generic-form', callbackUrl: '/callback',
|
|
142
|
+
fetchFormDetail: async () => ({ formUniqueId: 'generic-form', disabled: false, formDefinition }),
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(view.container.querySelectorAll('.skopon-compact-upload')).toHaveLength(3))
|
|
146
|
+
const cards = view.container.querySelectorAll('.skopon-compact-upload')
|
|
147
|
+
const text = (index: number) => (cards[index].textContent || '').replace(/\s/g, '')
|
|
148
|
+
expect(text(0)).toContain('物料·仅上传')
|
|
149
|
+
expect(text(0)).toContain('上传')
|
|
150
|
+
expect(text(0)).not.toContain('选择素材')
|
|
151
|
+
expect(text(1)).toContain('素材·仅选择')
|
|
152
|
+
expect(text(1)).toContain('选择素材')
|
|
153
|
+
expect(text(1)).not.toContain('上传')
|
|
154
|
+
expect(text(2)).toContain('物料或素材')
|
|
155
|
+
expect(text(2)).toContain('上传')
|
|
156
|
+
expect(text(2)).toContain('选择素材')
|
|
157
|
+
})
|