fix: 修复导出下载链路(Electron 保存对话框 + 原生文件写入 + revoke 竞态)

This commit is contained in:
root
2026-10-16 13:02:33 +08:00
parent b87e895766
commit 599aa09ab8
15 changed files with 115 additions and 43 deletions
+12 -4
View File
@@ -1,16 +1,24 @@
import { Capacitor } from '@capacitor/core'
function csvCell(v: string | number | null | undefined): string {
const s = v == null ? '' : String(v)
return '"' + s.replace(/"/g, '""') + '"'
}
// 导出 UTF-8 BOM 的 CSV,Excel 可直接打开中文不乱码
export function downloadCsv(filename: string, rows: (string | number | null | undefined)[][]): void {
// 导出 UTF-8 BOM 的 CSV,Excel 可直接打开中文不乱码;返回落盘位置(原生平台)
export async function downloadCsv(filename: string, rows: (string | number | null | undefined)[][]): Promise<string> {
const content = rows.map(r => r.map(csvCell).join(',')).join('\r\n')
const blob = new Blob(['\ufeff' + content], { type: 'text/csv;charset=utf-8' })
const text = '\ufeff' + content
if (Capacitor.isNativePlatform()) {
const { downloadText } = await import('./download')
return downloadText(filename, text)
}
const blob = new Blob([text], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
window.setTimeout(() => URL.revokeObjectURL(url), 10000)
return ''
}
+31 -2
View File
@@ -1,9 +1,38 @@
export function downloadText(filename: string, text: string): void {
import { Capacitor } from '@capacitor/core'
// 导出文本文件:返回落盘位置(原生平台返回文件 URI;浏览器返回空串)
// 原生环境写入公共文档目录,失败时回退应用缓存目录
export async function downloadText(filename: string, text: string): Promise<string> {
if (Capacitor.isNativePlatform()) {
const { Filesystem, Directory, Encoding } = await import('@capacitor/filesystem')
try {
const r = await Filesystem.writeFile({
path: filename,
data: text,
directory: Directory.Documents,
encoding: Encoding.UTF8,
recursive: true
})
return r.uri
} catch {
const r = await Filesystem.writeFile({
path: filename,
data: text,
directory: Directory.Cache,
encoding: Encoding.UTF8,
recursive: true
})
return r.uri
}
}
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
// 延迟释放,避免立即 revoke 导致下载被取消
window.setTimeout(() => URL.revokeObjectURL(url), 10000)
return ''
}
+3 -2
View File
@@ -84,9 +84,10 @@ onMounted(async () => {
await generate()
})
function doExport() {
async function doExport() {
const t = todayStr().replace(/-/g, '')
downloadText('实习工作总结_' + t + '.md', markdown.value)
const p = await downloadText('实习工作总结_' + t + '.md', markdown.value)
showToast(p ? '总结已导出到:' + p : '总结已导出到系统下载目录')
}
async function doCopy() {
+6 -5
View File
@@ -139,8 +139,8 @@ async function doExportLogsCsv() {
it.remark || ''
])
}
downloadCsv('工作记录_' + t.replace(/-/g, '') + '.csv', rows)
showToast('工作记录 CSV 已导出')
const p = await downloadCsv('工作记录_' + t.replace(/-/g, '') + '.csv', rows)
showToast(p ? 'CSV 已导出到:' + p : 'CSV 已导出到系统下载目录')
}
async function doExportAttendanceCsv() {
@@ -163,8 +163,8 @@ async function doExportAttendanceCsv() {
a.note || ''
])
}
downloadCsv('考勤记录_' + t.replace(/-/g, '') + '.csv', rows)
showToast('考勤 CSV 已导出')
const p = await downloadCsv('考勤记录_' + t.replace(/-/g, '') + '.csv', rows)
showToast(p ? 'CSV 已导出到:' + p : 'CSV 已导出到系统下载目录')
}
onMounted(load)
@@ -219,7 +219,8 @@ async function toggleArchive(p: Project) {
async function doExport() {
const text = await exportAll()
downloadText('实习日志备份_' + todayStr().replace(/-/g, '') + '.json', text)
const p = await downloadText('实习日志备份_' + todayStr().replace(/-/g, '') + '.json', text)
showToast(p ? '备份已导出到:' + p : '备份已导出到系统下载目录')
}
function onFile(e: Event) {