From 59ad79fa58b5d33ba31fc51bde68c0d1e351da2f Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sun, 18 Jan 2026 13:20:13 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-assistant):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E9=94=AE=E5=88=87=E6=8D=A2=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按 ↑ 键切换到上一条历史输入(光标在第一行时生效) - 按 ↓ 键切换到下一条历史输入(光标在最后一行时生效) - 历史记录使用 IndexedDB 持久化存储,最多保存 50 条 - 重复输入会移动到末尾而非重复添加 - 弹窗关闭时自动重置导航状态 --- .../js/components/AIAssistant/index.vue | 158 +++++++++++++++++- 1 file changed, 155 insertions(+), 3 deletions(-) diff --git a/resources/assets/js/components/AIAssistant/index.vue b/resources/assets/js/components/AIAssistant/index.vue index d4d2b0fd5..6d3ae3ac7 100644 --- a/resources/assets/js/components/AIAssistant/index.vue +++ b/resources/assets/js/components/AIAssistant/index.vue @@ -254,6 +254,13 @@ export default { // 编辑历史问题 editingIndex: -1, // 正在编辑的响应索引,-1 表示不在编辑模式 editingValue: '', // 编辑中的文本内容 + + // 输入历史(上下键切换) + inputHistoryList: [], // 历史输入列表 + inputHistoryIndex: 0, // 当前历史索引 + inputHistoryCurrent: '', // 切换前的当前输入 + inputHistoryCacheKey: 'aiAssistant.inputHistory', + inputHistoryLimit: 50, } }, created() { @@ -265,6 +272,7 @@ export default { mounted() { emitter.on('openAIAssistant', this.onOpenAIAssistant); this.loadCachedModel(); + this.loadInputHistory(); this.mountFloatButton(); }, beforeDestroy() { @@ -560,16 +568,32 @@ export default { }, /** - * 输入框键盘事件:回车发送,Shift+回车换行 + * 输入框键盘事件:回车发送,Shift+回车换行,上下键切换历史 * 注意:输入法组合输入时(如中文候选字)不发送 */ onInputKeydown(e) { - if (!e.shiftKey && !this.isComposing) { + if (this.isComposing) { + return; + } + if (!e.shiftKey) { if (e.key === 'Enter') { e.preventDefault(); this.onSubmit(); - } else if (e.key === 'Escape' && this.displayMode === 'chat') { + return; + } + if (e.key === 'Escape' && this.displayMode === 'chat') { this.showModal = false; + return; + } + } + // 上下键切换历史输入 + if (e.key === 'ArrowUp') { + if (!this.navigateInputHistory('up')) { + e.preventDefault(); + } + } else if (e.key === 'ArrowDown') { + if (!this.navigateInputHistory('down')) { + e.preventDefault(); } } }, @@ -622,6 +646,7 @@ export default { context, }); + this.persistInputHistory(prompt); this.startStream(streamKey, responseEntry); return true; } catch (error) { @@ -1047,6 +1072,7 @@ export default { this.pendingAutoSubmit = false; this.clearAutoSubmitTimer(); this.clearActiveSSEClients(); + this.resetInputHistoryNavigation(); this.showModal = false; this.responses = []; setTimeout(() => { @@ -1338,6 +1364,132 @@ export default { return time.format('YYYY-MM-DD HH:mm'); }, + // ==================== 输入历史(上下键切换)==================== + + /** + * 加载输入历史 + */ + async loadInputHistory() { + try { + const history = await $A.IDBValue(this.inputHistoryCacheKey); + if (Array.isArray(history)) { + this.inputHistoryList = history; + } else { + this.inputHistoryList = []; + } + } catch (e) { + this.inputHistoryList = []; + } + this.inputHistoryIndex = this.inputHistoryList.length; + this.inputHistoryCurrent = ''; + }, + + /** + * 保存输入到历史 + */ + persistInputHistory(content) { + const trimmed = (content || '').trim(); + if (!trimmed) { + return; + } + const history = Array.isArray(this.inputHistoryList) ? [...this.inputHistoryList] : []; + // 如果和最后一条相同,不重复添加 + if (history[history.length - 1] === trimmed) { + this.inputHistoryIndex = history.length; + this.inputHistoryCurrent = ''; + return; + } + // 如果已存在,移动到末尾 + const existIndex = history.indexOf(trimmed); + if (existIndex !== -1) { + history.splice(existIndex, 1); + } + history.push(trimmed); + // 限制最大条数 + if (history.length > this.inputHistoryLimit) { + history.splice(0, history.length - this.inputHistoryLimit); + } + this.inputHistoryList = history; + this.inputHistoryIndex = history.length; + this.inputHistoryCurrent = ''; + $A.IDBSet(this.inputHistoryCacheKey, history).catch(() => {}); + }, + + /** + * 重置历史导航状态 + */ + resetInputHistoryNavigation() { + this.inputHistoryIndex = this.inputHistoryList.length; + this.inputHistoryCurrent = ''; + }, + + /** + * 导航输入历史 + * @param {string} direction - 'up' 或 'down' + * @returns {boolean} - 是否允许默认行为 + */ + navigateInputHistory(direction) { + if (!this.inputHistoryList.length) { + return true; + } + const textarea = this.$refs.inputRef?.$el?.querySelector('textarea'); + if (!textarea) { + return true; + } + const cursorPos = textarea.selectionStart; + const cursorEnd = textarea.selectionEnd; + const value = this.inputValue || ''; + // 如果有选中文本,允许默认行为 + if (cursorPos !== cursorEnd) { + return true; + } + if (direction === 'up') { + // 只有光标在第一行时才切换历史 + const beforeCursor = value.substring(0, cursorPos); + if (beforeCursor.includes('\n')) { + return true; + } + // 保存当前输入 + if (this.inputHistoryIndex === this.inputHistoryList.length) { + this.inputHistoryCurrent = value; + } + if (this.inputHistoryIndex > 0) { + this.inputHistoryIndex--; + this.inputValue = this.inputHistoryList[this.inputHistoryIndex] || ''; + this.$nextTick(() => { + const ta = this.$refs.inputRef?.$el?.querySelector('textarea'); + ta?.setSelectionRange(0, 0); + }); + return false; + } + } else if (direction === 'down') { + // 只有光标在最后一行时才切换历史 + const afterCursor = value.substring(cursorPos); + if (afterCursor.includes('\n')) { + return true; + } + if (this.inputHistoryIndex >= this.inputHistoryList.length) { + return true; + } + if (this.inputHistoryIndex < this.inputHistoryList.length - 1) { + this.inputHistoryIndex++; + this.inputValue = this.inputHistoryList[this.inputHistoryIndex] || ''; + } else { + this.inputHistoryIndex = this.inputHistoryList.length; + this.inputValue = this.inputHistoryCurrent || ''; + } + this.$nextTick(() => { + const ta = this.$refs.inputRef?.$el?.querySelector('textarea'); + if (ta) { + const len = (this.inputValue || '').length; + ta.setSelectionRange(len, len); + } + }); + return false; + } + return true; + }, + // ==================== 编辑历史问题 ==================== /**