fix: 修复导出下载链路(Electron 保存对话框 + 原生文件写入 + revoke 竞态)
This commit is contained in:
@@ -9,7 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-filesystem')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
include ':capacitor-android'
|
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')
|
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="icon" href="./icons/icon.svg" type="image/svg+xml" />
|
||||||
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
|
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
|
||||||
<title>实习工作日志</title>
|
<title>实习工作日志</title>
|
||||||
<script type="module" crossorigin src="./assets/index-DMG3WiLM.js"></script>
|
<script type="module" crossorigin src="./assets/index-UNHXXnki.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-hTAHZZ1V.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-CwZ2l8Dk.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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 path = require('path')
|
||||||
|
|
||||||
const DEV_URL = process.env.VITE_DEV_SERVER_URL || ''
|
const DEV_URL = process.env.VITE_DEV_SERVER_URL || ''
|
||||||
@@ -20,6 +20,20 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
Menu.setApplicationMenu(null)
|
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 }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (/^https?:/i.test(url)) shell.openExternal(url)
|
if (/^https?:/i.test(url)) shell.openExternal(url)
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"@capacitor/core": "^6.2.0"
|
"@capacitor/core": "^6.2.0",
|
||||||
|
"@capacitor/filesystem": "^6.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
|||||||
Generated
+12
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@capacitor/core':
|
'@capacitor/core':
|
||||||
specifier: ^6.2.0
|
specifier: ^6.2.0
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
|
'@capacitor/filesystem':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.4(@capacitor/core@6.2.2)
|
||||||
dexie:
|
dexie:
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.4.6
|
version: 4.4.6
|
||||||
@@ -75,6 +78,11 @@ packages:
|
|||||||
'@capacitor/core@6.2.2':
|
'@capacitor/core@6.2.2':
|
||||||
resolution: {integrity: sha512-qEI3DcwJEPzhW+hVGKH8m4jbujtkdUiGkpMLHmpR8epACU7wEsV6NF8pCHzpsMYjc80oK7Cmpcw+MIienv/YRw==}
|
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':
|
'@develar/schema-utils@2.6.5':
|
||||||
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
|
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
|
||||||
engines: {node: '>= 8.9.0'}
|
engines: {node: '>= 8.9.0'}
|
||||||
@@ -2008,6 +2016,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
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':
|
'@develar/schema-utils@2.6.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
|
||||||
function csvCell(v: string | number | null | undefined): string {
|
function csvCell(v: string | number | null | undefined): string {
|
||||||
const s = v == null ? '' : String(v)
|
const s = v == null ? '' : String(v)
|
||||||
return '"' + s.replace(/"/g, '""') + '"'
|
return '"' + s.replace(/"/g, '""') + '"'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出 UTF-8 BOM 的 CSV,Excel 可直接打开中文不乱码
|
// 导出 UTF-8 BOM 的 CSV,Excel 可直接打开中文不乱码;返回落盘位置(原生平台)
|
||||||
export function downloadCsv(filename: string, rows: (string | number | null | undefined)[][]): void {
|
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 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 url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = filename
|
a.download = filename
|
||||||
a.click()
|
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 blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = filename
|
a.download = filename
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
// 延迟释放,避免立即 revoke 导致下载被取消
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(url), 10000)
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,10 @@ onMounted(async () => {
|
|||||||
await generate()
|
await generate()
|
||||||
})
|
})
|
||||||
|
|
||||||
function doExport() {
|
async function doExport() {
|
||||||
const t = todayStr().replace(/-/g, '')
|
const t = todayStr().replace(/-/g, '')
|
||||||
downloadText('实习工作总结_' + t + '.md', markdown.value)
|
const p = await downloadText('实习工作总结_' + t + '.md', markdown.value)
|
||||||
|
showToast(p ? '总结已导出到:' + p : '总结已导出到系统下载目录')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doCopy() {
|
async function doCopy() {
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ async function doExportLogsCsv() {
|
|||||||
it.remark || ''
|
it.remark || ''
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
downloadCsv('工作记录_' + t.replace(/-/g, '') + '.csv', rows)
|
const p = await downloadCsv('工作记录_' + t.replace(/-/g, '') + '.csv', rows)
|
||||||
showToast('工作记录 CSV 已导出')
|
showToast(p ? 'CSV 已导出到:' + p : 'CSV 已导出到系统下载目录')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doExportAttendanceCsv() {
|
async function doExportAttendanceCsv() {
|
||||||
@@ -163,8 +163,8 @@ async function doExportAttendanceCsv() {
|
|||||||
a.note || ''
|
a.note || ''
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
downloadCsv('考勤记录_' + t.replace(/-/g, '') + '.csv', rows)
|
const p = await downloadCsv('考勤记录_' + t.replace(/-/g, '') + '.csv', rows)
|
||||||
showToast('考勤 CSV 已导出')
|
showToast(p ? 'CSV 已导出到:' + p : 'CSV 已导出到系统下载目录')
|
||||||
}
|
}
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
|
|
||||||
@@ -219,7 +219,8 @@ async function toggleArchive(p: Project) {
|
|||||||
|
|
||||||
async function doExport() {
|
async function doExport() {
|
||||||
const text = await exportAll()
|
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) {
|
function onFile(e: Event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user