feat: 添加本地文件读取功能并优化构建脚本

添加从本地文件URL提取路径和通过Electron API读取本地文件的功能,支持多种数据格式转换。同时新增构建脚本快捷方式bs和bt,优化开发工作流程。
This commit is contained in:
hanshiyang 2025-11-20 14:34:58 +08:00
parent f5dbe4470f
commit d6444c2798
2 changed files with 263 additions and 39 deletions

View File

@ -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/'"

View File

@ -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 || '已保存'