feat: 添加本地文件读取功能并优化构建脚本
添加从本地文件URL提取路径和通过Electron API读取本地文件的功能,支持多种数据格式转换。同时新增构建脚本快捷方式bs和bt,优化开发工作流程。
This commit is contained in:
parent
f5dbe4470f
commit
d6444c2798
|
|
@ -49,6 +49,8 @@
|
||||||
"release": "node scripts/release.js",
|
"release": "node scripts/release.js",
|
||||||
"ball": "vite build && pnpm ct && pnpm cs && pnpm cw",
|
"ball": "vite build && pnpm ct && pnpm cs && pnpm cw",
|
||||||
"bc": "vite build && pnpm cw",
|
"bc": "vite build && pnpm cw",
|
||||||
|
"bs": "vite build && pnpm cs",
|
||||||
|
"bt": "vite build && pnpm ct",
|
||||||
"ct": "rm -rf 'E:/XuZhou/xz-teacher/public/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'E:/XuZhou/xz-teacher/public/vocd/'",
|
"ct": "rm -rf 'E:/XuZhou/xz-teacher/public/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'E:/XuZhou/xz-teacher/public/vocd/'",
|
||||||
"cs": "rm -rf 'E:/XuZhou/xz-student/src/static/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'E:/XuZhou/xz-student/src/static/vocd/'",
|
"cs": "rm -rf 'E:/XuZhou/xz-student/src/static/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'E:/XuZhou/xz-student/src/static/vocd/'",
|
||||||
"cw": "rm -rf 'F:/SVN/工程装备数智化教学软件平台/07 程序代码/gczbjx-ui/public/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'F:/SVN/工程装备数智化教学软件平台/07 程序代码/gczbjx-ui/public/vocd/'"
|
"cw": "rm -rf 'F:/SVN/工程装备数智化教学软件平台/07 程序代码/gczbjx-ui/public/vocd/' && scp -r 'E:/XuZhou/by-onlineeditor/dist/.' 'F:/SVN/工程装备数智化教学软件平台/07 程序代码/gczbjx-ui/public/vocd/'"
|
||||||
|
|
|
||||||
274
src/main.ts
274
src/main.ts
|
|
@ -27,6 +27,179 @@ import { formatPrismToken } from './utils/prism'
|
||||||
import { Signature } from './components/signature/Signature'
|
import { Signature } from './components/signature/Signature'
|
||||||
import { debounce, nextTick, scrollIntoView } from './utils'
|
import { debounce, nextTick, scrollIntoView } from './utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地文件URL中提取文件路径
|
||||||
|
*/
|
||||||
|
function extractLocalFilePathFromUrl(url: string): string | null {
|
||||||
|
console.log('[special] extractLocalFilePathFromUrl: input url =', url)
|
||||||
|
const localPrefix = 'http://localfile/'
|
||||||
|
const beginToken = '/begin-path/'
|
||||||
|
const endToken = '/path-end/'
|
||||||
|
|
||||||
|
// 1. 检查前缀
|
||||||
|
if (!url.startsWith(localPrefix)) {
|
||||||
|
console.warn('[special] prefix not matched', { localPrefix, url })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找路径标记
|
||||||
|
const beginIndex: number = url.indexOf(beginToken)
|
||||||
|
const endIndex: number = url.indexOf(endToken)
|
||||||
|
console.log('[special] token indexes', { beginIndex, endIndex })
|
||||||
|
|
||||||
|
if (beginIndex === -1 || endIndex === -1 || endIndex <= beginIndex) {
|
||||||
|
console.warn('[special] token invalid', { beginIndex, endIndex })
|
||||||
|
return null // 标记不全或顺序错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提取 filePath(不含 /begin-path/ 和 /path-end/)
|
||||||
|
const filePathStart: number = beginIndex + beginToken.length
|
||||||
|
const filePath: string = url.substring(filePathStart, endIndex)
|
||||||
|
console.log('[special] extracted localFilePath =', filePath)
|
||||||
|
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求本地文件并返回ArrayBuffer
|
||||||
|
*/
|
||||||
|
function requestLocalFile(
|
||||||
|
localFilePath: string,
|
||||||
|
onProgress?: (loaded: number, total: number) => void
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||||
|
console.log('[special] requestLocalFile: input path =', localFilePath)
|
||||||
|
|
||||||
|
type ElectronAPI = {
|
||||||
|
readFile?: (path: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
content?: unknown
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const electronAPI = (window as any).electronAPI as ElectronAPI | undefined
|
||||||
|
if (!electronAPI?.readFile) {
|
||||||
|
console.error('[special] Electron API 不可用或未暴露 readFile')
|
||||||
|
return reject(
|
||||||
|
new Error('Electron API 不可用,请确保在 Electron 环境中运行')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码文件路径,处理 URL 编码
|
||||||
|
let decodedPath: string
|
||||||
|
try {
|
||||||
|
decodedPath = decodeURIComponent(localFilePath)
|
||||||
|
// 移除可能的 file:// 前缀
|
||||||
|
if (decodedPath.startsWith('file://')) {
|
||||||
|
decodedPath = decodedPath.substring(7)
|
||||||
|
}
|
||||||
|
// 在 Windows 上处理路径格式
|
||||||
|
const isWindows =
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
navigator.platform &&
|
||||||
|
(navigator.platform.indexOf('Win') !== -1 ||
|
||||||
|
navigator.platform.indexOf('win') !== -1)
|
||||||
|
console.log('[special] platform isWindows =', !!isWindows)
|
||||||
|
if (isWindows && decodedPath.startsWith('/')) {
|
||||||
|
decodedPath = decodedPath.substring(1)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[special] 路径解码失败:', error)
|
||||||
|
decodedPath = localFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[special] requestLocalFile 读取文件 decodedPath =', decodedPath)
|
||||||
|
console.log('[special] electronAPI.readFile 调用')
|
||||||
|
|
||||||
|
electronAPI
|
||||||
|
.readFile!(decodedPath)
|
||||||
|
.then(result => {
|
||||||
|
console.log('[special] Electron API 返回结果:', result)
|
||||||
|
|
||||||
|
if (!result || !result.success) {
|
||||||
|
const errorMsg = (result && result.error) || '读取文件失败'
|
||||||
|
console.error('[special] 文件读取失败:', errorMsg)
|
||||||
|
return reject(new Error(errorMsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: any = result.content
|
||||||
|
if (!content) {
|
||||||
|
console.error('[special] 文件内容为空')
|
||||||
|
return reject(new Error('文件内容为空'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理不同的数据格式,确保返回 ArrayBuffer
|
||||||
|
let arrayBuffer: ArrayBuffer
|
||||||
|
try {
|
||||||
|
if (content instanceof ArrayBuffer) {
|
||||||
|
arrayBuffer = content
|
||||||
|
} else if (content instanceof Uint8Array) {
|
||||||
|
arrayBuffer = content.buffer.slice(
|
||||||
|
content.byteOffset,
|
||||||
|
content.byteOffset + content.byteLength
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
typeof Buffer !== 'undefined' &&
|
||||||
|
(content as any) instanceof Buffer
|
||||||
|
) {
|
||||||
|
// Node.js Buffer 转换为 ArrayBuffer
|
||||||
|
arrayBuffer = (content as any).buffer.slice(
|
||||||
|
(content as any).byteOffset,
|
||||||
|
(content as any).byteOffset + (content as any).byteLength
|
||||||
|
)
|
||||||
|
} else if (Array.isArray(content)) {
|
||||||
|
// 数组转换为 ArrayBuffer
|
||||||
|
arrayBuffer = new Uint8Array(content as number[]).buffer
|
||||||
|
} else if ((content as any).data && Array.isArray((content as any).data)) {
|
||||||
|
// 包装的数组数据
|
||||||
|
arrayBuffer = new Uint8Array((content as any).data).buffer
|
||||||
|
} else if (
|
||||||
|
typeof content === 'object' &&
|
||||||
|
(content as any).type === 'Buffer' &&
|
||||||
|
Array.isArray((content as any).data)
|
||||||
|
) {
|
||||||
|
// Node.js Buffer 的 JSON 序列化格式
|
||||||
|
arrayBuffer = new Uint8Array((content as any).data).buffer
|
||||||
|
} else if (
|
||||||
|
typeof content === 'object' &&
|
||||||
|
(content as any).constructor === Object
|
||||||
|
) {
|
||||||
|
// 处理普通对象格式的 Buffer
|
||||||
|
const values = Object.values(content as Record<string, number>)
|
||||||
|
if (values.length > 0 && typeof values[0] === 'number') {
|
||||||
|
arrayBuffer = new Uint8Array(values as number[]).buffer
|
||||||
|
} else {
|
||||||
|
console.error('[special] 无效的本地文件数据格式对象:', content)
|
||||||
|
throw new Error('无效的本地文件数据格式')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('[special] 未知的数据格式:', typeof content, content)
|
||||||
|
throw new Error(`未知的数据格式: ${typeof content}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[special] 文件读取成功,大小:', arrayBuffer.byteLength, '字节')
|
||||||
|
|
||||||
|
// 调用进度回调
|
||||||
|
if (onProgress) {
|
||||||
|
console.log('[special] onProgress 回调上报最终大小')
|
||||||
|
onProgress(arrayBuffer.byteLength, arrayBuffer.byteLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回 ArrayBuffer,不需要创建 blob URL
|
||||||
|
resolve(arrayBuffer)
|
||||||
|
} catch (conversionError: any) {
|
||||||
|
console.error('[special] 数据格式转换失败:', conversionError)
|
||||||
|
reject(new Error(`数据格式转换失败: ${conversionError.message}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[special] requestLocalFile 调用失败:', err)
|
||||||
|
reject(new Error('读取本地文件失败: ' + (err && (err.message || err))))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
const isApple =
|
const isApple =
|
||||||
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
|
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
|
||||||
|
|
@ -50,28 +223,68 @@ window.onload = function () {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const filePath = params.get('filePath')
|
const filePath = params.get('filePath')
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const url = new URL(filePath, window.location.href).toString()
|
// 特殊路径识别和特殊获取逻辑
|
||||||
fetch(url)
|
const localFilePath = extractLocalFilePathFromUrl(filePath)
|
||||||
.then(res => res.text())
|
if (localFilePath) {
|
||||||
.then(text => {
|
// 走特殊情况:通过 Electron API 读取本地文件
|
||||||
try {
|
requestLocalFile(localFilePath, (loaded, total) => {
|
||||||
let editorData
|
console.log(`[iframe] 本地文件读取进度: ${loaded}/${total}`)
|
||||||
|
})
|
||||||
|
.then(arrayBuffer => {
|
||||||
|
let text = ''
|
||||||
try {
|
try {
|
||||||
editorData = JSON.parse(text)
|
const decoder = new TextDecoder('utf-8')
|
||||||
} catch {
|
text = decoder.decode(arrayBuffer)
|
||||||
editorData = { main: [{ value: text }] }
|
} catch (e) {
|
||||||
|
const uint8 = new Uint8Array(arrayBuffer)
|
||||||
|
text = Array.from(uint8)
|
||||||
|
.map(code => String.fromCharCode(code))
|
||||||
|
.join('')
|
||||||
}
|
}
|
||||||
instance.command.executeSetValue(editorData)
|
|
||||||
console.log('[iframe] 根据 filePath 加载完成:', url)
|
try {
|
||||||
} catch (err) {
|
let editorData
|
||||||
console.error('[iframe] 加载失败:', err)
|
try {
|
||||||
alert('通过参数加载文件失败,请检查格式与同源权限')
|
editorData = JSON.parse(text)
|
||||||
}
|
} catch {
|
||||||
})
|
editorData = { main: [{ value: text }] }
|
||||||
.catch(err => {
|
}
|
||||||
console.error('[iframe] 获取文件失败:', err)
|
instance.command.executeSetValue(editorData)
|
||||||
alert('无法获取 filePath 指定的文件')
|
console.log('[iframe] 通过 Electron 加载本地文件完成:', localFilePath)
|
||||||
})
|
} catch (err) {
|
||||||
|
console.error('[iframe] 本地文件解析失败:', err)
|
||||||
|
alert('本地文件解析失败,请检查格式')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[iframe] 本地文件读取失败:', err)
|
||||||
|
alert('读取本地文件失败,请检查路径格式或环境')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 非特殊情况:保持原有同源路径 fetch 行为
|
||||||
|
const url = new URL(filePath, window.location.href).toString()
|
||||||
|
fetch(url)
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(text => {
|
||||||
|
try {
|
||||||
|
let editorData
|
||||||
|
try {
|
||||||
|
editorData = JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
editorData = { main: [{ value: text }] }
|
||||||
|
}
|
||||||
|
instance.command.executeSetValue(editorData)
|
||||||
|
console.log('[iframe] 根据 filePath 加载完成:', url)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[iframe] 加载失败:', err)
|
||||||
|
alert('通过参数加载文件失败,请检查格式与同源权限')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[iframe] 获取文件失败:', err)
|
||||||
|
alert('无法获取 filePath 指定的文件')
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单弹窗销毁
|
// 菜单弹窗销毁
|
||||||
|
|
@ -153,7 +366,10 @@ window.onload = function () {
|
||||||
|
|
||||||
const downloadLink = document.createElement('a')
|
const downloadLink = document.createElement('a')
|
||||||
downloadLink.href = url
|
downloadLink.href = url
|
||||||
downloadLink.download = `document_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`
|
downloadLink.download = `document_${new Date()
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace(/:/g, '-')}.json`
|
||||||
downloadLink.style.display = 'none'
|
downloadLink.style.display = 'none'
|
||||||
|
|
||||||
document.body.appendChild(downloadLink)
|
document.body.appendChild(downloadLink)
|
||||||
|
|
@ -170,14 +386,16 @@ window.onload = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存(调用父窗口回调)
|
// 保存(调用父窗口回调)
|
||||||
const saveCallbackDom = document.querySelector<HTMLDivElement>('.menu-item__save')!
|
const saveCallbackDom =
|
||||||
|
document.querySelector<HTMLDivElement>('.menu-item__save')!
|
||||||
saveCallbackDom.onclick = function () {
|
saveCallbackDom.onclick = function () {
|
||||||
try {
|
try {
|
||||||
const editorValue = instance.command.getValue()
|
const editorValue = instance.command.getValue()
|
||||||
const content = JSON.stringify(editorValue.data, null, 2)
|
const content = JSON.stringify(editorValue.data, null, 2)
|
||||||
|
|
||||||
const parentWin: any = window.parent || window
|
const parentWin: any = window.parent || window
|
||||||
const viewer = (parentWin && parentWin.OCDViewer) || (window as any).OCDViewer
|
const viewer =
|
||||||
|
(parentWin && parentWin.OCDViewer) || (window as any).OCDViewer
|
||||||
|
|
||||||
if (viewer && typeof viewer.onSave === 'function') {
|
if (viewer && typeof viewer.onSave === 'function') {
|
||||||
viewer.onSave(content)
|
viewer.onSave(content)
|
||||||
|
|
@ -194,12 +412,14 @@ window.onload = function () {
|
||||||
|
|
||||||
// 全局快捷键:Ctrl+S / ⌘+S 触发保存
|
// 全局快捷键:Ctrl+S / ⌘+S 触发保存
|
||||||
window.addEventListener('keydown', function (e) {
|
window.addEventListener('keydown', function (e) {
|
||||||
const isSaveKey = (e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')
|
const isSaveKey =
|
||||||
|
(e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')
|
||||||
if (!isSaveKey) return
|
if (!isSaveKey) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const isEditMode = params.get('mode') === 'edit' || params.get('model') === 'edit'
|
const isEditMode =
|
||||||
|
params.get('mode') === 'edit' || params.get('model') === 'edit'
|
||||||
if (!isEditMode) {
|
if (!isEditMode) {
|
||||||
console.warn('当前为只读模式,Ctrl+S 不可用')
|
console.warn('当前为只读模式,Ctrl+S 不可用')
|
||||||
alert('当前为只读模式,不能保存')
|
alert('当前为只读模式,不能保存')
|
||||||
|
|
@ -214,7 +434,9 @@ window.onload = function () {
|
||||||
const winAny = window as any
|
const winAny = window as any
|
||||||
winAny.showSaveStatus = (text: string, color?: string, delay?: number) => {
|
winAny.showSaveStatus = (text: string, color?: string, delay?: number) => {
|
||||||
try {
|
try {
|
||||||
const indicator = document.getElementById('save-status-indicator') as HTMLDivElement | null
|
const indicator = document.getElementById(
|
||||||
|
'save-status-indicator'
|
||||||
|
) as HTMLDivElement | null
|
||||||
if (!indicator) return
|
if (!indicator) return
|
||||||
const duration = typeof delay === 'number' ? delay : 2000
|
const duration = typeof delay === 'number' ? delay : 2000
|
||||||
indicator.textContent = text || '已保存'
|
indicator.textContent = text || '已保存'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user