mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
fix(chat-export): 修复批量导出默认范围与会话选择异常
- 修复未选中会话时导出弹窗落入无效默认范围的问题 - 支持按全部、群聊、单聊快速切换批量导出范围,并默认选中当前筛选结果 - 优化会话列表整行点击、高亮反馈和选择去重逻辑
This commit is contained in:
@@ -1290,52 +1290,77 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="flex flex-wrap items-end gap-6">
|
||||
<div class="flex flex-wrap items-end gap-3 xl:flex-nowrap">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-800 mb-2">范围</div>
|
||||
<div class="flex flex-wrap gap-2 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border cursor-pointer transition-colors" :class="exportScope === 'current' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<input type="radio" value="current" v-model="exportScope" class="hidden" />
|
||||
<span>当前会话</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border cursor-pointer transition-colors" :class="exportScope === 'selected' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<input type="radio" value="selected" v-model="exportScope" class="hidden" />
|
||||
<span>选择会话(批量)</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
class="px-2.5 py-1 text-xs rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="exportScope === 'current' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
|
||||
:disabled="!selectedContact?.username"
|
||||
@click="exportScope = 'current'"
|
||||
>
|
||||
当前会话
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
|
||||
:class="exportScope === 'selected' && exportListTab === 'all' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
|
||||
@click="onExportBatchScopeClick('all')"
|
||||
>
|
||||
全部 {{ exportContactCounts.total }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
|
||||
:class="exportScope === 'selected' && exportListTab === 'groups' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
|
||||
@click="onExportBatchScopeClick('groups')"
|
||||
>
|
||||
群聊 {{ exportContactCounts.groups }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
|
||||
:class="exportScope === 'selected' && exportListTab === 'singles' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
|
||||
@click="onExportBatchScopeClick('singles')"
|
||||
>
|
||||
单聊 {{ exportContactCounts.singles }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-800 mb-2">格式</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'json' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<label class="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'json' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<input type="radio" value="json" v-model="exportFormat" class="hidden" />
|
||||
<span>JSON</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'txt' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<label class="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'txt' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<input type="radio" value="txt" v-model="exportFormat" class="hidden" />
|
||||
<span>TXT</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'html' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<label class="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border cursor-pointer transition-colors" :class="exportFormat === 'html' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'">
|
||||
<input type="radio" value="html" v-model="exportFormat" class="hidden" />
|
||||
<span>HTML</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-[320px]">
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<div class="text-sm font-medium text-gray-800 mb-2">时间范围(可选)</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<div class="flex items-center gap-1.5 flex-wrap">
|
||||
<input
|
||||
v-model="exportStartLocal"
|
||||
type="datetime-local"
|
||||
class="px-2.5 py-1.5 text-sm rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
|
||||
class="px-2 py-1 text-xs rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
|
||||
/>
|
||||
<span class="text-gray-400">-</span>
|
||||
<input
|
||||
v-model="exportEndLocal"
|
||||
type="datetime-local"
|
||||
class="px-2.5 py-1.5 text-sm rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
|
||||
class="px-2 py-1 text-xs rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1367,32 +1392,8 @@
|
||||
</div>
|
||||
|
||||
<div v-if="exportScope === 'selected'" class="mt-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs px-2 py-1 rounded border border-gray-200"
|
||||
:class="exportListTab === 'all' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
|
||||
@click="exportListTab = 'all'"
|
||||
>
|
||||
全部 {{ exportContactCounts.total }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs px-2 py-1 rounded border border-gray-200"
|
||||
:class="exportListTab === 'groups' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
|
||||
@click="exportListTab = 'groups'"
|
||||
>
|
||||
群聊 {{ exportContactCounts.groups }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs px-2 py-1 rounded border border-gray-200"
|
||||
:class="exportListTab === 'singles' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
|
||||
@click="exportListTab = 'singles'"
|
||||
>
|
||||
单聊 {{ exportContactCounts.singles }}
|
||||
</button>
|
||||
<div class="ml-auto text-xs text-gray-500">点击 tab 筛选</div>
|
||||
<div class="mb-2 text-xs text-gray-500">
|
||||
点击上方范围可筛选并默认全选当前结果,再次点击可取消全选;下方整行可点选会话
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
@@ -1404,26 +1405,27 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="border border-gray-200 rounded-md max-h-56 overflow-y-auto">
|
||||
<div
|
||||
<label
|
||||
v-for="c in exportFilteredContacts"
|
||||
:key="c.username"
|
||||
class="px-3 py-2 border-b border-gray-100 flex items-center gap-2 hover:bg-gray-50"
|
||||
class="px-3 py-2 border-b border-gray-100 flex items-center gap-2 cursor-pointer transition-colors"
|
||||
:class="isExportContactSelected(c.username) ? 'bg-[#03C160]/5 hover:bg-[#03C160]/10' : 'hover:bg-gray-50'"
|
||||
>
|
||||
<input type="checkbox" :value="c.username" v-model="exportSelectedUsernames" />
|
||||
<input type="checkbox" :value="c.username" v-model="exportSelectedUsernames" class="cursor-pointer" />
|
||||
<div class="w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
|
||||
<img v-if="c.avatar" :src="c.avatar" :alt="c.name + '头像'" class="w-full h-full object-cover" referrerpolicy="no-referrer" @error="onAvatarError($event, c)" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center text-xs font-bold text-gray-600">
|
||||
{{ (c.name || c.username || '?').charAt(0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0" :class="{ 'privacy-blur': privacyMode }">
|
||||
<div class="min-w-0 flex-1" :class="{ 'privacy-blur': privacyMode }">
|
||||
<div class="text-sm text-gray-800 truncate">
|
||||
{{ c.name }}
|
||||
<span class="text-xs text-gray-500">{{ c.isGroup ? '(群)' : '' }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 truncate">{{ c.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="exportFilteredContacts.length === 0" class="px-3 py-3 text-sm text-gray-500">
|
||||
无匹配会话
|
||||
</div>
|
||||
|
||||
@@ -73,20 +73,35 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
|
||||
return Math.round(clamp01(done / total) * 100)
|
||||
})
|
||||
|
||||
const exportFilteredContacts = computed(() => {
|
||||
const query = String(exportSearchQuery.value || '').trim().toLowerCase()
|
||||
const normalizeExportSelectedUsernames = (list) => {
|
||||
const seen = new Set()
|
||||
return (Array.isArray(list) ? list : []).reduce((acc, item) => {
|
||||
const username = String(item || '').trim()
|
||||
if (!username || seen.has(username)) return acc
|
||||
seen.add(username)
|
||||
acc.push(username)
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
const getExportFilteredContacts = ({ tab = exportListTab.value, query = exportSearchQuery.value } = {}) => {
|
||||
const normalizedQuery = String(query || '').trim().toLowerCase()
|
||||
let list = Array.isArray(contacts.value) ? contacts.value : []
|
||||
|
||||
const tab = String(exportListTab.value || 'all')
|
||||
if (tab === 'groups') list = list.filter((contact) => !!contact?.isGroup)
|
||||
if (tab === 'singles') list = list.filter((contact) => !contact?.isGroup)
|
||||
const normalizedTab = String(tab || 'all')
|
||||
if (normalizedTab === 'groups') list = list.filter((contact) => !!contact?.isGroup)
|
||||
if (normalizedTab === 'singles') list = list.filter((contact) => !contact?.isGroup)
|
||||
|
||||
if (!query) return list
|
||||
if (!normalizedQuery) return list
|
||||
return list.filter((contact) => {
|
||||
const name = String(contact?.name || '').toLowerCase()
|
||||
const username = String(contact?.username || '').toLowerCase()
|
||||
return name.includes(query) || username.includes(query)
|
||||
return name.includes(normalizedQuery) || username.includes(normalizedQuery)
|
||||
})
|
||||
}
|
||||
|
||||
const exportFilteredContacts = computed(() => {
|
||||
return getExportFilteredContacts()
|
||||
})
|
||||
|
||||
const exportContactCounts = computed(() => {
|
||||
@@ -96,6 +111,60 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
|
||||
return { total, groups, singles: total - groups }
|
||||
})
|
||||
|
||||
const exportSelectedUsernameSet = computed(() => {
|
||||
return new Set(normalizeExportSelectedUsernames(exportSelectedUsernames.value))
|
||||
})
|
||||
|
||||
const setExportSelectedUsernames = (list) => {
|
||||
exportSelectedUsernames.value = normalizeExportSelectedUsernames(list)
|
||||
}
|
||||
|
||||
const getExportFilteredUsernames = (tab = exportListTab.value) => {
|
||||
return getExportFilteredContacts({ tab })
|
||||
.map((contact) => String(contact?.username || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const selectExportFilteredContacts = (tab = exportListTab.value) => {
|
||||
setExportSelectedUsernames(getExportFilteredUsernames(tab))
|
||||
}
|
||||
|
||||
const clearExportFilteredContacts = () => {
|
||||
setExportSelectedUsernames([])
|
||||
}
|
||||
|
||||
const areExportFilteredContactsAllSelected = (tab = exportListTab.value) => {
|
||||
const usernames = getExportFilteredUsernames(tab)
|
||||
if (usernames.length !== exportSelectedUsernameSet.value.size) return false
|
||||
return usernames.every((username) => exportSelectedUsernameSet.value.has(username))
|
||||
}
|
||||
|
||||
const onExportListTabClick = (tab) => {
|
||||
const nextTab = String(tab || 'all')
|
||||
const isSameTab = String(exportListTab.value || 'all') === nextTab
|
||||
exportListTab.value = nextTab
|
||||
|
||||
if (isSameTab) {
|
||||
if (areExportFilteredContactsAllSelected(nextTab)) {
|
||||
clearExportFilteredContacts(nextTab)
|
||||
} else {
|
||||
selectExportFilteredContacts(nextTab)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
selectExportFilteredContacts(nextTab)
|
||||
}
|
||||
|
||||
const isExportContactSelected = (username) => {
|
||||
return exportSelectedUsernameSet.value.has(String(username || '').trim())
|
||||
}
|
||||
|
||||
const onExportBatchScopeClick = (tab) => {
|
||||
exportScope.value = 'selected'
|
||||
onExportListTabClick(tab)
|
||||
}
|
||||
|
||||
const isDesktopExportRuntime = () => {
|
||||
return !!(process.client && window?.wechatDesktop?.chooseDirectory)
|
||||
}
|
||||
@@ -269,12 +338,17 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
|
||||
exportModalOpen.value = true
|
||||
exportError.value = ''
|
||||
exportSaveMsg.value = ''
|
||||
exportSearchQuery.value = ''
|
||||
exportListTab.value = 'all'
|
||||
exportSelectedUsernames.value = []
|
||||
exportStartLocal.value = ''
|
||||
exportEndLocal.value = ''
|
||||
exportMessageTypes.value = exportMessageTypeOptions.map((item) => item.value)
|
||||
exportAutoSavedFor.value = ''
|
||||
exportScope.value = selectedContact.value?.username ? 'current' : 'all'
|
||||
exportScope.value = selectedContact.value?.username ? 'current' : 'selected'
|
||||
if (!selectedContact.value?.username) {
|
||||
selectExportFilteredContacts('all')
|
||||
}
|
||||
}
|
||||
|
||||
const closeExportModal = () => {
|
||||
@@ -296,6 +370,12 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
|
||||
}
|
||||
})
|
||||
|
||||
watch(exportScope, (scope, previousScope) => {
|
||||
if (scope !== 'selected' || previousScope === 'selected') return
|
||||
if (exportSelectedUsernames.value.length > 0) return
|
||||
selectExportFilteredContacts(exportListTab.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
exportId: String(exportJob.value?.exportId || ''),
|
||||
@@ -447,6 +527,9 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
|
||||
exportCurrentPercent,
|
||||
exportFilteredContacts,
|
||||
exportContactCounts,
|
||||
onExportBatchScopeClick,
|
||||
onExportListTabClick,
|
||||
isExportContactSelected,
|
||||
hasWebExportFolder,
|
||||
chooseExportFolder,
|
||||
getExportDownloadUrl,
|
||||
|
||||
Reference in New Issue
Block a user