fix(staffchat): 搜索聚合取最新命中(ASC页1陷阱)+tab切换不清搜索结果+is_send守卫兜底名+截断提示+TAB_KEYS单源
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,11 +29,21 @@ const errorMsg = ref<string | null>(null)
|
||||
const pinnedOpen = ref(true)
|
||||
/** search 模式聚合结果(q 命中的消息按会话去重,取首条作为该会话快照)。 */
|
||||
const searchResults = ref<ChatSession[]>([])
|
||||
/** 搜索命中总数与截断标记(后端单页上限 100,超出时只聚合已取回条内的会话)。 */
|
||||
const searchTotal = ref(0)
|
||||
const searchTruncated = ref(false)
|
||||
|
||||
/** 清搜索态: 结果与截断提示一起清,不残留上次的统计。 */
|
||||
function clearSearch() {
|
||||
searchResults.value = []
|
||||
searchTotal.value = 0
|
||||
searchTruncated.value = false
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
errorMsg.value = null
|
||||
searchResults.value = []
|
||||
clearSearch()
|
||||
try {
|
||||
const res = await getSessions({
|
||||
owner_account_id: props.ownerId,
|
||||
@@ -48,16 +58,21 @@ async function load() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
watch(() => [props.ownerId, props.kind, friendTab.value] as const, load, { immediate: true })
|
||||
watch(() => [props.ownerId, props.kind] as const, load, { immediate: true })
|
||||
/** 好友/已删 tab 只过滤 list 会话列表(is_del);search 模式的消息搜索与 is_del
|
||||
无关——切 tab 不重载、不清关键词/结果(搜索跨 tab 保持有效)。 */
|
||||
watch(friendTab, () => {
|
||||
if (mode.value === 'list') load()
|
||||
})
|
||||
|
||||
/** 切模式清关键词与旧结果,避免 list 过滤词被误当搜索词。 */
|
||||
watch(mode, () => {
|
||||
keyword.value = ''
|
||||
searchResults.value = []
|
||||
clearSearch()
|
||||
})
|
||||
/** search 模式清空关键词 → 回到未搜索态(不残留上次聚合结果)。 */
|
||||
watch(keyword, (k) => {
|
||||
if (mode.value === 'search' && !k.trim()) searchResults.value = []
|
||||
if (mode.value === 'search' && !k.trim()) clearSearch()
|
||||
})
|
||||
|
||||
/** 置顶区: 仅 单聊+未删好友+list 模式(后端 pinned 目前只有 filehelper)。 */
|
||||
@@ -106,7 +121,7 @@ function sessionKey(s: ChatSession): string {
|
||||
async function runSearch() {
|
||||
const k = keyword.value.trim()
|
||||
if (mode.value !== 'search' || !k) {
|
||||
searchResults.value = []
|
||||
clearSearch()
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
@@ -119,10 +134,14 @@ async function runSearch() {
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
searchTotal.value = res.total
|
||||
searchTruncated.value = res.total > res.items.length
|
||||
// 已加载的会话列表提供昵称/头像;未命中(search 只回消息)再以 sender_name/键兜底
|
||||
const known = new Map(sessions.value.map(s => [sessionKey(s), s]))
|
||||
const seen = new Map<string, ChatSession>()
|
||||
for (const m of res.items) {
|
||||
// 陷阱: 后端 /messages 按 timestamp ASC 排序,page 1 取回的是最旧的 100 条命中。
|
||||
// 倒序遍历(新→旧)+ 首见即取,保证每个会话的快照时间 = 已取回命中里最新的那条。
|
||||
for (const m of [...res.items].reverse()) {
|
||||
const key = m.room_id ?? m.talker_id ?? ''
|
||||
if (!key || seen.has(key)) continue
|
||||
const hit = known.get(key)
|
||||
@@ -131,7 +150,8 @@ async function runSearch() {
|
||||
type: m.room_id ? 'group' : 'friend',
|
||||
talker_id: m.room_id ? undefined : m.talker_id ?? undefined,
|
||||
room_id: m.room_id ?? undefined,
|
||||
name: hit?.name ?? m.sender_name ?? key,
|
||||
// is_send=true 时 sender_name 是员工自己,不能当卡片标题
|
||||
name: hit?.name ?? (!m.is_send ? m.sender_name : key),
|
||||
avatar: hit?.avatar ?? undefined,
|
||||
today_count: 0,
|
||||
last_chat_time: m.timestamp ?? null,
|
||||
@@ -142,7 +162,7 @@ async function runSearch() {
|
||||
)
|
||||
} catch (e) {
|
||||
errorMsg.value = (e as Error).message
|
||||
searchResults.value = []
|
||||
clearSearch()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -212,6 +232,11 @@ async function runSearch() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ── 搜索截断提示: 命中超单页上限,聚合只覆盖已取回的最旧 100 条 ── -->
|
||||
<div v-if="mode === 'search' && searchTruncated" class="sc-sessions__hint">
|
||||
共 {{ searchTotal }} 条命中,仅聚合最旧 100 条内的会话
|
||||
</div>
|
||||
|
||||
<!-- ── 主列表 ── -->
|
||||
<div v-if="!loading && visible.length === 0" class="yk-empty">{{ emptyText }}</div>
|
||||
<div
|
||||
@@ -330,7 +355,7 @@ async function runSearch() {
|
||||
font-size: 12px;
|
||||
transition: transform 0.25s cubic-bezier(0.22, 0.8, 0.36, 1);
|
||||
}
|
||||
/* 展开时箭头朝下(未转),折叠时转 180° 朝上 */
|
||||
/* 展开时(is-open)转 180° 箭头朝上,折叠时未转朝下 */
|
||||
.sc-sessions__fold-icon.is-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -338,6 +363,14 @@ async function runSearch() {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 搜索截断提示(弱化小字,同 __time 的灰) */
|
||||
.sc-sessions__hint {
|
||||
padding: 4px 8px 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #8a94a4;
|
||||
}
|
||||
|
||||
/* 会话卡(置顶/主列表共用) */
|
||||
.sc-sessions__item {
|
||||
display: flex;
|
||||
|
||||
@@ -7,6 +7,8 @@ const NAVS = [
|
||||
|
||||
/** 员工聊天窗口导航键(与路由 :tab 段同名)。 */
|
||||
export type TabKey = (typeof NAVS)[number]['key']
|
||||
/** TabKey 的运行时键列表(StaffChat.vue 路由 tab 收窄复用,NAVS 单一事实源)。 */
|
||||
export const TAB_KEYS: readonly TabKey[] = NAVS.map(n => n.key)
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getAccounts } from '@/api'
|
||||
import type { Account } from '@/api/types'
|
||||
import StaffChatSide, { type TabKey } from '@/components/staffchat/StaffChatSide.vue'
|
||||
import StaffChatSide, { TAB_KEYS, type TabKey } from '@/components/staffchat/StaffChatSide.vue'
|
||||
import SessionPane from '@/components/staffchat/SessionPane.vue'
|
||||
import ChatMain from '@/components/staffchat/ChatMain.vue'
|
||||
import MomentsPane from '@/components/staffchat/MomentsPane.vue'
|
||||
@@ -15,8 +15,8 @@ import StatsPane from '@/components/staffchat/StatsPane.vue'
|
||||
const props = defineProps<{ ownerId: string; tab?: string; talkerId?: string }>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
// TabKey 从 StaffChatSide 导出(单一事实源);VALID 仅做运行时兜底
|
||||
const VALID: readonly TabKey[] = ['single', 'group', 'moments', 'contacts', 'stats']
|
||||
// TabKey/TAB_KEYS 从 StaffChatSide 导出(NAVS 单一事实源);VALID 仅做运行时兜底
|
||||
const VALID: readonly TabKey[] = TAB_KEYS
|
||||
function isTabKey(v: string | undefined): v is TabKey {
|
||||
return !!v && (VALID as readonly string[]).includes(v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user