diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue index 6e15411..4e8752b 100644 --- a/pages_tool/ai-chat/ai-chat-message.vue +++ b/pages_tool/ai-chat/ai-chat-message.vue @@ -32,7 +32,7 @@ :key="getMessageKey(message, index)" :id="getMessageKey(message, index)" class="message-item" - :class="[message.role, { 'first-message': index === 0, 'search-active': showSearchBar && searchResultFilteredIndex === index }]"> + :class="[message.role, { 'first-message': index === 0, 'search-current': showSearchBar && searchResultFilteredIndex === index, 'search-flash': showSearchBar && searchFlashAnimatingIndex === index }]"> @@ -441,7 +441,9 @@ export default { // 聊天记录搜索状态(由 AiChatSearch 组件事件同步) showSearchBar: false, searchKeyword: '', - searchResultFilteredIndex: -1 + searchResultFilteredIndex: -1, + searchFlashAnimatingIndex: -1, // 当前正在播放 flash 动画的消息索引 + searchFlashTimer: null // flash 动画定时器 } }, computed: { @@ -477,6 +479,10 @@ export default { // 新增:组件销毁时清除所有定时器 beforeDestroy() { if (this.scrollTimer) clearTimeout(this.scrollTimer); + if (this.searchFlashTimer) { + clearTimeout(this.searchFlashTimer); + this.searchFlashTimer = null; + } // 清理所有加载状态定时器 this.messages.forEach(msg => { if (msg._rotationTimer) { @@ -522,8 +528,25 @@ export default { methods: { // ==================== 搜索组件事件处理 ==================== onSearchScrollTo(messageKey) { - this.scrollTop = undefined - this.scrollIntoViewId = messageKey + this.scrollIntoViewId = '' + // 清除之前的 flash 定时器 + if (this.searchFlashTimer) { + clearTimeout(this.searchFlashTimer) + this.searchFlashTimer = null + } + this.searchFlashAnimatingIndex = -1 + this.$nextTick(() => { + this.scrollIntoViewId = messageKey + // 触发 flash 脉冲动画 - 使用 setTimeout 让动画持续足够时间 + if (this.searchResultFilteredIndex >= 0) { + this.searchFlashAnimatingIndex = this.searchResultFilteredIndex + // 动画持续 1.2s 后自动移除 flash 状态 + this.searchFlashTimer = setTimeout(() => { + this.searchFlashAnimatingIndex = -1 + this.searchFlashTimer = null + }, 1200) + } + }) }, onSearchActiveChange(isActive) { @@ -531,7 +554,10 @@ export default { }, onSearchResultChange(filteredIndex) { - this.searchResultFilteredIndex = filteredIndex + this.searchResultFilteredIndex = -1 + this.$nextTick(() => { + this.searchResultFilteredIndex = filteredIndex + }) }, onSearchKeywordChange(keyword) { @@ -663,7 +689,9 @@ export default { // 生成消息的唯一 key(兼容非 H5 平台) getMessageKey(message, index) { - const id = message.id || message.timestamp || index; + const rawId = message.id || message.timestamp || index; + // 标准化 ID:转为字符串并替换非法字符(. 和其他非 HTML-safe 字符) + const id = String(rawId).replace(/[^a-zA-Z0-9_-]/g, '-'); return 'msg-' + id + '-' + index; }, @@ -2151,6 +2179,26 @@ _handleStreamError(msg) { .message-item { margin-bottom: 20rpx; + scroll-margin-top: 300rpx; + scroll-margin-bottom: 200rpx; + + // 搜索定位 - 当前结果:持续绿色发光效果 + &.search-current { + position: relative; + z-index: 1; + border-radius: 18rpx; + + .user-message .message-bubble, + .ai-message .message-bubble { + box-shadow: 0 0 0 4rpx rgba(7, 193, 96, 0.7), 0 4rpx 24rpx rgba(7, 193, 96, 0.35) !important; + transition: box-shadow 0.3s ease; + } + } + + // 搜索定位 - Flash 脉冲动画(导航到新结果时触发) + &.search-flash { + animation: searchPulse 1.2s ease-out; + } &.first-message { margin-top: 16rpx; @@ -2978,6 +3026,31 @@ _handleStreamError(msg) { } } +/* 搜索定位脉冲动画 */ +@keyframes searchPulse { + 0% { + transform: translateY(0); + } + 15% { + transform: translateY(-12rpx); + } + 30% { + transform: translateY(4rpx); + } + 45% { + transform: translateY(-4rpx); + } + 60% { + transform: translateY(0); + } + 0%, 60%, 100% { + filter: brightness(1); + } + 15%, 45% { + filter: brightness(1.15); + } +} + @keyframes shine { 0% { left: -100%; @@ -3343,16 +3416,6 @@ _handleStreamError(msg) { -webkit-user-select: text; } - // 当前搜索匹配的消息高亮(使用 outline + !important 覆盖用户/AI气泡的独立样式) - .message-item.search-active { - .message-bubble { - outline: 3rpx solid #07c160 !important; - outline-offset: 2rpx; - box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.25) !important; - transition: outline 0.25s ease, box-shadow 0.25s ease; - } - } - // 用户消息 rich-text 高亮(搜索时替代 ) .message-rich-text { font-size: 28rpx; diff --git a/pages_tool/ai-chat/components/ai-chat-search.vue b/pages_tool/ai-chat/components/ai-chat-search.vue index 85c72d7..096be10 100644 --- a/pages_tool/ai-chat/components/ai-chat-search.vue +++ b/pages_tool/ai-chat/components/ai-chat-search.vue @@ -9,7 +9,7 @@ - + {{ $lang('searchClose') }} + + ! + {{ $lang('noMatchResult') }} + @@ -68,6 +72,7 @@ export default { searchKeyword: '', searchResults: [], // 匹配结果在过滤后数组中的索引列表 searchResultIndex: -1, // 当前在 searchResults 中的位置 + searched: false, // 是否已执行过搜索(用于显示无结果提示) } }, @@ -92,7 +97,9 @@ export default { methods: { // 生成消息唯一 key(与父组件 getMessageKey 保持一致) getMessageKey(message, index) { - return `msg-${message.id || message.tempId || index}-${message.role}` + const rawId = message.id || message.timestamp || index; + const id = String(rawId).replace(/[^a-zA-Z0-9_-]/g, '-'); + return 'msg-' + id + '-' + index; }, toggleSearch() { @@ -113,6 +120,7 @@ export default { this.searchKeyword = '' this.searchResults = [] this.searchResultIndex = -1 + this.searched = false this.$emit('result-change', -1) this.$emit('keyword-change', '') }, @@ -132,6 +140,7 @@ export default { if (!keyword) { this.searchResults = [] this.searchResultIndex = -1 + this.searched = false this.$emit('result-change', -1) this.$emit('keyword-change', '') return @@ -147,6 +156,7 @@ export default { this.searchResults = results this.searchResultIndex = results.length > 0 ? 0 : -1 + this.searched = true const filteredIdx = results.length > 0 ? results[0] : -1 this.$emit('result-change', filteredIdx) @@ -156,8 +166,6 @@ export default { this.$nextTick(() => { this._emitScrollTo(filteredIdx) }) - } else { - uni.showToast({ title: this.$lang('noMatchResult'), icon: 'none' }) } }, @@ -413,4 +421,46 @@ export default { } } } + +// 搜索状态提示 +.search-bar-status { + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 14rpx 16rpx; + background: #fffae6; + border-top: 1rpx solid #f0e6c0; + + .search-bar-status-icon { + width: 32rpx; + height: 32rpx; + border-radius: 50%; + background: #f5a623; + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + line-height: 1; + } + + .search-bar-status-text { + font-size: 24rpx; + color: #8a7a4a; + line-height: 1.4; + } +} + +// 搜索输入框 - 有结果时绿色边框 +.search-bar-input-wrap.has-results { + border-color: #07c160; +} + +// 搜索输入框 - 无结果时橙色边框 +.search-bar-input-wrap.no-results { + border-color: #f5a623; +}