fix(staffchat): 搜索聚合取最新命中(ASC页1陷阱)+tab切换不清搜索结果+is_send守卫兜底名+截断提示+TAB_KEYS单源

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 17:42:43 +08:00
parent 0f7dee9693
commit 026381f6e6
3 changed files with 47 additions and 12 deletions
@@ -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">