Skip to content

Commit 0a4d30e

Browse files
committed
fix(chat-export): 修复批量导出默认范围与会话选择异常
- 修复未选中会话时导出弹窗落入无效默认范围的问题 - 支持按全部、群聊、单聊快速切换批量导出范围,并默认选中当前筛选结果 - 优化会话列表整行点击、高亮反馈和选择去重逻辑
1 parent 251c61f commit 0a4d30e

File tree

2 files changed

+141
-56
lines changed

2 files changed

+141
-56
lines changed

frontend/components/chat/ChatOverlays.vue

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,52 +1290,77 @@
12901290
</div>
12911291

12921292
<div class="space-y-5">
1293-
<div class="flex flex-wrap items-end gap-6">
1293+
<div class="flex flex-wrap items-end gap-3 xl:flex-nowrap">
12941294
<div>
12951295
<div class="text-sm font-medium text-gray-800 mb-2">范围</div>
1296-
<div class="flex flex-wrap gap-2 text-sm text-gray-700">
1297-
<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'">
1298-
<input type="radio" value="current" v-model="exportScope" class="hidden" />
1299-
<span>当前会话</span>
1300-
</label>
1301-
<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'">
1302-
<input type="radio" value="selected" v-model="exportScope" class="hidden" />
1303-
<span>选择会话(批量)</span>
1304-
</label>
1296+
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-700">
1297+
<button
1298+
type="button"
1299+
class="px-2.5 py-1 text-xs rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1300+
:class="exportScope === 'current' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
1301+
:disabled="!selectedContact?.username"
1302+
@click="exportScope = 'current'"
1303+
>
1304+
当前会话
1305+
</button>
1306+
<button
1307+
type="button"
1308+
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
1309+
:class="exportScope === 'selected' && exportListTab === 'all' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
1310+
@click="onExportBatchScopeClick('all')"
1311+
>
1312+
全部 {{ exportContactCounts.total }}
1313+
</button>
1314+
<button
1315+
type="button"
1316+
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
1317+
:class="exportScope === 'selected' && exportListTab === 'groups' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
1318+
@click="onExportBatchScopeClick('groups')"
1319+
>
1320+
群聊 {{ exportContactCounts.groups }}
1321+
</button>
1322+
<button
1323+
type="button"
1324+
class="px-2.5 py-1 text-xs rounded-md border transition-colors"
1325+
:class="exportScope === 'selected' && exportListTab === 'singles' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50'"
1326+
@click="onExportBatchScopeClick('singles')"
1327+
>
1328+
单聊 {{ exportContactCounts.singles }}
1329+
</button>
13051330
</div>
13061331
</div>
13071332

13081333
<div>
13091334
<div class="text-sm font-medium text-gray-800 mb-2">格式</div>
13101335
<div class="flex items-center gap-2 text-sm text-gray-700">
1311-
<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'">
1336+
<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'">
13121337
<input type="radio" value="json" v-model="exportFormat" class="hidden" />
13131338
<span>JSON</span>
13141339
</label>
1315-
<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'">
1340+
<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'">
13161341
<input type="radio" value="txt" v-model="exportFormat" class="hidden" />
13171342
<span>TXT</span>
13181343
</label>
1319-
<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'">
1344+
<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'">
13201345
<input type="radio" value="html" v-model="exportFormat" class="hidden" />
13211346
<span>HTML</span>
13221347
</label>
13231348
</div>
13241349
</div>
13251350

1326-
<div class="flex-1 min-w-[320px]">
1351+
<div class="flex-1 min-w-[280px]">
13271352
<div class="text-sm font-medium text-gray-800 mb-2">时间范围(可选)</div>
1328-
<div class="flex items-center gap-2 flex-wrap">
1353+
<div class="flex items-center gap-1.5 flex-wrap">
13291354
<input
13301355
v-model="exportStartLocal"
13311356
type="datetime-local"
1332-
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"
1357+
class="px-2 py-1 text-xs rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
13331358
/>
13341359
<span class="text-gray-400">-</span>
13351360
<input
13361361
v-model="exportEndLocal"
13371362
type="datetime-local"
1338-
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"
1363+
class="px-2 py-1 text-xs rounded-md border border-gray-200 focus:outline-none focus:ring-2 focus:ring-[#03C160]/30"
13391364
/>
13401365
</div>
13411366
</div>
@@ -1367,32 +1392,8 @@
13671392
</div>
13681393

