fix: 修复导出下载链路(Electron 保存对话框 + 原生文件写入 + revoke 竞态)
This commit is contained in:
@@ -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
@@ -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};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
-24
File diff suppressed because one or more lines are too long
+24
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+12
@@ -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
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user