diff --git a/src/components/common/ChatRecord/ChatRecordDrawer.vue b/src/components/common/ChatRecord/ChatRecordDrawer.vue index f119b45c..2a577a25 100644 --- a/src/components/common/ChatRecord/ChatRecordDrawer.vue +++ b/src/components/common/ChatRecord/ChatRecordDrawer.vue @@ -3,7 +3,7 @@ * 聊天记录查看器 Drawer * 主组件,组合筛选面板、消息列表、会话时间线等子组件 */ -import { ref, watch, toRaw, nextTick, onMounted } from 'vue' +import { computed, ref, watch, toRaw, nextTick, onMounted } from 'vue' import { useI18n } from 'vue-i18n' import FilterPanel from './FilterPanel.vue' import MessageList from './MessageList.vue' @@ -13,6 +13,7 @@ import { useLayoutStore } from '@/stores/layout' import { useSessionStore } from '@/stores/session' import { useSessionIndexService } from '@/services' import { storeToRefs } from 'pinia' +import { preserveChatRecordSessionId, resolveChatRecordSessionId } from './query-session' const { t } = useI18n() const layoutStore = useLayoutStore() @@ -31,6 +32,7 @@ const messageListRef = ref | null>(null) // 本地查询条件(可编辑的副本) const localQuery = ref({}) +const effectiveSessionId = computed(() => resolveChatRecordSessionId(localQuery.value, currentSessionId.value)) // 消息数量 const messageCount = ref(0) @@ -49,12 +51,12 @@ const matchedSessionIds = ref | undefined>(undefined) // 应用筛选 function handleApplyFilter(query: ChatRecordQuery) { - localQuery.value = query + localQuery.value = preserveChatRecordSessionId(query, localQuery.value) } // 重置筛选 function handleResetFilter() { - localQuery.value = {} + localQuery.value = preserveChatRecordSessionId({}, localQuery.value) matchedSessionIds.value = undefined } @@ -130,17 +132,16 @@ function handleSessionSelect(_sessionId: number, firstMessageId: number) { // 处理跳转到消息(查看上下文) function handleJumpToMessage(messageId: number) { // 清空筛选条件,只保留 scrollToMessageId - localQuery.value = { - scrollToMessageId: messageId, - } + localQuery.value = preserveChatRecordSessionId({ scrollToMessageId: messageId }, localQuery.value) } // 加载会话列表缓存 async function loadSessionsCache() { - if (!currentSessionId.value) return + const sessionId = effectiveSessionId.value + if (!sessionId) return try { - const sessions = await useSessionIndexService().getSessions(currentSessionId.value) + const sessions = await useSessionIndexService().getSessions(sessionId) sessionsCache.value = sessions.map((s) => ({ id: s.id, startTs: s.startTs, @@ -206,9 +207,9 @@ watch(
() const sessionStore = useSessionStore() +const effectiveSessionId = computed(() => resolveChatRecordSessionId(props.query, sessionStore.currentSessionId)) // 判断是否使用外部传入的消息 const isExternalMode = computed(() => !!props.externalMessages?.length) @@ -150,7 +152,7 @@ async function loadInitialMessages() { return } - const sessionId = sessionStore.currentSessionId + const sessionId = effectiveSessionId.value if (!sessionId) { messages.value = [] emit('count-change', 0) @@ -266,7 +268,7 @@ function scrollToBottom() { async function loadMoreBefore() { if (isLoadingMore.value || !hasMoreBefore.value || messages.value.length === 0) return - const sessionId = sessionStore.currentSessionId + const sessionId = effectiveSessionId.value if (!sessionId) return const firstMessage = messages.value[0] @@ -320,7 +322,7 @@ async function loadMoreBefore() { async function loadMoreAfter() { if (isLoadingMore.value || !hasMoreAfter.value || messages.value.length === 0) return - const sessionId = sessionStore.currentSessionId + const sessionId = effectiveSessionId.value if (!sessionId) return isLoadingMore.value = true diff --git a/src/components/common/ChatRecord/query-session.test.ts b/src/components/common/ChatRecord/query-session.test.ts new file mode 100644 index 00000000..b0e95641 --- /dev/null +++ b/src/components/common/ChatRecord/query-session.test.ts @@ -0,0 +1,30 @@ +/** + * Run: pnpm test -- src/components/common/ChatRecord/query-session.test.ts + */ + +import assert from 'node:assert/strict' +import test from 'node:test' +import { preserveChatRecordSessionId, resolveChatRecordSessionId } from './query-session' + +test('uses the chat record query session id before the current route session id', () => { + assert.equal(resolveChatRecordSessionId({ sessionId: 'source-session' }, 'current-session'), 'source-session') +}) + +test('falls back to the current route session id when query has no session id', () => { + assert.equal(resolveChatRecordSessionId({}, 'current-session'), 'current-session') +}) + +test('returns null when neither query nor route has a session id', () => { + assert.equal(resolveChatRecordSessionId({}, null), null) +}) + +test('preserves the current chat record session id when applying drawer filters', () => { + assert.deepEqual( + preserveChatRecordSessionId({ keywords: ['refund'] }, { sessionId: 'source-session', memberName: 'Alice' }), + { keywords: ['refund'], sessionId: 'source-session' } + ) +}) + +test('does not add an empty session id when the current query is not session scoped', () => { + assert.deepEqual(preserveChatRecordSessionId({ keywords: ['refund'] }, {}), { keywords: ['refund'] }) +}) diff --git a/src/components/common/ChatRecord/query-session.ts b/src/components/common/ChatRecord/query-session.ts new file mode 100644 index 00000000..7a3b1956 --- /dev/null +++ b/src/components/common/ChatRecord/query-session.ts @@ -0,0 +1,13 @@ +import type { ChatRecordQuery } from './types' + +export function resolveChatRecordSessionId(query: ChatRecordQuery, fallbackSessionId?: string | null): string | null { + return query.sessionId?.trim() || fallbackSessionId || null +} + +export function preserveChatRecordSessionId( + nextQuery: ChatRecordQuery, + currentQuery: ChatRecordQuery +): ChatRecordQuery { + const sessionId = currentQuery.sessionId?.trim() + return sessionId ? { ...nextQuery, sessionId } : nextQuery +} diff --git a/src/i18n/locales/en-US/contacts.json b/src/i18n/locales/en-US/contacts.json index fefe1602..9c326524 100644 --- a/src/i18n/locales/en-US/contacts.json +++ b/src/i18n/locales/en-US/contacts.json @@ -47,6 +47,8 @@ "unmarkFriend": "Unmark friend", "clearSearch": "Clear search", "clearSelection": "Clear selection", + "openSourceSession": "Go to chat", + "viewSourceRecords": "View chat records", "viewDetail": "View details for {name}" }, "detail": { diff --git a/src/i18n/locales/ja-JP/contacts.json b/src/i18n/locales/ja-JP/contacts.json index 3eb2166b..9fe28202 100644 --- a/src/i18n/locales/ja-JP/contacts.json +++ b/src/i18n/locales/ja-JP/contacts.json @@ -47,6 +47,8 @@ "unmarkFriend": "マークを解除", "clearSearch": "検索をクリア", "clearSelection": "選択を解除", + "openSourceSession": "会話へ移動", + "viewSourceRecords": "チャット履歴を見る", "viewDetail": "{name} の詳細を表示" }, "detail": { diff --git a/src/i18n/locales/zh-CN/contacts.json b/src/i18n/locales/zh-CN/contacts.json index 9d0f5acc..c727975b 100644 --- a/src/i18n/locales/zh-CN/contacts.json +++ b/src/i18n/locales/zh-CN/contacts.json @@ -47,6 +47,8 @@ "unmarkFriend": "取消标记", "clearSearch": "清空搜索", "clearSelection": "取消选中", + "openSourceSession": "前往对话", + "viewSourceRecords": "查看聊天记录", "viewDetail": "查看 {name} 的详情" }, "detail": { diff --git a/src/i18n/locales/zh-TW/contacts.json b/src/i18n/locales/zh-TW/contacts.json index f869587d..ae9059dc 100644 --- a/src/i18n/locales/zh-TW/contacts.json +++ b/src/i18n/locales/zh-TW/contacts.json @@ -47,6 +47,8 @@ "unmarkFriend": "取消標記", "clearSearch": "清空搜尋", "clearSelection": "取消選中", + "openSourceSession": "前往對話", + "viewSourceRecords": "查看聊天記錄", "viewDetail": "查看 {name} 的詳情" }, "detail": { diff --git a/src/pages/contacts/components/ContactDetailPanel.vue b/src/pages/contacts/components/ContactDetailPanel.vue index eb9888ae..6d1ceab5 100644 --- a/src/pages/contacts/components/ContactDetailPanel.vue +++ b/src/pages/contacts/components/ContactDetailPanel.vue @@ -13,6 +13,7 @@ const props = defineProps<{ const emit = defineEmits<{ clear: [] openSource: [source: ContactItem['sourceSessions'][number]] + viewSourceRecords: [source: ContactItem['sourceSessions'][number]] markFriend: [] unmarkFriend: [] }>() @@ -130,12 +131,10 @@ function avatarText(contact: ContactItem): string { {{ t('contacts.detail.sources') }}
- +
+ + {{ t('contacts.actions.openSourceSession') }} + + + {{ t('contacts.actions.viewSourceRecords') }} + +
+
diff --git a/src/pages/contacts/index.vue b/src/pages/contacts/index.vue index 02b93af8..f67317db 100644 --- a/src/pages/contacts/index.vue +++ b/src/pages/contacts/index.vue @@ -13,6 +13,7 @@ import type { } from '@openchatlab/shared-types' import { useDataService } from '@/services' import { useToast } from '@/composables/useToast' +import { useLayoutStore } from '@/stores/layout' import PageHeader from '@/components/layout/PageHeader.vue' import { LoadingState, SubTabs } from '@/components/UI' import ContactDetailPanel from './components/ContactDetailPanel.vue' @@ -47,6 +48,7 @@ const CONTACTS_LOAD_MORE_REMAINING = 20 const { t } = useI18n() const toast = useToast() const dataService = useDataService() +const layoutStore = useLayoutStore() const router = useRouter() const isRecomputing = ref(false) @@ -732,6 +734,10 @@ function openSourceSession(source: ContactItem['sourceSessions'][number]) { }) } +function viewSourceSessionRecords(source: ContactItem['sourceSessions'][number]) { + layoutStore.openChatRecordDrawer({ sessionId: source.id }) +} + function handleListScroll(event: Event) { const target = event.target as HTMLElement activeState.value.scrollOffset = target.scrollTop @@ -1128,6 +1134,7 @@ function getGroupSectionScrollTop(): number | null { :is-friend-action-loading="isFriendActionSaving" @clear="clearSelectedContact" @open-source="openSourceSession" + @view-source-records="viewSourceSessionRecords" @mark-friend="markSelectedContactAsFriend" @unmark-friend="unmarkSelectedContactAsFriend" /> diff --git a/src/types/format.ts b/src/types/format.ts index 1c182268..ab4068f4 100644 --- a/src/types/format.ts +++ b/src/types/format.ts @@ -150,6 +150,9 @@ export interface MergeResult { * 支持组合查询:多个条件可同时生效 */ export interface ChatRecordQuery { + /** 指定要查看的会话;未提供时使用当前页面会话 */ + sessionId?: string + /** 定位到指定消息(初始加载时以此消息为中心) */ scrollToMessageId?: number