13691394
<div v-if="exportScope === 'selected'" class="mt-3">
1370-
<div class="flex items-center gap-2 mb-2">
1371-
<button
1372-
type="button"
1373-
class="text-xs px-2 py-1 rounded border border-gray-200"
1374-
:class="exportListTab === 'all' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
1375-
@click="exportListTab = 'all'"
1376-
>
1377-
全部 {{ exportContactCounts.total }}
1378-
</button>
1379-
<button
1380-
type="button"
1381-
class="text-xs px-2 py-1 rounded border border-gray-200"
1382-
:class="exportListTab === 'groups' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
1383-
@click="exportListTab = 'groups'"
1384-
>
1385-
群聊 {{ exportContactCounts.groups }}
1386-
</button>
1387-
<button
1388-
type="button"
1389-
class="text-xs px-2 py-1 rounded border border-gray-200"
1390-
:class="exportListTab === 'singles' ? 'bg-[#03C160] text-white border-[#03C160]' : 'bg-white hover:bg-gray-50 text-gray-700'"
1391-
@click="exportListTab = 'singles'"
1392-
>
1393-
单聊 {{ exportContactCounts.singles }}
1394-
</button>
1395-
<div class="ml-auto text-xs text-gray-500">点击 tab 筛选</div>
1395+
<div class="mb-2 text-xs text-gray-500">
1396+
点击上方范围可筛选并默认全选当前结果,再次点击可取消全选;下方整行可点选会话
13961397
</div>
13971398
<div class="flex items-center gap-2 mb-2">
13981399
<input
@@ -1404,26 +1405,27 @@
14041405
/>
14051406
</div>
14061407
<div class="border border-gray-200 rounded-md max-h-56 overflow-y-auto">
1407-
<div
1408+
<label
14081409
v-for="c in exportFilteredContacts"
14091410
:key="c.username"
1410-
class="px-3 py-2 border-b border-gray-100 flex items-center gap-2 hover:bg-gray-50"
1411+
class="px-3 py-2 border-b border-gray-100 flex items-center gap-2 cursor-pointer transition-colors"
1412+
:class="isExportContactSelected(c.username) ? 'bg-[#03C160]/5 hover:bg-[#03C160]/10' : 'hover:bg-gray-50'"
14111413
>
1412-
<input type="checkbox" :value="c.username" v-model="exportSelectedUsernames" />
1414+
<input type="checkbox" :value="c.username" v-model="exportSelectedUsernames" class="cursor-pointer" />
14131415
<div class="w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
14141416
<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)" />
14151417
<div v-else class="w-full h-full flex items-center justify-center text-xs font-bold text-gray-600">
14161418
{{ (c.name || c.username || '?').charAt(0) }}
14171419
</div>
14181420
</div>
1419-
<div class="min-w-0" :class="{ 'privacy-blur': privacyMode }">
1421+
<div class="min-w-0 flex-1" :class="{ 'privacy-blur': privacyMode }">
14201422
<div class="text-sm text-gray-800 truncate">
14211423
{{ c.name }}
14221424
<span class="text-xs text-gray-500">{{ c.isGroup ? '(群)' : '' }}</span>
14231425
</div>
14241426
<div class="text-xs text-gray-500 truncate">{{ c.username }}</div>
14251427
</div>
1426-
</div>
1428+
</label>
14271429
<div v-if="exportFilteredContacts.length === 0" class="px-3 py-3 text-sm text-gray-500">
14281430
无匹配会话
14291431
</div>

frontend/composables/chat/useChatExport.js

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,35 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
7373
return Math.round(clamp01(done / total) * 100)
7474
})
7575

