diff --git a/.gitignore b/.gitignore index af6656d400..a0dea569b3 100755 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ Dockerfile robotMsg.json .gitlogmap .eslintcache +.codebuddy +.tico \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index db50e1696e..7e99754fea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,12 @@ { "eslint.format.enable": true, - "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"], + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue" + ], "[vue]": { "editor.formatOnSave": true, "editor.defaultFormatter": "dbaeumer.vscode-eslint" @@ -42,6 +48,8 @@ "tmenu", "tnode", "vnode", - "wechat" + "wechat", + "Actionbar", + "vueify" ] } diff --git a/internal/builds/vue-next-chat/build-components.ts b/internal/builds/vue-next-chat/build-components.ts index edf2aa0c7f..1c1ebea004 100644 --- a/internal/builds/vue-next-chat/build-components.ts +++ b/internal/builds/vue-next-chat/build-components.ts @@ -179,7 +179,11 @@ export const buildEs = async () => { input: [...inputList, `!${joinProComponentsChatRoot('index-lib.ts')}`], // 为了保留 style/css.js treeshake: false, - external: esExternal, + external: [ + ...esExternal, + /\.css$/, // 排除所有 CSS 文件 + /tdesign-web-components.*\.css$/, // 排除 tdesign-web-components 的 CSS 文件 + ], plugins: [multiInput({ relative: joinProComponentsChatRoot() }), ...getPlugins({ cssBuildType: 'multi' })], }); bundle.write({ @@ -203,7 +207,13 @@ export const buildEs = async () => { export const buildEsm = async () => { const bundle = await rollup({ input: [...inputList, `!${joinProComponentsChatRoot('index-lib.ts')}`], - external: [...externalDeps, ...externalPeerDeps, /@tdesign\/common-style/], + external: [ + ...externalDeps, + ...externalPeerDeps, + /@tdesign\/common-style/, + /\.css$/, // 排除所有 CSS 文件 + /tdesign-web-components.*\.css$/, // 排除 tdesign-web-components 的 CSS 文件 + ], plugins: [multiInput({ relative: joinProComponentsChatRoot() }), ...getPlugins({ cssBuildType: 'source' })], }); await bundle.write({ diff --git a/internal/builds/vue-next-chat/build-types.ts b/internal/builds/vue-next-chat/build-types.ts index f4238d2d73..7a68e66dc7 100644 --- a/internal/builds/vue-next-chat/build-types.ts +++ b/internal/builds/vue-next-chat/build-types.ts @@ -11,7 +11,7 @@ const generateSourceTypes = async () => { const typesRoot = joinWorkspaceRoot(typesTempDir); // 2. 删除 style 目录 - const styleDirPaths = await glob(`${joinPosix(typesRoot, 'packages/**/style')}`); + const styleDirPaths = await glob(`${joinPosix(typesRoot, 'packages/pro-components/chat/**/style')}`); await Promise.all( styleDirPaths.map(async (styleDirPath) => { await remove(styleDirPath); @@ -27,20 +27,17 @@ const generateTargetTypes = async (target: 'es' | 'esm' | 'lib' | 'cjs') => { // 1. 复制 packages/pro-components/chat 到 packages/tdesign-vue-next-chat/target 下 const targetDir = joinTdesignVueNextChatRoot(`${target}`); - // TODO - // temp delete 'dist/types/packages/pro-components/chat/_example' + // should be use correct tsconfig.json to generate correct types - await remove(joinPosix(typesRoot, `packages/pro-components/chat/_example`)); await copy(joinPosix(typesRoot, `packages/pro-components/chat`), targetDir); // 2. 替换 @tdesign/common-js 为 tdesign-vue-next/common/js - // TODO: check if this is needed, NOW chat does not use common-js - // const dtsPaths = await glob(`${joinPosix(targetDir, '**/*.d.ts')}`); - // const rewrite = dtsPaths.map(async (filePath) => { - // const content = await readFile(filePath, 'utf8'); - // await writeFile(filePath, content.replace(/@tdesign\/common-js/g, `tdesign-vue-next/${target}/common/js`), 'utf8'); - // }); - // await Promise.all(rewrite); + const dtsPaths = await glob(`${joinPosix(targetDir, '**/*.d.ts')}`); + const rewrite = dtsPaths.map(async (filePath) => { + const content = await readFile(filePath, 'utf8'); + await writeFile(filePath, content.replace(/@tdesign\/common-js/g, `tdesign-vue-next/${target}/common/js`), 'utf8'); + }); + await Promise.all(rewrite); }; const removeSourceTypes = async () => { @@ -52,7 +49,6 @@ export const buildTypes = async () => { try { await removeSourceTypes(); await generateSourceTypes(); - // const targets = ['es', 'esm', 'lib', 'cjs'] as const; const targets = ['es', 'esm'] as const; await Promise.all( targets.map(async (target) => { diff --git a/internal/utils/src/catalogs.ts b/internal/utils/src/catalogs.ts index 9681ab8014..e5ad69f706 100644 --- a/internal/utils/src/catalogs.ts +++ b/internal/utils/src/catalogs.ts @@ -60,7 +60,7 @@ export const catalogs = { sortablejs: '^1.15.0', tinycolor2: '^1.6.0', validator: '^13.9.0', - vue: '^3.3.9', + vue: '^3.5.0', 'vue-router': '^4.2.4', }, docs: { diff --git a/package.json b/package.json index 4f1b12bfcf..89c32d2ed1 100644 --- a/package.json +++ b/package.json @@ -83,4 +83,4 @@ "eslint --fix --cache" ] } -} +} \ No newline at end of file diff --git a/packages/components/affix/affix.tsx b/packages/components/affix/affix.tsx index 1cd9d12e08..d9401678fa 100644 --- a/packages/components/affix/affix.tsx +++ b/packages/components/affix/affix.tsx @@ -14,7 +14,7 @@ export default defineComponent({ const COMPONENT_NAME = usePrefixClass('affix'); const renderContent = useContent(); - const affixWrapRef = ref(null); + const affixWrapRef = ref(null); const affixRef = ref(null); const placeholderEL = ref(document?.createElement('div')); // 占位节点 const ticking = ref(false); diff --git a/packages/components/alert/alert.tsx b/packages/components/alert/alert.tsx index 4f23b14e57..c3ac2872ac 100644 --- a/packages/components/alert/alert.tsx +++ b/packages/components/alert/alert.tsx @@ -40,9 +40,9 @@ export default defineComponent({ const renderIconTNode = useIcon(); // alert的dom引用 - const alertRef = ref(null); + const alertRef = ref(); // description的dom引用 - const descriptionRef = ref(null); + const descriptionRef = ref(); // desc高度 const descHeight = ref(0); // 是否可见,关闭后置为false diff --git a/packages/components/anchor/anchor-target.tsx b/packages/components/anchor/anchor-target.tsx index fd62a17287..b39ab9282c 100644 --- a/packages/components/anchor/anchor-target.tsx +++ b/packages/components/anchor/anchor-target.tsx @@ -30,6 +30,7 @@ export default defineComponent({ const className = [`${COMPONENT_NAME.value}__target`]; const iconClassName = `${classPrefix.value}-copy`; return ( + // @ts-expect-error {children && children(null)} diff --git a/packages/components/anchor/anchor.tsx b/packages/components/anchor/anchor.tsx index 2b43dda4dc..7f8d4dafc5 100644 --- a/packages/components/anchor/anchor.tsx +++ b/packages/components/anchor/anchor.tsx @@ -40,7 +40,7 @@ export default defineComponent({ const anchorRef = ref(null); const links = ref([]); const active = ref(''); - const scrollContainer = ref(null); + const scrollContainer = ref(); const handleScrollLock = ref(false); const activeLineStyle = reactive({}); const COMPONENT_NAME = usePrefixClass('anchor'); diff --git a/packages/components/avatar/__tests__/avatar.test.tsx b/packages/components/avatar/__tests__/avatar.test.tsx index f65501212c..921e71479f 100644 --- a/packages/components/avatar/__tests__/avatar.test.tsx +++ b/packages/components/avatar/__tests__/avatar.test.tsx @@ -13,6 +13,7 @@ describe('Avatar', () => { describe('props', () => { let wrapper: VueWrapper> | null = null; beforeEach(() => { + // @ts-ignore TODO wrapper = mount(Avatar) as VueWrapper>; }); diff --git a/packages/components/breadcrumb/breadcrumb-item.tsx b/packages/components/breadcrumb/breadcrumb-item.tsx index fdc45307f2..5f328f7398 100644 --- a/packages/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/components/breadcrumb/breadcrumb-item.tsx @@ -56,6 +56,7 @@ export default defineComponent({ }); const handleClick = () => { + // @ts-ignore const router = props.router || proxy.$router; if (props.to && router) { diff --git a/packages/components/calendar/calendar.tsx b/packages/components/calendar/calendar.tsx index 00cf19fd4a..7687a2762f 100644 --- a/packages/components/calendar/calendar.tsx +++ b/packages/components/calendar/calendar.tsx @@ -275,7 +275,6 @@ export default defineComponent({
); }; @@ -189,7 +188,6 @@ export default defineComponent({ option={props.option} options={props.options} empty={props.empty} - visible={visible} trigger={props.trigger} loading={props.loading} loadingText={props.loadingText} diff --git a/packages/components/color-picker/color-picker-panel.tsx b/packages/components/color-picker/color-picker-panel.tsx index da77c940c7..740a3dab84 100644 --- a/packages/components/color-picker/color-picker-panel.tsx +++ b/packages/components/color-picker/color-picker-panel.tsx @@ -11,6 +11,6 @@ export default defineComponent({ setup(props, { attrs }) { const newProps = computed(() => pickBy({ ...props, ...attrs }, (v) => v !== undefined)); const prefix = usePrefixClass(); - return () => ; + return () => ; }, }); diff --git a/packages/components/color-picker/components/panel/saturation.tsx b/packages/components/color-picker/components/panel/saturation.tsx index e2dfc0d33b..f64b49e6c5 100644 --- a/packages/components/color-picker/components/panel/saturation.tsx +++ b/packages/components/color-picker/components/panel/saturation.tsx @@ -12,7 +12,7 @@ export default defineComponent({ props: baseProps, setup(props) { const baseClassName = useBaseClassName(); - const refPanel = ref(null); + const refPanel = ref(); const refThumb = ref(null); const dragInstance = ref(null); const panelRect = reactive({ diff --git a/packages/components/color-picker/components/panel/slider.tsx b/packages/components/color-picker/components/panel/slider.tsx index 48013301b3..2ab397937b 100644 --- a/packages/components/color-picker/components/panel/slider.tsx +++ b/packages/components/color-picker/components/panel/slider.tsx @@ -30,7 +30,7 @@ export default defineComponent({ }, setup(props) { const baseClassName = useBaseClassName(); - const refPanel = ref(null); + const refPanel = ref(); const refThumb = ref(null); const dragInstance = ref(null); const panelRect = reactive({ diff --git a/packages/components/config-provider/type.ts b/packages/components/config-provider/type.ts index eee886a56e..4bb50a9691 100644 --- a/packages/components/config-provider/type.ts +++ b/packages/components/config-provider/type.ts @@ -318,6 +318,11 @@ export interface ChatConfig { * @default '' */ dislikeTipText?: string; + /** + * 语言配置,“分享”占位描述文本 + * @default '' + */ + shareTipText?: string; /** * 语言配置,“复制代码”占位描述文本 * @default '' diff --git a/packages/components/date-picker/DatePicker.tsx b/packages/components/date-picker/DatePicker.tsx index 47f98c0a65..dc0ca044f4 100644 --- a/packages/components/date-picker/DatePicker.tsx +++ b/packages/components/date-picker/DatePicker.tsx @@ -389,7 +389,6 @@ export default defineComponent({ } popupVisible={!isReadOnly.value && popupVisible.value} valueDisplay={() => renderTNodeJSX('valueDisplay', { params: valueDisplayParams.value })} - needConfirm={props.needConfirm} {...(props.selectInputProps as TdDatePickerProps['selectInputProps'])} panel={() => } tagInputProps={{ diff --git a/packages/components/empty/empty.tsx b/packages/components/empty/empty.tsx index 35a9931388..f8e0578400 100644 --- a/packages/components/empty/empty.tsx +++ b/packages/components/empty/empty.tsx @@ -5,7 +5,7 @@ import { useConfig, useTNodeJSX, usePrefixClass, useCommonClassName } from '@tde import props from './props'; import type { TdEmptyProps } from './type'; -import Image from '../image'; +import Image, { ImageProps } from '../image'; import MaintenanceSvg from './components/MaintenanceSvg'; import NetworkErrorSvg from './components/NetworkErrorSvg'; import EmptySvg from './components/EmptySvg'; @@ -82,7 +82,7 @@ export default defineComponent({ } else if (data && Reflect.has(data, 'setup')) { result = h(data as unknown); } else if (isPlainObject(data)) { - result = ; + result = ; } return data ? result : null; diff --git a/packages/components/form/__tests__/form-item.test.tsx b/packages/components/form/__tests__/form-item.test.tsx index 6961bcf793..9f91b22e44 100644 --- a/packages/components/form/__tests__/form-item.test.tsx +++ b/packages/components/form/__tests__/form-item.test.tsx @@ -10,7 +10,7 @@ describe('FormItem', () => { const wrapper = mount(
- +
, ); @@ -22,7 +22,7 @@ describe('FormItem', () => { const wrapper = mount(
- +
, ); @@ -33,7 +33,7 @@ describe('FormItem', () => { const wrapperSlot = mount(
'help text' }}> - +
, ); @@ -43,7 +43,7 @@ describe('FormItem', () => { const wrapperFunction = mount(
- +
, ); @@ -54,7 +54,7 @@ describe('FormItem', () => { const wrapper = mount(
- +
, ); @@ -65,7 +65,7 @@ describe('FormItem', () => { const wrapperSlot = mount(
'label' }}> - +
, ); @@ -75,7 +75,7 @@ describe('FormItem', () => { const wrapperFunction = mount(
- +
, ); @@ -85,7 +85,7 @@ describe('FormItem', () => { it('labelAlign[string]', () => { const formItemWrapper = mount( - + , ); @@ -99,7 +99,7 @@ describe('FormItem', () => { mount(
- +
, ); @@ -114,7 +114,7 @@ describe('FormItem', () => { mount(
- +
, ); @@ -134,7 +134,7 @@ describe('FormItem', () => { return () => (
- +
); diff --git a/packages/components/form/__tests__/form.hooks.test.tsx b/packages/components/form/__tests__/form.hooks.test.tsx index 47722be7f8..1282940fe5 100644 --- a/packages/components/form/__tests__/form.hooks.test.tsx +++ b/packages/components/form/__tests__/form.hooks.test.tsx @@ -20,6 +20,7 @@ describe('Form hooks', () => { const wrapper = mount(TestComponent, { props: { disabled: true }, }); + // @ts-ignore: TODO expect(wrapper.vm.disabledState).toBe(true); }); @@ -32,6 +33,7 @@ describe('Form hooks', () => { }, }, }); + // @ts-ignore: TODO expect(wrapper.vm.disabledState).toBe(true); }); @@ -49,6 +51,7 @@ describe('Form hooks', () => { return { disabledState }; }, }); + // @ts-ignore: TODO expect(wrapper.vm.disabledState).toBe(true); }); @@ -66,6 +69,7 @@ describe('Form hooks', () => { return { disabledState }; }, }); + // @ts-ignore: TODO expect(wrapper.vm.disabledState).toBe(false); }); }); diff --git a/packages/components/form/__tests__/form.test.tsx b/packages/components/form/__tests__/form.test.tsx index 86ed989a4c..8c2b00bafb 100644 --- a/packages/components/form/__tests__/form.test.tsx +++ b/packages/components/form/__tests__/form.test.tsx @@ -14,6 +14,7 @@ describe('Form', () => { describe('props', () => { let wrapper: VueWrapper> | null = null; beforeEach(() => { + // @ts-ignore wrapper = mount(
diff --git a/packages/components/form/form.tsx b/packages/components/form/form.tsx index 4042020fd4..8d18bf9fa7 100644 --- a/packages/components/form/form.tsx +++ b/packages/components/form/form.tsx @@ -34,7 +34,7 @@ export default defineComponent({ readonly, }); - const formRef = ref(null); + const formRef = ref(); const children = ref([]); const { diff --git a/packages/components/grid/col.tsx b/packages/components/grid/col.tsx index a963299771..0273c09931 100644 --- a/packages/components/grid/col.tsx +++ b/packages/components/grid/col.tsx @@ -1,4 +1,4 @@ -import { computed, defineComponent, inject } from 'vue'; +import { computed, defineComponent, inject, h } from 'vue'; import props from './col-props'; import { useRowSize } from './hooks'; import { RowProviderType, parseFlex, calcColPadding, getColClasses } from './utils'; @@ -31,10 +31,14 @@ export default defineComponent({ return () => { const { tag: TAG } = props; - return ( - - {renderTNodeJSX('default')} - + + return h( + TAG, + { + class: colClasses.value, + style: colStyle.value, + }, + [renderTNodeJSX('default')], ); }; }, diff --git a/packages/components/grid/row.tsx b/packages/components/grid/row.tsx index 130db51256..6969b4630a 100644 --- a/packages/components/grid/row.tsx +++ b/packages/components/grid/row.tsx @@ -1,4 +1,4 @@ -import { defineComponent, provide, computed, toRefs, reactive } from 'vue'; +import { defineComponent, provide, computed, toRefs, reactive, h } from 'vue'; import props from './row-props'; import { useRowSize } from './hooks'; import { getRowClasses, RowProviderType, calcRowStyle } from './utils'; @@ -27,10 +27,13 @@ export default defineComponent({ return () => { const { tag: TAG } = props; - return ( - - {renderTNodeJSX('default')} - + return h( + TAG, + { + class: rowClasses.value, + style: rowStyle.value, + }, + [renderTNodeJSX('default')], ); }; }, diff --git a/packages/components/guide/guide.tsx b/packages/components/guide/guide.tsx index da78cb8c3a..9cb50ac304 100644 --- a/packages/components/guide/guide.tsx +++ b/packages/components/guide/guide.tsx @@ -42,7 +42,7 @@ export default defineComponent({ // dialog ref const dialogTooltipRef = ref(); // ! popup ref 不确定这里的类型是否完全正确 - const popupTooltipRef = ref>(); + const popupTooltipRef = ref(); // 是否开始展示 const actived = ref(false); // 步骤总数 diff --git a/packages/components/image-viewer/base/ImageViewerUtils.tsx b/packages/components/image-viewer/base/ImageViewerUtils.tsx index c46d9ca512..5f1b9649ae 100644 --- a/packages/components/image-viewer/base/ImageViewerUtils.tsx +++ b/packages/components/image-viewer/base/ImageViewerUtils.tsx @@ -60,7 +60,6 @@ export default defineComponent({ } onClick={props.onZoomOut} /> diff --git a/packages/components/image-viewer/image-viewer.tsx b/packages/components/image-viewer/image-viewer.tsx index 167c267a24..a623993da8 100644 --- a/packages/components/image-viewer/image-viewer.tsx +++ b/packages/components/image-viewer/image-viewer.tsx @@ -193,7 +193,6 @@ export default defineComponent({ src={image.thumbnail || image.mainImage} error="" class={`${COMPONENT_NAME.value}__header-img`} - onClick={() => onImgClick(index)} />
))} @@ -236,13 +235,7 @@ export default defineComponent({ const imageSrc = typeof firstImage === 'string' ? firstImage : firstImage.mainImage || firstImage.thumbnail; return (
- preview openHandler()} - /> + preview
openHandler()}> diff --git a/packages/components/image/image.tsx b/packages/components/image/image.tsx index e37a58905a..61b8e2362f 100644 --- a/packages/components/image/image.tsx +++ b/packages/components/image/image.tsx @@ -12,8 +12,8 @@ export default defineComponent({ name: 'TImage', props, setup(props) { - const divRef = ref(null); - const imgRef = ref(null); + const divRef = ref(); + const imgRef = ref(); let io: IntersectionObserver = null; const { src } = toRefs(props); diff --git a/packages/components/input-number/input-number.tsx b/packages/components/input-number/input-number.tsx index 3e16abb00c..aacd76de13 100644 --- a/packages/components/input-number/input-number.tsx +++ b/packages/components/input-number/input-number.tsx @@ -51,6 +51,7 @@ export default defineComponent({ readonly={p.isReadonly.value} autocomplete="off" placeholder={props.placeholder} + // @ts-expect-error unselectable={p.isReadonly.value ? 'on' : 'off'} autoWidth={props.autoWidth} align={props.align || (props.theme === 'row' ? 'center' : undefined)} diff --git a/packages/components/input/hooks/useInput.ts b/packages/components/input/hooks/useInput.ts index 17a096e187..130bfef857 100644 --- a/packages/components/input/hooks/useInput.ts +++ b/packages/components/input/hooks/useInput.ts @@ -31,7 +31,7 @@ export function useInput(props: ExtendsTdInputProps, expose: (exposed: Record(null); + const inputRef = ref(); const limitParams = computed(() => ({ value: [undefined, null].includes(innerValue.value) ? undefined : String(innerValue.value), diff --git a/packages/components/input/hooks/useInputWidth.ts b/packages/components/input/hooks/useInputWidth.ts index 202689a117..042e41a8eb 100644 --- a/packages/components/input/hooks/useInputWidth.ts +++ b/packages/components/input/hooks/useInputWidth.ts @@ -6,7 +6,7 @@ const ANIMATION_TIME = 100; export function useInputWidth(props: TdInputProps, inputRef: Ref, innerValue: Ref) { const { autoWidth, placeholder } = toRefs(props); - const inputPreRef = ref(null); + const inputPreRef = ref(); const observerTimer = ref(null); const updateInputWidth = () => { diff --git a/packages/components/loading/__tests__/loading.test.tsx b/packages/components/loading/__tests__/loading.test.tsx index d635aecf52..0630f3d78c 100644 --- a/packages/components/loading/__tests__/loading.test.tsx +++ b/packages/components/loading/__tests__/loading.test.tsx @@ -90,7 +90,7 @@ describe('Loading', () => { }); it(':fullscreen[boolean]', async () => { - const wrapper = mount(); + const wrapper = mount( document.body} />); await nextTick(); expect(wrapper.find('.t-loading__fullscreen').exists()).toBe(true); expect(wrapper.element).toMatchSnapshot(); @@ -153,7 +153,7 @@ describe('Loading', () => { it(':preventScrollThrough[boolean]', async () => { const wrapper = mount( - , + document.body} />, ); await nextTick(); expect(document.body.classList.contains('t-loading--lock')).toBe(false); diff --git a/packages/components/loading/loading.tsx b/packages/components/loading/loading.tsx index 77bd54758e..039adbbf98 100644 --- a/packages/components/loading/loading.tsx +++ b/packages/components/loading/loading.tsx @@ -105,7 +105,7 @@ export default defineComponent({ return () => { const { fullScreenClasses, baseClasses, withContentClasses, attachClasses, normalClasses } = classes.value; - const defaultIndicator = ; + const defaultIndicator = ; const indicator = loading.value && renderTNodeJSX('indicator', defaultIndicator); const text = showText.value &&
{renderTNodeJSX('text')}
; diff --git a/packages/components/menu/__tests__/head-menu.test.tsx b/packages/components/menu/__tests__/head-menu.test.tsx index 2ec7ee5c36..3063efde8a 100644 --- a/packages/components/menu/__tests__/head-menu.test.tsx +++ b/packages/components/menu/__tests__/head-menu.test.tsx @@ -22,15 +22,6 @@ describe('HeadMenu', () => { }); expect(wrapper.element).toMatchSnapshot(); }); - - it(':height', () => { - const wrapper = mount({ - render() { - return ; - }, - }); - expect(wrapper.element).toMatchSnapshot(); - }); }); describe('slot', () => { diff --git a/packages/components/menu/__tests__/menu-item.test.tsx b/packages/components/menu/__tests__/menu-item.test.tsx index c4acd09a88..a1990b9e09 100644 --- a/packages/components/menu/__tests__/menu-item.test.tsx +++ b/packages/components/menu/__tests__/menu-item.test.tsx @@ -7,38 +7,6 @@ const $routerMock = { push: vi.fn() }; describe('MenuItem', () => { // test props api describe('props', () => { - it(':name', () => { - const wrapper = mount( - { - render() { - return ( - - - - ); - }, - }, - { global: { mocks: { $router: $routerMock } } }, - ); - expect(wrapper.element).toMatchSnapshot(); - }); - - it(':route', () => { - const wrapper = mount( - { - render() { - return ( - - - - ); - }, - }, - { global: { mocks: { $router: $routerMock } } }, - ); - expect(wrapper.element).toMatchSnapshot(); - }); - it(':disabled', () => { const wrapper = mount( { diff --git a/packages/components/menu/__tests__/menu.test.tsx b/packages/components/menu/__tests__/menu.test.tsx index d044c8e3eb..8d23932767 100644 --- a/packages/components/menu/__tests__/menu.test.tsx +++ b/packages/components/menu/__tests__/menu.test.tsx @@ -41,15 +41,6 @@ describe('Menu', () => { expect(wrapper.element).toMatchSnapshot(); }); - it(':height', () => { - const wrapper = mount({ - render() { - return ; - }, - }); - expect(wrapper.element).toMatchSnapshot(); - }); - it(':collapsed', () => { const wrapper = mount({ render() { @@ -58,15 +49,6 @@ describe('Menu', () => { }); expect(wrapper.element).toMatchSnapshot(); }); - - it(':collapsedWidth', () => { - const wrapper = mount({ - render() { - return ; - }, - }); - expect(wrapper.element).toMatchSnapshot(); - }); }); describe('slot', () => { diff --git a/packages/components/menu/__tests__/submenu.test.tsx b/packages/components/menu/__tests__/submenu.test.tsx index 6197ddcec9..be50658424 100644 --- a/packages/components/menu/__tests__/submenu.test.tsx +++ b/packages/components/menu/__tests__/submenu.test.tsx @@ -13,18 +13,6 @@ const Menu = { describe('Submenu', () => { // test props api describe('props', () => { - it(':name', () => { - const wrapper = mount({ - provide: { - TdMenu: Menu, - }, - render() { - return ; - }, - }); - expect(wrapper.element).toMatchSnapshot(); - }); - it(':disabled', () => { const wrapper = mount({ provide: { diff --git a/packages/components/menu/_usage/index.vue b/packages/components/menu/_usage/index.vue index 46361e95ac..de3d872362 100644 --- a/packages/components/menu/_usage/index.vue +++ b/packages/components/menu/_usage/index.vue @@ -1,8 +1,8 @@ diff --git a/packages/pro-components/chat/_example/error-message.vue b/packages/pro-components/chat/chat-item/_example/error-message.vue similarity index 93% rename from packages/pro-components/chat/_example/error-message.vue rename to packages/pro-components/chat/chat-item/_example/error-message.vue index 47f1240fe2..e309a63dc6 100644 --- a/packages/pro-components/chat/_example/error-message.vue +++ b/packages/pro-components/chat/chat-item/_example/error-message.vue @@ -5,7 +5,7 @@ name="自己" datetime="今天16:38" content="!!!请求出错" - role="error" + status="error" > diff --git a/packages/pro-components/chat/chat-item-props.ts b/packages/pro-components/chat/chat-item/chat-item-props.ts similarity index 91% rename from packages/pro-components/chat/chat-item-props.ts rename to packages/pro-components/chat/chat-item/chat-item-props.ts index cf6c0a993e..d546400a9a 100644 --- a/packages/pro-components/chat/chat-item-props.ts +++ b/packages/pro-components/chat/chat-item/chat-item-props.ts @@ -3,11 +3,13 @@ /** * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TdChatItemProps } from './type'; +import { TdChatItemProps } from '../type'; import { PropType } from 'vue'; export default { - /** 自定义的操作内容 */ + /** 自定义的操作内容 + * @deprecated + */ actions: { type: [String, Function] as PropType, }, @@ -60,4 +62,8 @@ export default { return ['base', 'outline', 'text'].includes(val); }, }, + status: { + type: String as PropType, + default: '' as TdChatItemProps['status'], + }, }; diff --git a/packages/pro-components/chat/chat-item.md b/packages/pro-components/chat/chat-item/chat-item.md similarity index 83% rename from packages/pro-components/chat/chat-item.md rename to packages/pro-components/chat/chat-item/chat-item.md index 613b48934c..e205b05b24 100644 --- a/packages/pro-components/chat/chat-item.md +++ b/packages/pro-components/chat/chat-item/chat-item.md @@ -6,13 +6,15 @@ 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- -actions | String / Slot / Function | - | 自定义的操作内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +actions | String / Slot / Function | - | 自定义的操作内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)`待废弃,请尽快使用actionbar` | N +actionbar | String / Slot / Function | - | 自定义的操作内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)| N animation | String | skeleton | 动画效果,支持「渐变加载动画」,「闪烁加载动画」, 「骨架屏」三种。可选项:skeleton/moving/gradient | N avatar | String / Object / Slot / Function | - | 自定义的头像配置。TS 类型:`String \| AvatarProps \| TNode `,[Avatar API Documents](./avatar?tab=api)。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/pro-components/chat/type.ts) | N content | String / Slot / Function | - | 对话单元的内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N datetime | String / Slot / Function | - | 对话单元的时间配置。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N name | String / Slot / Function | - | 自定义的昵称。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N reasoning | String / Boolean / Object | false | 值为false不显示思维链,为string则显示内置推理内容交互,为对象则单独配置推理内容。TS 类型:`boolean \| TdChatReasoning ` ` interface TdChatReasoning { expandIconPlacement?: 'left' \| 'right';onExpandChange?: (isExpand: boolean) => void; collapsePanelProps?: Object } `。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/pro-components/chat/type.ts) | N -role | String | - | 角色,不同选项配置不同的样式,支持类型包括用户、助手、错误、模型切换、系统消息。可选项:user/assistant/error/model-change/system | N +role | String | - | 角色,不同选项配置不同的样式,支持类型包括用户、助手、模型切换、系统消息。可选项:user/assistant/model-change/system | N textLoading | Boolean | false | 新消息是否处于加载状态,加载状态默认显示骨架屏,接口请求返回数据时请将新消息加载状态置为false | N variant | String | text | 气泡框样式,支持基础、线框、文字三种类型。可选项:base/outline/text | N +status | String | '' | 消息状态。可选项:''/error | N diff --git a/packages/pro-components/chat/chat-item.tsx b/packages/pro-components/chat/chat-item/chat-item.tsx similarity index 86% rename from packages/pro-components/chat/chat-item.tsx rename to packages/pro-components/chat/chat-item/chat-item.tsx index 62c10fc7dd..380f181926 100644 --- a/packages/pro-components/chat/chat-item.tsx +++ b/packages/pro-components/chat/chat-item/chat-item.tsx @@ -6,10 +6,10 @@ import { usePrefixClass, useTNodeJSX } from '@tdesign/shared-hooks'; import props from './chat-item-props'; import { isString, isObject } from 'lodash-es'; import { Skeleton } from 'tdesign-vue-next'; -import Text from './chat-content'; +import Text from '../chat-content/chat-content'; import { CheckCircleIcon } from 'tdesign-icons-vue-next'; -import ChatLoading from './chat-loading'; -import ChatReasoning from './chat-reasoning'; +import ChatLoading from '../chat-loading'; +import ChatReasoning from '../chat-reasoning/chat-reasoning'; export default defineComponent({ name: 'TChatItem', @@ -20,7 +20,7 @@ export default defineComponent({ default: false, }, }, - emits: ['operation'], + emits: [], setup(props) { const COMPONENT_NAME = usePrefixClass('chat'); const { globalConfig } = useConfig('chat'); @@ -60,7 +60,7 @@ export default defineComponent({ const textLoading = props.textLoading; const reasoningLoading = props.reasoningLoading; // 内置操作按钮,assistantActions和插槽判断 t-chat注入的属性获取不到默认为false - const showActions = computed(() => renderTNodeJSX('actions')); + const showActions = computed(() => renderTNodeJSX('actionbar') || renderTNodeJSX('actions')); const renderHeader = () => { const { loadingText, loadingEndText } = globalConfig.value; if (reasoningLoading) { @@ -88,13 +88,12 @@ export default defineComponent({ (props.animation === 'skeleton' ? ( ) : ( - + ))} {!textLoading && (
{isObject(props.reasoning) && role.value === 'assistant' && ( ).expandIconPlacement} onExpandChange={(props.reasoning as Record).onExpandChange} collapse-panel-props={{ @@ -105,19 +104,25 @@ export default defineComponent({ {/* 适配t-chat传入data */} {isString(props.reasoning) && role.value === 'assistant' && ( , + content: ( + + ), }} > )} - {isString(content) ? : content} + {isString(content) ? : content}
)} {role.value === 'assistant' && showActions.value && ( -
{renderTNodeJSX('actions')}
+
+ {renderTNodeJSX('actionbar') || renderTNodeJSX('actions')} +
)}
diff --git a/packages/pro-components/chat/chat-item/index.ts b/packages/pro-components/chat/chat-item/index.ts new file mode 100644 index 0000000000..a15a9c217d --- /dev/null +++ b/packages/pro-components/chat/chat-item/index.ts @@ -0,0 +1,2 @@ +import _ChatItem from './chat-item'; +export default _ChatItem; diff --git a/packages/pro-components/chat/_example/base.vue b/packages/pro-components/chat/chat-list/_example/base.vue similarity index 65% rename from packages/pro-components/chat/_example/base.vue rename to packages/pro-components/chat/chat-list/_example/base.vue index 2568110be6..299ab60dda 100644 --- a/packages/pro-components/chat/_example/base.vue +++ b/packages/pro-components/chat/chat-list/_example/base.vue @@ -1,68 +1,55 @@ + -../_example-mock/sseRequest diff --git a/packages/pro-components/chat/_example/chat-drawer.vue b/packages/pro-components/chat/chat-list/_example/chat-drawer.vue similarity index 51% rename from packages/pro-components/chat/_example/chat-drawer.vue rename to packages/pro-components/chat/chat-list/_example/chat-drawer.vue index 8919f2f02d..c995ede326 100644 --- a/packages/pro-components/chat/_example/chat-drawer.vue +++ b/packages/pro-components/chat/chat-list/_example/chat-drawer.vue @@ -7,47 +7,60 @@ Hi,  我是AI - + diff --git a/packages/pro-components/chat/chat-list/_example/chat-with-message.vue b/packages/pro-components/chat/chat-list/_example/chat-with-message.vue new file mode 100644 index 0000000000..51b3bca9c4 --- /dev/null +++ b/packages/pro-components/chat/chat-list/_example/chat-with-message.vue @@ -0,0 +1,385 @@ + + + diff --git a/packages/pro-components/chat/_example/mock-data/sseRequest-reasoning.ts b/packages/pro-components/chat/chat-list/_example/mock-data/sseRequest-reasoning.ts similarity index 100% rename from packages/pro-components/chat/_example/mock-data/sseRequest-reasoning.ts rename to packages/pro-components/chat/chat-list/_example/mock-data/sseRequest-reasoning.ts diff --git a/packages/pro-components/chat/_example/mock-data/sseRequest.ts b/packages/pro-components/chat/chat-list/_example/mock-data/sseRequest.ts similarity index 100% rename from packages/pro-components/chat/_example/mock-data/sseRequest.ts rename to packages/pro-components/chat/chat-list/_example/mock-data/sseRequest.ts diff --git a/packages/pro-components/chat/chat-list/_usage/index.vue b/packages/pro-components/chat/chat-list/_usage/index.vue new file mode 100644 index 0000000000..dd9b5af1d8 --- /dev/null +++ b/packages/pro-components/chat/chat-list/_usage/index.vue @@ -0,0 +1,52 @@ + + + + diff --git a/packages/pro-components/chat/chat-list/_usage/props.json b/packages/pro-components/chat/chat-list/_usage/props.json new file mode 100644 index 0000000000..37d56013cd --- /dev/null +++ b/packages/pro-components/chat/chat-list/_usage/props.json @@ -0,0 +1,60 @@ +[ + { + "name": "animation", + "type": "enum", + "defaultValue": "skeleton", + "options": [ + { + "label": "skeleton", + "value": "skeleton" + }, + { + "label": "moving", + "value": "moving" + }, + { + "label": "gradient", + "value": "gradient" + }, + { + "label": "dots", + "value": "dots" + }, + { + "label": "circle", + "value": "circle" + }, + { + "label": "dot", + "value": "dot" + } + ] + }, + { + "name": "layout", + "type": "enum", + "defaultValue": "both", + "options": [ + { + "label": "both", + "value": "both" + }, + { + "label": "single", + "value": "single" + } + ] + }, + { + "name": "clearHistory", + "type": "Boolean", + "defaultValue": true, + "options": [] + }, + { + "name": "textLoading", + "type": "Boolean", + "defaultValue": false, + "options": [] + } +] diff --git a/packages/pro-components/chat/chat.md b/packages/pro-components/chat/chat-list/chat-list.md similarity index 78% rename from packages/pro-components/chat/chat.md rename to packages/pro-components/chat/chat-list/chat-list.md index 3c2d85fd99..f0a4a3c696 100644 --- a/packages/pro-components/chat/chat.md +++ b/packages/pro-components/chat/chat-list/chat-list.md @@ -2,34 +2,35 @@ ## API -### Chat Props +### ChatList Props 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- -actions | Slot / Function | - | 自定义操作按钮的插槽。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +actions | Slot / Function | - | 自定义操作按钮的插槽。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) `待废弃,请尽快使用actionbar` | N +actionbar | Slot / Function | - | 自定义操作按钮的插槽。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N animation | String | skeleton | 动画效果,支持「渐变加载动画」,「闪烁加载动画」, 「骨架屏」三种。可选项:skeleton/moving/gradient | N avatar | Slot / Function | - | 自定义每个对话单元的头像插槽。TS 类型:`TNode<{ item: TdChatItemProps, index: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N clearHistory | Boolean | true | 是否显示清空历史 | N content | Slot / Function | - | 自定义每个对话单独的聊天内容。TS 类型:`TNode<{ item: TdChatItemProps, index: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -data | Array | - | 对话列表的数据。TS 类型:`Array` ` interface TdChatItemMeta { avatar?: string; name?:string; role?:string; datetime?: string; content?: string; reasoning?: string }`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/pro-components/chat/type.ts) | N +data | Array | - | 对话列表的数据。TS 类型:`Array` ` interface TdChatItemMeta { avatar?: string; name?:string; role?:string; datetime?: string; content?: string \| object; reasoning?: string }`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/pro-components/chat/type.ts) | N datetime | Slot / Function | - | 自定义每个对话单元的时间。TS 类型:`TNode<{ item: TdChatItemProps, index: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -isStreamLoad | Boolean | false | 流式加载是否结束 | N -layout | String | both | 对话布局形式,支持两侧对齐与左对齐。可选项:both/single | N +layout | String | both | 对话布局形式,支持两侧对齐与左对齐。可选项:both/single `使用默认插槽时无效,请使用ChatMessage的placement设置消息显示位置` | N name | Slot / Function | - | 自定义每个对话单元的昵称。TS 类型:`TNode<{ item: TdChatItemProps, index: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -reasoning | Slot / Function | - | 自定义每个对话单元的思考过程的插槽。TS 类型:`TNode<{ item: TdChatItemProps, index: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -reverse | Boolean | true | 是否表现为倒序 | N +reverse | Boolean | false | 默认为正序 | N textLoading | Boolean | false | 新消息是否处于加载状态,加载状态默认显示骨架屏,接口请求返回数据时请将新消息加载状态置为false | N onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
点击清空历史按钮回调。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N onScroll | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
滚动事件的回调。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +autoScroll | Boolean | true | 是否开启自动滚动到底部 | N +showScrollButton | Boolean | true | 是否显示滚动到底部按钮 | N -### Chat Events +### ChatList Events 名称 | 参数 | 描述 -- | -- | -- clear | `(context: { e: MouseEvent })` | 点击清空历史按钮回调。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) scroll | `(context: { e: MouseEvent })` | 滚动事件的回调。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) -### ChatInstanceFunctions 组件实例方法 +### ChatListInstanceFunctions 组件实例方法 名称 | 参数 | 返回值 | 描述 -- | -- | -- | -- diff --git a/packages/pro-components/chat/chat-list/chat-list.tsx b/packages/pro-components/chat/chat-list/chat-list.tsx new file mode 100644 index 0000000000..2d790001e6 --- /dev/null +++ b/packages/pro-components/chat/chat-list/chat-list.tsx @@ -0,0 +1,299 @@ +import { defineComponent, computed, provide, ref, onMounted, onUnmounted } from 'vue'; +import { ClearIcon, ArrowDownIcon } from 'tdesign-icons-vue-next'; +import { useConfig } from 'tdesign-vue-next/es/config-provider/hooks'; +import { isArray, throttle, debounce } from 'lodash-es'; + +import props from './props'; +import { Divider, Popconfirm, Button } from 'tdesign-vue-next'; +import { usePrefixClass, useTNodeJSX } from '@tdesign/shared-hooks'; +import ChatMessage from '../chat-message'; +import { TdChatItemMeta, ScrollToBottomParams } from '../type'; + +const handleScrollToBottom = (target: HTMLDivElement, behavior?: 'auto' | 'smooth') => { + const currentScrollHeight = target.scrollHeight; + const currentClientHeight = target.clientHeight; + + const innerBehavior = behavior ?? 'auto'; + if (innerBehavior === 'auto') { + target.scrollTop = currentScrollHeight - currentClientHeight; + } else { + const startScrollTop = target.scrollTop; + const endScrollTop = currentScrollHeight - currentClientHeight; + const duration = 300; + const step = (endScrollTop - startScrollTop) / duration; + let startTime: number | undefined; + // 平滑地修改scrollTop值 + const animateScroll = (time: number) => { + if (!startTime) { + startTime = time; + } + const elapsed = time - startTime; + const top = Math.min(endScrollTop, startScrollTop + elapsed * step); + target.scrollTop = top; + if (top < endScrollTop) { + requestAnimationFrame(animateScroll); + } + }; + + requestAnimationFrame(animateScroll); + } +}; + +export default defineComponent({ + name: 'TChatList', + props, + emits: ['clear', 'scroll'], + setup(props, { emit, expose }) { + const COMPONENT_NAME = usePrefixClass('chat'); + const { globalConfig } = useConfig('chat'); + const renderTNodeJSX = useTNodeJSX(); + provide('textLoading', props.textLoading); + provide('animation', props.animation); + provide('reverse', props.reverse); + const classes = computed(() => { + return [ + COMPONENT_NAME.value, + { + [`${COMPONENT_NAME.value}--normal`]: props.layout === 'both', + }, + ]; + }); + // 默认反转布局 + const listClasses = computed(() => { + return [ + `${COMPONENT_NAME.value}__list`, + { + [`${COMPONENT_NAME.value}__list--reverse`]: props.reverse, + }, + ]; + }); + const renderBody = () => { + /** + * 1. 两种方式获取要渲染的 list + * a. props 传 data + * b. slots t-chat-item + * a 优先级更高 + */ + const data = renderTNodeJSX('data'); + if (isArray(data) && data.length > 0) { + // 根据layout来设置placement,both时仅对user、assistant设置placement,其他值使用默认left + const setPlacement = (item: TdChatItemMeta) => { + if (props.layout === 'both') { + if (item.role === 'assistant') return 'left'; + if (item.role === 'user') return 'right'; + return 'left'; // 其他role使用默认值 + } + return 'left'; + }; + return data.map((item: TdChatItemMeta, index: number) => ( + + renderTNodeJSX('actionbar', { + params: { item, index }, + }) || + renderTNodeJSX('actions', { + params: { item, index }, + }), + name: () => renderTNodeJSX('name', { params: { item, index } }), + avatar: () => renderTNodeJSX('avatar', { params: { item, index } }), + datetime: () => renderTNodeJSX('datetime', { params: { item, index } }), + header: () => renderTNodeJSX('header', { params: { item, index } }), + content: () => renderTNodeJSX('content', { params: { item, index } }), + }} + /> + )); + } else { + return renderTNodeJSX('default'); + } + }; + const clearConfirm = (context: { e: MouseEvent }) => { + emit('clear', context); + }; + const defaultClearHistory = ( + + + + {globalConfig.value.clearHistoryBtnText} + + + ); + const showFooter = computed(() => renderTNodeJSX('footer')); + const listRef = ref(); + const innerRef = ref(); + // 回到底部按钮显示 + const scrollButtonVisible = ref(false); + /** 检测并显示滚到底部按钮(阈值 140px,debounce 70ms) */ + const checkAndShowScrollButton = debounce(() => { + // 关闭时不显示按钮也不计算 + if (!props.showScrollButton) { + scrollButtonVisible.value = false; + return; + } + const list = listRef.value; + if (!list) return; + if (!props.reverse) { + if (list && list.scrollHeight - list.clientHeight - list.scrollTop > 0) { + scrollButtonVisible.value = true; + } else { + scrollButtonVisible.value = false; + } + } else { + scrollButtonVisible.value = list.scrollTop < 0; + } + }, 70); + // 自动滚动相关状态 + const scrollTopTmp = ref(0); + const scrollHeightTmp = ref(0); + const preventAutoScroll = ref(false); + const isAutoScrollEnabled = ref(false); + const observer = ref(null); + + // 滚动到底部 + const scrollToBottom = (data?: ScrollToBottomParams) => { + if (!listRef.value) return; + const behavior = data?.behavior ?? 'auto'; + handleScrollToBottom(listRef.value, behavior); + }; + + /** 触发自动滚动 */ + const handleAutoScroll = throttle(() => { + const { autoScroll, defaultScrollTo, reverse } = props; + if (!autoScroll || !isAutoScrollEnabled.value || reverse) { + return; + } + + if (!listRef.value) return; + + if (defaultScrollTo === 'top') { + listRef.value.scrollTo({ + top: 0, + behavior: 'auto', + }); + } else { + scrollToBottom({ + behavior: 'auto', + }); + } + }, 50); + + /** 检测自动滚动是否触发 */ + const checkAutoScroll = throttle(() => { + if (!listRef.value || props.reverse) return; + const { scrollTop, scrollHeight, clientHeight } = listRef.value; + const { defaultScrollTo } = props; + + // 判断上滚:总高度未变更 && 滚动diff大于阈值 + const scrollDiff = scrollTopTmp.value - scrollTop; + const upScroll = scrollHeight === scrollHeightTmp.value && scrollDiff >= 10 ? true : false; + // 用户主动上滚,取消自动滚动,标记为手动阻止 + if (upScroll) { + isAutoScrollEnabled.value = false; + preventAutoScroll.value = true; + } else { + const threshold = 50; + let isNearTarget = false; + + if (defaultScrollTo === 'top') { + // 滚动到顶部模式:检查是否接近顶部 + isNearTarget = scrollTop <= threshold; + } else { + // 滚动到底部模式:检查是否接近底部 + isNearTarget = scrollHeight - (scrollTop + clientHeight) <= threshold; + } + + // 如果手动阻止,必须滚动至目标位置阈值内才可恢复自动滚动 + if (preventAutoScroll.value) { + if (isNearTarget) { + isAutoScrollEnabled.value = true; + preventAutoScroll.value = false; + } + // 未手动阻止,可触发自动滚动 + } else { + isAutoScrollEnabled.value = true; + } + } + scrollTopTmp.value = scrollTop; + }, 60); + + const handleScroll = (e: Event) => { + checkAutoScroll(); + // 使用防抖检测是否显示“回到底部”按钮(距离底部超过阈值显示) + checkAndShowScrollButton(); + emit('scroll', { + e, + }); + }; + + // 初始化自动滚动 + onMounted(() => { + const { defaultScrollTo } = props; + defaultScrollTo === 'bottom' && !props.reverse && (isAutoScrollEnabled.value = true); + + const list = listRef.value; + const inner = innerRef.value; + + // 初始化“回到底部”按钮显示状态 + checkAndShowScrollButton(); + if (list) { + observer.value = new ResizeObserver(() => { + // 高度变化,触发滚动校验 + if (list?.scrollHeight !== scrollHeightTmp.value) { + handleAutoScroll(); + } + scrollHeightTmp.value = list?.scrollHeight || 0; + }); + if (inner) { + observer.value?.observe(inner); + } + } + }); + + onUnmounted(() => { + observer.value?.disconnect(); + }); + + // 平滑回到底部 + const backBottom = () => { + scrollToBottom({ behavior: 'smooth' }); + }; + + expose({ + scrollToBottom, + }); + + // clearHistory为true时,清空历史记录显示 + // return里的props是响应式 + // 倒序渲染不影响清空历史的位置 + return () => ( +
+
+ {props.reverse &&
} + {props.reverse && props.clearHistory && renderTNodeJSX('clearHistory', defaultClearHistory)} + {props.reverse ? renderBody() :
{renderBody()}
} + {!props.reverse && props.clearHistory && renderTNodeJSX('clearHistory', defaultClearHistory)} +
+ {showFooter.value &&
{showFooter.value}
} + {props.showScrollButton && scrollButtonVisible.value && ( + + )} +
+ ); + }, +}); diff --git a/packages/pro-components/chat/chat-list/index.ts b/packages/pro-components/chat/chat-list/index.ts new file mode 100644 index 0000000000..12319e92e9 --- /dev/null +++ b/packages/pro-components/chat/chat-list/index.ts @@ -0,0 +1,2 @@ +import _ChatList from './chat-list'; +export default _ChatList; diff --git a/packages/pro-components/chat/props.ts b/packages/pro-components/chat/chat-list/props.ts similarity index 72% rename from packages/pro-components/chat/props.ts rename to packages/pro-components/chat/chat-list/props.ts index b269207a5f..33d24726ee 100644 --- a/packages/pro-components/chat/props.ts +++ b/packages/pro-components/chat/chat-list/props.ts @@ -4,14 +4,35 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TdChatProps } from './type'; +import { TdChatProps } from '../type'; import { PropType } from 'vue'; export default { - /** 自定义操作按钮的插槽 */ + /** 自定义操作按钮的插槽 + * @deprecated + */ actions: { type: Function as PropType, }, + /** 自定义操作按钮的插槽(推荐使用) */ + actionbar: { + type: Function as PropType, + }, + /** 是否开启自动滚动 */ + autoScroll: { + type: Boolean, + default: true, + }, + /** 默认滚动位置 */ + defaultScrollTo: { + type: String as PropType, + default: 'bottom' as TdChatProps['defaultScrollTo'], + validator(val: TdChatProps['defaultScrollTo']): boolean { + if (!val) return true; + return ['top', 'bottom'].includes(val); + }, + }, + /** 动画效果,支持「渐变加载动画」,「闪烁加载动画」, 「骨架屏」三种 */ animation: { type: String as PropType, @@ -61,9 +82,14 @@ export default { reasoning: { type: Function as PropType, }, - /** 是否表现为倒序 */ + /** 是否表现为倒序,默认为正序 */ reverse: { type: Boolean, + default: false, + }, + // 是否显示“回到底部”按钮 + showScrollButton: { + type: Boolean as PropType, default: true, }, /** 新消息是否处于加载状态,加载状态默认显示骨架屏,接口请求返回数据时请将新消息加载状态置为false */ diff --git a/packages/pro-components/chat/chat-loading.tsx b/packages/pro-components/chat/chat-loading.tsx deleted file mode 100644 index b52017ee61..0000000000 --- a/packages/pro-components/chat/chat-loading.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { defineComponent } from 'vue'; -import { usePrefixClass } from '@tdesign/shared-hooks'; -import props from './chat-loading-props'; - -export default defineComponent({ - name: 'TChatLoading', - props, - setup(props) { - const componentName = usePrefixClass('chat-loading'); - - return () => ( -
- {props.animation === 'moving' ? ( -
-
-
-
-
- ) : ( -
- )} -
{props.text}
-
- ); - }, -}); diff --git a/packages/pro-components/chat/chat-loading/__tests__/index.test.jsx b/packages/pro-components/chat/chat-loading/__tests__/index.test.jsx new file mode 100644 index 0000000000..d5cf97a02c --- /dev/null +++ b/packages/pro-components/chat/chat-loading/__tests__/index.test.jsx @@ -0,0 +1,37 @@ +import { mount } from '@vue/test-utils'; +import ChatLoading from '@tdesign/pro-components-chat/chat-loading/index'; +import { omiVueify } from 'omi-vueify'; + +describe('ChatLoading', () => { + describe(':props', () => { + it(':animation - moving', () => { + const wrapper = mount(ChatLoading, { + props: { + animation: 'moving', + }, + }); + expect(wrapper.find('.t-chat-loading__indicator--moving').exists()).toBe(true); + expect(wrapper.element).toMatchSnapshot(); + }); + + it(':animation - gradient', () => { + const wrapper = mount(ChatLoading, { + props: { + animation: 'gradient', + }, + }); + expect(wrapper.find('.t-chat-loading__indicator--gradient').exists()).toBe(true); + expect(wrapper.element).toMatchSnapshot(); + }); + + it(':text', () => { + const wrapper = mount(ChatLoading, { + props: { + text: 'Loading...', + }, + }); + expect(wrapper.find('.t-chat-loading__text').text()).toBe('Loading...'); + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/pro-components/chat/_example/chat-loading-text.vue b/packages/pro-components/chat/chat-loading/_example/chat-loading-text.vue similarity index 100% rename from packages/pro-components/chat/_example/chat-loading-text.vue rename to packages/pro-components/chat/chat-loading/_example/chat-loading-text.vue diff --git a/packages/pro-components/chat/_example/chat-loading.vue b/packages/pro-components/chat/chat-loading/_example/chat-loading.vue similarity index 55% rename from packages/pro-components/chat/_example/chat-loading.vue rename to packages/pro-components/chat/chat-loading/_example/chat-loading.vue index 20692ad9b5..0d3a5685a0 100644 --- a/packages/pro-components/chat/_example/chat-loading.vue +++ b/packages/pro-components/chat/chat-loading/_example/chat-loading.vue @@ -1,7 +1,8 @@ diff --git a/packages/pro-components/chat/chat-loading-props.ts b/packages/pro-components/chat/chat-loading/chat-loading-props.ts similarity index 92% rename from packages/pro-components/chat/chat-loading-props.ts rename to packages/pro-components/chat/chat-loading/chat-loading-props.ts index af61c27882..ac4a0939b2 100644 --- a/packages/pro-components/chat/chat-loading-props.ts +++ b/packages/pro-components/chat/chat-loading/chat-loading-props.ts @@ -4,7 +4,7 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TdChatLoadingProps } from './type'; +import { TdChatLoadingProps } from '../type'; import { PropType } from 'vue'; export default { diff --git a/packages/pro-components/chat/chat-loading.md b/packages/pro-components/chat/chat-loading/chat-loading.md similarity index 84% rename from packages/pro-components/chat/chat-loading.md rename to packages/pro-components/chat/chat-loading/chat-loading.md index 7a3b98ec08..c316f1ea61 100644 --- a/packages/pro-components/chat/chat-loading.md +++ b/packages/pro-components/chat/chat-loading/chat-loading.md @@ -6,5 +6,5 @@ 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- -animation | String | moving | 加载的状态形式。可选项:moving/gradient | N +animation | String | moving | 加载的状态形式。可选项:skeleton/moving/gradient/dots/circle | N text | String | - | 加载过程展示的文字内容 | N \ No newline at end of file diff --git a/packages/pro-components/chat/chat-loading/index.ts b/packages/pro-components/chat/chat-loading/index.ts new file mode 100644 index 0000000000..3e8c7965b1 --- /dev/null +++ b/packages/pro-components/chat/chat-loading/index.ts @@ -0,0 +1,11 @@ +// import _ChatLoading from './chat-loading'; +// export default _ChatLoading; +import { TdChatLoadingProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-loading'; +import type { DefineComponent } from 'vue'; +import { omiVueify } from 'omi-vueify'; +// 附件 +export const ChatLoading = omiVueify('t-chat-loading', { + methodNames: [], +}) as DefineComponent; +export default ChatLoading; diff --git a/packages/pro-components/chat/chat-markdown/_example/base.vue b/packages/pro-components/chat/chat-markdown/_example/base.vue new file mode 100644 index 0000000000..fff6c480a6 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/base.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/event.vue b/packages/pro-components/chat/chat-markdown/_example/event.vue new file mode 100644 index 0000000000..0f6915478b --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/event.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/footnote.vue b/packages/pro-components/chat/chat-markdown/_example/footnote.vue new file mode 100644 index 0000000000..7af799f07c --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/footnote.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/plugin.vue b/packages/pro-components/chat/chat-markdown/_example/plugin.vue new file mode 100644 index 0000000000..e7fb5ec088 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/plugin.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/theme.vue b/packages/pro-components/chat/chat-markdown/_example/theme.vue new file mode 100644 index 0000000000..6eeade673a --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/theme.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown.md b/packages/pro-components/chat/chat-markdown/chat-markdown.md new file mode 100644 index 0000000000..5a12d0ce15 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/chat-markdown.md @@ -0,0 +1,9 @@ +:: BASE_DOC :: + +## API +### ChatMarkdown Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +content | String | - | 需要渲染的 Markdown 内容 | N +options | Object | - | Markdown 解析器基础配置。TS类型:`TdChatContentMDOptions` | N \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/index.ts b/packages/pro-components/chat/chat-markdown/index.ts new file mode 100644 index 0000000000..7ddb7f03d0 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/index.ts @@ -0,0 +1,10 @@ +import 'tdesign-web-components/lib/chat-message/content/markdown-content'; +import type { DefineComponent } from 'vue'; +import { omiVueify } from 'omi-vueify'; +import type { TdChatMarkdownContentProps } from 'tdesign-web-components/lib/chat-message/content/markdown-content'; + +// Markdown内容 +export const ChatMarkdown = omiVueify('t-chat-md-content', { + methodNames: [], +}) as DefineComponent; +export default ChatMarkdown; diff --git a/packages/pro-components/chat/chat-message/_example/action.vue b/packages/pro-components/chat/chat-message/_example/action.vue new file mode 100644 index 0000000000..dcc48e5e17 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/action.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/pro-components/chat/chat-message/_example/chat-message-base.vue b/packages/pro-components/chat/chat-message/_example/chat-message-base.vue new file mode 100644 index 0000000000..308786bd13 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/chat-message-base.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/pro-components/chat/chat-message/_example/configure.vue b/packages/pro-components/chat/chat-message/_example/configure.vue new file mode 100644 index 0000000000..7d936f7d45 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/configure.vue @@ -0,0 +1,70 @@ + + + diff --git a/packages/pro-components/chat/chat-message/_example/content.vue b/packages/pro-components/chat/chat-message/_example/content.vue new file mode 100644 index 0000000000..d470a25705 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/content.vue @@ -0,0 +1,133 @@ + + + diff --git a/packages/pro-components/chat/chat-message/_example/custom.vue b/packages/pro-components/chat/chat-message/_example/custom.vue new file mode 100644 index 0000000000..6a76d01f99 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/custom.vue @@ -0,0 +1,80 @@ + + + + diff --git a/packages/pro-components/chat/chat-message/_example/status.vue b/packages/pro-components/chat/chat-message/_example/status.vue new file mode 100644 index 0000000000..0aac9aec31 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_example/status.vue @@ -0,0 +1,58 @@ + + + + diff --git a/packages/pro-components/chat/chat-message/_usage/index.vue b/packages/pro-components/chat/chat-message/_usage/index.vue new file mode 100644 index 0000000000..f11806149d --- /dev/null +++ b/packages/pro-components/chat/chat-message/_usage/index.vue @@ -0,0 +1,69 @@ + + + diff --git a/packages/pro-components/chat/chat-message/_usage/props.json b/packages/pro-components/chat/chat-message/_usage/props.json new file mode 100644 index 0000000000..63ac82180f --- /dev/null +++ b/packages/pro-components/chat/chat-message/_usage/props.json @@ -0,0 +1,63 @@ +[ + { + "name": "variant", + "type": "enum", + "defaultValue": "base", + "options": [ + { + "label": "base", + "value": "base" + }, + { + "label": "outline", + "value": "outline" + }, + { + "label": "text", + "value": "text" + } + ] + }, + { + "name": "animation", + "type": "enum", + "defaultValue": "skeleton", + "options": [ + { + "label": "skeleton", + "value": "skeleton" + }, + { + "label": "moving", + "value": "moving" + }, + { + "label": "gradient", + "value": "gradient" + }, + { + "label": "dots", + "value": "dots" + }, + { + "label": "circle", + "value": "circle" + } + ] + }, + { + "name": "placement", + "type": "enum", + "defaultValue": "left", + "options": [ + { + "label": "left", + "value": "left" + }, + { + "label": "right", + "value": "right" + } + ] + } +] diff --git a/packages/pro-components/chat/chat-message/chat-message-props.ts b/packages/pro-components/chat/chat-message/chat-message-props.ts new file mode 100644 index 0000000000..9cf0b14207 --- /dev/null +++ b/packages/pro-components/chat/chat-message/chat-message-props.ts @@ -0,0 +1,56 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ +import { TdChatItemProps } from '../type'; +import type { TdChatMessageProps } from 'tdesign-web-components'; +import { PropType } from 'vue'; +import type { TNode } from 'tdesign-vue-next'; + +export default { + /** 动画效果 */ + animation: { + type: String as PropType, + default: 'circle' as TdChatMessageProps['animation'], + validator(val: TdChatMessageProps['animation']): boolean { + if (!val) return true; + return ['skeleton', 'moving', 'gradient', 'circle'].includes(val); + }, + }, + /** 自定义的头像配置 */ + avatar: { + type: [String, Object, Function] as PropType, + }, + /** 对话单元的时间配置 */ + datetime: { + type: [String, Function] as PropType, + }, + /** 自定义的昵称 */ + name: { + type: [String, Function] as PropType, + }, + /** 气泡框样式,支持基础、线框、文字三种类型 */ + variant: { + type: String as PropType, + default: 'text' as TdChatMessageProps['variant'], + validator(val: TdChatMessageProps['variant']): boolean { + if (!val) return true; + return ['base', 'outline', 'text'].includes(val); + }, + }, + message: { + type: Object as PropType, + }, + placement: { + type: String as PropType, + default: 'left' as TdChatMessageProps['placement'], + }, + chatContentProps: { + type: Object as PropType, + }, + allowContentSegmentCustom: { + type: Boolean, + default: false, + }, +}; diff --git a/packages/pro-components/chat/chat-message/chat-message.md b/packages/pro-components/chat/chat-message/chat-message.md new file mode 100644 index 0000000000..5ff07023d7 --- /dev/null +++ b/packages/pro-components/chat/chat-message/chat-message.md @@ -0,0 +1,55 @@ +:: BASE_DOC :: + +## API +### ChatMessage Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +name | String / Slot / Function | - | 发送者名称 | N +avatar | String / Slot / Function | - | 发送者头像 | N +datetime | String / Slot / Function | - | 消息发送时间 | N +content | Array | - | 消息内容对象。类型定义见 `Message` | Y +role | String | assistant | 消息角色。可选项:user/assistant/system | N +status | String | - | 消息状态。可选项:pending/streaming/complete/stop/error | N +placement | String | left | 消息位置。可选项:left/right | N +variant | String | text | 消息变体样式。可选项:base/outline/text | N +chatContentProps | Object | - | 消息内容属性配置。类型支持见 `chatContentProps` | N +animation | String | circle | 加载动画类型。可选项:skeleton/moving/gradient/circle | N +allowContentSegmentCustom | Boolean | false | 是否允许自定义局部消息内容,其他消息内容实用默认样式 | N + + +#### UserMessageContent 内容类型支持 +- 文本消息 (`TextContent`) +- 附件消息 (`AttachmentContent`) + +#### AIMessageContent 内容类型支持 +- 文本消息 (`TextContent`) +- Markdown 消息 (`MarkdownContent`) +- 搜索消息 (`SearchContent`) +- 建议消息 (`SuggestionContent`) +- 思考状态 (`ThinkingContent`) +- 图片消息 (`ImageContent`) +- 附件消息 (`AttachmentContent`) +- 自定义消息 (`AIContentTypeOverrides`) + +几种类型都继承自`ChatBaseContent`,包含通用字段: +字段 | 类型 | 必传 | 默认值 | 说明 +--|--|--|--|-- +type | `ChatContentType` | Y | - | 内容类型标识(text/markdown/search等) +data | 泛型TData | Y | - | 具体内容数据,类型由type决定 +status | `ChatMessageStatus \| ((currentStatus?: ChatMessageStatus) => ChatMessageStatus)` | N | - | 内容状态或状态计算函数 +id | string | N | - | 内容块唯一标识 + +每种类型的data字段有不同的结构,具体可参考下方表格,[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/blob/develop/src/chatbot/core/type.ts#L17) + + +### 插槽 + +| 插槽名 | 说明 | +|--------|------| +| content | 自定义消息内容 | +| avatar | 自定义头像 | +| name | 自定义名称 | +| datetime | 自定义时间 | +| actionbar | 自定义操作栏 | + diff --git a/packages/pro-components/chat/chat-message/chat-message.tsx b/packages/pro-components/chat/chat-message/chat-message.tsx new file mode 100644 index 0000000000..18a6cccd0e --- /dev/null +++ b/packages/pro-components/chat/chat-message/chat-message.tsx @@ -0,0 +1,67 @@ +import { defineComponent } from 'vue'; +import type { DefineComponent } from 'vue'; +import type { TdChatMessageProps } from 'tdesign-web-components'; +// 封装tdesign-web-components 的 chat-message +import 'tdesign-web-components/lib/chat-message'; +import { omiVueify } from 'omi-vueify'; +import props from './chat-message-props'; +import { useTNodeJSX } from '@tdesign/shared-hooks'; + +const BaseChatMessage = omiVueify('t-chat-item', { + methodNames: [], +}) as DefineComponent; +export default defineComponent({ + name: 'ChatMessage', + props, + setup(props, { slots }) { + return () => { + const renderTNodeJSX = useTNodeJSX(); + const baseSlots = { + actionbar: () => { + const actionbar = renderTNodeJSX('actionbar', { slotFirst: true }) && slots.actionbar?.(); + return actionbar ?
{actionbar}
: null; + }, + name: () => { + const name = renderTNodeJSX('name', { slotFirst: true }) && slots.name?.(); + return name ?
{name}
: null; + }, + avatar: () => { + // renderTNodeJSX如果没有插槽,会返回属性值,所以需要判断插槽是否存在 + const avatar = renderTNodeJSX('avatar', { slotFirst: true }) && slots.avatar?.(); + return avatar ?
{avatar}
: null; + }, + datetime: () => { + const datetime = renderTNodeJSX('datetime', { slotFirst: true }) && slots.datetime?.(); + return datetime ?
{datetime}
: null; + }, + header: () => { + const header = renderTNodeJSX('header', { slotFirst: true }) && slots.header?.(); + return header ?
{header}
: null; + }, + }; + let vSlots = null; + if (props.allowContentSegmentCustom) { + vSlots = { + ...slots, + ...baseSlots, + }; + } else { + vSlots = { + ...baseSlots, + content: () => { + const content = (renderTNodeJSX('content', { slotFirst: true }) && slots.content?.()) || slots.default?.(); + return content ?
{content}
: null; + }, + }; + } + return ( + + ); + }; + }, +}) as any; diff --git a/packages/pro-components/chat/chat-message/index.ts b/packages/pro-components/chat/chat-message/index.ts new file mode 100644 index 0000000000..6b557a9524 --- /dev/null +++ b/packages/pro-components/chat/chat-message/index.ts @@ -0,0 +1,2 @@ +import _ChatMessage from './chat-message'; +export default _ChatMessage; diff --git a/packages/pro-components/chat/chat-reasoning/__tests__/__snapshots__/index.test.jsx.snap b/packages/pro-components/chat/chat-reasoning/__tests__/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000000..8c83d13500 --- /dev/null +++ b/packages/pro-components/chat/chat-reasoning/__tests__/__snapshots__/index.test.jsx.snap @@ -0,0 +1,469 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ChatReasoning > :props > :collapsed - boolean 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ + + +
+
+ +
+
+`; + +exports[`ChatReasoning > :props > :collapsed 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ + + +
+
+ +
+
+`; + +exports[`ChatReasoning > :props > :expandIconPlacement - string 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+`; + +exports[`ChatReasoning > :props > :expandIconPlacement 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+`; + +exports[`ChatReasoning > :props > :layout - string 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+`; + +exports[`ChatReasoning > :props > :layout 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+`; + +exports[`ChatReasoning > > default slot 1`] = ` +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ +
+ custom content +
+ +
+
+
+
+
+ +
+
+`; diff --git a/packages/pro-components/chat/chat-reasoning/__tests__/index.test.jsx b/packages/pro-components/chat/chat-reasoning/__tests__/index.test.jsx new file mode 100644 index 0000000000..ea403d0e65 --- /dev/null +++ b/packages/pro-components/chat/chat-reasoning/__tests__/index.test.jsx @@ -0,0 +1,81 @@ +import { mount } from '@vue/test-utils'; +import { vi } from 'vitest'; +import ChatReasoning from '@tdesign/pro-components-chat/chat-reasoning/index'; + +describe('ChatReasoning', () => { + const provideMock = { + role: { value: 'user' }, + }; + + describe(':props', () => { + it(':collapsed - boolean', () => { + const wrapper = mount(ChatReasoning, { + props: { + collapsed: true, + }, + global: { + provide: provideMock, + }, + }); + expect(wrapper.element).toMatchSnapshot(); + }); + + it(':layout - string', () => { + const wrapper = mount(ChatReasoning, { + props: { + layout: 'border', + }, + global: { + provide: provideMock, + }, + }); + expect(wrapper.find('.t-chat__detail-reasoning-border').exists()).toBe(true); + expect(wrapper.element).toMatchSnapshot(); + }); + + it(':expandIconPlacement - string', () => { + const wrapper = mount(ChatReasoning, { + props: { + expandIconPlacement: 'right', + }, + global: { + provide: provideMock, + }, + }); + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('@event', () => { + it('onExpandChange', async () => { + const fn = vi.fn(); + const wrapper = mount(ChatReasoning, { + props: { + collapsed: false, + onExpandChange: fn, + }, + global: { + provide: provideMock, + }, + }); + + // Simulate clicking the expand icon + await wrapper.find('.t-collapse-panel__header').trigger('click'); + expect(fn).toHaveBeenCalledWith(true); + }); + }); + + describe('', () => { + it('default slot', () => { + const wrapper = mount(ChatReasoning, { + slots: { + default: '
custom content
', + }, + global: { + provide: provideMock, + }, + }); + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest-reasoning.ts b/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest-reasoning.ts new file mode 100644 index 0000000000..4e5ddc1daf --- /dev/null +++ b/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest-reasoning.ts @@ -0,0 +1,101 @@ +export class MockSSEResponse { + private controller!: ReadableStreamDefaultController; + private encoder = new TextEncoder(); + private stream: ReadableStream; + private error: boolean; + private currentPhase: 'reasoning' | 'content' = 'reasoning'; + + constructor( + private data: { + reasoning: string; // 推理内容 + content: string; // 正式内容 + }, + private delay: number = 100, + error = false, + ) { + this.error = error; + + this.stream = new ReadableStream({ + start: (controller) => { + this.controller = controller; + if (!this.error) { + // 如果不是错误情况,则开始推送数据 + setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据 + } + }, + cancel() {}, + }); + } + + private pushData() { + try { + if (this.currentPhase === 'reasoning') { + // 推送推理内容 + if (this.data.reasoning.length > 0) { + const chunk = JSON.stringify({ + delta: { + reasoning_content: this.data.reasoning.slice(0, 1), + content: '', + }, + finished: false, + }); + this.controller.enqueue(this.encoder.encode(chunk)); + this.data.reasoning = this.data.reasoning.slice(1); + // 设置下次推送 + setTimeout(() => this.pushData(), this.delay); + } else { + // 推理内容推送完成,切换到正式内容 + this.currentPhase = 'content'; + setTimeout(() => this.pushData(), this.delay); // 立即开始推送正式内容 + return; + } + } + + if (this.currentPhase === 'content') { + // 推送正式内容 + if (this.data.content.length > 0) { + const chunk = JSON.stringify({ + delta: { + reasoning_content: '', + content: this.data.content.slice(0, 1), + }, + finished: this.data.content.length === 1, // 最后一个字符时标记完成 + }); + this.controller.enqueue(this.encoder.encode(chunk)); + this.data.content = this.data.content.slice(1); + + // 设置下次推送 + setTimeout(() => this.pushData(), this.delay); + } else { + // const finalPayload = JSON.stringify({ + // delta: { + // reasoning_content: '', + // content: '', + // }, + // finished: true, + // }); + // this.controller.enqueue(this.encoder.encode(`${finalPayload}`)); + // 全部内容推送完成 + setTimeout(() => this.controller.close(), this.delay); + return; + } + } + } catch {} + } + + getResponse(): Promise { + return new Promise((resolve) => { + // 使用setTimeout来模拟网络延迟 + setTimeout(() => { + if (this.error) { + const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' }; + + // 返回模拟的网络错误响应,这里我们使用500状态码作为示例 + resolve(new Response(null, errorResponseOptions)); + } else { + resolve(new Response(this.stream)); + } + }, this.delay); // 使用构造函数中设置的delay值作为延迟时间 + }); + } +} diff --git a/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest.ts b/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest.ts new file mode 100644 index 0000000000..c144e1b067 --- /dev/null +++ b/packages/pro-components/chat/chat-reasoning/_example/mock-data/sseRequest.ts @@ -0,0 +1,61 @@ +export class MockSSEResponse { + private controller!: ReadableStreamDefaultController; + private encoder = new TextEncoder(); + private stream: ReadableStream; + private error: boolean; + + constructor( + private data: string, + private delay: number = 300, + error = false, // 新增参数,默认为false + ) { + this.error = error; + + this.stream = new ReadableStream({ + start: (controller) => { + this.controller = controller; + if (!this.error) { + // 如果不是错误情况,则开始推送数据 + setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据 + } + }, + cancel() {}, + }); + } + + private pushData() { + if (this.data.length === 0) { + this.controller.close(); + return; + } + try { + const chunk = this.data.slice(0, 1); + this.data = this.data.slice(1); + + this.controller.enqueue(this.encoder.encode(chunk)); + + if (this.data.length > 0) { + setTimeout(() => this.pushData(), this.delay); + } else { + // 数据全部发送完毕后关闭流 + setTimeout(() => this.controller.close(), this.delay); + } + } catch {} + } + + getResponse(): Promise { + return new Promise((resolve) => { + // 使用setTimeout来模拟网络延迟 + setTimeout(() => { + if (this.error) { + const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' }; + + // 返回模拟的网络错误响应,这里我们使用500状态码作为示例 + resolve(new Response(null, errorResponseOptions)); + } else { + resolve(new Response(this.stream)); + } + }, this.delay); // 使用构造函数中设置的delay值作为延迟时间 + }); + } +} diff --git a/packages/pro-components/chat/_example/reasoning-custom-slot.vue b/packages/pro-components/chat/chat-reasoning/_example/reasoning-custom-slot.vue similarity index 56% rename from packages/pro-components/chat/_example/reasoning-custom-slot.vue rename to packages/pro-components/chat/chat-reasoning/_example/reasoning-custom-slot.vue index 8275bc85e0..5a7ecd4ca3 100644 --- a/packages/pro-components/chat/_example/reasoning-custom-slot.vue +++ b/packages/pro-components/chat/chat-reasoning/_example/reasoning-custom-slot.vue @@ -1,12 +1,10 @@ + + diff --git a/packages/pro-components/chat/_example/reasoning.vue b/packages/pro-components/chat/chat-reasoning/_example/reasoning.vue similarity index 87% rename from packages/pro-components/chat/_example/reasoning.vue rename to packages/pro-components/chat/chat-reasoning/_example/reasoning.vue index f12393b34e..2950399d3f 100644 --- a/packages/pro-components/chat/_example/reasoning.vue +++ b/packages/pro-components/chat/chat-reasoning/_example/reasoning.vue @@ -14,14 +14,15 @@ :name="item.name" :role="item.role" :datetime="item.datetime" - :text-loading="index === 0 && loading" + :text-loading="index === chatList.length - 1 && loading" :content="item.content" :reasoning="{ - collapsed: index === 0 && !isStreamLoad, + collapsed: index === chatList.length - 1 && !isStreamLoad, expandIconPlacement: 'right', onExpandChange: handleChange(value, { index }), collapsePanelProps: { - header: renderHeader(index === 0 && isStreamLoad && !item.content, item), + header: renderHeader(index === chatList.length - 1 && isStreamLoad && !item.content, item), + content: renderReasoningContent(item.reasoning), }, }" @@ -38,7 +39,7 @@ @stop="onStop" @send="inputEnter" > - diff --git a/packages/pro-components/chat/chat-sender/_example/chat-sender-base.vue b/packages/pro-components/chat/chat-sender/_example/chat-sender-base.vue new file mode 100644 index 0000000000..1ece79912d --- /dev/null +++ b/packages/pro-components/chat/chat-sender/_example/chat-sender-base.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/pro-components/chat/chat-sender/_example/chat-sender-mix.vue b/packages/pro-components/chat/chat-sender/_example/chat-sender-mix.vue new file mode 100644 index 0000000000..10e381aa68 --- /dev/null +++ b/packages/pro-components/chat/chat-sender/_example/chat-sender-mix.vue @@ -0,0 +1,219 @@ + + + diff --git a/packages/pro-components/chat/_example/chat-sender-slot.vue b/packages/pro-components/chat/chat-sender/_example/chat-sender-slot.vue similarity index 72% rename from packages/pro-components/chat/_example/chat-sender-slot.vue rename to packages/pro-components/chat/chat-sender/_example/chat-sender-slot.vue index 6f5a7498e3..2a60468e41 100644 --- a/packages/pro-components/chat/_example/chat-sender-slot.vue +++ b/packages/pro-components/chat/chat-sender/_example/chat-sender-slot.vue @@ -4,16 +4,24 @@ v-model="inputValue" class="chat-sender" :textarea-props="{ - placeholder: '请输入消息...', + placeholder: options.filter((item) => item.value === scene)[0].placeholder, }" :loading="loading" @send="inputEnter" > -