diff --git a/frontend/app.vue b/frontend/app.vue
index cb55b98..f1b0426 100644
--- a/frontend/app.vue
+++ b/frontend/app.vue
@@ -30,7 +30,7 @@ onBeforeUnmount(() => {
})
const route = useRoute()
-const isChatRoute = computed(() => route.path?.startsWith('/chat') || route.path?.startsWith('/sns'))
+const isChatRoute = computed(() => route.path?.startsWith('/chat') || route.path?.startsWith('/sns') || route.path?.startsWith('/contacts'))
const rootClass = computed(() => {
const base = 'bg-gradient-to-br from-green-50 via-emerald-50 to-green-100'
diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js
index cbf1c5f..63d1209 100644
--- a/frontend/composables/useApi.js
+++ b/frontend/composables/useApi.js
@@ -292,6 +292,7 @@ export const useApi = () => {
message_types: Array.isArray(data.message_types) ? data.message_types : [],
include_media: data.include_media == null ? true : !!data.include_media,
media_kinds: Array.isArray(data.media_kinds) ? data.media_kinds : ['image', 'emoji', 'video', 'video_thumb', 'voice', 'file'],
+ output_dir: data.output_dir == null ? null : String(data.output_dir || '').trim(),
allow_process_key_extract: !!data.allow_process_key_extract,
privacy_mode: !!data.privacy_mode,
file_name: data.file_name || null
@@ -313,6 +314,36 @@ export const useApi = () => {
return await request(`/chat/exports/${encodeURIComponent(String(exportId))}`, { method: 'DELETE' })
}
+ // 联系人
+ const listChatContacts = async (params = {}) => {
+ const query = new URLSearchParams()
+ if (params && params.account) query.set('account', params.account)
+ if (params && params.keyword) query.set('keyword', params.keyword)
+ if (params && params.include_friends != null) query.set('include_friends', String(!!params.include_friends))
+ if (params && params.include_groups != null) query.set('include_groups', String(!!params.include_groups))
+ if (params && params.include_officials != null) query.set('include_officials', String(!!params.include_officials))
+ const url = '/chat/contacts' + (query.toString() ? `?${query.toString()}` : '')
+ return await request(url)
+ }
+
+ const exportChatContacts = async (payload = {}) => {
+ return await request('/chat/contacts/export', {
+ method: 'POST',
+ body: {
+ account: payload.account || null,
+ output_dir: payload.output_dir || '',
+ format: payload.format || 'json',
+ include_avatar_link: payload.include_avatar_link == null ? true : !!payload.include_avatar_link,
+ keyword: payload.keyword || null,
+ contact_types: {
+ friends: payload?.contact_types?.friends == null ? true : !!payload.contact_types.friends,
+ groups: payload?.contact_types?.groups == null ? true : !!payload.contact_types.groups,
+ officials: payload?.contact_types?.officials == null ? true : !!payload.contact_types.officials,
+ }
+ }
+ })
+ }
+
// WeChat Wrapped(年度总结)
const getWrappedAnnual = async (params = {}) => {
const query = new URLSearchParams()
@@ -373,6 +404,8 @@ export const useApi = () => {
getChatExport,
listChatExports,
cancelChatExport,
+ listChatContacts,
+ exportChatContacts,
getWrappedAnnual,
getWrappedAnnualMeta,
getWrappedAnnualCard
diff --git a/frontend/pages/contacts.vue b/frontend/pages/contacts.vue
new file mode 100644
index 0000000..b72d6ea
--- /dev/null
+++ b/frontend/pages/contacts.vue
@@ -0,0 +1,572 @@
+
+
+
+