feat: 添加本地文件读取功能并优化构建脚本
添加从本地文件URL提取路径和通过Electron API读取本地文件的功能,支持多种数据格式转换。同时新增构建脚本快捷方式bs和bt,优化开发工作流程。
This commit is contained in:
parent
f5dbe4470f
commit
d6444c2798
|
|
@ -49,6 +49,8 @@
|
|||
"release": "node scripts/release.js",
|
||||
"ball": "vite build && pnpm ct && pnpm cs && 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/'",
|
||||
"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/'"
|
||||
|
|
|
|||
300
src/main.ts
300
src/main.ts
|
|
@ -27,6 +27,179 @@ import { formatPrismToken } from './utils/prism'
|
|||
import { Signature } from './components/signature/Signature'
|
||||
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 () {
|
||||
const isApple =
|
||||
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
|
||||
|
|
@ -50,28 +223,68 @@ window.onload = function () {
|
|||
const params = new URLSearchParams(window.location.search)
|
||||
const filePath = params.get('filePath')
|
||||
if (filePath) {
|
||||
const url = new URL(filePath, window.location.href).toString()
|
||||
fetch(url)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
try {
|
||||
let editorData
|
||||
// 特殊路径识别和特殊获取逻辑
|
||||
const localFilePath = extractLocalFilePathFromUrl(filePath)
|
||||
if (localFilePath) {
|
||||
// 走特殊情况:通过 Electron API 读取本地文件
|
||||
requestLocalFile(localFilePath, (loaded, total) => {
|
||||
console.log(`[iframe] 本地文件读取进度: ${loaded}/${total}`)
|
||||
})
|
||||
.then(arrayBuffer => {
|
||||
let text = ''
|
||||
try {
|
||||
editorData = JSON.parse(text)
|
||||
} catch {
|
||||
editorData = { main: [{ value: text }] }
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
text = decoder.decode(arrayBuffer)
|
||||
} 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)
|
||||
} catch (err) {
|
||||
console.error('[iframe] 加载失败:', err)
|
||||
alert('通过参数加载文件失败,请检查格式与同源权限')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[iframe] 获取文件失败:', err)
|
||||
alert('无法获取 filePath 指定的文件')
|
||||
})
|
||||
|
||||
try {
|
||||
let editorData
|
||||
try {
|
||||
editorData = JSON.parse(text)
|
||||
} catch {
|
||||
editorData = { main: [{ value: text }] }
|
||||
}
|
||||
instance.command.executeSetValue(editorData)
|
||||
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 指定的文件')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单弹窗销毁
|
||||
|
|
@ -88,28 +301,28 @@ window.onload = function () {
|
|||
)
|
||||
|
||||
// 2. | 打开 | 保存 | 撤销 | 重做 | 格式刷 | 清除格式 |
|
||||
|
||||
|
||||
// 打开文档功能
|
||||
const openDom = document.querySelector<HTMLDivElement>('.menu-item__open')!
|
||||
openDom.onclick = function () {
|
||||
console.log('open document')
|
||||
|
||||
|
||||
// 创建文件输入元素
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.json,.txt'
|
||||
fileInput.style.display = 'none'
|
||||
|
||||
|
||||
fileInput.onchange = function (event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (e) {
|
||||
try {
|
||||
const content = e.target?.result as string
|
||||
let editorData
|
||||
|
||||
|
||||
// 尝试解析JSON格式
|
||||
try {
|
||||
editorData = JSON.parse(content)
|
||||
|
|
@ -119,7 +332,7 @@ window.onload = function () {
|
|||
main: [{ value: content }]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 设置编辑器内容
|
||||
instance.command.executeSetValue(editorData)
|
||||
console.log('文档已打开')
|
||||
|
|
@ -128,11 +341,11 @@ window.onload = function () {
|
|||
alert('打开文档失败,请检查文件格式')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
reader.readAsText(file)
|
||||
document.body.removeChild(fileInput)
|
||||
}
|
||||
|
||||
|
||||
document.body.appendChild(fileInput)
|
||||
fileInput.click()
|
||||
}
|
||||
|
|
@ -141,25 +354,28 @@ window.onload = function () {
|
|||
const saveDom = document.querySelector<HTMLDivElement>('.menu-item__export')!
|
||||
saveDom.onclick = function () {
|
||||
console.log('export document')
|
||||
|
||||
|
||||
try {
|
||||
// 获取编辑器内容
|
||||
const editorValue = instance.command.getValue()
|
||||
const content = JSON.stringify(editorValue.data, null, 2)
|
||||
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([content], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
|
||||
const downloadLink = document.createElement('a')
|
||||
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'
|
||||
|
||||
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
|
||||
|
||||
// 清理URL对象
|
||||
URL.revokeObjectURL(url)
|
||||
console.log('文档已保存')
|
||||
|
|
@ -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 () {
|
||||
try {
|
||||
const editorValue = instance.command.getValue()
|
||||
const content = JSON.stringify(editorValue.data, null, 2)
|
||||
|
||||
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') {
|
||||
viewer.onSave(content)
|
||||
|
|
@ -194,12 +412,14 @@ window.onload = function () {
|
|||
|
||||
// 全局快捷键:Ctrl+S / ⌘+S 触发保存
|
||||
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
|
||||
e.preventDefault()
|
||||
|
||||
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) {
|
||||
console.warn('当前为只读模式,Ctrl+S 不可用')
|
||||
alert('当前为只读模式,不能保存')
|
||||
|
|
@ -214,7 +434,9 @@ window.onload = function () {
|
|||
const winAny = window as any
|
||||
winAny.showSaveStatus = (text: string, color?: string, delay?: number) => {
|
||||
try {
|
||||
const indicator = document.getElementById('save-status-indicator') as HTMLDivElement | null
|
||||
const indicator = document.getElementById(
|
||||
'save-status-indicator'
|
||||
) as HTMLDivElement | null
|
||||
if (!indicator) return
|
||||
const duration = typeof delay === 'number' ? delay : 2000
|
||||
indicator.textContent = text || '已保存'
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user