Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/pro-components/chat/_util/reactify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ const reactify = <T extends AnyProps = AnyProps>(
this.ref.current?.addEventListener(event, val);
}

// 根据shadow DOM中实际存在的slot解析正确的slot名
private resolveSlotName(prop: string) {
const baseName = prop.endsWith('Slot') ? prop.slice(0, -4) : prop;
const kebabName = hyphenate(baseName);
const hasKebabSlot = this.ref.current?.shadowRoot?.querySelector(`slot[name="${kebabName}"]`) !== null;
return hasKebabSlot ? kebabName : baseName;
}

// 处理slot相关的prop
handleSlotProp(prop: string, val: any) {
const webComponent = this.ref.current as any;
Expand Down Expand Up @@ -120,15 +128,17 @@ const reactify = <T extends AnyProps = AnyProps>(
const webComponent = this.ref.current;
if (!webComponent) return;

const resolveSlotName = this.resolveSlotName(slotName);

let instance = this.slotInstances.get(slotName);

if (!instance) {
// 检查是否已经有相同的slot容器存在
let container = webComponent.querySelector(`[slot="${slotName}"]`) as HTMLElement;
let container = webComponent.querySelector(`[slot="${resolveSlotName}"]`) as HTMLElement;
if (!container) {
container = document.createElement('div');
container.style.display = 'contents';
container.setAttribute('slot', slotName);
container.setAttribute('slot', resolveSlotName);
webComponent.appendChild(container);
}

Expand Down
36 changes: 32 additions & 4 deletions packages/pro-components/chat/chat-actionbar/_example/custom.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import React from 'react';
import { Space } from 'tdesign-react';
import React, { useState } from 'react';
import { MessagePlugin, Space } from 'tdesign-react';
import { ChatActionBar } from '@tdesign-react/chat';
import type { ChatActionBarAction } from '@tdesign-react/chat';
import { HeartFilledIcon, HeartIcon } from 'tdesign-icons-react';

const ChatActionBarExample = () => {
const [isCustomActionActive, setIsCustomActionActive] = useState(false);

const onActions = (name, data) => {
console.log('消息事件触发:', name, data);
console.log('触发自定义事件:', name, data);
};

const handleHeartClick = () => {
setIsCustomActionActive((prev) => !prev);
MessagePlugin.success(isCustomActionActive ? '取消' : '点赞');
console.log('自定义按钮状态:', isCustomActionActive ? '取消' : '点赞');
};

const customIconActions: ChatActionBarAction[] = [
// ChatActionBar提供的预设项
'good',
'share',

// 自定项:可传自定义icon、通过onClick定义事件回调
isCustomActionActive ? (
<HeartFilledIcon
size="16px"
color="var(--td-brand-color)"
onClick={handleHeartClick}
key="custom-heart-icon-active"
/>
) : (
<HeartIcon size="16px" onClick={handleHeartClick} key="custom-heart-icon-inactive" />
),
];

return (
<Space>
<ChatActionBar actionBar={['good', 'bad', 'replay']} handleAction={onActions}></ChatActionBar>
<ChatActionBar actionBar={customIconActions} handleAction={onActions} />
</Space>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ spline: aigc

## 自定义

目前仅支持有限的自定义,包括调整顺序,展示指定项
支持完全自定义action

{{ custom }}

Expand Down
93 changes: 47 additions & 46 deletions packages/pro-components/chat/chat-actionbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,53 @@
import { TdChatActionProps } from 'tdesign-web-components';
import React from 'react';
import { TdChatActionProps, TdChatActionsName } from 'tdesign-web-components';
import 'tdesign-web-components/lib/chat-action';
import reactify from '../_util/reactify';

export const ChatActionBar: React.ForwardRefExoticComponent<
Omit<TdChatActionProps, 'ref'> &
React.RefAttributes<HTMLElement | undefined> & {
[key: string]: any;
}
> = reactify<TdChatActionProps>('t-chat-action');
type ChatActionBarAction =
| TdChatActionsName
| React.ReactElement
| {
name: string;
render?: React.ReactNode;
ignoreWrapper?: boolean;
};

type ChatActionBarProps = Omit<TdChatActionProps, 'actionBar' | 'ref'> & {
actionBar?: boolean | ChatActionBarAction[];
ref?: React.Ref<HTMLElement | undefined>;
};

const BaseChatActionBar = reactify<TdChatActionProps>('t-chat-action');

const normalizeSlotName = (raw: string) => raw.replace(/[^a-zA-Z0-9_-]/g, '-');

export const ChatActionBar = (props: ChatActionBarProps) => {
const { actionBar, ref, ...rest } = props;
const slotProps: Record<string, React.ReactNode> = {};
let mappedActionBar = actionBar;

if (Array.isArray(actionBar)) {
mappedActionBar = actionBar.map((action, index) => {
if (React.isValidElement(action)) {
const key = action.key != null ? String(action.key) : `item-${index}`;
const slotName = normalizeSlotName(`action-${key}`);
slotProps[`${slotName}Slot`] = action;
return { name: slotName };
}
return action;
});
}

return (
<BaseChatActionBar
{...(rest as TdChatActionProps)}
actionBar={mappedActionBar as TdChatActionProps['actionBar']}
ref={ref}
{...slotProps}
/>
);
};

export default ChatActionBar;
export type { ChatActionBarAction };
export type { TdChatActionProps, TdChatActionsName } from 'tdesign-web-components';

// 方案1
// import { reactifyLazy } from './_util/reactifyLazy';
// const ChatActionBar = reactifyLazy<{
// size: 'small' | 'medium' | 'large',
// variant: 'primary' | 'secondary' | 'outline'
// }>(
// 't-chat-action',
// 'tdesign-web-components/esm/chat-action'
// );

// import ChatAction from 'tdesign-web-components/esm/chat-action';
// import React, { forwardRef, useEffect } from 'react';

// // 注册Web Components组件
// const registerChatAction = () => {
// if (!customElements.get('t-chat-action')) {
// customElements.define('t-chat-action', ChatAction);
// }
// };

// // 在组件挂载时注册
// const useRegisterWebComponent = () => {
// useEffect(() => {
// registerChatAction();
// }, []);
// };

// // 使用reactify创建React组件
// const BaseChatActionBar = reactify<TdChatActionProps>('t-chat-action');

// // 包装组件,确保Web Components已注册
// export const ChatActionBar2 = forwardRef<
// HTMLElement | undefined,
// Omit<TdChatActionProps, 'ref'> & { [key: string]: any }
// >((props, ref) => {
// useRegisterWebComponent();
// return <BaseChatActionBar {...props} ref={ref} />;
// });
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ export default function ChatMessageExample() {
name="TDesignAI"
role={message.role}
content={message.content}
>
{/* 植入插槽用来追加消息底部操作栏 */}
<ChatActionBar slot="actionbar" copyText={getMessageContentForCopy(message)}></ChatActionBar>
</ChatMessage>
// 植入插槽用来追加消息底部操作栏
actionbar={<ChatActionBar copyText={getMessageContentForCopy(message)}></ChatActionBar>}
/>
</Space>
);
}
39 changes: 19 additions & 20 deletions packages/pro-components/chat/chat-sender/_example/custom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,9 @@ const ChatSenderExample = () => {
attachmentsProps={{
items: files,
}}
>
{/* 自定义输入框上方区域,可用来引用内容或提示场景 */}
{showRef && (
<div slot="inner-header">
// 自定义输入框上方区域,可用来引用内容或提示场景
innerHeader={
showRef ? (
<Space
style={{
width: '100%',
Expand All @@ -143,10 +142,10 @@ const ChatSenderExample = () => {
<CloseIcon size="14px" style={{ color: 'rgba(0, 0, 0, 0.26)' }} />
</div>
</Space>
</div>
)}
{/* 自定义输入框底部区域slot,可以增加模型选项 */}
<div slot="footer-prefix">
) : null
}
// 自定义输入框底部区域,可以增加模型选项
footerPrefix={
<Space align="center" size={'small'}>
<Tooltip content="只支持上传图片,总大小不超过20M">
<Button shape="round" variant="outline" size="small" icon={<AttachIcon />} onClick={onAttachClick} />
Expand All @@ -171,30 +170,30 @@ const ChatSenderExample = () => {
联网查询
</Button>
</Space>
</div>
{/* 自定义输入框左侧区域slot,可以用来触发工具场景切换 */}
<div slot="input-prefix">
}
// 自定义输入框左侧区域,可以用来触发工具场景切换
inputPrefix={
<Dropdown options={options} onClick={switchScene} trigger="click" style={{ padding: 0 }}>
<Tag shape="round" variant="light" color="#0052D9" style={{ marginRight: 4, cursor: 'pointer' }}>
{options.filter((item) => item.value === scene)[0].content}
</Tag>
</Dropdown>
</div>
{/* 自定义提交区域slot */}
<div slot="actions">
{!loading ? (
}
// 自定义提交区域
actions={
!loading ? (
<Button
shape="circle"
disabled={inputValue === ''}
icon={<ArrowUpIcon size={24} />}
onClick={handleSend}
style={{ opacity: inputValue ? '1' : '0.5' }}
></Button>
/>
) : (
<Button shape="circle" icon={<StopIcon size={32} />} onClick={handleStop}></Button>
)}
</div>
</ChatSender>
<Button shape="circle" icon={<StopIcon size={32} />} onClick={handleStop} />
)
}
/>
);
};

Expand Down
Loading