diff --git a/electron/main/worker/query/sessions.ts b/electron/main/worker/query/sessions.ts index 54f25c0c..13b7bb5b 100644 --- a/electron/main/worker/query/sessions.ts +++ b/electron/main/worker/query/sessions.ts @@ -174,6 +174,7 @@ export function getAllSessions(): any[] { groupAvatar: meta.group_avatar || null, ownerId: meta.owner_id || null, memberAvatar, // 私聊对方头像 + lastMessageTs: overview?.lastMessageTs ?? null, summaryCount, aiConversationCount: 0, // 将在 IPC 层由主进程填充 }) diff --git a/src/components/UI/SubTabs.vue b/src/components/UI/SubTabs.vue index 7a2a685d..d1d48a35 100644 --- a/src/components/UI/SubTabs.vue +++ b/src/components/UI/SubTabs.vue @@ -21,6 +21,10 @@ interface Props { persistKey?: string /** 方向:horizontal(水平)或 vertical(垂直) */ orientation?: 'horizontal' | 'vertical' + /** 尺寸:sm 适用于侧边栏等紧凑场景 */ + size?: 'sm' | 'md' + /** 是否显示底部边框线 */ + bordered?: boolean } interface Emits { @@ -30,6 +34,7 @@ interface Emits { const props = withDefaults(defineProps(), { orientation: 'horizontal', + size: 'md', }) const emit = defineEmits() @@ -143,8 +148,12 @@ watch(
@@ -152,16 +161,20 @@ watch( v-for="tab in items" :key="tab.id" :ref="(el) => setTabRef(tab.id, el as HTMLElement)" - class="flex items-center gap-2 text-sm font-medium transition-colors" + class="flex items-center font-medium transition-colors" :class="[ - isVertical ? 'justify-start px-3 py-2' : 'px-4 py-3', + isVertical + ? 'justify-start gap-2 px-3 py-2 text-sm' + : size === 'sm' + ? 'gap-1.5 px-2 py-1.5 text-xs' + : 'gap-2 px-4 py-3 text-sm', activeTab === tab.id ? 'text-primary-600 dark:text-primary-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300', ]" @click="handleTabClick(tab.id)" > - + {{ tab.label }} diff --git a/src/components/common/Sidebar.vue b/src/components/common/Sidebar.vue index c0b66bdf..a0145973 100644 --- a/src/components/common/Sidebar.vue +++ b/src/components/common/Sidebar.vue @@ -4,21 +4,18 @@ import { ref, computed, onMounted, nextTick } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import type { AnalysisSession } from '@/types/base' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import 'dayjs/locale/zh-cn' -import 'dayjs/locale/en' import SidebarButton from './sidebar/SidebarButton.vue' import SidebarFooter from './sidebar/SidebarFooter.vue' +import SidebarSortPopover from './sidebar/SidebarSortPopover.vue' +import SubTabs from '@/components/UI/SubTabs.vue' import { useSessionStore } from '@/stores/session' import { useLayoutStore } from '@/stores/layout' -dayjs.extend(relativeTime) const { t } = useI18n() const sessionStore = useSessionStore() const layoutStore = useLayoutStore() -const { sessions, sortedSessions } = storeToRefs(sessionStore) +const { sessions, sortedSessions, filterType } = storeToRefs(sessionStore) const { isSidebarCollapsed: isCollapsed } = storeToRefs(layoutStore) const { toggleSidebar } = layoutStore const router = useRouter() @@ -44,6 +41,13 @@ const version = ref('') const showSearch = ref(false) const searchQuery = ref('') +// 筛选 Tab 配置 +const filterTabItems = computed(() => [ + { id: 'all', label: t('layout.filter.all') }, + { id: 'private', label: t('layout.filter.private') }, + { id: 'group', label: t('layout.filter.group') }, +]) + // 过滤后的会话列表 const filteredSortedSessions = computed(() => { if (!searchQuery.value.trim()) { @@ -76,10 +80,6 @@ function handleImport() { router.push('/') } -function formatTime(timestamp: number): string { - return dayjs.unix(timestamp).fromNow() -} - // 打开重命名弹窗 function openRenameModal(session: AnalysisSession) { renameTarget.value = session @@ -229,35 +229,35 @@ function getSessionAvatar(session: AnalysisSession): string | null {
- -
-
- -
-
{{ t('layout.chatHistory') }}
- + +
+ + + -
+

- {{ t('layout.sessionInfo', { count: session.messageCount, time: formatTime(session.importedAt) }) }} + {{ t('layout.sessionInfo', { count: session.messageCount }) }}

diff --git a/src/components/common/sidebar/SidebarSortPopover.vue b/src/components/common/sidebar/SidebarSortPopover.vue new file mode 100644 index 00000000..4ecedbf4 --- /dev/null +++ b/src/components/common/sidebar/SidebarSortPopover.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/i18n/locales/en-US/layout.json b/src/i18n/locales/en-US/layout.json index 5cb4f2d5..7fb6cfc4 100644 --- a/src/i18n/locales/en-US/layout.json +++ b/src/i18n/locales/en-US/layout.json @@ -21,13 +21,24 @@ "title": "Confirm Delete", "message": "Are you sure you want to delete \"{name}\"? This action cannot be undone." }, + "filter": { + "all": "All", + "private": "Private", + "group": "Group" + }, + "sort": { + "importedAt": "Import Time", + "lastMessageTs": "Last Message", + "messageCount": "Message Count" + }, "tooltip": { "expand": "Expand sidebar", "collapse": "Collapse sidebar", "hint": "Right-click to rename or delete", - "search": "Search chat records" + "search": "Search chat records", + "sort": "Sort" }, - "sessionInfo": "{count} messages · {time}", + "sessionInfo": "{count} messages", "footer": { "helpAndFeedback": "Help & Feedback", "settings": "Settings" diff --git a/src/i18n/locales/ja-JP/layout.json b/src/i18n/locales/ja-JP/layout.json index a74f43e5..fe0a2e8c 100644 --- a/src/i18n/locales/ja-JP/layout.json +++ b/src/i18n/locales/ja-JP/layout.json @@ -21,13 +21,24 @@ "title": "削除の確認", "message": "チャット記録「{name}」を削除しますか?この操作は取り消せません。" }, + "filter": { + "all": "すべて", + "private": "個人", + "group": "グループ" + }, + "sort": { + "importedAt": "インポート日時", + "lastMessageTs": "最新メッセージ", + "messageCount": "メッセージ数" + }, "tooltip": { "expand": "サイドバーを展開", "collapse": "サイドバーを折りたたむ", "hint": "右クリックでチャット記録の削除や名前変更ができます", - "search": "チャット記録を検索" + "search": "チャット記録を検索", + "sort": "並べ替え" }, - "sessionInfo": "{count} 件のメッセージ · {time}", + "sessionInfo": "{count} 件のメッセージ", "footer": { "helpAndFeedback": "フィードバックとヘルプ", "settings": "設定" diff --git a/src/i18n/locales/zh-CN/layout.json b/src/i18n/locales/zh-CN/layout.json index b5fefd90..e81de7d5 100644 --- a/src/i18n/locales/zh-CN/layout.json +++ b/src/i18n/locales/zh-CN/layout.json @@ -21,13 +21,24 @@ "title": "确认删除", "message": "确定要删除聊天记录 \"{name}\" 吗?此操作无法撤销。" }, + "filter": { + "all": "全部", + "private": "私聊", + "group": "群聊" + }, + "sort": { + "importedAt": "导入时间", + "lastMessageTs": "最近对话", + "messageCount": "消息数量" + }, "tooltip": { "expand": "展开侧边栏", "collapse": "收起侧边栏", "hint": "右键可删除或重命名聊天记录", - "search": "搜索聊天记录" + "search": "搜索聊天记录", + "sort": "排序" }, - "sessionInfo": "{count} 条消息 · {time}", + "sessionInfo": "{count} 条消息", "footer": { "helpAndFeedback": "反馈和帮助", "settings": "设置" diff --git a/src/i18n/locales/zh-TW/layout.json b/src/i18n/locales/zh-TW/layout.json index 31caa450..6df1bba3 100644 --- a/src/i18n/locales/zh-TW/layout.json +++ b/src/i18n/locales/zh-TW/layout.json @@ -21,13 +21,24 @@ "title": "確認刪除", "message": "確定要刪除聊天紀錄「{name}」嗎?此操作無法復原。" }, + "filter": { + "all": "全部", + "private": "私聊", + "group": "群聊" + }, + "sort": { + "importedAt": "匯入時間", + "lastMessageTs": "最近對話", + "messageCount": "訊息數量" + }, "tooltip": { "expand": "展開側邊欄", "collapse": "收起側邊欄", "hint": "右鍵可刪除或重新命名聊天紀錄", - "search": "搜尋聊天紀錄" + "search": "搜尋聊天紀錄", + "sort": "排序" }, - "sessionInfo": "{count} 條訊息 · {time}", + "sessionInfo": "{count} 條訊息", "footer": { "helpAndFeedback": "回饋與協助", "settings": "設定" diff --git a/src/stores/session.ts b/src/stores/session.ts index 2aeb2c06..d12e7888 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -1,6 +1,15 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import type { AnalysisSession, ImportProgress } from '@/types/base' +import type { AnalysisSession, ImportProgress, ChatType } from '@/types/base' + +/** 侧边栏筛选类型 */ +export type SessionFilterType = 'all' | ChatType + +/** 侧边栏排序字段 */ +export type SessionSortField = 'importedAt' | 'lastMessageTs' | 'messageCount' + +/** 排序方向 */ +export type SessionSortOrder = 'asc' | 'desc' /** 迁移信息 */ export interface MigrationInfo { @@ -696,12 +705,24 @@ export const useSessionStore = defineStore( // 置顶会话 ID 列表 const pinnedSessionIds = ref([]) - // 排序后的会话列表 - const sortedSessions = computed(() => { - // 建立索引映射,index 越大表示越晚置顶 - const pinIndexMap = new Map(pinnedSessionIds.value.map((id, index) => [id, index])) + // 侧边栏筛选/排序状态 + const filterType = ref('all') + const sortField = ref('importedAt') + const sortOrder = ref('desc') - return [...sessions.value].sort((a, b) => { + // 排序后的会话列表(含筛选 + 排序 + 置顶) + const sortedSessions = computed(() => { + // 1. 筛选 + let filtered = sessions.value + if (filterType.value !== 'all') { + filtered = filtered.filter((s) => s.type === filterType.value) + } + + // 2. 建立置顶索引映射 + const pinIndexMap = new Map(pinnedSessionIds.value.map((id, index) => [id, index])) + const dir = sortOrder.value === 'desc' ? -1 : 1 + + return [...filtered].sort((a, b) => { const aPinIndex = pinIndexMap.get(a.id) const bPinIndex = pinIndexMap.get(b.id) const aPinned = aPinIndex !== undefined @@ -715,7 +736,11 @@ export const useSessionStore = defineStore( if (aPinned && !bPinned) return -1 if (!aPinned && bPinned) return 1 - // 都不置顶:保持原顺序(通常是按时间倒序) + // 都不置顶:按用户选择的字段排序 + const field = sortField.value + const aVal = a[field] ?? 0 + const bVal = b[field] ?? 0 + if (aVal !== bVal) return (aVal - bVal) * dir return 0 }) }) @@ -743,6 +768,9 @@ export const useSessionStore = defineStore( sessions, sortedSessions, pinnedSessionIds, + filterType, + sortField, + sortOrder, currentSessionId, isImporting, importProgress, @@ -791,7 +819,7 @@ export const useSessionStore = defineStore( storage: sessionStorage, }, { - pick: ['pinnedSessionIds'], + pick: ['pinnedSessionIds', 'filterType', 'sortField', 'sortOrder'], storage: localStorage, }, ], diff --git a/src/types/base.ts b/src/types/base.ts index 8ffb7525..658b6408 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -239,6 +239,7 @@ export interface AnalysisSession { groupAvatar: string | null // 群头像(base64 Data URL) ownerId: string | null // 所有者/导出者的 platformId memberAvatar: string | null // 私聊对方头像(base64 Data URL) + lastMessageTs: number | null // 最后一条消息时间戳(秒) summaryCount: number // 已生成摘要的会话片段数 aiConversationCount: number // AI 对话数 }