@@ -35,7 +35,12 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
3535 const exportFolderHandle = ref ( null )
3636 const exportSaveBusy = ref ( false )
3737 const exportSaveMsg = ref ( '' )
38+ const exportSaveError = ref ( '' )
39+ const exportSaveState = ref ( 'idle' )
40+ const exportSaveBytesWritten = ref ( 0 )
41+ const exportSaveBytesTotal = ref ( 0 )
3842 const exportAutoSavedFor = ref ( '' )
43+ const exportCancelRequested = ref ( false )
3944
4045 const exportSearchQuery = ref ( '' )
4146 const exportListTab = ref ( 'all' )
@@ -50,6 +55,27 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
5055 const next = Number ( value )
5156 return Number . isFinite ( next ) ? next : 0
5257 }
58+ const formatBytes = ( value ) => {
59+ const bytes = Number ( value )
60+ if ( ! Number . isFinite ( bytes ) || bytes <= 0 ) return '0 B'
61+ const units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ]
62+ let size = bytes
63+ let index = 0
64+ while ( size >= 1024 && index < units . length - 1 ) {
65+ size /= 1024
66+ index += 1
67+ }
68+ const digits = size >= 100 || index === 0 ? 0 : size >= 10 ? 1 : 2
69+ return `${ size . toFixed ( digits ) } ${ units [ index ] } `
70+ }
71+ const resetExportSaveFeedback = ( { resetAutoSavedFor = false } = { } ) => {
72+ exportSaveMsg . value = ''
73+ exportSaveError . value = ''
74+ exportSaveState . value = 'idle'
75+ exportSaveBytesWritten . value = 0
76+ exportSaveBytesTotal . value = 0
77+ if ( resetAutoSavedFor ) exportAutoSavedFor . value = ''
78+ }
5379
5480 const exportOverallPercent = computed ( ( ) => {
5581 const job = exportJob . value
@@ -72,6 +98,17 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
7298 if ( total <= 0 ) return null
7399 return Math . round ( clamp01 ( done / total ) * 100 )
74100 } )
101+ const exportBackendZipPath = computed ( ( ) => {
102+ return String ( exportJob . value ?. zipPath || '' ) . trim ( )
103+ } )
104+ const exportSaveProgressText = computed ( ( ) => {
105+ if ( exportSaveState . value !== 'saving' ) return ''
106+ const fileName = guessExportZipName ( exportJob . value )
107+ if ( exportSaveBytesTotal . value > 0 ) {
108+ return `正在保存到浏览器目录:${ fileName } (${ formatBytes ( exportSaveBytesWritten . value ) } / ${ formatBytes ( exportSaveBytesTotal . value ) } )`
109+ }
110+ return `正在保存到浏览器目录:${ fileName } (${ formatBytes ( exportSaveBytesWritten . value ) } )`
111+ } )
75112
76113 const normalizeExportSelectedUsernames = ( list ) => {
77114 const seen = new Set ( )
@@ -179,7 +216,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
179216
180217 const chooseExportFolder = async ( ) => {
181218 exportError . value = ''
182- exportSaveMsg . value = ''
219+ resetExportSaveFeedback ( )
183220 try {
184221 if ( ! process . client ) {
185222 exportError . value = '当前环境不支持选择导出目录'
@@ -206,6 +243,10 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
206243
207244 exportError . value = '当前浏览器不支持目录选择,请使用桌面端或 Chromium 新版浏览器'
208245 } catch ( error ) {
246+ const message = String ( error ?. message || '' ) . trim ( )
247+ if ( error ?. name === 'AbortError' || message . includes ( 'The user aborted a request' ) ) {
248+ return
249+ }
209250 exportError . value = error ?. message || '选择导出目录失败'
210251 }
211252 }
@@ -227,7 +268,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
227268 const saveExportToSelectedFolder = async ( options = { } ) => {
228269 const autoSave = ! ! options ?. auto
229270 exportError . value = ''
230- exportSaveMsg . value = ''
271+ resetExportSaveFeedback ( )
231272 if ( ! process . client || ! isWebDirectoryPickerSupported ( ) ) {
232273 exportError . value = '当前环境不支持保存到浏览器目录'
233274 return
@@ -245,6 +286,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
245286 }
246287
247288 exportSaveBusy . value = true
289+ exportSaveState . value = 'saving'
248290 try {
249291 const response = await fetch ( getExportDownloadUrl ( exportId ) )
250292 if ( ! response . ok ) {
@@ -256,18 +298,46 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
256298 } )
257299 throw new Error ( `下载导出文件失败(${ response . status } )` )
258300 }
259- const blob = await response . blob ( )
301+ exportSaveBytesTotal . value = asNumber ( response . headers . get ( 'Content-Length' ) )
260302 const fileName = guessExportZipName ( exportJob . value )
261303 const fileHandle = await handle . getFileHandle ( fileName , { create : true } )
262304 const writable = await fileHandle . createWritable ( )
263- await writable . write ( blob )
264- await writable . close ( )
305+ if ( response . body && typeof response . body . getReader === 'function' ) {
306+ const reader = response . body . getReader ( )
307+ try {
308+ while ( true ) {
309+ const { done, value } = await reader . read ( )
310+ if ( done ) break
311+ if ( ! value || ! value . byteLength ) continue
312+ await writable . write ( value )
313+ exportSaveBytesWritten . value += value . byteLength
314+ }
315+ await writable . close ( )
316+ } catch ( error ) {
317+ try {
318+ await reader . cancel ( )
319+ } catch { }
320+ try {
321+ await writable . abort ( )
322+ } catch { }
323+ throw error
324+ }
325+ } else {
326+ const blob = await response . blob ( )
327+ exportSaveBytesWritten . value = asNumber ( blob . size )
328+ if ( exportSaveBytesTotal . value <= 0 ) exportSaveBytesTotal . value = exportSaveBytesWritten . value
329+ await writable . write ( blob )
330+ await writable . close ( )
331+ }
265332 exportAutoSavedFor . value = String ( exportId )
333+ exportSaveState . value = 'success'
334+ const folderLabel = String ( exportFolder . value || '' ) . trim ( ) || '已选目录'
266335 exportSaveMsg . value = autoSave
267- ? `已自动保存到已选目录 :${ fileName } `
268- : `已保存到已选目录 :${ fileName } `
336+ ? `浏览器目录自动保存成功 :${ fileName } \n位置: ${ folderLabel } `
337+ : `浏览器目录保存成功 :${ fileName } \n位置: ${ folderLabel } `
269338 } catch ( error ) {
270- exportError . value = error ?. message || '保存到浏览器目录失败'
339+ exportSaveState . value = 'error'
340+ exportSaveError . value = `浏览器目录保存失败:${ error ?. message || '未知错误' } `
271341 } finally {
272342 exportSaveBusy . value = false
273343 }
@@ -337,7 +407,8 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
337407 const openExportModal = ( ) => {
338408 exportModalOpen . value = true
339409 exportError . value = ''
340- exportSaveMsg . value = ''
410+ resetExportSaveFeedback ( { resetAutoSavedFor : true } )
411+ exportCancelRequested . value = false
341412 exportSearchQuery . value = ''
342413 exportListTab . value = 'all'
343414 exportSelectedUsernames . value = [ ]
@@ -356,6 +427,12 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
356427 exportError . value = ''
357428 }
358429
430+ const clearExportFolderSelection = ( ) => {
431+ exportFolder . value = ''
432+ exportFolderHandle . value = null
433+ resetExportSaveFeedback ( { resetAutoSavedFor : true } )
434+ }
435+
359436 watch ( exportModalOpen , ( open ) => {
360437 if ( ! process . client ) return
361438 if ( ! open ) {
@@ -382,6 +459,9 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
382459 status : String ( exportJob . value ?. status || '' )
383460 } ) ,
384461 async ( { exportId, status } ) => {
462+ if ( status !== 'queued' && status !== 'running' ) {
463+ exportCancelRequested . value = false
464+ }
385465 if ( ! process . client || status !== 'done' || ! exportId ) return
386466 if ( ! hasWebExportFolder . value ) return
387467 if ( exportAutoSavedFor . value === exportId ) return
@@ -392,7 +472,8 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
392472
393473 const startChatExport = async ( ) => {
394474 exportError . value = ''
395- exportSaveMsg . value = ''
475+ resetExportSaveFeedback ( { resetAutoSavedFor : true } )
476+ exportCancelRequested . value = false
396477 if ( ! selectedAccount . value ) {
397478 exportError . value = '未选择账号'
398479 return
@@ -490,13 +571,17 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
490571
491572 const cancelCurrentExport = async ( ) => {
492573 const exportId = exportJob . value ?. exportId
493- if ( ! exportId ) return
574+ const status = String ( exportJob . value ?. status || '' )
575+ if ( ! exportId || ( status !== 'queued' && status !== 'running' ) || exportCancelRequested . value ) return
494576
577+ exportError . value = ''
578+ exportCancelRequested . value = true
495579 try {
496580 await api . cancelChatExport ( exportId )
497581 const response = await api . getChatExport ( exportId )
498582 exportJob . value = response ?. job || exportJob . value
499583 } catch ( error ) {
584+ exportCancelRequested . value = false
500585 exportError . value = error ?. message || '取消导出失败'
501586 }
502587 }
@@ -518,7 +603,12 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
518603 exportFolderHandle,
519604 exportSaveBusy,
520605 exportSaveMsg,
606+ exportSaveError,
607+ exportSaveState,
608+ exportSaveProgressText,
609+ exportBackendZipPath,
521610 exportAutoSavedFor,
611+ exportCancelRequested,
522612 exportSearchQuery,
523613 exportListTab,
524614 exportSelectedUsernames,
@@ -532,6 +622,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte
532622 isExportContactSelected,
533623 hasWebExportFolder,
534624 chooseExportFolder,
625+ clearExportFolderSelection,
535626 getExportDownloadUrl,
536627 saveExportToSelectedFolder,
537628 openExportModal,
0 commit comments