76-
const exportFilteredContacts = computed(() => {
77-
const query = String(exportSearchQuery.value || '').trim().toLowerCase()
76+
const normalizeExportSelectedUsernames = (list) => {
77+
const seen = new Set()
78+
return (Array.isArray(list) ? list : []).reduce((acc, item) => {
79+
const username = String(item || '').trim()
80+
if (!username || seen.has(username)) return acc
81+
seen.add(username)
82+
acc.push(username)
83+
return acc
84+
}, [])
85+
}
86+
87+
const getExportFilteredContacts = ({ tab = exportListTab.value, query = exportSearchQuery.value } = {}) => {
88+
const normalizedQuery = String(query || '').trim().toLowerCase()
7889
let list = Array.isArray(contacts.value) ? contacts.value : []
7990

80-
const tab = String(exportListTab.value || 'all')
81-
if (tab === 'groups') list = list.filter((contact) => !!contact?.isGroup)
82-
if (tab === 'singles') list = list.filter((contact) => !contact?.isGroup)
91+
const normalizedTab = String(tab || 'all')
92+
if (normalizedTab === 'groups') list = list.filter((contact) => !!contact?.isGroup)
93+
if (normalizedTab === 'singles') list = list.filter((contact) => !contact?.isGroup)
8394

84-
if (!query) return list
95+
if (!normalizedQuery) return list
8596
return list.filter((contact) => {
8697
const name = String(contact?.name || '').toLowerCase()
8798
const username = String(contact?.username || '').toLowerCase()
88-
return name.includes(query) || username.includes(query)
99+
return name.includes(normalizedQuery) || username.includes(normalizedQuery)
89100
})
101+
}
102+
103+
const exportFilteredContacts = computed(() => {
104+
return getExportFilteredContacts()
90105
})
91106

92107
const exportContactCounts = computed(() => {
@@ -96,6 +111,60 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
96111
return { total, groups, singles: total - groups }
97112
})
98113

114+
const exportSelectedUsernameSet = computed(() => {
115+
return new Set(normalizeExportSelectedUsernames(exportSelectedUsernames.value))
116+
})
117+
118+
const setExportSelectedUsernames = (list) => {
119+
exportSelectedUsernames.value = normalizeExportSelectedUsernames(list)
120+
}
121+
122+
const getExportFilteredUsernames = (tab = exportListTab.value) => {
123+
return getExportFilteredContacts({ tab })
124+
.map((contact) => String(contact?.username || '').trim())
125+
.filter(Boolean)
126+
}
127+
128+
const selectExportFilteredContacts = (tab = exportListTab.value) => {
129+
setExportSelectedUsernames(getExportFilteredUsernames(tab))
130+
}
131+
132+
const clearExportFilteredContacts = () => {
133+
setExportSelectedUsernames([])
134+
}
135+
136+
const areExportFilteredContactsAllSelected = (tab = exportListTab.value) => {
137+
const usernames = getExportFilteredUsernames(tab)
138+
if (usernames.length !== exportSelectedUsernameSet.value.size) return false
139+
return usernames.every((username) => exportSelectedUsernameSet.value.has(username))
140+
}
141+
142+
const onExportListTabClick = (tab) => {
143+
const nextTab = String(tab || 'all')
144+
const isSameTab = String(exportListTab.value || 'all') === nextTab
145+
exportListTab.value = nextTab
146+
147+
if (isSameTab) {
148+
if (areExportFilteredContactsAllSelected(nextTab)) {
149+
clearExportFilteredContacts(nextTab)
150+
} else {
151+
selectExportFilteredContacts(nextTab)
152+
}
153+
return
154+
}
155+
156+
selectExportFilteredContacts(nextTab)
157+
}
158+
159+
const isExportContactSelected = (username) => {
160+
return exportSelectedUsernameSet.value.has(String(username || '').trim())
161+
}
162+
163+
const onExportBatchScopeClick = (tab) => {
164+
exportScope.value = 'selected'
165+
onExportListTabClick(tab)
166+
}
167+
99168
const isDesktopExportRuntime = () => {
100169
return !!(process.client && window?.wechatDesktop?.chooseDirectory)
101170
}
@@ -269,12 +338,17 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
269338
exportModalOpen.value = true
270339
exportError.value = ''
271340
exportSaveMsg.value = ''
341+
exportSearchQuery.value = ''
272342
exportListTab.value = 'all'
343+
exportSelectedUsernames.value = []
273344
exportStartLocal.value = ''
274345
exportEndLocal.value = ''
275346
exportMessageTypes.value = exportMessageTypeOptions.map((item) => item.value)
276347
exportAutoSavedFor.value = ''
277-
exportScope.value = selectedContact.value?.username ? 'current' : 'all'
348+
exportScope.value = selectedContact.value?.username ? 'current' : 'selected'
349+
if (!selectedContact.value?.username) {
350+
selectExportFilteredContacts('all')
351+
}
278352
}
279353

280354
const closeExportModal = () => {
@@ -296,6 +370,12 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
296370
}
297371
})
298372

373+
watch(exportScope, (scope, previousScope) => {
374+
if (scope !== 'selected' || previousScope === 'selected') return
375+
if (exportSelectedUsernames.value.length > 0) return
376+
selectExportFilteredContacts(exportListTab.value)
377+
})
378+
299379
watch(
300380
() => ({
301381
exportId: String(exportJob.value?.exportId || ''),
@@ -447,6 +527,9 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
447527
exportCurrentPercent,
448528
exportFilteredContacts,
449529
exportContactCounts,
530+
onExportBatchScopeClick,
531+
onExportListTabClick,
532+
isExportContactSelected,
450533
hasWebExportFolder,
451534
chooseExportFolder,
452535
getExportDownloadUrl,

0 commit comments

Comments
 (0)