From 11152a7c4bb92a66f9cb0ffef112961880391966 Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 26 Jun 2026 18:28:05 +0800 Subject: [PATCH] feat(web): add contacts page --- src/components/common/Sidebar.vue | 15 + src/i18n/locales/en-US/contacts.json | 65 +++ src/i18n/locales/en-US/index.ts | 2 + src/i18n/locales/en-US/layout.json | 1 + src/i18n/locales/ja-JP/contacts.json | 65 +++ src/i18n/locales/ja-JP/index.ts | 2 + src/i18n/locales/ja-JP/layout.json | 1 + src/i18n/locales/zh-CN/contacts.json | 65 +++ src/i18n/locales/zh-CN/index.ts | 2 + src/i18n/locales/zh-CN/layout.json | 1 + src/i18n/locales/zh-TW/contacts.json | 65 +++ src/i18n/locales/zh-TW/index.ts | 2 + src/i18n/locales/zh-TW/layout.json | 1 + src/pages/contacts/index.vue | 736 +++++++++++++++++++++++++++ src/routes/index.ts | 6 + src/services/data/fetch.ts | 30 +- src/services/data/types.ts | 21 +- 17 files changed, 1078 insertions(+), 2 deletions(-) create mode 100644 src/i18n/locales/en-US/contacts.json create mode 100644 src/i18n/locales/ja-JP/contacts.json create mode 100644 src/i18n/locales/zh-CN/contacts.json create mode 100644 src/i18n/locales/zh-TW/contacts.json create mode 100644 src/pages/contacts/index.vue diff --git a/src/components/common/Sidebar.vue b/src/components/common/Sidebar.vue index 474d2551..40b36add 100644 --- a/src/components/common/Sidebar.vue +++ b/src/components/common/Sidebar.vue @@ -32,9 +32,13 @@ const { isSidebarCollapsed: isCollapsed } = storeToRefs(layoutStore) const { toggleSidebar } = layoutStore const router = useRouter() const route = useRoute() +const CONTACTS_PRIVATE_SESSION_THRESHOLD = 10 // 是否在首页 const isHomePage = computed(() => route.path === '/') +const isContactsPage = computed(() => route.name === 'contacts') +const privateSessionCount = computed(() => sessions.value.filter((session) => session.type === 'private').length) +const showContactsEntry = computed(() => privateSessionCount.value > CONTACTS_PRIVATE_SESSION_THRESHOLD) // 重命名相关状态 const showRenameModal = ref(false) @@ -108,6 +112,10 @@ function handleImport() { router.push('/') } +function openContacts() { + router.push({ name: 'contacts' }) +} + function readUpdateCheckCache(): UpdateNoticeCache | null { try { const raw = window.localStorage.getItem(UPDATE_CHECK_CACHE_KEY) @@ -438,6 +446,13 @@ function getAvatarColorClass(session: AnalysisSession, isActive: boolean) { :active="isHomePage" @click="handleImport" /> + diff --git a/src/i18n/locales/en-US/contacts.json b/src/i18n/locales/en-US/contacts.json new file mode 100644 index 00000000..ac1bab0b --- /dev/null +++ b/src/i18n/locales/en-US/contacts.json @@ -0,0 +1,65 @@ +{ + "title": "Contacts", + "subtitle": "{count} private chats imported", + "disabled": "Current private chat count is {count}. The sidebar entry appears after more than 10 private chats.", + "empty": "No matching contacts", + "emptyValue": "None", + "search": "Search contacts, IDs, or aliases", + "score": "Score {value}", + "stats": { + "total": "Contacts", + "core": "Core", + "friends": "Friends", + "nonFriends": "Group contacts" + }, + "filters": { + "allPools": "All sources", + "friends": "Friends", + "nonFriends": "Group contacts", + "allTiers": "All tiers", + "showLow": "Show low interaction" + }, + "pool": { + "friend": "Friend", + "nonFriend": "Non-friend" + }, + "tier": { + "core": "Core", + "friend": "Friend", + "acquaintance": "Acquaintance", + "high_interaction": "High interaction", + "medium_interaction": "Medium interaction", + "low_interaction": "Low interaction" + }, + "actions": { + "recompute": "Recompute", + "lockCore": "Lock core", + "unlock": "Unlock" + }, + "detail": { + "score": "Score", + "lastInteraction": "Last interaction", + "sources": "Source chats" + }, + "metrics": { + "privateMessages": "{count} private messages", + "privateMessagesLabel": "Private messages", + "activeMonths": "Active months", + "commonGroups": "Common groups", + "groups": "{count} common groups", + "coOccurrence": "Co-occurrences", + "replies": "Replies", + "groupSignals": "{count} co-occurrences" + }, + "stale": { + "title": "Contacts may be stale", + "message": "Chat data or manual locks changed. Recompute contacts now?", + "inline": "Contacts may be stale.", + "later": "Later" + }, + "toast": { + "recomputed": "Contacts recomputed", + "recomputeFailed": "Recompute failed", + "overrideFailed": "Failed to save contact settings" + } +} diff --git a/src/i18n/locales/en-US/index.ts b/src/i18n/locales/en-US/index.ts index 3791422b..c1d6e953 100644 --- a/src/i18n/locales/en-US/index.ts +++ b/src/i18n/locales/en-US/index.ts @@ -11,6 +11,7 @@ import members from './members.json' import records from './records.json' import views from './views.json' import wordFilter from './wordFilter.json' +import contacts from './contacts.json' export default { common, @@ -26,4 +27,5 @@ export default { records, views, wordFilter, + contacts, } diff --git a/src/i18n/locales/en-US/layout.json b/src/i18n/locales/en-US/layout.json index 3aca092d..4f22ae98 100644 --- a/src/i18n/locales/en-US/layout.json +++ b/src/i18n/locales/en-US/layout.json @@ -1,6 +1,7 @@ { "brand": "ChatLab", "newAnalysis": "New Analysis", + "contacts": "Contacts", "manage": "Manage", "noRecords": "No records", "searchPlaceholder": "Search conversation name", diff --git a/src/i18n/locales/ja-JP/contacts.json b/src/i18n/locales/ja-JP/contacts.json new file mode 100644 index 00000000..1b2a3212 --- /dev/null +++ b/src/i18n/locales/ja-JP/contacts.json @@ -0,0 +1,65 @@ +{ + "title": "連絡先", + "subtitle": "{count} 件の個人チャットをインポート済み", + "disabled": "現在の個人チャット数は {count} 件です。サイドバー項目は個人チャットが 10 件を超えると表示されます。", + "empty": "一致する連絡先はありません", + "emptyValue": "なし", + "search": "連絡先、ID、別名を検索", + "score": "スコア {value}", + "stats": { + "total": "連絡先", + "core": "コア", + "friends": "友人", + "nonFriends": "グループ連絡先" + }, + "filters": { + "allPools": "すべて", + "friends": "友人", + "nonFriends": "グループ連絡先", + "allTiers": "すべての階層", + "showLow": "低インタラクションを表示" + }, + "pool": { + "friend": "友人", + "nonFriend": "非友人" + }, + "tier": { + "core": "コア関係", + "friend": "友人", + "acquaintance": "知人", + "high_interaction": "高インタラクション", + "medium_interaction": "中インタラクション", + "low_interaction": "低インタラクション" + }, + "actions": { + "recompute": "再計算", + "lockCore": "コアに固定", + "unlock": "固定を解除" + }, + "detail": { + "score": "総合スコア", + "lastInteraction": "最終インタラクション", + "sources": "元チャット" + }, + "metrics": { + "privateMessages": "個人チャット {count} 件", + "privateMessagesLabel": "個人メッセージ", + "activeMonths": "アクティブ月", + "commonGroups": "共通グループ", + "groups": "共通グループ {count} 件", + "coOccurrence": "同時出現", + "replies": "返信", + "groupSignals": "同時出現 {count} 回" + }, + "stale": { + "title": "連絡先データが古い可能性があります", + "message": "チャットデータまたは手動固定が変更されました。今すぐ再計算しますか?", + "inline": "連絡先データが古い可能性があります。", + "later": "後で" + }, + "toast": { + "recomputed": "連絡先を再計算しました", + "recomputeFailed": "再計算に失敗しました", + "overrideFailed": "連絡先設定の保存に失敗しました" + } +} diff --git a/src/i18n/locales/ja-JP/index.ts b/src/i18n/locales/ja-JP/index.ts index 3791422b..c1d6e953 100644 --- a/src/i18n/locales/ja-JP/index.ts +++ b/src/i18n/locales/ja-JP/index.ts @@ -11,6 +11,7 @@ import members from './members.json' import records from './records.json' import views from './views.json' import wordFilter from './wordFilter.json' +import contacts from './contacts.json' export default { common, @@ -26,4 +27,5 @@ export default { records, views, wordFilter, + contacts, } diff --git a/src/i18n/locales/ja-JP/layout.json b/src/i18n/locales/ja-JP/layout.json index 3ea3c81b..3c071027 100644 --- a/src/i18n/locales/ja-JP/layout.json +++ b/src/i18n/locales/ja-JP/layout.json @@ -1,6 +1,7 @@ { "brand": "ChatLab", "newAnalysis": "新しいチャットを分析", + "contacts": "連絡先", "manage": "管理", "noRecords": "記録がありません", "searchPlaceholder": "チャット名を検索", diff --git a/src/i18n/locales/zh-CN/contacts.json b/src/i18n/locales/zh-CN/contacts.json new file mode 100644 index 00000000..3a22a7d1 --- /dev/null +++ b/src/i18n/locales/zh-CN/contacts.json @@ -0,0 +1,65 @@ +{ + "title": "联系人", + "subtitle": "已导入 {count} 个私聊会话", + "disabled": "当前私聊会话数为 {count},联系人入口默认在私聊会话数超过 10 后显示。", + "empty": "没有匹配的联系人", + "emptyValue": "暂无", + "search": "搜索联系人、ID 或别名", + "score": "分数 {value}", + "stats": { + "total": "联系人", + "core": "核心", + "friends": "好友", + "nonFriends": "群聊联系人" + }, + "filters": { + "allPools": "全部来源", + "friends": "好友", + "nonFriends": "群聊联系人", + "allTiers": "全部分层", + "showLow": "显示低互动" + }, + "pool": { + "friend": "好友", + "nonFriend": "非好友" + }, + "tier": { + "core": "核心关系", + "friend": "朋友", + "acquaintance": "普通熟人", + "high_interaction": "高互动", + "medium_interaction": "中互动", + "low_interaction": "低互动" + }, + "actions": { + "recompute": "重新计算", + "lockCore": "锁定核心", + "unlock": "取消锁定" + }, + "detail": { + "score": "综合分", + "lastInteraction": "最近互动", + "sources": "来源会话" + }, + "metrics": { + "privateMessages": "{count} 条私聊", + "privateMessagesLabel": "私聊消息", + "activeMonths": "活跃月份", + "commonGroups": "共同群", + "groups": "{count} 个共同群", + "coOccurrence": "同框次数", + "replies": "回复互动", + "groupSignals": "{count} 次同框" + }, + "stale": { + "title": "联系人数据可能已过期", + "message": "检测到聊天库或手动锁定发生变化,是否立即重新计算联系人?", + "inline": "联系人数据可能已过期。", + "later": "稍后" + }, + "toast": { + "recomputed": "联系人已重新计算", + "recomputeFailed": "重新计算失败", + "overrideFailed": "保存联系人设置失败" + } +} diff --git a/src/i18n/locales/zh-CN/index.ts b/src/i18n/locales/zh-CN/index.ts index 3791422b..c1d6e953 100644 --- a/src/i18n/locales/zh-CN/index.ts +++ b/src/i18n/locales/zh-CN/index.ts @@ -11,6 +11,7 @@ import members from './members.json' import records from './records.json' import views from './views.json' import wordFilter from './wordFilter.json' +import contacts from './contacts.json' export default { common, @@ -26,4 +27,5 @@ export default { records, views, wordFilter, + contacts, } diff --git a/src/i18n/locales/zh-CN/layout.json b/src/i18n/locales/zh-CN/layout.json index 8b1fed2c..76a27f87 100644 --- a/src/i18n/locales/zh-CN/layout.json +++ b/src/i18n/locales/zh-CN/layout.json @@ -1,6 +1,7 @@ { "brand": "ChatLab", "newAnalysis": "分析新对话", + "contacts": "联系人", "manage": "管理", "noRecords": "暂无记录", "searchPlaceholder": "搜索对话名称", diff --git a/src/i18n/locales/zh-TW/contacts.json b/src/i18n/locales/zh-TW/contacts.json new file mode 100644 index 00000000..062117a9 --- /dev/null +++ b/src/i18n/locales/zh-TW/contacts.json @@ -0,0 +1,65 @@ +{ + "title": "聯絡人", + "subtitle": "已匯入 {count} 個私聊會話", + "disabled": "目前私聊會話數為 {count},聯絡人入口預設在私聊會話數超過 10 後顯示。", + "empty": "沒有符合的聯絡人", + "emptyValue": "暫無", + "search": "搜尋聯絡人、ID 或別名", + "score": "分數 {value}", + "stats": { + "total": "聯絡人", + "core": "核心", + "friends": "好友", + "nonFriends": "群聊聯絡人" + }, + "filters": { + "allPools": "全部來源", + "friends": "好友", + "nonFriends": "群聊聯絡人", + "allTiers": "全部分層", + "showLow": "顯示低互動" + }, + "pool": { + "friend": "好友", + "nonFriend": "非好友" + }, + "tier": { + "core": "核心關係", + "friend": "朋友", + "acquaintance": "普通熟人", + "high_interaction": "高互動", + "medium_interaction": "中互動", + "low_interaction": "低互動" + }, + "actions": { + "recompute": "重新計算", + "lockCore": "鎖定核心", + "unlock": "取消鎖定" + }, + "detail": { + "score": "綜合分", + "lastInteraction": "最近互動", + "sources": "來源會話" + }, + "metrics": { + "privateMessages": "{count} 則私聊", + "privateMessagesLabel": "私聊訊息", + "activeMonths": "活躍月份", + "commonGroups": "共同群", + "groups": "{count} 個共同群", + "coOccurrence": "同框次數", + "replies": "回覆互動", + "groupSignals": "{count} 次同框" + }, + "stale": { + "title": "聯絡人資料可能已過期", + "message": "偵測到聊天庫或手動鎖定發生變化,是否立即重新計算聯絡人?", + "inline": "聯絡人資料可能已過期。", + "later": "稍後" + }, + "toast": { + "recomputed": "聯絡人已重新計算", + "recomputeFailed": "重新計算失敗", + "overrideFailed": "儲存聯絡人設定失敗" + } +} diff --git a/src/i18n/locales/zh-TW/index.ts b/src/i18n/locales/zh-TW/index.ts index 3791422b..c1d6e953 100644 --- a/src/i18n/locales/zh-TW/index.ts +++ b/src/i18n/locales/zh-TW/index.ts @@ -11,6 +11,7 @@ import members from './members.json' import records from './records.json' import views from './views.json' import wordFilter from './wordFilter.json' +import contacts from './contacts.json' export default { common, @@ -26,4 +27,5 @@ export default { records, views, wordFilter, + contacts, } diff --git a/src/i18n/locales/zh-TW/layout.json b/src/i18n/locales/zh-TW/layout.json index 8d1784f4..6eb78c79 100644 --- a/src/i18n/locales/zh-TW/layout.json +++ b/src/i18n/locales/zh-TW/layout.json @@ -1,6 +1,7 @@ { "brand": "ChatLab", "newAnalysis": "分析新對話", + "contacts": "聯絡人", "manage": "管理", "noRecords": "暫無紀錄", "searchPlaceholder": "搜尋聊天名稱", diff --git a/src/pages/contacts/index.vue b/src/pages/contacts/index.vue new file mode 100644 index 00000000..772e448e --- /dev/null +++ b/src/pages/contacts/index.vue @@ -0,0 +1,736 @@ + + + + + diff --git a/src/routes/index.ts b/src/routes/index.ts index e5732998..6aca78fa 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -25,6 +25,11 @@ export const router = createRouter({ name: 'private-chat', component: () => import('@/pages/private-chat/index.vue'), }, + { + path: '/contacts', + name: 'contacts', + component: () => import('@/pages/contacts/index.vue'), + }, ], history: createWebHashHistory(), }) @@ -56,6 +61,7 @@ function preloadCriticalRoutes() { requestIdleCallback(() => { import('@/pages/group-chat/index.vue') import('@/pages/private-chat/index.vue') + import('@/pages/contacts/index.vue') }) } diff --git a/src/services/data/fetch.ts b/src/services/data/fetch.ts index e1474d6e..3dc2b770 100644 --- a/src/services/data/fetch.ts +++ b/src/services/data/fetch.ts @@ -5,7 +5,13 @@ */ import type { AnalysisSession, MessageType } from '@/types/base' -import type { TimeFilter, ApplyOwnerProfileResult, SetOwnerAndApplyProfileResult } from '@openchatlab/shared-types' +import type { + TimeFilter, + ApplyOwnerProfileResult, + SetOwnerAndApplyProfileResult, + ContactOverridePatch, + ContactsResponse, +} from '@openchatlab/shared-types' import type { MemberActivity, MemberWithStats, @@ -35,6 +41,7 @@ import type { } from '@openchatlab/core' import type { DataAdapter, + ContactsFetchOptions, PaginationParams, PaginatedResult, SQLResult, @@ -97,6 +104,27 @@ export class FetchDataAdapter implements DataAdapter { return result.success } + // ==================== 联系人 ==================== + + getContacts(options?: ContactsFetchOptions): Promise { + const params = new URLSearchParams() + if (options?.acceptStale) params.set('acceptStale', '1') + const qs = params.toString() + return get(`/contacts${qs ? `?${qs}` : ''}`) + } + + recomputeContacts(): Promise { + return post('/contacts/recompute', {}) + } + + setContactOverride(key: string, patchBody: ContactOverridePatch): Promise { + return patch(`/contacts/${encodeURIComponent(key)}/override`, patchBody) + } + + deleteContactOverride(key: string): Promise { + return del(`/contacts/${encodeURIComponent(key)}/override`) + } + // ==================== 时间范围 ==================== getAvailableYears(sessionId: string): Promise { diff --git a/src/services/data/types.ts b/src/services/data/types.ts index a43dc515..fba0b520 100644 --- a/src/services/data/types.ts +++ b/src/services/data/types.ts @@ -6,7 +6,13 @@ */ import type { AnalysisSession, MessageType } from '@/types/base' -import type { TimeFilter, ApplyOwnerProfileResult, SetOwnerAndApplyProfileResult } from '@openchatlab/shared-types' +import type { + TimeFilter, + ApplyOwnerProfileResult, + SetOwnerAndApplyProfileResult, + ContactOverridePatch, + ContactsResponse, +} from '@openchatlab/shared-types' import type { MemberActivity, MemberWithStats, @@ -72,6 +78,12 @@ export interface TableSchema { }> } +// ==================== Contacts ==================== + +export interface ContactsFetchOptions { + acceptStale?: boolean +} + // ==================== Mention Graph ==================== export interface MentionGraphData { @@ -101,6 +113,13 @@ export interface DataAdapter { setOwnerAndApplyProfile(sessionId: string, ownerPlatformId: string): Promise dismissOwnerPrompt(sessionId: string): Promise + // ==================== 联系人 ==================== + + getContacts(options?: ContactsFetchOptions): Promise + recomputeContacts(): Promise + setContactOverride(key: string, patch: ContactOverridePatch): Promise + deleteContactOverride(key: string): Promise + // ==================== 时间范围 ==================== getAvailableYears(sessionId: string): Promise