@baishuyun/ai-design 2.1.0-alpha.2 → 2.1.0
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/.storybook/preview.ts +2 -0
- package/CHANGELOG.md +189 -20
- package/package.json +8 -9
- package/scripts/post-build.cjs +32 -0
- package/src/components/agent-drawer.tsx +119 -0
- package/src/components/agent-helper/agent-entry-card/index.tsx +3 -8
- package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
- package/src/components/agent-helper/app-select/index.tsx +119 -84
- package/src/components/agent-helper/condition-input/index.tsx +18 -21
- package/src/components/agent-helper/condition-item/index.tsx +1 -0
- package/src/components/agent-helper/field-edit-preference-select/index.tsx +31 -4
- package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
- package/src/components/agent-helper/field-value-input/index.tsx +3 -2
- package/src/components/agent-helper/field-value-input/picker-inputs.tsx +20 -18
- package/src/components/agent-helper/form-select/index.tsx +12 -25
- package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
- package/src/components/agent-helper/table/index.tsx +6 -2
- package/src/components/agent-helper/team-wide-form-select/data-adapter.ts +109 -0
- package/src/components/agent-helper/team-wide-form-select/index.tsx +289 -0
- package/src/components/agent-helper/team-wide-form-select/tree-panel.tsx +180 -0
- package/src/components/agent-helper/team-wide-form-select/types.ts +36 -0
- package/src/components/agent-helper/team-wide-form-select/use-form-tree.ts +108 -0
- package/src/components/agent-helper/trigger-select/index.tsx +1 -1
- package/src/components/agent-select.tsx +115 -35
- package/src/components/bot-avatar-name.tsx +1 -1
- package/src/components/bs-entry-select/build-tree.ts +0 -6
- package/src/components/bs-entry-select/entry-item.tsx +7 -11
- package/src/components/bs-entry-select/other-app-section.tsx +2 -4
- package/src/components/bs-entry-select/scroll-slider.tsx +6 -11
- package/src/components/bs-icons.tsx +752 -206
- package/src/components/card.tsx +55 -10
- package/src/components/circle-checker.tsx +18 -3
- package/src/components/collapsible-txt-msg.tsx +18 -13
- package/src/components/fake-bot-msg.tsx +18 -7
- package/src/components/field-checker-list-msg.tsx +4 -3
- package/src/components/fields-design-info-table.tsx +9 -26
- package/src/components/fields-portal.tsx +2 -2
- package/src/components/fields-previewer.tsx +1 -0
- package/src/components/font-icon.tsx +20 -9
- package/src/components/form-info-editor.tsx +21 -6
- package/src/components/legacy-slot.tsx +11 -16
- package/src/components/memoized-markdown.tsx +32 -19
- package/src/components/message-content.tsx +7 -3
- package/src/components/output-config-editor.tsx +39 -25
- package/src/components/primary-confirm-btn.tsx +6 -1
- package/src/components/scroll-to-bottom-btn.tsx +3 -2
- package/src/components/shadow-content.tsx +19 -0
- package/src/components/shadow-portal.tsx +23 -0
- package/src/components/ui/dialog.tsx +4 -1
- package/src/components/ui/drawer.tsx +3 -1
- package/src/components/ui/enhanced-drawer.tsx +134 -0
- package/src/components/ui/popover.tsx +3 -2
- package/src/components/ui/select.tsx +3 -2
- package/src/components/ui/tooltip.tsx +3 -2
- package/src/components/user-txt-msg-bubble.tsx +41 -19
- package/src/components/warning-msg.tsx +29 -12
- package/src/const/shadow.ts +17 -0
- package/src/const/ui.ts +2 -2
- package/src/hooks/use-column-appear.ts +1 -1
- package/src/hooks/use-portal-container.ts +6 -3
- package/src/hooks/use-shadow-boundary.ts +67 -0
- package/src/hooks/use-shadow.ts +24 -7
- package/src/index.ts +21 -0
- package/src/stories/AppSelect.stories.tsx +44 -0
- package/src/stories/FieldValueInput.stories.tsx +6 -3
- package/src/stories/FormSelect.stories.tsx +40 -0
- package/src/stories/IconfontPicker.stories.tsx +106 -0
- package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
- package/src/stories/TeamWideFormSelect.stories.tsx +362 -0
- package/src/stories/UserTextMsgBubble.stories.ts +28 -0
- package/src/style.css +28 -1
- package/style.css +172 -130
package/src/components/card.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
1
2
|
import { transCls } from '../const/ui';
|
|
2
3
|
import { cn } from '../lib/utils';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
@@ -9,28 +10,72 @@ export interface CardProps {
|
|
|
9
10
|
icon?: ReactNode;
|
|
10
11
|
iconCode?: string;
|
|
11
12
|
onClick?: (title: string, iconCode?: string) => void;
|
|
13
|
+
/** 追加到根节点的类名(如移动端满宽 max-w-none) */
|
|
14
|
+
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* 紧凑模式(移动端推荐卡片,对齐 Figma 40040:30035):
|
|
17
|
+
* 标题行去内边距、高 26,标题与描述间距 6,卡片总高 92。
|
|
18
|
+
* 默认 false,不影响现有 PC 样式
|
|
19
|
+
*/
|
|
20
|
+
compact?: boolean;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
export const Card = ({
|
|
23
|
+
export const Card = ({
|
|
24
|
+
title,
|
|
25
|
+
desc,
|
|
26
|
+
index,
|
|
27
|
+
icon,
|
|
28
|
+
onClick,
|
|
29
|
+
iconCode,
|
|
30
|
+
className,
|
|
31
|
+
compact,
|
|
32
|
+
}: CardProps) => {
|
|
33
|
+
const iconFontCls = classNames(
|
|
34
|
+
{
|
|
35
|
+
'text-[18px]': compact,
|
|
36
|
+
},
|
|
37
|
+
'text-[#0265FF]'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const titleCls = classNames(
|
|
41
|
+
{
|
|
42
|
+
'text-[16px]': compact,
|
|
43
|
+
'font-[500]': compact,
|
|
44
|
+
},
|
|
45
|
+
'truncate !text-[#12111] group-hover:text-[#0265FF]'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const contentCls = classNames(
|
|
49
|
+
{
|
|
50
|
+
'text-[12px]': !compact,
|
|
51
|
+
'text-[14px]': compact,
|
|
52
|
+
},
|
|
53
|
+
'text-[#666] leading-5'
|
|
54
|
+
);
|
|
55
|
+
|
|
15
56
|
return (
|
|
16
57
|
<div
|
|
17
58
|
className={cn(
|
|
18
|
-
'bs-card group flex flex-col cursor-pointer p-2.5
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
'bs-card group flex flex-col cursor-pointer p-2.5 rounded-lg max-w-[350px] border border-[#e0e0e0] bg-white',
|
|
60
|
+
compact ? 'gap-1.5' : 'gap-2.5',
|
|
61
|
+
transCls,
|
|
62
|
+
className
|
|
22
63
|
)}
|
|
23
64
|
onClick={() => {
|
|
24
65
|
onClick?.(title, iconCode);
|
|
25
66
|
}}
|
|
26
67
|
>
|
|
27
|
-
<div
|
|
28
|
-
{
|
|
29
|
-
|
|
68
|
+
<div
|
|
69
|
+
className={cn('flex items-center gap-1.5 rounded-[4px] bg-white', {
|
|
70
|
+
'h-[26px]': compact,
|
|
71
|
+
'px-2.5 h-[34px]': !compact,
|
|
72
|
+
})}
|
|
73
|
+
>
|
|
74
|
+
{icon === undefined ? null : <div className={iconFontCls}>{icon}</div>}
|
|
30
75
|
{/* make title text ellipsis */}
|
|
31
|
-
<div className=
|
|
76
|
+
<div className={titleCls}>{title}</div>
|
|
32
77
|
</div>
|
|
33
|
-
<div className=
|
|
78
|
+
<div className={contentCls}>{desc}</div>
|
|
34
79
|
</div>
|
|
35
80
|
);
|
|
36
81
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { cn } from '../lib/utils';
|
|
3
3
|
import { CheckerInBoxIcon } from './bs-icons';
|
|
4
|
+
import classNames from 'classnames';
|
|
4
5
|
|
|
5
6
|
export interface CircleCheckerProps {
|
|
6
7
|
defaultChecked?: boolean;
|
|
@@ -9,6 +10,7 @@ export interface CircleCheckerProps {
|
|
|
9
10
|
children?: React.ReactNode;
|
|
10
11
|
disabled?: boolean;
|
|
11
12
|
className?: string;
|
|
13
|
+
isMobile?: boolean;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const CircleChecker = ({
|
|
@@ -17,6 +19,7 @@ export const CircleChecker = ({
|
|
|
17
19
|
onCheckedChange,
|
|
18
20
|
children,
|
|
19
21
|
disabled = false,
|
|
22
|
+
isMobile,
|
|
20
23
|
className,
|
|
21
24
|
}: CircleCheckerProps) => {
|
|
22
25
|
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
@@ -34,6 +37,16 @@ export const CircleChecker = ({
|
|
|
34
37
|
onCheckedChange?.(newChecked);
|
|
35
38
|
};
|
|
36
39
|
|
|
40
|
+
const sizeCls = classNames({
|
|
41
|
+
'w-4 h-4': !isMobile,
|
|
42
|
+
'w-[20px] h-[20px]': isMobile
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const fontCls = classNames({
|
|
46
|
+
'text-[14px]': !isMobile,
|
|
47
|
+
'text-[16px]': isMobile,
|
|
48
|
+
})
|
|
49
|
+
|
|
37
50
|
return (
|
|
38
51
|
<button
|
|
39
52
|
type="button"
|
|
@@ -43,14 +56,16 @@ export const CircleChecker = ({
|
|
|
43
56
|
onClick={handleClick}
|
|
44
57
|
className={cn(
|
|
45
58
|
'inline-flex items-center gap-[6px] cursor-pointer select-none',
|
|
46
|
-
'text-[#121111]
|
|
59
|
+
'text-[#121111]',
|
|
60
|
+
fontCls,
|
|
47
61
|
disabled && 'cursor-not-allowed opacity-50',
|
|
48
62
|
className
|
|
49
63
|
)}
|
|
50
64
|
>
|
|
51
65
|
<span
|
|
52
66
|
className={cn(
|
|
53
|
-
'inline-flex items-center justify-center
|
|
67
|
+
'inline-flex items-center justify-center rounded-full transition-colors duration-200 box-border border',
|
|
68
|
+
sizeCls,
|
|
54
69
|
isChecked ? 'bg-[#2E60FF] border-transparent' : 'bg-white border-[#e0e0e0]'
|
|
55
70
|
)}
|
|
56
71
|
>
|
|
@@ -60,7 +75,7 @@ export const CircleChecker = ({
|
|
|
60
75
|
</span>
|
|
61
76
|
)}
|
|
62
77
|
</span>
|
|
63
|
-
{children && <span className="
|
|
78
|
+
{children && <span className="">{children}</span>}
|
|
64
79
|
</button>
|
|
65
80
|
);
|
|
66
81
|
};
|
|
@@ -7,7 +7,8 @@ import classNames from 'classnames';
|
|
|
7
7
|
export interface CollapsibleTxtMsgProps {
|
|
8
8
|
title: string;
|
|
9
9
|
content?: string;
|
|
10
|
-
|
|
10
|
+
/** 不传时用 AnalysisIcon。null 不显示图标 */
|
|
11
|
+
icon?: ReactNode | null;
|
|
11
12
|
defaultOpen?: boolean;
|
|
12
13
|
children?: ReactNode;
|
|
13
14
|
|
|
@@ -15,7 +16,8 @@ export interface CollapsibleTxtMsgProps {
|
|
|
15
16
|
|
|
16
17
|
disableExpand?: boolean;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/** 移动端消息内容文字 16px(PC 端 14px) */
|
|
20
|
+
isMobile?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const CollapsibleTxtMsg = ({
|
|
@@ -26,24 +28,25 @@ export const CollapsibleTxtMsg = ({
|
|
|
26
28
|
disableExpand,
|
|
27
29
|
children,
|
|
28
30
|
defaultOpen,
|
|
29
|
-
|
|
31
|
+
isMobile,
|
|
30
32
|
}: CollapsibleTxtMsgProps) => {
|
|
31
33
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const isGhostOpen = isOpen && ghost;
|
|
34
|
+
const sizeCls = isMobile ? 'text-[16px]' : 'text-[14px]';
|
|
35
|
+
const iconSizeCls = 'w-[16px] h-[16px]';
|
|
35
36
|
|
|
36
37
|
const cls = classNames({
|
|
37
|
-
'bg-[#fafafa] border border-[#e0e0e0] border-solid p-2.5': !ghost
|
|
38
|
-
'shadow-lg p-2.5': withOpenShadow && isGhostOpen,
|
|
38
|
+
'bg-[#fafafa] border border-[#e0e0e0] border-solid p-2.5': !ghost
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const isGhostOpen = isOpen && ghost;
|
|
42
|
+
|
|
43
|
+
const t = isGhostOpen ? '' : title;
|
|
44
|
+
const shownIcon = icon === undefined ? <AnalysisIcon /> : icon;
|
|
42
45
|
|
|
43
46
|
return (
|
|
44
47
|
<div className={classNames("flex flex-col gap-2.5 rounded-lg w-full", cls)}>
|
|
45
48
|
<div
|
|
46
|
-
className={cn('flex items-center justify-between w-full
|
|
49
|
+
className={cn('flex items-center justify-between w-full', {
|
|
47
50
|
'cursor-pointer': !disableExpand,
|
|
48
51
|
})}
|
|
49
52
|
onClick={() => {
|
|
@@ -53,8 +56,10 @@ export const CollapsibleTxtMsg = ({
|
|
|
53
56
|
}}
|
|
54
57
|
>
|
|
55
58
|
<div className="flex gap-2.5 items-center">
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
{shownIcon ? (
|
|
60
|
+
<div className={classNames("shrink-0 flex items-center", iconSizeCls)}>{shownIcon}</div>
|
|
61
|
+
) : null}
|
|
62
|
+
<p className={`text-[#666] ${sizeCls} leading-normal`}>{t}</p>
|
|
58
63
|
</div>
|
|
59
64
|
<button
|
|
60
65
|
onClick={() => {
|
|
@@ -73,7 +78,7 @@ export const CollapsibleTxtMsg = ({
|
|
|
73
78
|
</button>
|
|
74
79
|
</div>
|
|
75
80
|
{isOpen && !disableExpand && (
|
|
76
|
-
<div className=
|
|
81
|
+
<div className={`text-[#121111] ${sizeCls} word-wrap`}>{content || children}</div>
|
|
77
82
|
)}
|
|
78
83
|
</div>
|
|
79
84
|
);
|
|
@@ -6,20 +6,23 @@ export const FakeBotMessage = ({
|
|
|
6
6
|
children,
|
|
7
7
|
className,
|
|
8
8
|
avatarJsx,
|
|
9
|
-
|
|
9
|
+
isMobile,
|
|
10
10
|
}: {
|
|
11
11
|
children: ReactNode;
|
|
12
12
|
offset?: boolean;
|
|
13
13
|
className?: string;
|
|
14
14
|
avatarJsx?: ReactNode;
|
|
15
|
-
|
|
15
|
+
/** 移动端消息内容文字 16px(PC 端 14px) */
|
|
16
|
+
isMobile?: boolean;
|
|
16
17
|
}) => {
|
|
17
|
-
if (
|
|
18
|
+
if (!avatarJsx) {
|
|
18
19
|
return (
|
|
19
20
|
<div>
|
|
20
21
|
<MessageContent
|
|
22
|
+
isMobile={isMobile}
|
|
21
23
|
className={cn(
|
|
22
24
|
'wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left w-full',
|
|
25
|
+
isMobile && 'gap-[10px]',
|
|
23
26
|
className
|
|
24
27
|
)}
|
|
25
28
|
data-testid="message-content"
|
|
@@ -30,18 +33,26 @@ export const FakeBotMessage = ({
|
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
|
|
34
36
|
return (
|
|
35
37
|
<div
|
|
36
|
-
className={cn('group/message w-full')}
|
|
38
|
+
className={cn('group/message w-full', className)}
|
|
37
39
|
data-role="assistant"
|
|
38
40
|
data-testid={`message-assistant`}
|
|
39
41
|
>
|
|
40
|
-
<div
|
|
42
|
+
<div
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex w-full flex-col items-start justify-start',
|
|
45
|
+
isMobile ? 'gap-[10px]' : 'gap-2.5'
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
41
48
|
{avatarJsx}
|
|
42
49
|
<div className="flex flex-col min-w-0 grow gap-2.5 w-full">
|
|
43
50
|
<MessageContent
|
|
44
|
-
|
|
51
|
+
isMobile={isMobile}
|
|
52
|
+
className={cn(
|
|
53
|
+
'w-full wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left',
|
|
54
|
+
isMobile && 'gap-[10px]'
|
|
55
|
+
)}
|
|
45
56
|
data-testid="message-content"
|
|
46
57
|
>
|
|
47
58
|
{children}
|
|
@@ -26,8 +26,9 @@ export const FieldCheckerListMsg = memo(
|
|
|
26
26
|
children?: ReactNode;
|
|
27
27
|
streaming: boolean;
|
|
28
28
|
confirmed?: boolean;
|
|
29
|
+
isMobile?: boolean;
|
|
29
30
|
}
|
|
30
|
-
>(({ fields, children, streaming, confirmed }, ref) => {
|
|
31
|
+
>(({ fields, children, streaming, confirmed, isMobile }, ref) => {
|
|
31
32
|
// Track individual field checked states (all default to true)
|
|
32
33
|
const [checkedMap, setCheckedMap] = useState<Record<string, boolean>>(() => {
|
|
33
34
|
const map: Record<string, boolean> = {};
|
|
@@ -72,11 +73,11 @@ export const FieldCheckerListMsg = memo(
|
|
|
72
73
|
);
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
|
-
<CollapsibleTxtMsg title="字段生成" icon={<LetterAIcon />} defaultOpen>
|
|
76
|
+
<CollapsibleTxtMsg title="字段生成" icon={<LetterAIcon />} defaultOpen isMobile={isMobile}>
|
|
76
77
|
{
|
|
77
78
|
<div
|
|
78
79
|
className={cn(
|
|
79
|
-
'flex gap-
|
|
80
|
+
'flex gap-[6px] px-4 py-2.5 rounded-[4px] w-full transition-colors cursor-pointer hover:bg-[#EFF0F6] items-center',
|
|
80
81
|
{
|
|
81
82
|
'pointer-events-none': confirmed || streaming,
|
|
82
83
|
}
|
|
@@ -38,7 +38,7 @@ function groupFields(fields: IFieldDesignInfo[]) {
|
|
|
38
38
|
return groups;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const cellCls = 'flex items-center px-1.5 py-2.5 text-
|
|
41
|
+
const cellCls = 'flex items-center px-1.5 py-2.5 text-[12px] leading-normal';
|
|
42
42
|
|
|
43
43
|
function FieldRow({
|
|
44
44
|
field,
|
|
@@ -59,11 +59,9 @@ function FieldRow({
|
|
|
59
59
|
<span>{field.fieldName}</span>
|
|
60
60
|
</div>
|
|
61
61
|
{/* 类型 */}
|
|
62
|
-
<div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px]')}>
|
|
63
|
-
{field.type}
|
|
64
|
-
</div>
|
|
62
|
+
<div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px]')}>{field.type}</div>
|
|
65
63
|
{/* 必填 */}
|
|
66
|
-
<div className={cn(cellCls, 'w-
|
|
64
|
+
<div className={cn(cellCls, 'w-10 shrink-0 min-h-[34px]')}>
|
|
67
65
|
{field.required ? '是' : '否'}
|
|
68
66
|
</div>
|
|
69
67
|
{/* 说明 */}
|
|
@@ -100,11 +98,9 @@ function SubFormGroup({
|
|
|
100
98
|
<span>{parent.fieldName}</span>
|
|
101
99
|
</div>
|
|
102
100
|
{/* 类型 */}
|
|
103
|
-
<div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px] ')}>
|
|
104
|
-
{parent.type}
|
|
105
|
-
</div>
|
|
101
|
+
<div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px] ')}>{parent.type}</div>
|
|
106
102
|
{/* 必填 */}
|
|
107
|
-
<div className={cn(cellCls, 'w-
|
|
103
|
+
<div className={cn(cellCls, 'w-10 shrink-0 min-h-[34px]')}>
|
|
108
104
|
{parent.required ? '是' : '否'}
|
|
109
105
|
</div>
|
|
110
106
|
{/* 说明 */}
|
|
@@ -126,23 +122,10 @@ export function FieldsDesignInfoTable({ fields }: FieldsDesignInfoTableProps) {
|
|
|
126
122
|
<div className="flex flex-col items-start w-full bg-white rounded-[10px] overflow-hidden text-black my-[10px] border border-[#E0E0E0]">
|
|
127
123
|
{/* 表头 */}
|
|
128
124
|
<div className="flex items-center w-full shrink-0 bg-[#E5E6EB] rounded-t-[10px] border-b border-[#E0E0E0]">
|
|
129
|
-
<div className={cn(cellCls, 'w-20 shrink-0 h-[34px] font-bold')}>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<div className={cn(cellCls, '
|
|
133
|
-
类型
|
|
134
|
-
</div>
|
|
135
|
-
<div
|
|
136
|
-
className={cn(
|
|
137
|
-
cellCls,
|
|
138
|
-
'w-9 shrink-0 h-[34px] font-bold justify-center'
|
|
139
|
-
)}
|
|
140
|
-
>
|
|
141
|
-
必填
|
|
142
|
-
</div>
|
|
143
|
-
<div className={cn(cellCls, 'flex-1 min-w-0 h-[34px] font-bold')}>
|
|
144
|
-
说明
|
|
145
|
-
</div>
|
|
125
|
+
<div className={cn(cellCls, 'w-20 shrink-0 h-[34px] font-bold')}>名称</div>
|
|
126
|
+
<div className={cn(cellCls, 'w-[72px] shrink-0 h-[34px] font-bold')}>类型</div>
|
|
127
|
+
<div className={cn(cellCls, 'w-10 shrink-0 h-[34px] font-bold justify-center')}>必填</div>
|
|
128
|
+
<div className={cn(cellCls, 'flex-1 min-w-0 h-[34px] font-bold')}>说明</div>
|
|
146
129
|
</div>
|
|
147
130
|
|
|
148
131
|
{/* 表体 */}
|
|
@@ -8,11 +8,11 @@ export interface FieldsPortalProps extends FieldsPreviewerProps {
|
|
|
8
8
|
children?: React.ReactNode;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal => {
|
|
11
|
+
export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal | null => {
|
|
12
12
|
const { shadowContainer: shadowEntry } = useShadow({
|
|
13
13
|
rootEl: portalEl,
|
|
14
14
|
id: 'chat-sdk-fields-portal',
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
return createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry
|
|
17
|
+
return shadowEntry ? createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry) : null;
|
|
18
18
|
};
|
|
@@ -117,6 +117,7 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
|
|
|
117
117
|
|
|
118
118
|
const { shadowContainer } = useShadow({
|
|
119
119
|
id: 'dialog-container',
|
|
120
|
+
enabled: !disableShadowContainer,
|
|
120
121
|
rootEl: dialogContainer,
|
|
121
122
|
style: containerStyle,
|
|
122
123
|
wrapperStyle: {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getIconUnicode } from "@baishuyun/ai-feat-utils";
|
|
1
|
+
import { getIconUnicode, FORM_ICON_UNICODE_MAP } from "@baishuyun/ai-feat-utils";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
// 数字表单图标个别色值:对齐宿主 FONT_ICON_STYLE 里的特殊色
|
|
4
|
+
// (PC 的 .icon-bsy-form-1 / .icon-bsy-form-2),未命中回落到默认蓝 #0265ff。
|
|
5
|
+
const FORM_ICON_COLOR: Record<number, string> = {
|
|
6
|
+
1: '#ff6600',
|
|
7
|
+
2: '#52c41a',
|
|
8
|
+
};
|
|
8
9
|
|
|
9
10
|
export const AppIcon = ({
|
|
10
11
|
name
|
|
@@ -31,7 +32,10 @@ export const FontIcon = ({
|
|
|
31
32
|
style?: React.CSSProperties;
|
|
32
33
|
color?: string;
|
|
33
34
|
}) => {
|
|
34
|
-
|
|
35
|
+
// 判空:只有 null/undefined 才算「无图标」。
|
|
36
|
+
// 注意不能用 !name——icon 为 0 是合法图标(表单默认 icon 0),
|
|
37
|
+
// !0 为真会把它误判成无图标,导致绝大多数表单图标不渲染。
|
|
38
|
+
if (!code && (name === undefined || name === null)) {
|
|
35
39
|
return null;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -47,10 +51,17 @@ export const FontIcon = ({
|
|
|
47
51
|
|
|
48
52
|
const iconCode = code || getIconUnicode(name?.toString() || '');
|
|
49
53
|
if (!iconCode) {
|
|
54
|
+
// 数字表单图标:不用 .icon-bsy-form-N 的 :before 内码(部分环境
|
|
55
|
+
// shadow DOM / WebView 中 :before content 无法生成),改为把 unicode
|
|
56
|
+
// 字符直接放进文本内容(与 sdk-icon 同法),font-family 走 finex/finex-new
|
|
57
|
+
const unicode = FORM_ICON_UNICODE_MAP[name!];
|
|
58
|
+
const fontFamily = name! >= 25 ? 'finex-new' : 'finex';
|
|
59
|
+
const iconColor = FORM_ICON_COLOR[name!] ?? '#0265ff';
|
|
50
60
|
return (
|
|
51
61
|
<i
|
|
52
|
-
className={
|
|
53
|
-
style={{ fontSize: 16, lineHeight: 1 }}
|
|
62
|
+
className={className}
|
|
63
|
+
style={{ fontSize: 16, lineHeight: 1, fontStyle: 'normal', fontFamily, color: iconColor, ...style }}
|
|
64
|
+
dangerouslySetInnerHTML={{ __html: unicode ? `&#x${unicode};` : '' }}
|
|
54
65
|
/>
|
|
55
66
|
)
|
|
56
67
|
}
|
|
@@ -66,6 +66,7 @@ export interface FormInfoEditorProps {
|
|
|
66
66
|
onMount?: () => void;
|
|
67
67
|
readonly?: boolean;
|
|
68
68
|
iconRenderer?: (icon: FormIconOption & { size: number}) => React.ReactNode; // Optional custom renderer for icons
|
|
69
|
+
mobile?: boolean;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
@@ -85,6 +86,7 @@ export const FormInfoEditor = memo(
|
|
|
85
86
|
expanded: controlledExpanded,
|
|
86
87
|
onExpandedChange,
|
|
87
88
|
iconRenderer,
|
|
89
|
+
mobile,
|
|
88
90
|
className,
|
|
89
91
|
}: FormInfoEditorProps) => {
|
|
90
92
|
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
@@ -103,6 +105,16 @@ export const FormInfoEditor = memo(
|
|
|
103
105
|
onMount?.();
|
|
104
106
|
}, []);
|
|
105
107
|
|
|
108
|
+
const fontCls = cn({
|
|
109
|
+
'text-[16px]': mobile,
|
|
110
|
+
'text-[14px]': !mobile,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const sizeCls = cn({
|
|
114
|
+
'w-[16px] h-[16px]': !mobile,
|
|
115
|
+
'w-[20px] h-[20px]': !mobile
|
|
116
|
+
})
|
|
117
|
+
|
|
106
118
|
// Default Status (Collapsed)
|
|
107
119
|
if (!expanded || readonly) {
|
|
108
120
|
return (
|
|
@@ -121,7 +133,7 @@ export const FormInfoEditor = memo(
|
|
|
121
133
|
{/* Left side: Form icon and name */}
|
|
122
134
|
<div className="flex items-center gap-[10px] min-w-0 flex-1 mr-[10px]">
|
|
123
135
|
<FormIcon size={36} code={iconOpt?.code} render={iconRenderer} value={iconOpt?.value} />
|
|
124
|
-
<p className="text-
|
|
136
|
+
<p className={cn("text-black leading-normal truncate", fontCls)}>{nameVal}</p>
|
|
125
137
|
</div>
|
|
126
138
|
|
|
127
139
|
<button
|
|
@@ -130,19 +142,21 @@ export const FormInfoEditor = memo(
|
|
|
130
142
|
setExpanded(true);
|
|
131
143
|
}}
|
|
132
144
|
className={cn(
|
|
133
|
-
'
|
|
145
|
+
'flex items-center justify-center cursor-pointer hover:text-[#0265ff]',
|
|
134
146
|
transCls,
|
|
147
|
+
sizeCls,
|
|
135
148
|
{
|
|
136
149
|
'pointer-events-none opacity-0': readonly,
|
|
137
150
|
}
|
|
138
151
|
)}
|
|
139
152
|
>
|
|
140
|
-
<EditIcon />
|
|
153
|
+
<EditIcon size={mobile ? 20 : 16} />
|
|
141
154
|
</button>
|
|
142
155
|
</div>
|
|
143
156
|
);
|
|
144
157
|
}
|
|
145
158
|
|
|
159
|
+
|
|
146
160
|
// Expanded Status (Edit mode)
|
|
147
161
|
return (
|
|
148
162
|
<div
|
|
@@ -155,7 +169,7 @@ export const FormInfoEditor = memo(
|
|
|
155
169
|
>
|
|
156
170
|
{/* Name field */}
|
|
157
171
|
<div className="flex flex-col gap-[10px] w-full">
|
|
158
|
-
<p className="text-[
|
|
172
|
+
<p className={cn("text-[#030303] font-bold leading-normal", fontCls)}>名称</p>
|
|
159
173
|
<input
|
|
160
174
|
type="text"
|
|
161
175
|
value={nameVal}
|
|
@@ -167,9 +181,10 @@ export const FormInfoEditor = memo(
|
|
|
167
181
|
}}
|
|
168
182
|
className={cn(
|
|
169
183
|
'bg-white border border-[#e0e0e0] rounded-[4px] h-[34px] px-[10px] py-[7px] w-full',
|
|
170
|
-
'text-[
|
|
184
|
+
'text-[#030303] leading-normal outline-none',
|
|
171
185
|
'focus:border-[#0265ff]',
|
|
172
186
|
'hover:border-[#0265ff]',
|
|
187
|
+
fontCls,
|
|
173
188
|
transCls
|
|
174
189
|
)}
|
|
175
190
|
/>
|
|
@@ -177,7 +192,7 @@ export const FormInfoEditor = memo(
|
|
|
177
192
|
|
|
178
193
|
{/* Icon field */}
|
|
179
194
|
<div className="flex flex-col gap-[10px] w-full">
|
|
180
|
-
<p className="text-[
|
|
195
|
+
<p className={cn("text-[#030303] font-bold leading-normal", fontCls)}>图标</p>
|
|
181
196
|
<div className="relative" style={{ zIndex: 10 }}>
|
|
182
197
|
<button
|
|
183
198
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { LegacySlotRenderer } from '../types';
|
|
3
|
+
import { syncHostStylesIntoShadow } from '@baishuyun/ai-feat-utils';
|
|
4
|
+
import { useInjectedShadowStyle } from '../hooks/use-shadow-boundary';
|
|
3
5
|
|
|
4
6
|
export interface LegacySlotProps {
|
|
5
7
|
renderLegacy: LegacySlotRenderer;
|
|
@@ -10,8 +12,6 @@ export interface LegacySlotProps {
|
|
|
10
12
|
onSetRef?: (ref: HTMLDivElement) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
const STYLE_INJECTED_ATTR = 'data-style-injected';
|
|
14
|
-
|
|
15
15
|
const injectStylesIntoShadow = (currentElement: Node) => {
|
|
16
16
|
if (!currentElement) return;
|
|
17
17
|
|
|
@@ -31,20 +31,8 @@ const injectStylesIntoShadow = (currentElement: Node) => {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
if (!shadowRoot) return;
|
|
34
|
-
if (shadowRoot.querySelector(`style[${STYLE_INJECTED_ATTR}]`)) return;
|
|
35
|
-
|
|
36
|
-
document.head.querySelectorAll('style').forEach((styleEl) => {
|
|
37
|
-
const cloned = styleEl.cloneNode(true) as HTMLElement;
|
|
38
|
-
cloned.setAttribute(STYLE_INJECTED_ATTR, '');
|
|
39
|
-
shadowRoot!.appendChild(cloned);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
34
|
// if style is loaded by link tag, we also need to copy it into shadow DOM
|
|
43
|
-
|
|
44
|
-
const cloned = linkEl.cloneNode(true) as HTMLElement;
|
|
45
|
-
cloned.setAttribute(STYLE_INJECTED_ATTR, '');
|
|
46
|
-
shadowRoot!.appendChild(cloned);
|
|
47
|
-
});
|
|
35
|
+
return syncHostStylesIntoShadow(shadowRoot, shadowRoot.host as HTMLElement);
|
|
48
36
|
};
|
|
49
37
|
|
|
50
38
|
export function LegacySlot({
|
|
@@ -66,13 +54,14 @@ export function LegacySlot({
|
|
|
66
54
|
const host = hostRef.current;
|
|
67
55
|
onSetRef?.(host);
|
|
68
56
|
let target: HTMLElement | ShadowRoot = host;
|
|
57
|
+
let disposeStyles: (() => void) | undefined;
|
|
69
58
|
|
|
70
59
|
if (shadow) {
|
|
71
60
|
const shadowRoot = host.shadowRoot || host.attachShadow({ mode: 'open' });
|
|
72
61
|
target = shadowRoot;
|
|
73
62
|
|
|
74
63
|
if (withHostStyles) {
|
|
75
|
-
|
|
64
|
+
host.setAttribute('data-bs-style-mode', 'host');
|
|
76
65
|
}
|
|
77
66
|
if (typeof injectedStyles === 'string') {
|
|
78
67
|
const styleEl = document.createElement('style');
|
|
@@ -86,12 +75,16 @@ export function LegacySlot({
|
|
|
86
75
|
|
|
87
76
|
const mountPoint = document.createElement('div');
|
|
88
77
|
mountPoint.style.display = 'contents';
|
|
78
|
+
mountPoint.setAttribute('data-bs-shadow-content', '');
|
|
89
79
|
target.appendChild(mountPoint);
|
|
90
80
|
mountPointRef.current = mountPoint;
|
|
81
|
+
if (shadow && withHostStyles) disposeStyles = injectStylesIntoShadow(host);
|
|
91
82
|
|
|
92
83
|
cleanupRef.current = renderLegacy(mountPoint, props as Record<string, any>);
|
|
93
84
|
|
|
94
85
|
return () => {
|
|
86
|
+
disposeStyles?.();
|
|
87
|
+
host.shadowRoot?.querySelector('style[data-legacy-injected-raw]')?.remove();
|
|
95
88
|
cleanupRef.current?.();
|
|
96
89
|
if (mountPointRef.current) {
|
|
97
90
|
mountPointRef.current.remove();
|
|
@@ -101,6 +94,8 @@ export function LegacySlot({
|
|
|
101
94
|
};
|
|
102
95
|
}, []);
|
|
103
96
|
|
|
97
|
+
useInjectedShadowStyle(hostRef, injectedStyles, 'data-legacy-injected-raw');
|
|
98
|
+
|
|
104
99
|
useEffect(() => {
|
|
105
100
|
if (!mountPointRef.current) return;
|
|
106
101
|
cleanupRef.current = renderLegacy(mountPointRef.current, props as Record<string, any>);
|