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
@@ -9,7 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-filesystem')
}
@@ -1,3 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@6.2.2_@capacitor+core@6.2.2/node_modules/@capacitor/android/capacitor')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@6.0.4_@capacitor+core@6.2.2/node_modules/@capacitor/filesystem/android')
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-wVXIQy1j.js","./index-UNHXXnki.js","./index-CwZ2l8Dk.css"])))=>i.map(i=>d[i]);
import{r as s,_ as i}from"./index-UNHXXnki.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-wVXIQy1j.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -8,8 +8,8 @@
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
<title>实习工作日志</title>
<script type="module" crossorigin src="./assets/index-DMG3WiLM.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-hTAHZZ1V.css">
<script type="module" crossorigin src="./assets/index-UNHXXnki.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CwZ2l8Dk.css">
</head>
<body>
<div id="app"></div>
+15 -1
View File
@@ -1,4 +1,4 @@
const { app, BrowserWindow, Menu, shell } = require('electron')
const { app, BrowserWindow, Menu, shell, dialog } = require('electron')
const path = require('path')
const DEV_URL = process.env.VITE_DEV_SERVER_URL || ''
@@ -20,6 +20,20 @@ function createWindow() {
}
})
Menu.setApplicationMenu(null)
// 下载接管:导出的 JSON/CSV/Markdown 弹出标准保存对话框
win.webContents.session.on('will-download', (event, item, webContents) => {
const result = dialog.showSaveDialogSync(win, {
title: '保存文件',
defaultPath: path.join(app.getPath('downloads'), item.getFilename())
})
if (!result) {
event.preventDefault()
return
}
item.setSavePath(result)
})
win.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:/i.test(url)) shell.openExternal(url)
return { action: 'deny' }
+2 -1
View File
@@ -14,7 +14,8 @@
"dependencies": {
"dexie": "^4.0.8",
"vue": "^3.5.13",
"@capacitor/core": "^6.2.0"
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
+12
View File
@@ -11,6 +11,9 @@ importers:
'@capacitor/core':
specifier: ^6.2.0
version: 6.2.2
'@capacitor/filesystem':
specifier: ^6.0.2
version: 6.0.4(@capacitor/core@6.2.2)
dexie:
specifier: ^4.0.8
version: 4.4.6
@@ -75,6 +78,11 @@ packages:
'@capacitor/core@6.2.2':
resolution: {integrity: sha512-qEI3DcwJEPzhW+hVGKH8m4jbujtkdUiGkpMLHmpR8epACU7wEsV6NF8pCHzpsMYjc80oK7Cmpcw+MIienv/YRw==}
'@capacitor/filesystem@6.0.4':
resolution: {integrity: sha512-eFlg/ZrwYA4Y6ClLRRikudVu2XvuZxfX/XC0ky9MgfbC9dyqTnVkkEoWM6vr1xR89YNY4mB0EeVTet1m1Jcumw==}
peerDependencies:
'@capacitor/core': ^6.0.0
'@develar/schema-utils@2.6.5':
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
@@ -2008,6 +2016,10 @@ snapshots:
dependencies:
tslib: 2.8.1
'@capacitor/filesystem@6.0.4(@capacitor/core@6.2.2)':
dependencies:
'@capacitor/core': 6.2.2
'@develar/schema-utils@2.6.5':
dependencies:
ajv: 6.15.0
+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) {