From 6afd36fa6408624d2e579fbc35e6e697ac463e6e Mon Sep 17 00:00:00 2001 From: zwight Date: Wed, 12 Nov 2025 16:43:32 +0800 Subject: [PATCH 1/6] feat(chat): support multiple conversation --- package.json | 1 + pnpm-lock.yaml | 16 +- .../__snapshots__/button.test.tsx.snap | 2 +- .../__snapshots__/conversations.test.tsx.snap | 202 +++++++++++++++++ src/chat/__tests__/conversations.test.tsx | 210 +++++++++++++++++ src/chat/button/index.tsx | 18 +- src/chat/conversations/GroupTitle/index.scss | 6 + src/chat/conversations/GroupTitle/index.tsx | 28 +++ src/chat/conversations/Item/index.scss | 32 +++ src/chat/conversations/Item/index.tsx | 84 +++++++ src/chat/conversations/hooks/useGroupable.ts | 86 +++++++ src/chat/conversations/index.scss | 41 ++++ src/chat/conversations/index.tsx | 153 +++++++++++++ src/chat/conversations/interface.ts | 39 ++++ .../components/customConversationItem.tsx | 57 +++++ src/chat/demos/conversations.tsx | 102 +++++++++ src/chat/demos/global-state/conversations.tsx | 211 ++++++++++++++++++ src/chat/demos/index.scss | 16 ++ src/chat/entity.ts | 3 + src/chat/index.$tab-conversations.md | 44 ++++ src/chat/index.md | 2 + src/chat/index.tsx | 2 + src/chat/input/index.tsx | 8 +- src/chat/markdown/index.scss | 27 +++ src/chat/markdown/index.tsx | 3 + src/chat/message/index.scss | 3 + src/chat/useChat.ts | 10 +- src/locale/en-US.ts | 7 + src/locale/useLocale.tsx | 7 + src/locale/zh-CN.ts | 7 + 30 files changed, 1399 insertions(+), 28 deletions(-) create mode 100644 src/chat/__tests__/__snapshots__/conversations.test.tsx.snap create mode 100644 src/chat/__tests__/conversations.test.tsx create mode 100644 src/chat/conversations/GroupTitle/index.scss create mode 100644 src/chat/conversations/GroupTitle/index.tsx create mode 100644 src/chat/conversations/Item/index.scss create mode 100644 src/chat/conversations/Item/index.tsx create mode 100644 src/chat/conversations/hooks/useGroupable.ts create mode 100644 src/chat/conversations/index.scss create mode 100644 src/chat/conversations/index.tsx create mode 100644 src/chat/conversations/interface.ts create mode 100644 src/chat/demos/components/customConversationItem.tsx create mode 100644 src/chat/demos/conversations.tsx create mode 100644 src/chat/demos/global-state/conversations.tsx create mode 100644 src/chat/demos/index.scss create mode 100644 src/chat/index.$tab-conversations.md diff --git a/package.json b/package.json index 890d0dfb3..6800fefee 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "@handsontable/react": "2.1.0", "antd": "4.22.5", "classnames": "^2.2.6", + "dayjs": "^1.11.19", "handsontable": "6.2.2", "highlight.js": "^10.5.0", "immer": "~10.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57da62ed5..66459f6a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,7 @@ specifiers: babel-plugin-import: ^1.13.8 classnames: ^2.2.6 cz-conventional-changelog: ^3.3.0 + dayjs: ^1.11.19 dumi: ^2.2.12 eslint: ^8.23.0 father: ~4.1.0 @@ -66,6 +67,7 @@ dependencies: '@handsontable/react': registry.npmmirror.com/@handsontable/react/2.1.0_handsontable@6.2.2 antd: registry.npmmirror.com/antd/4.22.5_react-dom@18.2.0+react@18.2.0 classnames: registry.npmmirror.com/classnames/2.3.2 + dayjs: 1.11.19 handsontable: registry.npmmirror.com/handsontable/6.2.2 highlight.js: registry.npmmirror.com/highlight.js/10.7.3 immer: 10.1.1 @@ -204,7 +206,7 @@ packages: /@dtinsight/dt-utils/1.3.1: resolution: {integrity: sha512-bV3xfCUthEtPkBpsCV/798J/Fz9xhxq9QybAaXhOtfGlZRuqPrb4Irdp2ySj7UaFB4VmmDg0wTIyxv0HMyGctQ==} dependencies: - dayjs: 1.11.10 + dayjs: 1.11.19 lodash: 4.17.21 standard-version: 9.5.0 dev: false @@ -1240,8 +1242,8 @@ packages: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: false - /dayjs/1.11.10: - resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + /dayjs/1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} dev: false /debug/3.2.7: @@ -9338,12 +9340,6 @@ packages: version: 3.0.3 dev: true - registry.npmmirror.com/dayjs/1.11.10: - resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz} - name: dayjs - version: 1.11.10 - dev: false - registry.npmmirror.com/debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz} name: debug @@ -18518,7 +18514,7 @@ packages: '@babel/runtime': registry.npmmirror.com/@babel/runtime/7.23.1 classnames: registry.npmmirror.com/classnames/2.3.2 date-fns: registry.npmmirror.com/date-fns/2.30.0 - dayjs: registry.npmmirror.com/dayjs/1.11.10 + dayjs: 1.11.19 moment: registry.npmmirror.com/moment/2.29.4 rc-trigger: registry.npmmirror.com/rc-trigger/5.3.4_react-dom@18.2.0+react@18.2.0 rc-util: registry.npmmirror.com/rc-util/5.37.0_react-dom@18.2.0+react@18.2.0 diff --git a/src/chat/__tests__/__snapshots__/button.test.tsx.snap b/src/chat/__tests__/__snapshots__/button.test.tsx.snap index 653bc87eb..8f4ea5bd7 100644 --- a/src/chat/__tests__/__snapshots__/button.test.tsx.snap +++ b/src/chat/__tests__/__snapshots__/button.test.tsx.snap @@ -45,4 +45,4 @@ exports[`Test Chat Button Match the snapshot: secondary button 1`] = ` `; -exports[`Test Chat Button expect ONLY one global gradient div: global gradient 1`] = `""`; +exports[`Test Chat Button expect ONLY one global gradient div: global gradient 1`] = `""`; diff --git a/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap b/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap new file mode 100644 index 000000000..5f9c91dc5 --- /dev/null +++ b/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap @@ -0,0 +1,202 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test Chat Conversations Match snapshot 1`] = ` + +
+
    +
+
+`; + +exports[`Test Chat Conversations Match snapshot: collapsed 1`] = ` + +
+
    +
+
+`; + +exports[`Test Chat Conversations Match snapshot: groupable 1`] = ` + +
+
    +
  • +
    +
    + 昨天 +
    +
    +
      +
      +
      +
      + this is conversation 1 +
      +
      +
      +
    +
  • +
  • +
    +
    + 今天 +
    +
    +
      +
      +
      +
      + this is conversation 2 +
      +
      +
      +
    +
  • +
+
+
+`; + +exports[`Test Chat Conversations Match snapshot: handleCreateChat 1`] = ` + +
+ +
    +
+
+`; + +exports[`Test Chat Conversations Match snapshot: loading 1`] = ` + +
+
+ + + + + + +
+
+
+`; + +exports[`Test Chat Conversations Match snapshot: normal 1`] = ` + +
+
    +
    +
    +
    + this is conversation 1 +
    +
    +
    +
    +
    +
    + this is conversation 2 +
    +
    +
    +
+
+
+`; + +exports[`Test Chat Conversations Match snapshot: select 1`] = ` + +
+
    +
