diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c9c132410285a9913518f83e8075250a1b9d49..9f6e624e28b2396848b0282abe06edaefca59caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,15 @@ ## [Unreleased] +### Changed + - 更新错误消息对话框的样式,IA聊天暴露事件标识增加限定 +### Added + +- AI聊天窗支持拖动位置,拖拽边改变大小,并持久化 +- 支持最小化功能,且最小化后图标图可拖拽位置,并持久化 + ## [0.0.4] - 2024-05-08 - 更新提问框样式 diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index fb6455387a68016c2fd98cf0ec2af2e5559f5cc1..55f103e30d9abe4c55bd80dc1322936f7fd4c372 100644 --- a/src/components/chat-container/chat-container.scss +++ b/src/components/chat-container/chat-container.scss @@ -11,7 +11,7 @@ $ai-chat: ( 'hover-background-color': rgb(184 184 184 / 31%), 'hover-border-color': #e5e5e5, // 默认圆角 - 'border-radius': 5px, + 'border-radius': 5px ); @include b(chat-container) { @@ -37,9 +37,46 @@ $ai-chat: ( vertical-align: middle; fill: currentcolor; } + @include when(hidden) { + display: none; + } +} + +@include b(chat-container-minimize) { + @include set-component-css-var('ai-chat', $ai-chat); + + width: 56px; + height: 56px; + z-index: 99999; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + border-radius: 50%; + color: #{getCssVar('ai-chat', 'color')}; + border: 1px solid #{getCssVar('ai-chat', 'border-color')}; + background-color: #{getCssVar('ai-chat', 'background-color')}; + + &:hover { + cursor: pointer; + color: #{getCssVar('ai-chat', 'hover-color')}; + background-color: #{getCssVar('ai-chat', 'hover-background-color')}; + } + + svg { + display: inline-block; + vertical-align: middle; + fill: currentcolor; + } + + @include when(hidden) { + display: none; + } } @include b(chat-container-header) { + cursor: move; display: flex; flex-shrink: 0; align-items: center; @@ -49,7 +86,7 @@ $ai-chat: ( } @include b(chat-container-header-caption) { - padding: 0 6px 0 16px ; + padding: 0 6px 0 16px; font-size: 18px; font-weight: 800; } @@ -84,5 +121,5 @@ $ai-chat: ( @include b(chat-container-footer) { flex-shrink: 0; padding: 3px 6px; - border-top: 1px solid #{getCssVar('ai-chat', 'border-color')}; + border-top: 1px solid #{getCssVar('ai-chat', 'border-color')}; } diff --git a/src/components/chat-container/chat-container.tsx b/src/components/chat-container/chat-container.tsx index 2bd37422bee83a1b52a97077184c9fac6e17b65c..df9d396a4a684bbedda4cce3c4126f4c9761fdc3 100644 --- a/src/components/chat-container/chat-container.tsx +++ b/src/components/chat-container/chat-container.tsx @@ -4,9 +4,15 @@ import { Namespace } from '../../utils'; import { ChatMessages } from '../chat-messages/chat-messages'; import { ChatInput } from '../chat-input/chat-input'; import { AiChatController } from '../../controller'; -import { CloseSvg, FullScreenSvg, CloseFullScreenSvg } from '../../icons'; -import './chat-container.scss'; +import { + AISvg, + CloseSvg, + MinimizeSvg, + FullScreenSvg, + CloseFullScreenSvg, +} from '../../icons'; import { AIChatConst } from '../../constants'; +import './chat-container.scss'; export interface ChatContainerProps { /** @@ -32,6 +38,13 @@ export interface ChatContainerProps { */ fullscreen: (target: boolean) => void; + /** + * 最小化行为 + * + * @memberof ChatContainerProps + */ + minimize: (target: boolean) => void; + /** * 标题 * @@ -49,6 +62,14 @@ interface ChatContainerState { * @date 2024-05-07 15:10:31 */ isFullScreen: boolean; + + /** + * 最小化 + * + * @type {boolean} + * @memberof ChatContainerState + */ + isMinimize: boolean; } /** @@ -69,6 +90,7 @@ export class ChatContainer extends Component< // 初始化状态 this.state = { isFullScreen: false, + isMinimize: false, }; } @@ -78,30 +100,68 @@ export class ChatContainer extends Component< dragHandle = createRef(); + minimizeRef = createRef(); + data = { - x: window.innerWidth - 600 - 100, - y: 100, + x: window.innerWidth - 600, + y: 0, width: 600, - height: 600, + height: window.innerHeight, minWidth: 300, minHeight: 300, }; - calcStyle() { + minimizeData = { + x: window.innerWidth - 86, + y: window.innerHeight - 86, + }; + + /** + * 是否禁止拖动 + * - 拖拽边时应禁止拖动 + * @type {boolean} + * @memberof ChatContainer + */ + disabled: boolean = false; + + /** + * 最小化是否在拖拽中 + * - 在拖拽时不应触发点击事件 + * @type {boolean} + * @memberof ChatContainer + */ + isDragging: boolean = false; + + calcWindowStyle() { return { - right: `0px`, - top: `0px`, - height: `100vh`, + left: `${this.data.x}px`, + top: `${this.data.y}px`, + height: `${this.data.height}px`, width: `${this.data.width}px`, 'z-index': '1000', }; } + calcMinimizeStyle() { + return { + left: `${this.minimizeData.x}px`, + top: `${this.minimizeData.y}px`, + 'z-index': '99999', + }; + } + setStyle() { - Object.assign(this.containerRef.current!.style, this.calcStyle()); + Object.assign(this.containerRef.current!.style, this.calcWindowStyle()); + Object.assign(this.minimizeRef.current!.style, this.calcMinimizeStyle()); } componentDidMount(): void { + const minimizeCache = localStorage.getItem( + AIChatConst.MINIMIZE_STYLY_CHCHE, + ); + if (minimizeCache) { + this.minimizeData = JSON.parse(minimizeCache); + } const cache = localStorage.getItem(AIChatConst.STYLE_CACHE); if (cache) { this.data = JSON.parse(cache); @@ -115,33 +175,61 @@ export class ChatContainer extends Component< } this.setStyle(); const state = this.data; + // 注册最小化拖拽 + this.minimizeRef.current!.onmousedown = (e: MouseEvent): void => { + // 禁止选择文本,避免拖动时出现选择效果 + document.body.style.userSelect = 'none'; + const offsetX = e.clientX - this.minimizeRef.current!.offsetLeft; + const offsetY = e.clientY - this.minimizeRef.current!.offsetTop; + const start = Date.now(); + const onMouseMove = (evt: MouseEvent): void => { + this.minimizeData.x = evt.clientX - offsetX; + this.minimizeData.y = evt.clientY - offsetY; + this.setStyle(); + }; + const onMouseUp = (): void => { + // 恢复选择文本功能 + const end = Date.now(); + // 鼠标按下到抬起的时间超过300毫秒则认定为拖拽中 + this.isDragging = end - start > 300; + document.body.style.userSelect = ''; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; // 注册窗口拖拽 - // interact(this.dragHandle.current!).draggable({ - // modifiers: [ - // interact.modifiers.restrictRect({ - // restriction: document.body, - // endOnly: true, - // }), - // ], - // cursorChecker: () => { - // return 'move'; - // }, - // listeners: { - // move: event => { - // // 计算偏移量保持位置 - // state.x += event.dx; - // state.y += event.dy; - // this.setStyle(); - // }, - // }, - // }); + this.dragHandle.current!.onmousedown = (e: MouseEvent): void => { + if (this.disabled || this.state.isFullScreen) return; + // 禁止选择文本,避免拖动时出现选择效果 + document.body.style.userSelect = 'none'; + const offsetX = e.clientX - this.containerRef.current!.offsetLeft; + const offsetY = e.clientY - this.containerRef.current!.offsetTop; + const onMouseMove = (evt: MouseEvent): void => { + state.x = evt.clientX - offsetX; + state.y = evt.clientY - offsetY; + this.setStyle(); + }; + + const onMouseUp = (): void => { + // 恢复选择文本功能 + document.body.style.userSelect = ''; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; // 注册窗口大小变更 interact(this.containerRef.current!).resizable({ // 可拖拽的边缘 edges: { - top: false, + top: true, right: true, - bottom: false, + bottom: true, left: true, }, margin: 6, @@ -156,19 +244,34 @@ export class ChatContainer extends Component< inertia: true, listeners: { move: event => { - state.x += event.deltaRect.left; - state.y += event.deltaRect.top; + if (this.state.isFullScreen) return; + state.x = event.rect.left; + state.y = event.rect.top; // 更新宽高 state.width = event.rect.width; state.height = event.rect.height; this.setStyle(); }, + start: () => { + // 禁止选择文本,避免拖动时出现选择效果 + this.disabled = true; + document.body.style.userSelect = 'none'; + }, + end: () => { + // 恢复选择文本功能 + this.disabled = false; + document.body.style.userSelect = ''; + }, }, }); } componentWillUnmount(): void { localStorage.setItem(AIChatConst.STYLE_CACHE, JSON.stringify(this.data)); + localStorage.setItem( + AIChatConst.MINIMIZE_STYLY_CHCHE, + JSON.stringify(this.minimizeData), + ); } /** @@ -207,59 +310,124 @@ export class ChatContainer extends Component< document?.exitFullscreen(); this.setState({ isFullScreen: false }); this.props.fullscreen(false); + this.setStyle(); } } + /** + * 最小化 + * + * @memberof ChatContainer + */ + minimize(): void { + this.setState({ isMinimize: true }); + this.props.minimize(true); + } + + /** + * 退出最小化 + * + * @memberof ChatContainer + */ + exitMinimize(): void { + if (this.isDragging) return; + this.setState({ isMinimize: false }); + this.props.minimize(false); + } + + /** + * 阻止冒泡 + * - 防止点击头部行为时误触发拖动监听 + * @param {MouseEvent} evt + * @memberof ChatContainer + */ + stopPropagation(evt: MouseEvent): void { + evt.stopPropagation(); + } + render() { return ( -
+
-
-
- {this.props.caption || 'AIChart'} -
-
- {this.state.isFullScreen ? ( + ${this.ns.is('full-screen', this.state.isFullScreen)} + ${this.ns.is('hidden', this.state.isMinimize)}`} + ref={this.containerRef} + > +
+
+ {this.props.caption || 'AI助手'} +
+
- +
- ) : ( + {this.state.isFullScreen ? ( +
+ +
+ ) : ( +
+ +
+ )}
- +
- )} -
-
+
+ +
+
+ +
-
- -
-
- +
+
); diff --git a/src/components/chat-messages/chat-messages.scss b/src/components/chat-messages/chat-messages.scss index fb1a34cda570c497876a48d5f8f92af17670461c..26111919663f5355961119342e3d87679d9b1f5a 100644 --- a/src/components/chat-messages/chat-messages.scss +++ b/src/components/chat-messages/chat-messages.scss @@ -1,7 +1,7 @@ @include b(chat-messages) { - width: 100%; - height: 100%; - padding: 6px; + margin: 6px; + width: calc(100% - 12px); + height: calc(100% - 12px); overflow-x: hidden; overflow-y: auto; diff --git a/src/constants/index.ts b/src/constants/index.ts index bfb57d3b692c8063bf4bd2934c9ee52a7c9fd314..6b7fd3b6429f6e0f6e4018fc94f6936e8a0a25e1 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,4 +4,9 @@ export enum AIChatConst { * 聊天窗口样式缓存 */ STYLE_CACHE = 'ai-chat-style-cache', + + /** + * 最小化样式缓存 + */ + MINIMIZE_STYLY_CHCHE = 'ai-chat-minimize-style-cache', } diff --git a/src/controller/chat/chat.controller.ts b/src/controller/chat/chat.controller.ts index 72c444bd84145c4daa721ad40e563d2042743a1e..d6a5fe2aac03fe28a8a0bf75c3baf118035ce0b2 100644 --- a/src/controller/chat/chat.controller.ts +++ b/src/controller/chat/chat.controller.ts @@ -49,6 +49,11 @@ export class ChatController { opts.fullscreen(target); } }, + minimize: (target: boolean) => { + if (opts.minimize) { + opts.minimize(target); + } + }, }), this.container, ); diff --git a/src/icons/ai-svg.tsx b/src/icons/ai-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..42e06f25408a71d3a7f683029eba17870cf6f173 --- /dev/null +++ b/src/icons/ai-svg.tsx @@ -0,0 +1,10 @@ +export const AISvg = () => ( + + + + + + + + +); diff --git a/src/icons/index.ts b/src/icons/index.ts index e29f0c13d6bd7f8cc7e7150d8925c0f2a02e273d..98286684b56466407f51225b85aba7eaec3f1705 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -6,3 +6,5 @@ export { RefreshSvg } from './refresh-svg'; export { CopySvg } from './copy-svg'; export { FullScreenSvg } from './full-screen-svg'; export { CloseFullScreenSvg } from './close-full-screen-svg'; +export { MinimizeSvg } from './minimize-svg'; +export { AISvg } from './ai-svg'; diff --git a/src/icons/minimize-svg.tsx b/src/icons/minimize-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eedf0656486f157a26222c3e45123d18d9d20876 --- /dev/null +++ b/src/icons/minimize-svg.tsx @@ -0,0 +1,5 @@ +export const MinimizeSvg = () => ( + + + +); diff --git a/src/interface/i-chat-options/i-chat-options.ts b/src/interface/i-chat-options/i-chat-options.ts index 44b02ea4a090ac4db5b1b73dedc82ea40d684b48..26279e9bc96284af2b841d9552232e0c41f51d74 100644 --- a/src/interface/i-chat-options/i-chat-options.ts +++ b/src/interface/i-chat-options/i-chat-options.ts @@ -38,7 +38,7 @@ export interface IChatOptions { * @return {*} {Promise} 等待操作,用于显示 loading 并获取最终成功与否 */ action?( - action: 'backfill' | 'question' | 'deletemsg' | 'refreshmsg', + action: 'backfill' | 'question' | 'deletemsg' | 'refreshmsg' | 'copymsg', params?: T, ): Promise; @@ -50,6 +50,14 @@ export interface IChatOptions { */ fullscreen?(target: boolean): void; + /** + * 最小化操作 + * + * @param {boolean} target true为最小化,false为退出最小化 + * @memberof IChatOptions + */ + minimize?(target: boolean): void; + /** * 聊天窗口呈现 *