|
|
@@ -1,17 +1,77 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
<header v-if="$route.path !== '/chat/dialogue'" class="app-header">
|
|
|
- <span class="title">{{ currentTitle }}</span>
|
|
|
- <div class="header-icons">
|
|
|
- <i class="el-icon-search" />
|
|
|
- <i class="el-icon-plus" style="margin-left: 15px;" />
|
|
|
- </div>
|
|
|
+ <template v-if="!isSearching">
|
|
|
+ <span class="title">{{ currentTitle }}</span>
|
|
|
+ <div class="header-icons">
|
|
|
+ <i class="el-icon-search" @click="openSearch" />
|
|
|
+ <i class="el-icon-plus" style="margin-left: 15px;" @click="openPlus" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <div class="search-bar-wrapper">
|
|
|
+ <el-input
|
|
|
+ ref="searchInput"
|
|
|
+ v-model="searchQuery"
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
+ placeholder="搜索"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ class="wechat-search-input"
|
|
|
+ @input="handleSearchInput"
|
|
|
+ />
|
|
|
+ <span class="cancel-btn" @click="closeSearch">取消</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</header>
|
|
|
|
|
|
<main class="app-content">
|
|
|
<keep-alive>
|
|
|
<router-view />
|
|
|
</keep-alive>
|
|
|
+
|
|
|
+ <transition name="fade">
|
|
|
+ <div v-if="isSearching && searchQuery.trim()" class="search-result-panel">
|
|
|
+ <div v-if="filteredContacts.length > 0" class="result-group">
|
|
|
+ <div class="group-title">联系人</div>
|
|
|
+ <div
|
|
|
+ v-for="item in filteredContacts"
|
|
|
+ :key="'contact-' + item.id"
|
|
|
+ class="result-item"
|
|
|
+ @click="goToDialogue(item)"
|
|
|
+ >
|
|
|
+ <img :src="item.avatarUrl" class="result-avatar">
|
|
|
+ <div class="result-info">
|
|
|
+ <div class="result-name" v-html="highlightText(item.screenName, searchQuery)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="filteredMessages.length > 0" class="result-group">
|
|
|
+ <div class="group-title">聊天记录</div>
|
|
|
+ <div
|
|
|
+ v-for="item in filteredMessages"
|
|
|
+ :key="'msg-' + item.messageId"
|
|
|
+ class="result-item"
|
|
|
+ @click="goToDialogue(item)"
|
|
|
+ >
|
|
|
+ <img :src="item.sender.avatarUrl" class="result-avatar">
|
|
|
+ <div class="result-info">
|
|
|
+ <div class="result-meta">
|
|
|
+ <span class="result-name">{{ item.sender.screenName }}</span>
|
|
|
+ <span class="result-time">{{ item.createdAt }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="result-text" v-html="highlightText(item.content, searchQuery)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="filteredContacts.length === 0 && filteredMessages.length === 0" class="no-result">
|
|
|
+ 无法找到“{{ searchQuery }}”相关内容
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </transition>
|
|
|
</main>
|
|
|
|
|
|
<footer v-if="$route.path !== '/chat/dialogue'" class="app-footer">
|
|
|
@@ -45,13 +105,19 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getChatUnreadCount } from '@/api/chat'
|
|
|
+import { getChatUnreadCount, searchChatRecords } from '@/api/chat'
|
|
|
|
|
|
export default {
|
|
|
name: 'Chat',
|
|
|
data() {
|
|
|
return {
|
|
|
- unreadCount: 0
|
|
|
+ unreadCount: 0,
|
|
|
+ isSearching: false, // 是否处于搜索激活状态
|
|
|
+ searchQuery: '', // 搜索关键词
|
|
|
+ searchTimer: null, // 防抖定时器,
|
|
|
+ // ==== 模拟全局本地搜索数据库 ====
|
|
|
+ contactResult: [],
|
|
|
+ messageResult: []
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -60,6 +126,14 @@ export default {
|
|
|
if (this.$route.path.includes('contact')) return '通讯录'
|
|
|
if (this.$route.path.includes('me')) return '我'
|
|
|
return '微信'
|
|
|
+ },
|
|
|
+ // 动态计算筛选后的联系人
|
|
|
+ filteredContacts() {
|
|
|
+ return this.contactResult
|
|
|
+ },
|
|
|
+ // 动态计算筛选后的聊天记录
|
|
|
+ filteredMessages() {
|
|
|
+ return this.messageResult
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
@@ -74,6 +148,86 @@ export default {
|
|
|
if (this.$route.path !== path) {
|
|
|
this.$router.push(path)
|
|
|
}
|
|
|
+ },
|
|
|
+ openPlus() {
|
|
|
+ this.$message.info('功能开发中')
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 开启搜索:进入搜索模式并让输入框自动聚焦
|
|
|
+ */
|
|
|
+ openSearch() {
|
|
|
+ this.isSearching = true
|
|
|
+ // 使用 $nextTick 确保 DOM 渲染完成后再聚焦输入框
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.searchInput) {
|
|
|
+ this.$refs.searchInput.focus()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭搜索:清空数据并退回常规标题栏
|
|
|
+ */
|
|
|
+ closeSearch() {
|
|
|
+ this.isSearching = false
|
|
|
+ this.searchQuery = ''
|
|
|
+ this.triggerSearch('') // 清空搜索条件,让子组件列表恢复完整数据
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 监听输入框变化:加入标准防抖(Debounce),避免每打一个字就疯狂请求后端
|
|
|
+ */
|
|
|
+ handleSearchInput(val) {
|
|
|
+ if (this.searchTimer) {
|
|
|
+ clearTimeout(this.searchTimer)
|
|
|
+ }
|
|
|
+ this.searchTimer = setTimeout(() => {
|
|
|
+ this.triggerSearch(val.trim())
|
|
|
+ }, 300) // 延迟 300ms 触发真实业务搜索
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行实际搜索业务
|
|
|
+ */
|
|
|
+ triggerSearch(keyword) {
|
|
|
+ const queryParams = {}
|
|
|
+ queryParams.keyword = keyword
|
|
|
+ searchChatRecords(queryParams).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ // 将后端返回的数组赋值给响应式变量,前端 HTML 模板会自动刷新渲染
|
|
|
+ this.contactResult = resp.data.contactResult || []
|
|
|
+ this.messageResult = resp.data.messageResult || []
|
|
|
+ } else {
|
|
|
+ this.$message.error(resp.msg || '搜索失败')
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ console.error('搜索接口异常:', err)
|
|
|
+ })
|
|
|
+ // 💡 联动核心:由于搜索框在父组件 Chat.vue,而列表在子路由组件(如 ChatList.vue 或 ChatContact.vue)中。
|
|
|
+ // 最直接、干净的跨组件传递方式是通过事件总线 (EventBus) 或直接利用 Vuex。
|
|
|
+ // 如果没有配置 Vuex,可以直接使用 Vue 原生的自定义事件派发:
|
|
|
+ // this.$root.$emit('global-search', keyword)
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 关键词高亮函数
|
|
|
+ */
|
|
|
+ highlightText(text, keyword) {
|
|
|
+ if (!keyword) return text
|
|
|
+ const reg = new RegExp(`(${keyword})`, 'gi')
|
|
|
+ return text.replace(reg, '<span class="highlight">$1</span>')
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 点击搜索结果跳转到对应聊天窗口
|
|
|
+ */
|
|
|
+ goToDialogue(item) {
|
|
|
+ this.closeSearch()
|
|
|
+ this.$router.push({
|
|
|
+ path: '/chat/dialogue',
|
|
|
+ query: {
|
|
|
+ chatId: item.chatId,
|
|
|
+ lastMessageId: item.messageId
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -198,4 +352,142 @@ export default {
|
|
|
height: 16px;
|
|
|
line-height: 16px;
|
|
|
}
|
|
|
+
|
|
|
+/* ==================== 搜索模式栏专属样式 ==================== */
|
|
|
+.search-bar-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 覆写 Element UI 的输入框样式,使其更贴合微信的扁平化风格 */
|
|
|
+.wechat-search-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.wechat-search-input ::v-deep .el-input__inner {
|
|
|
+ background-color: #ffffff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #191919;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 微信风格的取消按钮 */
|
|
|
+.cancel-btn {
|
|
|
+ font-size: 15px;
|
|
|
+ color: #576b95; /* 微信专用深蓝色链接字,也可改成 #07c160 微信绿 */
|
|
|
+ cursor: pointer;
|
|
|
+ white-space: nowrap;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+.cancel-btn:active {
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+
|
|
|
+/* ==================== 搜索结果面板独立框架 ==================== */
|
|
|
+.search-result-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-color: #f7f7f7; /* 微信底色 */
|
|
|
+ z-index: 99; /* 确保盖在 router-view 的列表上面 */
|
|
|
+ overflow-y: auto;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组标题(如:联系人、聊天记录) */
|
|
|
+.result-group {
|
|
|
+ margin-top: 8px;
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+.result-group .group-title {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #b2b2b2;
|
|
|
+ padding: 16px 16px 4px 16px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 单个结果行 */
|
|
|
+.result-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #f5f5f5;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.1s;
|
|
|
+}
|
|
|
+.result-item:active {
|
|
|
+ background-color: #e5e5e5;
|
|
|
+}
|
|
|
+
|
|
|
+/* 头像 */
|
|
|
+.result-avatar {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-right: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右侧复合文本区 */
|
|
|
+.result-info {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.result-meta {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.result-name {
|
|
|
+ font-size: 15px;
|
|
|
+ color: #191919;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.result-time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #b2b2b2;
|
|
|
+}
|
|
|
+
|
|
|
+/* 聊天历史摘要描述 */
|
|
|
+.result-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #666;
|
|
|
+ margin-top: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+/* ==== 核心:微信高亮绿色字 ==== */
|
|
|
+::v-deep .highlight {
|
|
|
+ color: #07c160;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+/* 搜索无结果空状态 */
|
|
|
+.no-result {
|
|
|
+ text-align: center;
|
|
|
+ color: #b2b2b2;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-top: 60px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 浮层展开淡入淡出动画 */
|
|
|
+.fade-enter-active, .fade-leave-active {
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
+}
|
|
|
+.fade-enter, .fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
</style>
|