From 9d1f8dec17a7c9d0070c6859bfecd72fc8519446 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Thu, 20 Nov 2025 16:55:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=A1=8C=E5=86=85?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E7=BB=84=E4=BB=B6=E4=B8=BB=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=89=8D=E6=A0=A1=E9=AA=8C=E5=80=BC=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=EF=BC=8C=E4=BD=8D=E7=BD=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locale/en/index.ts | 2 + src/locale/zh-CN/index.ts | 2 + .../inline-ai-textarea.hook.ts | 138 +++++++++--- .../inline-ai-textarea.scss | 207 ++++++++++++++---- .../inline-ai-textarea/inline-ai-textarea.tsx | 157 +++++++------ 5 files changed, 375 insertions(+), 131 deletions(-) diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 1c85a4d5a..233ff0e20 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -899,6 +899,8 @@ export default { copyText: 'Copy Text', info: 'The content is generated by AI, please carefully discern.', stopEdit: 'Terminate editing', + warningTitle: 'Confirm termination', + warningDesc: 'Are you sure to terminate the creation?', }, }, // runTime diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index e0d52c43d..e7a907099 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -843,6 +843,8 @@ export default { copyText: '复制文本', info: '内容由 AI 生成,请仔细甄别。', stopEdit: '终止编辑', + warningTitle: '确认中止', + warningDesc: '确认中止创作吗?', }, }, // runTime diff --git a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.hook.ts b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.hook.ts index dac63e3ba..47749b4ab 100644 --- a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.hook.ts +++ b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.hook.ts @@ -37,17 +37,41 @@ export const computedInLineAIParams = ( /** * @description 监听点击事件 */ -export const useInLineAIContainerClick = (props: IData): void => { - const handclick = (evt: MouseEvent): void => { +export const useInLineAIContainerClick = ( + props: { + unMountAIChat: Function; + content: string; + }, + content: Ref, + isLoading: Ref, +): void => { + const handclick = async (evt: MouseEvent): Promise => { const target = evt.target as HTMLElement; - // 检查被点击元素或其父级元素中是否存在具有 ibiz-inline-ai-textarea-container 类名的元素 - if (!target.closest('.ibiz-inline-ai-textarea-container')) { - props.unMountAIChat(); + // 检查被点击元素或其父级元素中是否存在具有 ibiz-inline-ai-textarea-container 或 ibiz-inline-ai-alert 类名的元素 + if ( + !target.closest('.ibiz-inline-ai-textarea-container') && + !target.closest('.ibiz-inline-ai-alert') && + !isLoading.value + ) { + const isChange = props.content !== content.value; + let isClose = true; + if (isChange) { + isClose = await ibiz.confirm.warning({ + title: ibiz.i18n.t('util.inlineAiUtil.warningTitle'), + desc: ibiz.i18n.t('util.inlineAiUtil.warningDesc'), + options: { + modalClass: 'ibiz-inline-ai-alert', + }, + }); + } + if (isClose) props.unMountAIChat(); } }; + onMounted(() => { document.addEventListener('click', handclick, true); }); + onUnmounted(() => { document.removeEventListener('click', handclick, true); }); @@ -197,6 +221,7 @@ export const useBase = ( target: Ref, ): { actions: IActionItem[]; + theme: 'light' | 'dark'; actionStyle: Ref; containerStyle: Ref; contentStyle: Ref; @@ -237,6 +262,17 @@ export const useBase = ( }, ]; + const { options } = props; + const editorRect = options.editorElement.getBoundingClientRect(); + // 获取相对元素的偏移量 + const offsetX = options.left - editorRect.left; + const offsetY = options.top - editorRect.top; + + /** + * 主题 + */ + const theme = options.editorTheme || 'light'; + /** * 行为组样式 */ @@ -246,32 +282,63 @@ export const useBase = ( * 容器样式 */ const containerStyle = ref({ - width: `${props.options.width}px`, - left: `${props.options.left}px`, - top: `${props.options.top}px`, + width: `${options.width}px`, + left: `${options.left}px`, + top: `${options.top}px`, }); /** * 内容样式 */ const contentStyle = ref({ - height: `${props.options.height || 80}px`, - 'max-height': `${props.options.maxHeight}px`, + height: `${options.height || 80}px`, + 'max-height': `${options.maxHeight}px`, }); /** - * @description 计算样式 + * @description 更新位置 + * @returns {*} */ - const calcStyle = (isInit: boolean = false) => { + const updatePosition = () => { if (!container.value || !target.value) return; - const containerRect = container.value.getBoundingClientRect(); - const targetHeight = target.value.offsetHeight; - const spaceBelow = window.innerHeight - containerRect.bottom; - // 计算位置 - const position = - (isInit ? containerRect.height + 41 : containerRect.height) + 4; + const rect = options.editorElement.getBoundingClientRect(); + let top = rect.top + offsetY; + let left = rect.left + offsetX; + + // 获取容器元素的尺寸 + const containerWidth = container.value.offsetWidth; + const containerHeight = container.value.offsetHeight; - if (spaceBelow >= targetHeight + 8) { + // 获取窗口尺寸 + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + // 边界间距 + const margin = 8; + + if (left + containerWidth + margin > windowWidth) { + // 检查右边界 + left = windowWidth - containerWidth - margin; + } else if (left < margin) { + // 检查左边界 + left = margin; + } + if (top + containerHeight + margin > windowHeight) { + // 检查下边界 + top = windowHeight - containerHeight - margin; + } else if (top < margin) { + // 检查上边界 + top = margin; + } + + // 更新容器位置 + containerStyle.value.top = `${top}px`; + containerStyle.value.left = `${left}px`; + + // 计算行为列表位置 + const position = containerHeight + 4; + const targetHeight = target.value.offsetHeight; + if (windowHeight - (top + containerHeight + targetHeight) > margin) { // 下方空间足够,显示在下方 actionStyle.value.top = `${position}px`; actionStyle.value.bottom = 'auto'; @@ -282,18 +349,37 @@ export const useBase = ( } }; - const updatePosition = () => calcStyle(); + let ticking = false; + const optimizedUpdatePosition = () => { + if (!ticking) { + requestAnimationFrame(() => { + updatePosition(); + ticking = false; + }); + ticking = true; + } + }; onMounted(() => { - calcStyle(true); - // window.addEventListener('scroll', updatePosition, { passive: true }); - window.addEventListener('resize', updatePosition, { passive: true }); + updatePosition(); + document.addEventListener('scroll', optimizedUpdatePosition, { + passive: true, + capture: true, + }); + window.addEventListener('resize', optimizedUpdatePosition, { + passive: true, + }); }); onUnmounted(() => { - // window.removeEventListener('scroll', updatePosition); - window.removeEventListener('resize', updatePosition); + document.removeEventListener('scroll', optimizedUpdatePosition, { + passive: true, + capture: true, + }); + window.removeEventListener('resize', optimizedUpdatePosition, { + passive: true, + }); }); - return { actions, actionStyle, containerStyle, contentStyle }; + return { theme, actions, actionStyle, containerStyle, contentStyle }; }; diff --git a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.scss b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.scss index a0701453c..5d5ced74e 100644 --- a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.scss +++ b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.scss @@ -1,47 +1,94 @@ $inline-ai-context-menu: ( - color-bg: getCssVar(color,bg,2), - color-text: getCssVar(color,text,0), - color-bg-active: getCssVar(color,fill,0), + color-bg: getCssVar(color, bg, 2), + color-text: getCssVar(color, text, 0), + color-bg-active: getCssVar(color, fill, 0), ); @include b(inline-ai-container-context-menu) { - @include set-component-css-var('inline-ai-context-menu',$inline-ai-context-menu); - - --mx-menu-text: #{getCssVar(inline-ai-context-menu,color-text)}; - --mx-menu-backgroud: #{getCssVar(inline-ai-context-menu,color-bg)}; - --mx-menu-hover-backgroud: #{getCssVar(inline-ai-context-menu,color-bg-active)}; - --mx-menu-hover-text: #{getCssVar(inline-ai-context-menu,color-text)}; - --mx-menu-open-text: #{getCssVar(inline-ai-context-menu,color-text)}; - --mx-menu-open-backgroud:#{getCssVar(inline-ai-context-menu,color-bg-active)}; - --mx-menu-open-hover-text:#{getCssVar(inline-ai-context-menu,color-text)}; - --mx-menu-open-hover-backgroud:#{getCssVar(inline-ai-context-menu,color-bg-active)}; - - .scroll-content{ - pointer-events: unset; - } - - .mx-context-menu{ - cursor: pointer; - } + @include set-component-css-var('inline-ai-context-menu', $inline-ai-context-menu); + + --mx-menu-text: #{getCssVar(inline-ai-context-menu, color-text)}; + --mx-menu-backgroud: #{getCssVar(inline-ai-context-menu, color-bg)}; + --mx-menu-hover-backgroud: #{getCssVar(inline-ai-context-menu, color-bg-active)}; + --mx-menu-hover-text: #{getCssVar(inline-ai-context-menu, color-text)}; + --mx-menu-open-text: #{getCssVar(inline-ai-context-menu, color-text)}; + --mx-menu-open-backgroud: #{getCssVar(inline-ai-context-menu, color-bg-active)}; + --mx-menu-open-hover-text: #{getCssVar(inline-ai-context-menu, color-text)}; + --mx-menu-open-hover-backgroud: #{getCssVar(inline-ai-context-menu, color-bg-active)}; + + .scroll-content { + pointer-events: unset; + } + + .mx-context-menu { + cursor: pointer; + } } +// 默认是亮色主题 +$inline-ai-textarea-container: ( + color-bg-0: rgb(255 255 255 / 100%), + color-bg-1: rgb(249 249 249 / 100%), + color-border: rgb(29 31 35 / 10%), + color-text-0: rgb(29 31 35 / 100%), + color-text-1: rgb(29 31 35 / 35%), + color-text-2: rgb(85 125 165 / 100%), + color-text-hove-1: rgb(105 148 190 / 100%), + color-bg-hover-1: rgb(217 236 255 / 100%), + color-bg-hover-2: rgb(46 50 55 / 5%), + color-loading: #65b3fc, +); + @include b(inline-ai-textarea-container) { + @include set-component-css-var('inline-ai-textarea-container', $inline-ai-textarea-container); + + // 暗色主题 + @include m(dark) { + #{getCssVarName(inline-ai-textarea-container, color-bg-0)}: rgb(28 28 28 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-bg-1)}: rgb(53 54 60 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-border)}: rgb(255 255 255 / 8%); + #{getCssVarName(inline-ai-textarea-container, color-text-0)}: rgb(255 255 255 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-text-1)}: rgb(249 249 249 / 35%); + #{getCssVarName(inline-ai-textarea-container, color-text-2)}: rgb(70 107 144 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-text-hove-1)}: rgb(105 148 190 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-bg-hover-1)}: rgb(85 125 165 / 20%); + #{getCssVarName(inline-ai-textarea-container, color-bg-hover-2)}: rgb(67 68 74 / 100%); + #{getCssVarName(inline-ai-textarea-container, color-loading)}: rgb(204 204 204 / 60%); + } + position: absolute; - z-index: 99999999; + z-index: 2000; + color: getCssVar(inline-ai-textarea-container, color-text-0); user-select: none; - background-color: getCssVar(color, bg-1); - border: 1px solid getCssVar(color, border); - border-radius: getCssVar(border-radius, small); - box-shadow: rgb(0 0 0 / 8%) 0 0 16px 0; + + // 边和阴影特殊控制 + .#{bem(inline-ai-textarea-container, content)} { + border: 1px solid getCssVar(inline-ai-textarea-container, color-border); + box-shadow: 0 0 16px getCssVar(color, shadow); + } + + @include when(show-ai) { + border: 1px solid getCssVar(inline-ai-textarea-container, color-border); + border-radius: getCssVar(border-radius, small); + box-shadow: 0 0 16px getCssVar(color, shadow); + .#{bem(inline-ai-textarea-container, content)} { + border: none; + border-radius: getCssVar(border-radius, small) getCssVar(border-radius, small) 0 0; + box-shadow: none; + } + .#{bem(inline-ai-textarea-container, footer)} { + border-radius: 0 0 getCssVar(border-radius, small) getCssVar(border-radius, small); + } + } + @include e(content) { position: relative; display: flex; gap: getCssVar(spacing, tight); padding: getCssVar(spacing, base, tight); + background-color: getCssVar(inline-ai-textarea-container, color-bg-0); @include m(prefix) { flex-shrink: 0; - } - @include m(ai-icon) { - color: getCssVar(color, disabled, text); + color: getCssVar(inline-ai-textarea-container, color-text-1); } @include m(textarea) { position: relative; @@ -51,12 +98,14 @@ $inline-ai-context-menu: ( width: 100%; height: 100%; padding: 0; + color: getCssVar(inline-ai-textarea-container, color-text-0); resize: none; + background-color: getCssVar(inline-ai-textarea-container, color-bg-0); border: none; outline: none; - + &:disabled { - background-color: getCssVar(color, bg-1); + background-color: getCssVar(inline-ai-textarea-container, color-bg-0); } } } @@ -71,12 +120,15 @@ $inline-ai-context-menu: ( justify-content: center; width: 28px; height: 28px; - color: getCssVar(color, primary); + color: getCssVar(inline-ai-textarea-container, color-text-2); cursor: pointer; border-radius: getCssVar(spacing, extra, tight); &:hover { - background-color: getCssVar(color, primary, light, default); + background-color: getCssVar( + inline-ai-textarea-container, + color-bg-hover-1 + ); } } @include m(stop-icon) { @@ -84,11 +136,11 @@ $inline-ai-context-menu: ( gap: getCssVar(spacing, extra, tight); align-items: center; font-size: getCssVar(font-size, small); - color: getCssVar(color, disabled, text); + color: getCssVar(inline-ai-textarea-container, color-text-1); cursor: pointer; &:hover { - color: getCssVar(color, primary, hover); + color: getCssVar(inline-ai-textarea-container, color-text-hove-1); } } } @@ -96,9 +148,13 @@ $inline-ai-context-menu: ( @include e(footer) { padding: getCssVar(spacing, base, tight) getCssVar(spacing, base); font-size: getCssVar(font-size, small); - color: getCssVar(color, disabled, text); - background-color: getCssVar(color, disabled, fill); - border-top: 1px solid getCssVar(color, disabled, border); + color: getCssVar(inline-ai-textarea-container, color-text-1); + visibility: hidden; + background-color: getCssVar(inline-ai-textarea-container, color-bg-1); + border-top: 1px solid getCssVar(inline-ai-textarea-container, color-border); + @include when(show) { + visibility: visible; + } } @include e(actions) { @@ -107,10 +163,10 @@ $inline-ai-context-menu: ( padding: getCssVar(spacing, base, tight) 0; font-size: getCssVar(font-size, regular); visibility: hidden; - background-color: getCssVar(color, bg-1); - border: 1px solid getCssVar(color, border); + background-color: getCssVar(inline-ai-textarea-container, color-bg-0); + border: 1px solid getCssVar(inline-ai-textarea-container, color-border); border-radius: getCssVar(border-radius, small); - box-shadow: rgb(0 0 0 / 8%) 0 0 16px 0; + box-shadow: 0 0 16px getCssVar(color, shadow); @include when(show) { visibility: visible; } @@ -122,7 +178,10 @@ $inline-ai-context-menu: ( cursor: pointer; &:hover { - background-color: getCssVar(color, fill-0); + background-color: getCssVar( + inline-ai-textarea-container, + color-bg-hover-2 + ); } @include when(danger) { &:hover { @@ -132,7 +191,69 @@ $inline-ai-context-menu: ( } @include m(divider) { margin: getCssVar(spacing, extra, tight) getCssVar(spacing, base, loose); - border-top: 1px solid getCssVar(color, border); + border-top: 1px solid + getCssVar(inline-ai-textarea-container, color-border); + } + } + + @include e(loading) { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + color: getCssVar(inline-ai-textarea-container, color-loading); + @keyframes loading { + 0% { + opacity: 0; + transform: translateX(-300px); + } + + 33% { + opacity: 1; + transform: translateX(0); + } + + 66% { + opacity: 1; + transform: translateX(0); + } + + 100% { + opacity: 0; + transform: translateX(300px); + } + } + + label { + display: inline-block; + font-size: 20px; + opacity: 0; + + &:nth-child(6) { + animation: loading 3s infinite ease-in-out; + } + + &:nth-child(5) { + animation: loading 3s 0.1s infinite ease-in-out; + } + + &:nth-child(4) { + animation: loading 3s 0.2s infinite ease-in-out; + } + + &:nth-child(3) { + animation: loading 3s 0.3s infinite ease-in-out; + } + + &:nth-child(2) { + animation: loading 3s 0.4s infinite ease-in-out; + } + + &:nth-child(1) { + animation: loading 3s 0.5s infinite ease-in-out; + } } } } diff --git a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.tsx b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.tsx index 0c1bf1422..34366d29a 100644 --- a/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.tsx +++ b/src/util/inline-ai-util/inline-ai-textarea/inline-ai-textarea.tsx @@ -58,58 +58,86 @@ export const InlineAITextArea = defineComponent({ }, setup(props, ctx) { const ns = useNamespace('inline-ai-textarea-container'); - + /** + * 容器元素引用 + */ const containerRef = ref(); + /** + * 行为元素引用 + */ const actionsRef = ref(); - // 预置参数 - const { srfaiappendcurdata, srfaiautoappend, srfmode } = - computedInLineAIParams(props); - - const { actions, actionStyle, containerStyle, contentStyle } = useBase( - props, - containerRef, - actionsRef, - ); - - // 处理点击事件 - useInLineAIContainerClick(props); + /** + * 多行文本框元素引用 + */ + const textareaRef = ref(); - const { askAI } = useAI(props, { - srfaiappendcurdata, - srfmode, - }); /** * 多行文本框内容 */ const textareaContent = ref(props.content); + /** * 问题 */ let question: string; + /** * 内容类型(用户|助手) */ const contentType = ref<'USER' | 'ASSISTANT'>('USER'); + /** * 是否在加载状态 */ const isLoading = ref(false); /** - * 编辑器是否禁用状态 + * 内容区是否禁用状态 */ const disabled = computed(() => { // 助手回答或者加载状态中 return contentType.value === 'ASSISTANT' || isLoading.value; }); + /** + * 功能区是否显示 + */ + const isShow = computed(() => { + // 助手回答并且不在加载状态中 + return contentType.value === 'ASSISTANT' && !isLoading.value; + }); + + // 预置参数 + const { srfaiappendcurdata, srfaiautoappend, srfmode } = + computedInLineAIParams(props); + + const { theme, actions, actionStyle, containerStyle, contentStyle } = + useBase(props, containerRef, actionsRef); + + // 处理点击事件 + useInLineAIContainerClick(props, textareaContent, isLoading); + + const { askAI } = useAI(props, { + srfaiappendcurdata, + srfmode, + }); + + /** + * @description 还原焦点 + */ + const restoreFocus = (): void => { + textareaRef.value?.blur(); + props.restoreSelection(); + }; + /** * @description 发送问题 * @returns {*} {Promise} */ const sendQuestion = async (content: string): Promise => { + restoreFocus(); // 保存问题 question = content; textareaContent.value = ''; // 清空内容 @@ -171,15 +199,22 @@ export const InlineAITextArea = defineComponent({ }; onMounted(() => { - if (srfaiautoappend) sendQuestion(textareaContent.value); + if (srfaiautoappend) { + sendQuestion(textareaContent.value); + } else { + textareaRef.value?.focus(); + } }); return { ns, + theme, + isShow, actions, + disabled, isLoading, actionsRef, - disabled, + textareaRef, actionStyle, contentType, containerRef, @@ -194,7 +229,15 @@ export const InlineAITextArea = defineComponent({ }, render() { return ( -
+
{!this.disabled && (
@@ -203,58 +246,48 @@ export const InlineAITextArea = defineComponent({ )}
{this.isLoading && ( -
-
- - - -
+
+ + + + + +
)}