+
+`; diff --git a/src/chat/__tests__/conversations.test.tsx b/src/chat/__tests__/conversations.test.tsx new file mode 100644 index 000000000..493be2415 --- /dev/null +++ b/src/chat/__tests__/conversations.test.tsx @@ -0,0 +1,210 @@ +import React from 'react'; +import { NewChatOutlined } from '@dtinsight/react-icons'; +import { act, cleanup, fireEvent, render } from '@testing-library/react'; +import { Menu } from 'antd'; +import dayjs from 'dayjs'; +import '@testing-library/jest-dom/extend-expect'; + +import Conversations, { ConversationInfo } from '../conversations'; +import Chat from '..'; + +function generateConversation() { + const conversation = [ + { + id: 'conversation_1', + createdAt: 1736479532239, + updatedAt: dayjs().subtract(1, 'day').toDate().getTime(), + title: 'this is conversation 1', + assistantId: 'assistant_1', + }, + { + id: 'conversation_2', + createdAt: 1736479532239, + updatedAt: dayjs().toDate().getTime(), + title: 'this is conversation 2', + assistantId: 'assistant_2', + }, + ]; + return conversation; +} +jest.mock('../../ellipsisText', () => { + return (props: any) =>
{props.value}
; +}); +jest.mock('remark-gfm', () => () => ({})); + +describe('Test Chat Conversations', () => { + beforeEach(() => { + cleanup(); + }); + + it('Match snapshot', () => { + const conversation = generateConversation(); + const handleCreateChat = jest.fn(); + expect(render().asFragment()).toMatchSnapshot(); + expect(render().asFragment()).toMatchSnapshot( + 'collapsed' + ); + expect( + render().asFragment() + ).toMatchSnapshot('select'); + expect(render().asFragment()).toMatchSnapshot( + 'loading' + ); + expect( + render( + } + onClick={handleCreateChat} + className="prompt-float-chat-add" + style={{ + margin: '16px', + gap: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} + > + 开启新对话 + + } + /> + ).asFragment() + ).toMatchSnapshot('handleCreateChat'); + expect(render().asFragment()).toMatchSnapshot( + 'normal' + ); + expect( + render().asFragment() + ).toMatchSnapshot('groupable'); + }); + it('Should loading', () => { + const { container } = render(); + + const ele = container.querySelector('.dtc-conversations-wrapper')?.children[0]; + expect(ele).toBeInTheDocument(); + expect(ele?.className).toContain('dtc-conversations-spin-wrapper'); + }); + + it('Should collapsed', () => { + const { container } = render(); + + const ele = container.querySelector('.dtc-conversations-wrapper'); + expect(ele).toBeInTheDocument(); + expect(ele?.className).toContain('dtc-conversations--collapsed'); + }); + + it('Should select item', () => { + const conversation = generateConversation(); + const { container } = render( + + ); + + const ele = container.querySelector('.dtc-conversations-item'); + expect(ele).toBeInTheDocument(); + expect(ele?.className).toContain('dtc-conversations-item--active'); + }); + + it('Should group list title', () => { + const { container } = render( + + ); + + const ele = container.querySelectorAll('.dtc-conversations-title'); + expect(ele).toHaveLength(2); + }); + + it('Should support add new session', () => { + const handleCreateChat = jest.fn(); + const { container } = render( + } + onClick={handleCreateChat} + className="prompt-float-chat-add" + style={{ + margin: '16px', + gap: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} + > + 开启新对话 + + } + /> + ); + + const btn = container.querySelector('.prompt-float-chat-add'); + expect(handleCreateChat).not.toBeCalled(); + expect(btn).not.toBeNull(); + + act(() => { + fireEvent.click(btn!); + }); + expect(handleCreateChat).toBeCalledWith( + expect.objectContaining({ + type: 'click', + }) + ); + }); + it('Should support select item', () => { + const conversation = generateConversation(); + const onItemClick = jest.fn(); + const { container } = render( + + ); + + const nodeList = container.querySelectorAll('.dtc-conversations-item'); + const ele = nodeList?.item(nodeList?.length - 1); + + expect(onItemClick).not.toBeCalled(); + expect(ele).not.toBeNull(); + + fireEvent.click(ele!); + expect(onItemClick).toBeCalledWith(conversation[conversation.length - 1]); + }); + + test('Should render delete button', () => { + const conversation = generateConversation(); + const onDelete = jest.fn(); + const renderMenu = (info: ConversationInfo) => ({ + overlay: ( + e.domEvent.stopPropagation()}> + onDelete?.(info)}> + 删除 + + + ), + }); + const { container } = render( + + ); + + const icon = container.querySelectorAll('.ant-dropdown-trigger')[0]; + expect(icon).toBeInTheDocument(); + + act(() => { + fireEvent.click(icon); + }); + + const dropdownMenuItems = document.querySelectorAll( + '.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item' + ); + expect(dropdownMenuItems).toHaveLength(1); + + fireEvent.click(dropdownMenuItems[0]); + expect(onDelete).toBeCalledWith(conversation[0]); + }); +}); diff --git a/src/chat/button/index.tsx b/src/chat/button/index.tsx index f6eb0243f..87697a49e 100644 --- a/src/chat/button/index.tsx +++ b/src/chat/button/index.tsx @@ -25,11 +25,10 @@ export default function Button({ type = 'default', className, children, ...rest @@ -37,11 +36,10 @@ export default function Button({ type = 'default', className, children, ...rest diff --git a/src/chat/conversations/GroupTitle/index.scss b/src/chat/conversations/GroupTitle/index.scss new file mode 100644 index 000000000..1c98cb809 --- /dev/null +++ b/src/chat/conversations/GroupTitle/index.scss @@ -0,0 +1,6 @@ +.dtc-conversations-title { + color: #B1B4C5; + line-height: 20px; + font-size: 12px; + margin-bottom: 4px; +} diff --git a/src/chat/conversations/GroupTitle/index.tsx b/src/chat/conversations/GroupTitle/index.tsx new file mode 100644 index 000000000..c6fcf75e3 --- /dev/null +++ b/src/chat/conversations/GroupTitle/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import EllipsisText from '../../../ellipsisText'; +import './index.scss'; + +export interface IGroupTitleProps { + children?: React.ReactNode; + prefixCls?: string; +} + +const GroupTitle: React.FC = (props) => { + const { prefixCls = 'dtc-conversations' } = props; + return ( +
+ {props.children && ( + + )} +
+ ); +}; + +export default GroupTitle; diff --git a/src/chat/conversations/Item/index.scss b/src/chat/conversations/Item/index.scss new file mode 100644 index 000000000..cc8fba74d --- /dev/null +++ b/src/chat/conversations/Item/index.scss @@ -0,0 +1,32 @@ +.dtc-conversations-item { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + gap: 4px; + padding: 0 16px; + border-radius: 4px; + cursor: pointer; + color: #3D446E; + &-title { + flex: 1; + overflow: hidden; + } + &:hover { + background-color: #EBECF0; + .dtc-conversations-menu-icon { + display: block; + } + } + .dtc-conversations-menu-icon { + display: none; + } + &--active, &--active:hover { + color: #1D78FF; + background-color: #E8F1FF; + } + &--disabled, &--disabled:hover { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/src/chat/conversations/Item/index.tsx b/src/chat/conversations/Item/index.tsx new file mode 100644 index 000000000..04e99be91 --- /dev/null +++ b/src/chat/conversations/Item/index.tsx @@ -0,0 +1,84 @@ +import React, { HTMLAttributes } from 'react'; +import { MoreOutlined } from '@dtinsight/react-icons'; +import { Dropdown, DropdownProps } from 'antd'; +import classNames from 'classnames'; + +import EllipsisText from '../../../ellipsisText'; +import { ConversationInfo } from '../interface'; +import './index.scss'; + +export interface IConversationsItemProps extends Omit, 'onClick'> { + info: ConversationInfo; + active?: boolean; + dropdown?: DropdownProps & { + triggerDom?: + | React.ReactNode + | (( + conversation: ConversationInfo, + info: { originNode: React.ReactNode } + ) => React.ReactNode); + }; + prefixCls?: string; + onClick?: (info: ConversationInfo) => void; +} + +const Item: React.FC = (props) => { + const { info, active, prefixCls, dropdown, onClick } = props; + + const { disabled } = info; + const { triggerDom } = dropdown || {}; + + const handleClick = () => { + if (disabled || active) return; + onClick?.(info); + }; + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + const renderMenuTrigger = (conversation: ConversationInfo) => { + const originTriggerNode = ( + + ); + if (triggerDom) { + return typeof triggerDom === 'function' + ? triggerDom(conversation, { originNode: originTriggerNode }) + : triggerDom; + } + return originTriggerNode; + }; + return ( +
+ {info.icon &&
{info.icon}
} +
+ +
+ {!disabled && dropdown?.overlay && ( + + {renderMenuTrigger(info)} + + )} +
+ ); +}; + +export default Item; diff --git a/src/chat/conversations/hooks/useGroupable.ts b/src/chat/conversations/hooks/useGroupable.ts new file mode 100644 index 000000000..082dcada8 --- /dev/null +++ b/src/chat/conversations/hooks/useGroupable.ts @@ -0,0 +1,86 @@ +import { useMemo } from 'react'; +import dayjs from 'dayjs'; +import shortid from 'shortid'; + +import useLocale, { Locale } from '../../../locale/useLocale'; +import { ConversationInfo, Groupable, GroupInfo } from '../interface'; +import { IConversationsProps } from '..'; + +const DEFAULT_GROUP_KEY = 'updatedAt'; +type GroupMap = Record; +const useGroupable = ( + groupable: IConversationsProps['groupable'], + conversations: ConversationInfo[] +): [list: GroupInfo[], enable: boolean] => { + const locale = useLocale('Chat'); + const [enable, sort, title] = useMemo(() => { + if (!groupable) return [false, undefined, undefined]; + + let baseConfig: Groupable = { + sort: undefined, + title: undefined, + }; + + if (typeof groupable === 'object') { + baseConfig = { ...baseConfig, ...groupable }; + } + + return [true, baseConfig.sort, baseConfig.title]; + }, [groupable]); + + return useMemo(() => { + if (!enable) { + const groupList: GroupInfo[] = [ + { + id: `group_${shortid()}`, + conversations, + title: undefined, + }, + ]; + return [groupList, enable]; + } + const groupMap = conversations.reduce((prev, current) => { + const group = current.group || classifyDate(locale, current[DEFAULT_GROUP_KEY]); + if (!prev[group]) { + prev[group] = []; + } + prev[group].push(current); + return prev; + }, {}); + + const groupEntries = sort ? Object.entries(groupMap).sort(sort) : Object.entries(groupMap); + + const groupList: GroupInfo[] = groupEntries.map(([key, value]) => { + return { + id: `group_${shortid()}`, + title, + conversations: value, + name: key, + }; + }); + return [groupList, enable]; + }, [conversations, enable]); +}; + +export function classifyDate(locale: Locale['Chat'], date?: string | Date | number) { + const input = dayjs(date).startOf('day'); + const now = dayjs().startOf('day'); + + const diffDays = now.diff(input, 'days'); + + if (diffDays < 1) { + return locale.today; + } else if (diffDays < 2) { + return locale.yesterday; + } else if (diffDays < 7) { + return locale.recent7Days; + } else if (diffDays < 15) { + return locale.recent15Days; + } else if (diffDays < 30) { + return locale.recent30Days; + } else { + return locale.other; + } +} + +export default useGroupable; diff --git a/src/chat/conversations/index.scss b/src/chat/conversations/index.scss new file mode 100644 index 000000000..791a88f31 --- /dev/null +++ b/src/chat/conversations/index.scss @@ -0,0 +1,41 @@ +.dtc-conversations { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 4px; + margin: 0; + padding: 0 16px; + list-style: none; + overflow-y: auto; + flex: 1; + &--empty { + margin-top: 50%; + } + &-list { + display: flex; + flex-direction: column; + gap: 4px; + } + &-spin-wrapper { + width: 100%; + display: flex; + justify-content: center; + margin: 40px 0; + } + &-wrapper { + display: flex; + flex-direction: column; + width: 0; + height: 100%; + transition: width 0.3s ease; + overflow: hidden; + } + &--collapsed { + width: 240px; + } + &--hide { + opacity: 0; + pointer-events: none; + } +} diff --git a/src/chat/conversations/index.tsx b/src/chat/conversations/index.tsx new file mode 100644 index 000000000..c6e2a9da2 --- /dev/null +++ b/src/chat/conversations/index.tsx @@ -0,0 +1,153 @@ +import React, { useEffect } from 'react'; +import { Spin } from 'antd'; +import classNames from 'classnames'; + +import Empty from '../../empty'; +import useLocale from '../../locale/useLocale'; +import useGroupable from './hooks/useGroupable'; +import GroupTitle from './GroupTitle'; +import { ConversationInfo, Groupable } from './interface'; +import Item, { IConversationsItemProps } from './Item'; +import './index.scss'; + +export interface IConversationsProps extends React.HTMLAttributes { + conversations: ConversationInfo[]; + activeKey?: ConversationInfo['id']; + defaultActiveKey?: ConversationInfo['id']; + dropdown?: + | IConversationsItemProps['dropdown'] + | ((info: ConversationInfo) => IConversationsItemProps['dropdown']); + groupable?: boolean | Groupable; + className?: string; + style?: React.CSSProperties; + loading?: boolean; + header?: React.ReactNode | boolean; + collapsed?: boolean; + + onItemClick?: (info: ConversationInfo) => void; + renderItem?: (props: IConversationsItemProps) => React.ReactNode; +} + +const prefixCls = 'dtc-conversations'; +const Conversations = (props: IConversationsProps) => { + const { + conversations, + activeKey, + defaultActiveKey, + dropdown, + groupable, + className, + style, + + loading, + header, + collapsed = true, + onItemClick, + renderItem, + ...restProps + } = props; + const [value, setValue] = React.useState(activeKey || defaultActiveKey); + + const [groupList, enable] = useGroupable(groupable, conversations); + const [isHide, setIsHide] = React.useState(false); + const locale = useLocale('Chat'); + + const handleItemClick = (info: ConversationInfo) => { + setValue(info.id); + onItemClick?.(info); + }; + + useEffect(() => { + if (activeKey !== value) { + setValue(activeKey); + } + }, [activeKey]); + + const renderConversations = () => { + if (loading) { + return ; + } + console.log(groupList); + if (!groupList?.length) { + return ( + + ); + } + return ( +
    + {groupList.map((group) => { + const items = group.conversations.map((conversation) => { + const dropdownVal = + typeof dropdown === 'function' ? dropdown(conversation) : dropdown; + if (renderItem) { + return renderItem({ + info: conversation, + active: conversation.id === value, + prefixCls, + onClick: handleItemClick, + dropdown: dropdownVal, + }); + } + return ( + + ); + }); + if (enable) { + return ( +
  • + {group.title?.(group, { components: { GroupTitle } }) || ( + + {group.name} + + )} +
      {items}
    +
  • + ); + } + return items; + })} +
+ ); + }; + useEffect(() => { + if (collapsed) { + setIsHide(false); + } + }, [collapsed]); + + return ( +
{ + if (!collapsed) setIsHide(true); + }} + > + {header} + {renderConversations()} +
+ ); +}; + +export type { ConversationInfo }; + +Conversations.Item = Item; +Conversations.Title = GroupTitle; + +export default Conversations; diff --git a/src/chat/conversations/interface.ts b/src/chat/conversations/interface.ts new file mode 100644 index 000000000..2ac4678ff --- /dev/null +++ b/src/chat/conversations/interface.ts @@ -0,0 +1,39 @@ +import { ConversationProperties } from '../entity'; +import { IGroupTitleProps } from './GroupTitle'; + +export interface ConversationInfo extends ConversationProperties { + group?: string; + icon?: React.ReactNode; + disabled?: boolean; +} + +export type GroupSorter = Parameters<[string, ConversationInfo[]][]['sort']>[0]; + +export type GroupTitleRenderComponents = { + components: { + GroupTitle: React.ComponentType; + }; +}; + +export type GroupInfo = { + conversations: ConversationInfo[]; + id?: string; + title?: Groupable['title']; + name?: string; +}; +export type GroupTitleRender = + | ((groupInfo: GroupInfo, info: GroupTitleRenderComponents) => React.ReactNode) + | undefined; + +export interface Groupable { + /** + * @desc 分组排序函数 + * @descEN Group sorter + */ + sort?: GroupSorter; + /** + * @desc 自定义分组标签渲染 + * @descEN Semantic custom rendering + */ + title?: GroupTitleRender; +} diff --git a/src/chat/demos/components/customConversationItem.tsx b/src/chat/demos/components/customConversationItem.tsx new file mode 100644 index 000000000..863ae8c03 --- /dev/null +++ b/src/chat/demos/components/customConversationItem.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Input, message } from 'antd'; +import classNames from 'classnames'; + +import Conversation from '../../conversations'; +import { IConversationsItemProps } from '../../conversations/Item'; + +interface IProps extends IConversationsItemProps { + edit?: IConversationsItemProps['info']; + onRename?: (info: IConversationsItemProps['info'], value: string) => Promise; + setEdit: (edit?: IConversationsItemProps['info']) => void; +} + +export default function CustomConversionItem(props: IProps) { + const { info, prefixCls, active, edit, onRename, setEdit } = props; + const { disabled, title } = info || {}; + + const isEdit = edit?.id === info?.id; + + const handleRename = async (value: string) => { + if (!value) { + setEdit(undefined); + message.error('请输入对话名称'); + return; + } + const res = await onRename?.(info, value); + if (res) { + setEdit(undefined); + } + }; + return isEdit ? ( +
+ { + handleRename(target.value); + }} + onPressEnter={({ target, key }) => { + if (key === 'Enter') { + handleRename((target as HTMLInputElement).value); + } + }} + /> +
+ ) : ( + + ); +} diff --git a/src/chat/demos/conversations.tsx b/src/chat/demos/conversations.tsx new file mode 100644 index 000000000..9ba875d71 --- /dev/null +++ b/src/chat/demos/conversations.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { NewChatOutlined } from '@dtinsight/react-icons'; +import { Menu } from 'antd'; +import { Button, Chat } from 'dt-react-component'; +import { ConversationInfo } from 'dt-react-component/chat/conversations'; +import { ConversationProperties } from 'dt-react-component/chat/entity'; + +import CustomConversionItem from './components/customConversationItem'; +import './index.scss'; + +export default function ({ conversations = [] }: { conversations: ConversationProperties[] }) { + const [data, setData] = React.useState(conversations); + const [selectId, setSelectId] = React.useState('1'); + const [collapsed, setCollapsed] = React.useState(true); + const [edit, setEdit] = React.useState(); + + const handleSelectChat = (conversation: ConversationProperties) => { + setSelectId(conversation.id); + }; + + const handleRenameChat = (_conversation: ConversationProperties, _value: string) => { + data.forEach((item) => { + if (item.id === _conversation.id) { + item.title = _value; + } + }); + setData(data); + return Promise.resolve(true); + }; + + const handleClearChat = (conversation: ConversationProperties) => { + console.log(conversation); + }; + const handleNewChat = () => { + setData((prev) => { + const idx = prev.length + 1; + return [ + ...prev, + { + id: idx.toString(), + title: `对话${idx}`, + assistantId: idx.toString(), + createdAt: new Date().valueOf(), + updatedAt: new Date().valueOf(), + }, + ]; + }); + }; + const renderMenu = (info: ConversationInfo) => ({ + overlay: ( + e.domEvent.stopPropagation()}> + { + console.log(info); + setEdit(info); + }} + > + 重命名 + + handleClearChat?.(info)}> + 删除 + + + ), + }); + + return ( +
+ + ( + + )} + header={ + } + onClick={handleNewChat} + className="prompt-float-chat-add" + > + 开启新对话 + + } + /> +
+ ); +} diff --git a/src/chat/demos/global-state/conversations.tsx b/src/chat/demos/global-state/conversations.tsx new file mode 100644 index 000000000..a285a0a0d --- /dev/null +++ b/src/chat/demos/global-state/conversations.tsx @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from 'react'; +import { NewChatOutlined, ThumbsUpOutlined } from '@dtinsight/react-icons'; +import { Button, Menu } from 'antd'; +import { Chat, Flex } from 'dt-react-component'; +import { ConversationInfo } from 'dt-react-component/chat/conversations'; +import { ConversationProperties } from 'dt-react-component/chat/entity'; +import { produce } from 'immer'; +import { cloneDeep } from 'lodash-es'; + +import CustomConversionItem from '../components/customConversationItem'; +import { mockSSE } from '../mockSSE'; +import '../index.scss'; + +export default function () { + const chat = Chat.useChat(); + const [value, setValue] = useState(''); + const [convert, setConvert] = useState(false); + const [data, setData] = useState([]); + const [edit, setEdit] = React.useState(); + + const handleSelectChat = (conversation: ConversationProperties) => { + chat.conversation.remove(); + chat.conversation.create({ ...conversation }); + }; + + const handleRenameChat = (_conversation: ConversationProperties, _value: string) => { + setData((prev) => { + const idx = prev.findIndex((i) => i.id === _conversation.id); + if (idx === -1) return prev; + return produce(prev, (draft) => { + draft[idx].title = _value; + }); + }); + return Promise.resolve(true); + }; + + const handleDeleteChat = (conversation: ConversationProperties) => { + const list = cloneDeep(data).filter((i) => i.id !== conversation.id); + if (conversation.id === chat.conversation.get()?.id) { + chat.conversation.remove(); + if (list.length) { + handleSelectChat(list[0]); + chat.conversation.create({ ...list[0] }); + } + } + setData(list); + }; + const handleCreateChat = () => { + chat.conversation.remove(); + chat.conversation.create({ id: new Date().valueOf().toString() }); + }; + + const addData = (str: string) => { + setData((prev) => { + const idx = prev.length + 1; + return [ + ...prev, + { + id: chat.conversation.get()!.id, + title: str, + assistantId: idx.toString(), + createdAt: new Date().valueOf(), + updatedAt: new Date().valueOf(), + }, + ]; + }); + handleSelectChat(chat.conversation.get()!); + }; + + const handleSubmit = (raw = value) => { + const val = raw?.trim(); + if (chat.loading() || !val) return; + setValue(''); + const promptId = new Date().valueOf().toString(); + const messageId = (new Date().valueOf() + 1).toString(); + chat.prompt.create({ id: promptId, title: val }); + chat.message.create(promptId, { id: messageId, content: '' }); + mockSSE({ + message: val, + onopen() { + chat.start(promptId, messageId); + addData(val); + }, + onmessage(str) { + chat.push(promptId, messageId, str); + }, + onstop() { + chat.close(promptId, messageId); + }, + }); + }; + const renderMenu = (info: ConversationInfo) => ({ + overlay: ( + e.domEvent.stopPropagation()}> + { + console.log(info); + setEdit(info); + }} + > + 重命名 + + handleDeleteChat?.(info)}> + 删除 + + + ), + }); + + useEffect(() => { + chat.conversation.create({ id: new Date().valueOf().toString() }); + }, []); + + return ( +
+ } + components={{ + a: ({ children }) => ( + + ), + }} + > + + ( + + )} + header={ + } + onClick={handleCreateChat} + className="prompt-float-chat-add" + style={{ + margin: '16px', + gap: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} + > + 开启新对话 + + } + style={{ + backgroundColor: '#F9F9FA', + borderRight: '1px solid #E5E7EB', + }} + /> + + + +
+ + handleSubmit('请告诉我一首诗')}> + 返回一首诗 + + handleSubmit('生成 CodeBlock')}> + 生成 CodeBlock + + +
+ } + /> + handleSubmit()} + onPressShiftEnter={() => setValue((v) => v + '\n')} + button={{ + disabled: chat.loading() || !value?.trim(), + }} + placeholder="请输入想咨询的内容…" + /> + + + + + ); +} diff --git a/src/chat/demos/index.scss b/src/chat/demos/index.scss new file mode 100644 index 000000000..de1fab3f9 --- /dev/null +++ b/src/chat/demos/index.scss @@ -0,0 +1,16 @@ +li,ul { + padding: 0; + margin: 0; +} + +.dtc-conversations-wrapper { + border-right: 1px solid #E5E7EB; + background-color: #F9F9FA; + .ant-btn.dtc__aigc__button { + margin: 16px; + gap: 4px; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/src/chat/entity.ts b/src/chat/entity.ts index 0c55c960c..ac31659c7 100644 --- a/src/chat/entity.ts +++ b/src/chat/entity.ts @@ -29,6 +29,7 @@ export type ConversationProperties = { id: string; assistantId?: string; createdAt?: Timestamp; + updatedAt?: Timestamp; title?: string; prompts?: Prompt[]; }; @@ -58,6 +59,7 @@ export abstract class Conversation { // 后端 Id assistantId?: string; createdAt: Timestamp; + updatedAt?: Timestamp; title?: string; prompts: Prompt[]; @@ -67,6 +69,7 @@ export abstract class Conversation { this.id = props.id; this.assistantId = props.assistantId; this.createdAt = props.createdAt || new Date().valueOf(); + this.updatedAt = props.updatedAt || new Date().valueOf(); this.title = props.title; this.prompts = props.prompts || []; } diff --git a/src/chat/index.$tab-conversations.md b/src/chat/index.$tab-conversations.md new file mode 100644 index 000000000..e377cbd92 --- /dev/null +++ b/src/chat/index.$tab-conversations.md @@ -0,0 +1,44 @@ +--- +title: Conversations +group: 组件 +toc: content +demo: + cols: 2 +--- + +# Conversations + +## 何时使用 + +Conversations 组件用于展示会话列表 + +## 示例 + + + +## API + +| 参数 | 说明 | 类型 | 默认值 | +| ---------------- | ----------------- | ----------------------------------------------------------------------------------- | ---------- | --- | +| conversations | 会话列表 | `ConversationInfo[]` | - | +| activeKey | 激活的会话 ID | `ConversationInfo['id']` | - | +| defaultActiveKey | 默认激活的会话 ID | `ConversationInfo['id']` | - | +| dropdown | 下拉菜单 | [IConversationsItemProps](?tab=conversations#iconversationsitemprops)`['dropdown']` | - | +| groupable | 是否启用分组 | `boolean` | Groupable` | - | +| className | 自定义类名 | `string` | - | +| style | 自定义样式 | `React.CSSProperties` | - | +| loading | 是否加载中 | `boolean` | - | +| header | 会话列表头部 | `React.ReactNode` | boolean` | - | +| collapsed | 是否折叠 | `boolean` | - | +| onItemClick | 点击会话事件 | `(info: ConversationInfo) => void` | - | +| renderItem | 自定义渲染会话项 | `(props: IConversationsItemProps) => React.ReactNode` | - | + +## IConversationsItemProps + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | -------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- | --- | +| info | 对话数据 | `ConversationInfo[]` | - | +| active | 是否激活 | `boolean` | - | +| dropdown | 下拉菜单 | `DropdownProps & { triggerDom?: React.ReactNode | ((conversation: ConversationInfo, info: { originNode: React.ReactNode }) => React.ReactNode) }` | - | +| prefixCls | 前缀 | `string` | - | +| onClick | 点击事件 | `(conversation: ConversationInfo) => void` | - | diff --git a/src/chat/index.md b/src/chat/index.md index 10b913ae5..c1e1ca277 100644 --- a/src/chat/index.md +++ b/src/chat/index.md @@ -21,6 +21,7 @@ Chat 规范由多个组件复合使用实现落地场景,其中: - `Message` 组件是符合 AI 规范的回答框 - `Prompt` 组件是符合 AI 规范的提问框 - `Content` 组件是符合 AI 规范的正文内容 +- `Conversations` 组件是符合 AI 规范的会话列表组件 ## 何时使用 @@ -30,5 +31,6 @@ Chat 规范由多个组件复合使用实现落地场景,其中: + ## API diff --git a/src/chat/index.tsx b/src/chat/index.tsx index 2a57861f3..14f9b41cf 100644 --- a/src/chat/index.tsx +++ b/src/chat/index.tsx @@ -3,6 +3,7 @@ import React, { type PropsWithChildren } from 'react'; import Button from './button'; import CodeBlock from './codeBlock'; import Content from './content'; +import Conversations from './conversations'; import Input from './input'; import Loading from './loading'; import Markdown from './markdown'; @@ -66,6 +67,7 @@ Chat.Prompt = Prompt; Chat.Content = Content; Chat.Tag = Tag; Chat.Welcome = Welcome; +Chat.Conversations = Conversations; export { type IContentRef } from './content'; export default Chat; diff --git a/src/chat/input/index.tsx b/src/chat/input/index.tsx index 4666adae8..897656bb9 100644 --- a/src/chat/input/index.tsx +++ b/src/chat/input/index.tsx @@ -34,6 +34,10 @@ export default function Input({
{ @@ -46,10 +50,6 @@ export default function Input({ onPressEnter?.(e); } }} - autoSize={{ - minRows: 2, - maxRows: 7, - }} /> {button?.disabled ? ( diff --git a/src/chat/markdown/index.scss b/src/chat/markdown/index.scss index d076fe70a..08418932a 100644 --- a/src/chat/markdown/index.scss +++ b/src/chat/markdown/index.scss @@ -73,6 +73,33 @@ font-size: 14px; } } + &__table { + border: 1px solid #EBECF0; + width: 100%; + margin-block-end: 8px; + tr { + border-bottom: 1px solid #EBECF0; + height: 36px; + text-align: left; + font-size: 12px; + line-height: 20px; + color: #3D446E; + th, td { + padding: 8px 16px; + } + } + thead { + tr { + background-color: #F9F9FA; + font-weight: 500; + } + } + tbody { + tr { + background-color: #FFF; + } + } + } &__inlineCode { margin: 0 4px; padding: 2px 8px; diff --git a/src/chat/markdown/index.tsx b/src/chat/markdown/index.tsx index 1f2223b33..c5699f523 100644 --- a/src/chat/markdown/index.tsx +++ b/src/chat/markdown/index.tsx @@ -57,6 +57,9 @@ export default memo( return
{data.children}
; } }, + table({ children }) { + return {children}
; + }, img({ src, ...rest }) { return ( Prompt): void; - function _updatePrompt(promptId: Id, data: Partial>): void; function _updatePrompt( promptId: Id, - dataOrPredicate: Partial> | ((prompt: Prompt) => Prompt) + data: Partial[0], 'id'>> + ): void; + function _updatePrompt( + promptId: Id, + dataOrPredicate: + | Partial[0], 'id'>> + | ((prompt: Prompt) => Prompt) ) { if (!state.current) return; state.current = produce(state.current, (draft) => { diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 6f44cbf28..6141da637 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -14,6 +14,13 @@ const localeValues: Locale = { stopped: 'Answer Stopped', stop: 'Stop Answering', regenerate: 'Regenerate', + conversationEmpty: 'No conversation', + today: 'Today', + yesterday: 'Yesterday', + recent7Days: 'Recent 7 days', + recent15Days: 'Recent 15 days', + recent30Days: 'Recent 30 days', + other: 'Other', }, Copy: { copied: 'Copied', diff --git a/src/locale/useLocale.tsx b/src/locale/useLocale.tsx index 9d57f29e2..fe1454858 100644 --- a/src/locale/useLocale.tsx +++ b/src/locale/useLocale.tsx @@ -10,6 +10,13 @@ export interface Locale { stopped: string; stop: string; regenerate: string; + conversationEmpty: string; + today: string; + yesterday: string; + recent7Days: string; + recent15Days: string; + recent30Days: string; + other: string; }; Copy: { copied: string; copy: string }; Dropdown: { selectAll: string; resetText: string; okText: string }; diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 6e3e55449..c5622323e 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -14,6 +14,13 @@ const localeValues: Locale = { stopped: '回答已停止', stop: '停止回答', regenerate: '重新生成', + conversationEmpty: '暂无对话', + today: '今天', + yesterday: '昨天', + recent7Days: '近7天', + recent15Days: '近15天', + recent30Days: '近30天', + other: '其他', }, Copy: { copied: '复制成功', From f3bdc9f686413341ff8d7bea704ab81c9cd9adb3 Mon Sep 17 00:00:00 2001 From: zwight Date: Thu, 13 Nov 2025 14:40:35 +0800 Subject: [PATCH 2/6] fix(chat): update demo and document for conversations --- .../components/customConversationItem.tsx | 7 ++-- src/chat/index.$tab-conversations.md | 42 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/chat/demos/components/customConversationItem.tsx b/src/chat/demos/components/customConversationItem.tsx index 863ae8c03..d6ed633a5 100644 --- a/src/chat/demos/components/customConversationItem.tsx +++ b/src/chat/demos/components/customConversationItem.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { Input, message } from 'antd'; import classNames from 'classnames'; - -import Conversation from '../../conversations'; -import { IConversationsItemProps } from '../../conversations/Item'; +import { Chat } from 'dt-react-component'; +import { IConversationsItemProps } from 'dt-react-component/chat/conversations/Item'; interface IProps extends IConversationsItemProps { edit?: IConversationsItemProps['info']; @@ -52,6 +51,6 @@ export default function CustomConversionItem(props: IProps) { />
) : ( - + ); } diff --git a/src/chat/index.$tab-conversations.md b/src/chat/index.$tab-conversations.md index e377cbd92..50d6780f4 100644 --- a/src/chat/index.$tab-conversations.md +++ b/src/chat/index.$tab-conversations.md @@ -18,27 +18,27 @@ Conversations 组件用于展示会话列表 ## API -| 参数 | 说明 | 类型 | 默认值 | -| ---------------- | ----------------- | ----------------------------------------------------------------------------------- | ---------- | --- | -| conversations | 会话列表 | `ConversationInfo[]` | - | -| activeKey | 激活的会话 ID | `ConversationInfo['id']` | - | -| defaultActiveKey | 默认激活的会话 ID | `ConversationInfo['id']` | - | -| dropdown | 下拉菜单 | [IConversationsItemProps](?tab=conversations#iconversationsitemprops)`['dropdown']` | - | -| groupable | 是否启用分组 | `boolean` | Groupable` | - | -| className | 自定义类名 | `string` | - | -| style | 自定义样式 | `React.CSSProperties` | - | -| loading | 是否加载中 | `boolean` | - | -| header | 会话列表头部 | `React.ReactNode` | boolean` | - | -| collapsed | 是否折叠 | `boolean` | - | -| onItemClick | 点击会话事件 | `(info: ConversationInfo) => void` | - | -| renderItem | 自定义渲染会话项 | `(props: IConversationsItemProps) => React.ReactNode` | - | +| 参数 | 说明 | 类型 | 默认值 | +| ---------------- | ----------------- | ----------------------------------------------------------------------------------- | ------- | +| conversations | 会话列表 | `ConversationInfo[]` | - | +| activeKey | 激活的会话 ID | `ConversationInfo['id']` | - | +| defaultActiveKey | 默认激活的会话 ID | `ConversationInfo['id']` | - | +| dropdown | 下拉菜单 | [IConversationsItemProps](?tab=conversations#iconversationsitemprops)`['dropdown']` | `false` | +| groupable | 是否启用分组 | `boolean` | - | +| className | 自定义类名 | `string` | - | +| style | 自定义样式 | `React.CSSProperties` | - | +| loading | 是否加载中 | `boolean` | - | +| header | 会话列表头部 | `React.ReactNode` | - | +| collapsed | 是否折叠 | `boolean` | `true` | +| onItemClick | 点击会话事件 | `(info: ConversationInfo) => void` | - | +| renderItem | 自定义渲染会话项 | `(props: IConversationsItemProps) => React.ReactNode` | - | ## IConversationsItemProps -| 参数 | 说明 | 类型 | 默认值 | -| --------- | -------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- | --- | -| info | 对话数据 | `ConversationInfo[]` | - | -| active | 是否激活 | `boolean` | - | -| dropdown | 下拉菜单 | `DropdownProps & { triggerDom?: React.ReactNode | ((conversation: ConversationInfo, info: { originNode: React.ReactNode }) => React.ReactNode) }` | - | -| prefixCls | 前缀 | `string` | - | -| onClick | 点击事件 | `(conversation: ConversationInfo) => void` | - | +| 参数 | 说明 | 类型 | 默认值 | +| --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| info | 对话数据 | `ConversationInfo[]` | - | +| active | 是否激活 | `boolean` | - | +| dropdown | 下拉菜单 | `DropdownProps & { triggerDom?: React.ReactNode \| ((conversation: ConversationInfo, info: { originNode: React.ReactNode }) => React.ReactNode) }` | - | +| prefixCls | 前缀 | `string` | `dtc-conversations` | +| onClick | 点击事件 | `(conversation: ConversationInfo) => void` | - | From a8f0bfdaa687ebbc16c2c4eb2f03600c9b5830d2 Mon Sep 17 00:00:00 2001 From: zwight Date: Thu, 13 Nov 2025 15:25:25 +0800 Subject: [PATCH 3/6] fix(chat): remove log code --- src/chat/conversations/index.tsx | 1 - src/chat/demos/conversations.tsx | 1 - src/chat/demos/global-state/conversations.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/src/chat/conversations/index.tsx b/src/chat/conversations/index.tsx index c6e2a9da2..1d1d7ddbd 100644 --- a/src/chat/conversations/index.tsx +++ b/src/chat/conversations/index.tsx @@ -67,7 +67,6 @@ const Conversations = (props: IConversationsProps) => { if (loading) { return ; } - console.log(groupList); if (!groupList?.length) { return ( { - console.log(info); setEdit(info); }} > diff --git a/src/chat/demos/global-state/conversations.tsx b/src/chat/demos/global-state/conversations.tsx index a285a0a0d..d8745d182 100644 --- a/src/chat/demos/global-state/conversations.tsx +++ b/src/chat/demos/global-state/conversations.tsx @@ -95,7 +95,6 @@ export default function () { { - console.log(info); setEdit(info); }} > From 39294d10ff0ef2440a3bf5eb56fd1b36fc7e2cb6 Mon Sep 17 00:00:00 2001 From: zwight Date: Fri, 14 Nov 2025 15:44:33 +0800 Subject: [PATCH 4/6] fix: update folder name and interface --- src/chat/conversations/GroupTitle/index.tsx | 8 +- src/chat/conversations/Item/index.tsx | 23 +--- src/chat/conversations/groupTitle/index.scss | 6 + src/chat/conversations/groupTitle/index.tsx | 24 ++++ src/chat/conversations/hooks/useGroupable.ts | 5 +- src/chat/conversations/index.tsx | 26 +---- src/chat/conversations/interface.ts | 108 ++++++++++++++++-- src/chat/conversations/item/index.scss | 32 ++++++ src/chat/conversations/item/index.tsx | 69 +++++++++++ .../components/customConversationItem.tsx | 10 +- src/chat/index.$tab-conversations.md | 32 +++--- 11 files changed, 264 insertions(+), 79 deletions(-) create mode 100644 src/chat/conversations/groupTitle/index.scss create mode 100644 src/chat/conversations/groupTitle/index.tsx create mode 100644 src/chat/conversations/item/index.scss create mode 100644 src/chat/conversations/item/index.tsx diff --git a/src/chat/conversations/GroupTitle/index.tsx b/src/chat/conversations/GroupTitle/index.tsx index c6fcf75e3..930957464 100644 --- a/src/chat/conversations/GroupTitle/index.tsx +++ b/src/chat/conversations/GroupTitle/index.tsx @@ -1,14 +1,10 @@ import React from 'react'; import EllipsisText from '../../../ellipsisText'; +import { GroupTitleProps } from '../interface'; import './index.scss'; -export interface IGroupTitleProps { - children?: React.ReactNode; - prefixCls?: string; -} - -const GroupTitle: React.FC = (props) => { +const GroupTitle: React.FC = (props) => { const { prefixCls = 'dtc-conversations' } = props; return (
diff --git a/src/chat/conversations/Item/index.tsx b/src/chat/conversations/Item/index.tsx index 04e99be91..170dc3619 100644 --- a/src/chat/conversations/Item/index.tsx +++ b/src/chat/conversations/Item/index.tsx @@ -1,28 +1,13 @@ -import React, { HTMLAttributes } from 'react'; +import React from 'react'; import { MoreOutlined } from '@dtinsight/react-icons'; -import { Dropdown, DropdownProps } from 'antd'; +import { Dropdown } from 'antd'; import classNames from 'classnames'; import EllipsisText from '../../../ellipsisText'; -import { ConversationInfo } from '../interface'; +import { ConversationInfo, ConversationsItemProps } from '../interface'; import './index.scss'; -export interface IConversationsItemProps extends Omit, 'onClick'> { - info: ConversationInfo; - active?: boolean; - dropdown?: DropdownProps & { - triggerDom?: - | React.ReactNode - | (( - conversation: ConversationInfo, - info: { originNode: React.ReactNode } - ) => React.ReactNode); - }; - prefixCls?: string; - onClick?: (info: ConversationInfo) => void; -} - -const Item: React.FC = (props) => { +const Item: React.FC = (props) => { const { info, active, prefixCls, dropdown, onClick } = props; const { disabled } = info; diff --git a/src/chat/conversations/groupTitle/index.scss b/src/chat/conversations/groupTitle/index.scss new file mode 100644 index 000000000..1c98cb809 --- /dev/null +++ b/src/chat/conversations/groupTitle/index.scss @@ -0,0 +1,6 @@ +.dtc-conversations-title { + color: #B1B4C5; + line-height: 20px; + font-size: 12px; + margin-bottom: 4px; +} diff --git a/src/chat/conversations/groupTitle/index.tsx b/src/chat/conversations/groupTitle/index.tsx new file mode 100644 index 000000000..930957464 --- /dev/null +++ b/src/chat/conversations/groupTitle/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import EllipsisText from '../../../ellipsisText'; +import { GroupTitleProps } from '../interface'; +import './index.scss'; + +const GroupTitle: React.FC = (props) => { + const { prefixCls = 'dtc-conversations' } = props; + return ( +
+ {props.children && ( + + )} +
+ ); +}; + +export default GroupTitle; diff --git a/src/chat/conversations/hooks/useGroupable.ts b/src/chat/conversations/hooks/useGroupable.ts index 082dcada8..d3ea0b73e 100644 --- a/src/chat/conversations/hooks/useGroupable.ts +++ b/src/chat/conversations/hooks/useGroupable.ts @@ -3,13 +3,12 @@ import dayjs from 'dayjs'; import shortid from 'shortid'; import useLocale, { Locale } from '../../../locale/useLocale'; -import { ConversationInfo, Groupable, GroupInfo } from '../interface'; -import { IConversationsProps } from '..'; +import { ConversationInfo, ConversationsProps, Groupable, GroupInfo } from '../interface'; const DEFAULT_GROUP_KEY = 'updatedAt'; type GroupMap = Record; const useGroupable = ( - groupable: IConversationsProps['groupable'], + groupable: ConversationsProps['groupable'], conversations: ConversationInfo[] ): [list: GroupInfo[], enable: boolean] => { const locale = useLocale('Chat'); diff --git a/src/chat/conversations/index.tsx b/src/chat/conversations/index.tsx index 1d1d7ddbd..81e5bc7fc 100644 --- a/src/chat/conversations/index.tsx +++ b/src/chat/conversations/index.tsx @@ -5,31 +5,13 @@ import classNames from 'classnames'; import Empty from '../../empty'; import useLocale from '../../locale/useLocale'; import useGroupable from './hooks/useGroupable'; -import GroupTitle from './GroupTitle'; -import { ConversationInfo, Groupable } from './interface'; -import Item, { IConversationsItemProps } from './Item'; +import GroupTitle from './groupTitle'; +import { ConversationInfo, ConversationsProps } from './interface'; +import Item from './item'; import './index.scss'; -export interface IConversationsProps extends React.HTMLAttributes { - conversations: ConversationInfo[]; - activeKey?: ConversationInfo['id']; - defaultActiveKey?: ConversationInfo['id']; - dropdown?: - | IConversationsItemProps['dropdown'] - | ((info: ConversationInfo) => IConversationsItemProps['dropdown']); - groupable?: boolean | Groupable; - className?: string; - style?: React.CSSProperties; - loading?: boolean; - header?: React.ReactNode | boolean; - collapsed?: boolean; - - onItemClick?: (info: ConversationInfo) => void; - renderItem?: (props: IConversationsItemProps) => React.ReactNode; -} - const prefixCls = 'dtc-conversations'; -const Conversations = (props: IConversationsProps) => { +const Conversations = (props: ConversationsProps) => { const { conversations, activeKey, diff --git a/src/chat/conversations/interface.ts b/src/chat/conversations/interface.ts index 2ac4678ff..29a6fc753 100644 --- a/src/chat/conversations/interface.ts +++ b/src/chat/conversations/interface.ts @@ -1,30 +1,122 @@ +import { HTMLAttributes } from 'react'; +import { DropdownProps } from 'antd'; + import { ConversationProperties } from '../entity'; -import { IGroupTitleProps } from './GroupTitle'; +/** + * 单条会话信息结构 + * 用于描述侧边栏会话列表中的一项 + */ export interface ConversationInfo extends ConversationProperties { + /** 会话所属分组(用于分组展示,可选,默认以updateAt分组) */ group?: string; + /** 会话项自定义图标 */ icon?: React.ReactNode; + /** 是否禁用此会话(禁用点击与交互) */ disabled?: boolean; } - -export type GroupSorter = Parameters<[string, ConversationInfo[]][]['sort']>[0]; - -export type GroupTitleRenderComponents = { - components: { - GroupTitle: React.ComponentType; +/** + * Conversations 会话组件入参 + * 用于渲染会话列表与相关交互 + */ +export interface ConversationsProps extends React.HTMLAttributes { + /** 会话列表数据源 */ + conversations: ConversationInfo[]; + /** 当前激活会话的 id(受控模式) */ + activeKey?: ConversationInfo['id']; + /** 默认激活会话 id(非受控模式) */ + defaultActiveKey?: ConversationInfo['id']; + /** + * 自定义每一项的下拉菜单 + * - 传入对象时:所有项共享同一配置 + * - 传入方法时:可根据不同会话动态生成 + */ + dropdown?: + | ConversationsItemProps['dropdown'] + | ((info: ConversationInfo) => ConversationsItemProps['dropdown']); + /** 是否启用按 group 分组展示(true 时使用默认配置,也可传入自定义 Groupable 配置) */ + groupable?: boolean | Groupable; + className?: string; + style?: React.CSSProperties; + loading?: boolean; + /** 列表头部区域内容(false 表示不展示) */ + header?: React.ReactNode | boolean; + /** 是否为折叠状态(折叠时仅展示图标) */ + collapsed?: boolean; + /** 点击某条会话时触发 */ + onItemClick?: (info: ConversationInfo) => void; + /** 自定义渲染每一项的内容 */ + renderItem?: (props: ConversationsItemProps) => React.ReactNode; +} +/** + * Conversations.Item 单个会话项组件的入参 + * 用于渲染侧边栏中的一条会话 + */ +export interface ConversationsItemProps extends Omit, 'onClick'> { + /** 当前会话项的数据对象 */ + info: ConversationInfo; + /** 是否为激活状态 */ + active?: boolean; + /** + * 下拉菜单配置(用于操作会话项) + * - 可传入 DropdownProps + * - 支持通过 triggerDom 自定义触发节点 + */ + dropdown?: DropdownProps & { + triggerDom?: + | React.ReactNode + | (( + conversation: ConversationInfo, + info: { originNode: React.ReactNode } + ) => React.ReactNode); }; -}; + prefixCls?: string; + onClick?: (info: ConversationInfo) => void; +} + +/** + * 分组组件入参 + */ +export interface GroupTitleProps { + /** 分组标题内容 */ + children?: React.ReactNode; + prefixCls?: string; +} +/** + * 处理之后的分组数据 + */ export type GroupInfo = { + /** 分组内的会话列表 */ conversations: ConversationInfo[]; + /** 分组唯一标识(可选) */ id?: string; + /** 自定义渲染后的标题(Groupable.title 的结果) */ title?: Groupable['title']; name?: string; }; + +/** 分组排序函数类型,来自 Array.sort 的入参类型 */ +export type GroupSorter = Parameters<[string, ConversationInfo[]][]['sort']>[0]; + +/** 自定义分组标题渲染时可访问的内置组件 */ +export type GroupTitleRenderComponents = { + components: { + GroupTitle: React.ComponentType; + }; +}; +/** + * 分组标题渲染函数 + * 用于完全自定义分组标题渲染逻辑 + */ export type GroupTitleRender = | ((groupInfo: GroupInfo, info: GroupTitleRenderComponents) => React.ReactNode) | undefined; +/** + * 分组功能配置 + * 控制会话列表是否按 group 分组显示 + */ export interface Groupable { /** * @desc 分组排序函数 diff --git a/src/chat/conversations/item/index.scss b/src/chat/conversations/item/index.scss new file mode 100644 index 000000000..cc8fba74d --- /dev/null +++ b/src/chat/conversations/item/index.scss @@ -0,0 +1,32 @@ +.dtc-conversations-item { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + gap: 4px; + padding: 0 16px; + border-radius: 4px; + cursor: pointer; + color: #3D446E; + &-title { + flex: 1; + overflow: hidden; + } + &:hover { + background-color: #EBECF0; + .dtc-conversations-menu-icon { + display: block; + } + } + .dtc-conversations-menu-icon { + display: none; + } + &--active, &--active:hover { + color: #1D78FF; + background-color: #E8F1FF; + } + &--disabled, &--disabled:hover { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/src/chat/conversations/item/index.tsx b/src/chat/conversations/item/index.tsx new file mode 100644 index 000000000..170dc3619 --- /dev/null +++ b/src/chat/conversations/item/index.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { MoreOutlined } from '@dtinsight/react-icons'; +import { Dropdown } from 'antd'; +import classNames from 'classnames'; + +import EllipsisText from '../../../ellipsisText'; +import { ConversationInfo, ConversationsItemProps } from '../interface'; +import './index.scss'; + +const Item: React.FC = (props) => { + const { info, active, prefixCls, dropdown, onClick } = props; + + const { disabled } = info; + const { triggerDom } = dropdown || {}; + + const handleClick = () => { + if (disabled || active) return; + onClick?.(info); + }; + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + const renderMenuTrigger = (conversation: ConversationInfo) => { + const originTriggerNode = ( + + ); + if (triggerDom) { + return typeof triggerDom === 'function' + ? triggerDom(conversation, { originNode: originTriggerNode }) + : triggerDom; + } + return originTriggerNode; + }; + return ( +
+ {info.icon &&
{info.icon}
} +
+ +
+ {!disabled && dropdown?.overlay && ( + + {renderMenuTrigger(info)} + + )} +
+ ); +}; + +export default Item; diff --git a/src/chat/demos/components/customConversationItem.tsx b/src/chat/demos/components/customConversationItem.tsx index d6ed633a5..f526c1d05 100644 --- a/src/chat/demos/components/customConversationItem.tsx +++ b/src/chat/demos/components/customConversationItem.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { Input, message } from 'antd'; import classNames from 'classnames'; import { Chat } from 'dt-react-component'; -import { IConversationsItemProps } from 'dt-react-component/chat/conversations/Item'; +import { ConversationsItemProps } from 'dt-react-component/chat/conversations/interface'; -interface IProps extends IConversationsItemProps { - edit?: IConversationsItemProps['info']; - onRename?: (info: IConversationsItemProps['info'], value: string) => Promise; - setEdit: (edit?: IConversationsItemProps['info']) => void; +interface IProps extends ConversationsItemProps { + edit?: ConversationsItemProps['info']; + onRename?: (info: ConversationsItemProps['info'], value: string) => Promise; + setEdit: (edit?: ConversationsItemProps['info']) => void; } export default function CustomConversionItem(props: IProps) { diff --git a/src/chat/index.$tab-conversations.md b/src/chat/index.$tab-conversations.md index 50d6780f4..42029b682 100644 --- a/src/chat/index.$tab-conversations.md +++ b/src/chat/index.$tab-conversations.md @@ -18,22 +18,22 @@ Conversations 组件用于展示会话列表 ## API -| 参数 | 说明 | 类型 | 默认值 | -| ---------------- | ----------------- | ----------------------------------------------------------------------------------- | ------- | -| conversations | 会话列表 | `ConversationInfo[]` | - | -| activeKey | 激活的会话 ID | `ConversationInfo['id']` | - | -| defaultActiveKey | 默认激活的会话 ID | `ConversationInfo['id']` | - | -| dropdown | 下拉菜单 | [IConversationsItemProps](?tab=conversations#iconversationsitemprops)`['dropdown']` | `false` | -| groupable | 是否启用分组 | `boolean` | - | -| className | 自定义类名 | `string` | - | -| style | 自定义样式 | `React.CSSProperties` | - | -| loading | 是否加载中 | `boolean` | - | -| header | 会话列表头部 | `React.ReactNode` | - | -| collapsed | 是否折叠 | `boolean` | `true` | -| onItemClick | 点击会话事件 | `(info: ConversationInfo) => void` | - | -| renderItem | 自定义渲染会话项 | `(props: IConversationsItemProps) => React.ReactNode` | - | - -## IConversationsItemProps +| 参数 | 说明 | 类型 | 默认值 | +| ---------------- | ----------------- | ---------------------------------------------------------------------------------- | ------- | +| conversations | 会话列表 | `ConversationInfo[]` | - | +| activeKey | 激活的会话 ID | `ConversationInfo['id']` | - | +| defaultActiveKey | 默认激活的会话 ID | `ConversationInfo['id']` | - | +| dropdown | 下拉菜单 | [ConversationsItemProps](?tab=conversations#iconversationsitemprops)`['dropdown']` | `false` | +| groupable | 是否启用分组 | `boolean` | - | +| className | 自定义类名 | `string` | - | +| style | 自定义样式 | `React.CSSProperties` | - | +| loading | 是否加载中 | `boolean` | - | +| header | 会话列表头部 | `React.ReactNode` | - | +| collapsed | 是否折叠 | `boolean` | `true` | +| onItemClick | 点击会话事件 | `(info: ConversationInfo) => void` | - | +| renderItem | 自定义渲染会话项 | `(props: ConversationsItemProps) => React.ReactNode` | - | + +## ConversationsItemProps | 参数 | 说明 | 类型 | 默认值 | | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | From ca4fe7ae019506a3e92bb33f1a3a0841cd2045a7 Mon Sep 17 00:00:00 2001 From: zwight Date: Mon, 17 Nov 2025 19:08:32 +0800 Subject: [PATCH 5/6] fix(chat): update conversation component code style --- .../__snapshots__/conversations.test.tsx.snap | 52 +++++++++---------- src/chat/__tests__/conversations.test.tsx | 16 +++--- src/chat/conversations/GroupTitle/index.scss | 2 +- src/chat/conversations/GroupTitle/index.tsx | 4 +- src/chat/conversations/Item/index.scss | 8 +-- src/chat/conversations/Item/index.tsx | 15 +++--- src/chat/conversations/groupTitle/index.scss | 2 +- src/chat/conversations/groupTitle/index.tsx | 4 +- src/chat/conversations/index.scss | 8 +-- src/chat/conversations/index.tsx | 10 ++-- src/chat/conversations/interface.ts | 1 - src/chat/conversations/item/index.scss | 8 +-- src/chat/conversations/item/index.tsx | 15 +++--- src/chat/demos/index.scss | 2 +- src/chat/index.$tab-conversations.md | 13 +++-- 15 files changed, 79 insertions(+), 81 deletions(-) diff --git a/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap b/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap index 5f9c91dc5..8041d0c07 100644 --- a/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap +++ b/src/chat/__tests__/__snapshots__/conversations.test.tsx.snap @@ -3,10 +3,10 @@ exports[`Test Chat Conversations Match snapshot 1`] = `
@@ -15,10 +15,10 @@ exports[`Test Chat Conversations Match snapshot 1`] = ` exports[`Test Chat Conversations Match snapshot: collapsed 1`] = `
@@ -27,14 +27,14 @@ exports[`Test Chat Conversations Match snapshot: collapsed 1`] = ` exports[`Test Chat Conversations Match snapshot: groupable 1`] = `
        @@ -122,12 +122,12 @@ exports[`Test Chat Conversations Match snapshot: handleCreateChat 1`] = ` exports[`Test Chat Conversations Match snapshot: loading 1`] = `
          diff --git a/src/chat/__tests__/conversations.test.tsx b/src/chat/__tests__/conversations.test.tsx index 493be2415..8214b36dd 100644 --- a/src/chat/__tests__/conversations.test.tsx +++ b/src/chat/__tests__/conversations.test.tsx @@ -84,17 +84,17 @@ describe('Test Chat Conversations', () => { it('Should loading', () => { const { container } = render(); - const ele = container.querySelector('.dtc-conversations-wrapper')?.children[0]; + const ele = container.querySelector('.dtc__conversations__wrapper')?.children[0]; expect(ele).toBeInTheDocument(); - expect(ele?.className).toContain('dtc-conversations-spin-wrapper'); + expect(ele?.className).toContain('dtc__conversations__spin__wrapper'); }); it('Should collapsed', () => { const { container } = render(); - const ele = container.querySelector('.dtc-conversations-wrapper'); + const ele = container.querySelector('.dtc__conversations__wrapper'); expect(ele).toBeInTheDocument(); - expect(ele?.className).toContain('dtc-conversations--collapsed'); + expect(ele?.className).toContain('dtc__conversations--collapsed'); }); it('Should select item', () => { @@ -103,9 +103,9 @@ describe('Test Chat Conversations', () => { ); - const ele = container.querySelector('.dtc-conversations-item'); + const ele = container.querySelector('.dtc__conversations__item'); expect(ele).toBeInTheDocument(); - expect(ele?.className).toContain('dtc-conversations-item--active'); + expect(ele?.className).toContain('dtc__conversations__item--active'); }); it('Should group list title', () => { @@ -113,7 +113,7 @@ describe('Test Chat Conversations', () => { ); - const ele = container.querySelectorAll('.dtc-conversations-title'); + const ele = container.querySelectorAll('.dtc__conversations__title'); expect(ele).toHaveLength(2); }); @@ -166,7 +166,7 @@ describe('Test Chat Conversations', () => { /> ); - const nodeList = container.querySelectorAll('.dtc-conversations-item'); + const nodeList = container.querySelectorAll('.dtc__conversations__item'); const ele = nodeList?.item(nodeList?.length - 1); expect(onItemClick).not.toBeCalled(); diff --git a/src/chat/conversations/GroupTitle/index.scss b/src/chat/conversations/GroupTitle/index.scss index 1c98cb809..e48c3d57c 100644 --- a/src/chat/conversations/GroupTitle/index.scss +++ b/src/chat/conversations/GroupTitle/index.scss @@ -1,4 +1,4 @@ -.dtc-conversations-title { +.dtc__conversations__title { color: #B1B4C5; line-height: 20px; font-size: 12px; diff --git a/src/chat/conversations/GroupTitle/index.tsx b/src/chat/conversations/GroupTitle/index.tsx index 930957464..71b89822a 100644 --- a/src/chat/conversations/GroupTitle/index.tsx +++ b/src/chat/conversations/GroupTitle/index.tsx @@ -5,9 +5,9 @@ import { GroupTitleProps } from '../interface'; import './index.scss'; const GroupTitle: React.FC = (props) => { - const { prefixCls = 'dtc-conversations' } = props; + const { prefixCls = 'dtc__conversations' } = props; return ( -
          +
          {props.children && ( = (props) => { - const { info, active, prefixCls, dropdown, onClick } = props; + const { info, active, dropdown, onClick } = props; const { disabled } = info; const { triggerDom } = dropdown || {}; @@ -22,7 +23,7 @@ const Item: React.FC = (props) => { }; const renderMenuTrigger = (conversation: ConversationInfo) => { const originTriggerNode = ( - + ); if (triggerDom) { return typeof triggerDom === 'function' @@ -34,14 +35,14 @@ const Item: React.FC = (props) => { return (
          - {info.icon &&
          {info.icon}
          } -
          + {info.icon &&
          {info.icon}
          } +
          = (props) => { - const { prefixCls = 'dtc-conversations' } = props; + const { prefixCls = 'dtc__conversations' } = props; return ( -
          +
          {props.children && ( { const { conversations, @@ -47,7 +47,7 @@ const Conversations = (props: ConversationsProps) => { const renderConversations = () => { if (loading) { - return ; + return ; } if (!groupList?.length) { return ( @@ -68,7 +68,6 @@ const Conversations = (props: ConversationsProps) => { return renderItem({ info: conversation, active: conversation.id === value, - prefixCls, onClick: handleItemClick, dropdown: dropdownVal, }); @@ -79,7 +78,6 @@ const Conversations = (props: ConversationsProps) => { info={conversation} active={conversation.id === value} onClick={handleItemClick} - prefixCls={prefixCls} dropdown={dropdownVal} /> ); @@ -92,7 +90,7 @@ const Conversations = (props: ConversationsProps) => { {group.name} )} -
            {items}
          +
            {items}
          ); } @@ -110,7 +108,7 @@ const Conversations = (props: ConversationsProps) => { return (
          React.ReactNode); }; - prefixCls?: string; onClick?: (info: ConversationInfo) => void; } diff --git a/src/chat/conversations/item/index.scss b/src/chat/conversations/item/index.scss index cc8fba74d..6a7a1dadb 100644 --- a/src/chat/conversations/item/index.scss +++ b/src/chat/conversations/item/index.scss @@ -1,4 +1,4 @@ -.dtc-conversations-item { +.dtc__conversations__item { display: flex; align-items: center; justify-content: center; @@ -8,17 +8,17 @@ border-radius: 4px; cursor: pointer; color: #3D446E; - &-title { + &__title { flex: 1; overflow: hidden; } &:hover { background-color: #EBECF0; - .dtc-conversations-menu-icon { + .dtc__conversations__menu__icon { display: block; } } - .dtc-conversations-menu-icon { + .dtc__conversations__menu__icon { display: none; } &--active, &--active:hover { diff --git a/src/chat/conversations/item/index.tsx b/src/chat/conversations/item/index.tsx index 170dc3619..c2dea873d 100644 --- a/src/chat/conversations/item/index.tsx +++ b/src/chat/conversations/item/index.tsx @@ -7,8 +7,9 @@ import EllipsisText from '../../../ellipsisText'; import { ConversationInfo, ConversationsItemProps } from '../interface'; import './index.scss'; +const prefixCls = 'dtc__conversations'; const Item: React.FC = (props) => { - const { info, active, prefixCls, dropdown, onClick } = props; + const { info, active, dropdown, onClick } = props; const { disabled } = info; const { triggerDom } = dropdown || {}; @@ -22,7 +23,7 @@ const Item: React.FC = (props) => { }; const renderMenuTrigger = (conversation: ConversationInfo) => { const originTriggerNode = ( - + ); if (triggerDom) { return typeof triggerDom === 'function' @@ -34,14 +35,14 @@ const Item: React.FC = (props) => { return (
          - {info.icon &&
          {info.icon}
          } -
          + {info.icon &&
          {info.icon}
          } +
          React.ReactNode) }` | - | -| prefixCls | 前缀 | `string` | `dtc-conversations` | -| onClick | 点击事件 | `(conversation: ConversationInfo) => void` | - | +| 参数 | 说明 | 类型 | 默认值 | +| -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| info | 对话数据 | `ConversationInfo[]` | - | +| active | 是否激活 | `boolean` | - | +| dropdown | 下拉菜单 | `DropdownProps & { triggerDom?: React.ReactNode \| ((conversation: ConversationInfo, info: { originNode: React.ReactNode }) => React.ReactNode) }` | - | +| onClick | 点击事件 | `(conversation: ConversationInfo) => void` | - | From cf1cee71ab0fa3efac642d522f3153a5a8f56e2d Mon Sep 17 00:00:00 2001 From: zwight Date: Mon, 17 Nov 2025 19:24:26 +0800 Subject: [PATCH 6/6] fix(chat): remove capitalized word folder --- src/chat/conversations/GroupTitle/index.scss | 6 -- src/chat/conversations/GroupTitle/index.tsx | 24 ------- src/chat/conversations/Item/index.scss | 32 --------- src/chat/conversations/Item/index.tsx | 70 -------------------- 4 files changed, 132 deletions(-) delete mode 100644 src/chat/conversations/GroupTitle/index.scss delete mode 100644 src/chat/conversations/GroupTitle/index.tsx delete mode 100644 src/chat/conversations/Item/index.scss delete mode 100644 src/chat/conversations/Item/index.tsx diff --git a/src/chat/conversations/GroupTitle/index.scss b/src/chat/conversations/GroupTitle/index.scss deleted file mode 100644 index e48c3d57c..000000000 --- a/src/chat/conversations/GroupTitle/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.dtc__conversations__title { - color: #B1B4C5; - line-height: 20px; - font-size: 12px; - margin-bottom: 4px; -} diff --git a/src/chat/conversations/GroupTitle/index.tsx b/src/chat/conversations/GroupTitle/index.tsx deleted file mode 100644 index 71b89822a..000000000 --- a/src/chat/conversations/GroupTitle/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import EllipsisText from '../../../ellipsisText'; -import { GroupTitleProps } from '../interface'; -import './index.scss'; - -const GroupTitle: React.FC = (props) => { - const { prefixCls = 'dtc__conversations' } = props; - return ( -
          - {props.children && ( - - )} -
          - ); -}; - -export default GroupTitle; diff --git a/src/chat/conversations/Item/index.scss b/src/chat/conversations/Item/index.scss deleted file mode 100644 index 6a7a1dadb..000000000 --- a/src/chat/conversations/Item/index.scss +++ /dev/null @@ -1,32 +0,0 @@ -.dtc__conversations__item { - display: flex; - align-items: center; - justify-content: center; - height: 32px; - gap: 4px; - padding: 0 16px; - border-radius: 4px; - cursor: pointer; - color: #3D446E; - &__title { - flex: 1; - overflow: hidden; - } - &:hover { - background-color: #EBECF0; - .dtc__conversations__menu__icon { - display: block; - } - } - .dtc__conversations__menu__icon { - display: none; - } - &--active, &--active:hover { - color: #1D78FF; - background-color: #E8F1FF; - } - &--disabled, &--disabled:hover { - cursor: not-allowed; - opacity: 0.5; - } -} diff --git a/src/chat/conversations/Item/index.tsx b/src/chat/conversations/Item/index.tsx deleted file mode 100644 index c2dea873d..000000000 --- a/src/chat/conversations/Item/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { MoreOutlined } from '@dtinsight/react-icons'; -import { Dropdown } from 'antd'; -import classNames from 'classnames'; - -import EllipsisText from '../../../ellipsisText'; -import { ConversationInfo, ConversationsItemProps } from '../interface'; -import './index.scss'; - -const prefixCls = 'dtc__conversations'; -const Item: React.FC = (props) => { - const { info, active, dropdown, onClick } = props; - - const { disabled } = info; - const { triggerDom } = dropdown || {}; - - const handleClick = () => { - if (disabled || active) return; - onClick?.(info); - }; - const stopPropagation = (e: React.MouseEvent) => { - e.stopPropagation(); - }; - const renderMenuTrigger = (conversation: ConversationInfo) => { - const originTriggerNode = ( - - ); - if (triggerDom) { - return typeof triggerDom === 'function' - ? triggerDom(conversation, { originNode: originTriggerNode }) - : triggerDom; - } - return originTriggerNode; - }; - return ( -
          - {info.icon &&
          {info.icon}
          } -
          - -
          - {!disabled && dropdown?.overlay && ( - - {renderMenuTrigger(info)} - - )} -
          - ); -}; - -export default Item;