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