- 拆分 FileSystem.vue 为模块化组件架构 - 新增 Markdown Mermaid 图表渲染支持 - 新增 180+ 编程语言代码高亮 - 修复编辑/预览模式切换渲染问题 - 优化亮色/暗色模式主题适配 - 新增 TypeScript 类型定义
370 lines
9.0 KiB
JavaScript
370 lines
9.0 KiB
JavaScript
/**
|
||
* 文件编辑和保存逻辑 composable
|
||
*
|
||
* @module composables/useFileEdit
|
||
* @description 封装文件编辑、保存、草稿管理等逻辑
|
||
*/
|
||
|
||
import { ref, computed, watch } from 'vue'
|
||
import { Message, Modal } from '@arco-design/web-vue'
|
||
import { STORAGE_KEYS } from '@/utils/constants'
|
||
|
||
/**
|
||
* 草稿存储键
|
||
*/
|
||
const DRAFT_STORAGE_KEY = 'filesystem_draft_content'
|
||
|
||
/**
|
||
* 文件编辑 composable
|
||
* @param {Object} options - 配置选项
|
||
* @param {Ref<string>} options.filePath - 当前文件路径
|
||
* @param {Ref<string>} options.fileContent - 文件内容
|
||
* @param {Function} options.onWriteFile - 写入文件的函数
|
||
* @param {Function} options.onReset - 重置内容的函数
|
||
* @returns {UseFileEditReturn} 文件编辑操作 API
|
||
*/
|
||
export function useFileEdit(options = {}) {
|
||
const {
|
||
filePath,
|
||
fileContent,
|
||
onWriteFile,
|
||
onReset,
|
||
} = options
|
||
|
||
// ========== 编辑状态 ==========
|
||
|
||
/**
|
||
* 是否正在保存
|
||
* @type {Ref<boolean>}
|
||
*/
|
||
const isSaving = ref(false)
|
||
|
||
/**
|
||
* 是否是快捷键触发的保存
|
||
* @type {Ref<boolean>}
|
||
*/
|
||
const isShortcutSave = ref(false)
|
||
|
||
/**
|
||
* 保存成功提示消息
|
||
* @type {Ref<string>}
|
||
*/
|
||
const saveSuccessMessage = ref('')
|
||
|
||
/**
|
||
* 原始文件内容(用于检测变更)
|
||
* @type {Ref<string>}
|
||
*/
|
||
const originalContent = ref('')
|
||
|
||
/**
|
||
* 是否为编辑模式
|
||
* @type {Ref<boolean>}
|
||
*/
|
||
const isEditMode = ref(localStorage.getItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE) === 'true')
|
||
|
||
// ========== 计算属性 ==========
|
||
|
||
/**
|
||
* 文件内容是否已修改
|
||
*/
|
||
const isFileModified = computed(() => {
|
||
return originalContent.value !== undefined &&
|
||
originalContent.value !== fileContent.value
|
||
})
|
||
|
||
/**
|
||
* 内容是否发生变化(用于按钮禁用判断)
|
||
*/
|
||
const contentChanged = computed(() => {
|
||
return fileContent.value !== '' &&
|
||
fileContent.value !== originalContent.value
|
||
})
|
||
|
||
/**
|
||
* 是否可以保存文件
|
||
*/
|
||
const canSaveFile = computed(() => {
|
||
return isEditMode.value && contentChanged.value
|
||
})
|
||
|
||
/**
|
||
* 是否可以重置内容
|
||
*/
|
||
const canResetContent = computed(() => {
|
||
return isEditMode.value &&
|
||
contentChanged.value &&
|
||
originalContent.value !== undefined
|
||
})
|
||
|
||
// ========== 草稿管理 ==========
|
||
|
||
/**
|
||
* 保存草稿到 localStorage
|
||
*/
|
||
const saveDraft = () => {
|
||
try {
|
||
const draft = {
|
||
content: fileContent.value,
|
||
path: filePath.value,
|
||
timestamp: Date.now(),
|
||
}
|
||
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft))
|
||
localStorage.setItem(DRAFT_STORAGE_KEY + '_time', Date.now().toString())
|
||
} catch (error) {
|
||
console.warn('[saveDraft] 保存草稿失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除草稿
|
||
*/
|
||
const clearDraft = () => {
|
||
try {
|
||
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
||
localStorage.removeItem(DRAFT_STORAGE_KEY + '_time')
|
||
} catch (error) {
|
||
console.warn('[clearDraft] 清除草稿失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载草稿
|
||
* @returns {Object|null} 草稿数据
|
||
*/
|
||
const loadDraft = () => {
|
||
try {
|
||
const draftStr = localStorage.getItem(DRAFT_STORAGE_KEY)
|
||
if (!draftStr) return null
|
||
|
||
const draft = JSON.parse(draftStr)
|
||
|
||
// 检查草稿是否过期(24小时)
|
||
const timeStr = localStorage.getItem(DRAFT_STORAGE_KEY + '_time')
|
||
if (timeStr) {
|
||
const time = parseInt(timeStr, 10)
|
||
const now = Date.now()
|
||
const hours = (now - time) / (1000 * 60 * 60)
|
||
|
||
if (hours > 24) {
|
||
clearDraft()
|
||
return null
|
||
}
|
||
}
|
||
|
||
return draft
|
||
} catch (error) {
|
||
console.warn('[loadDraft] 加载草稿失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// ========== 保存操作 ==========
|
||
|
||
/**
|
||
* 显示手动保存对话框
|
||
* @param {boolean} isShortcut - 是否是快捷键触发
|
||
*/
|
||
const showManualSaveDialog = (isShortcut) => {
|
||
isShortcutSave.value = isShortcut
|
||
|
||
Modal.confirm({
|
||
title: '保存文件',
|
||
content: `确定要保存文件 ${filePath.value} 吗?`,
|
||
okText: '保存',
|
||
cancelText: '取消',
|
||
onOk: () => {
|
||
saveToFile(filePath.value, getFileName(filePath.value), isShortcut)
|
||
},
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 保存到文件
|
||
* @param {string} targetPath - 目标路径
|
||
* @param {string} fileName - 文件名
|
||
* @param {boolean} isShortcut - 是否是快捷键触发
|
||
* @returns {Promise<boolean>} 是否成功
|
||
*/
|
||
const saveToFile = async (targetPath, fileName, isShortcut) => {
|
||
isSaving.value = true
|
||
try {
|
||
const success = await onWriteFile(fileContent.value, targetPath, fileName, isShortcut)
|
||
|
||
if (success) {
|
||
originalContent.value = fileContent.value
|
||
clearDraft()
|
||
}
|
||
|
||
return success
|
||
} finally {
|
||
isSaving.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理保存内容
|
||
* @returns {Promise<boolean>} 是否成功
|
||
*/
|
||
const handleSaveContent = async () => {
|
||
if (!canSaveFile.value) {
|
||
return false
|
||
}
|
||
|
||
return await saveToFile(filePath.value, getFileName(filePath.value), false)
|
||
}
|
||
|
||
/**
|
||
* 另存为
|
||
*/
|
||
const handleSaveAs = async () => {
|
||
try {
|
||
// 简单实现:使用 prompt 获取路径
|
||
const targetPath = prompt('请输入保存路径:', filePath.value)
|
||
|
||
if (!targetPath) {
|
||
return false
|
||
}
|
||
|
||
const fileName = getFileName(targetPath)
|
||
return await saveToFile(targetPath, fileName, false)
|
||
} catch (error) {
|
||
Message.error(`保存对话框失败: ${error.message || error}`)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理写入文件(快捷键或按钮)
|
||
* @param {boolean} isShortcut - 是否是快捷键触发
|
||
* @returns {Promise<boolean>} 是否成功
|
||
*/
|
||
const handleWriteFile = async (isShortcut = false) => {
|
||
if (!fileContent.value || !filePath.value) {
|
||
Message.warning('没有可保存的内容')
|
||
return false
|
||
}
|
||
|
||
// 如果内容未修改,快捷键保存时静默返回
|
||
if (!isFileModified.value && isShortcut) {
|
||
return false
|
||
}
|
||
|
||
// 快捷键:静默保存
|
||
if (isShortcut) {
|
||
return await saveToFile(filePath.value, getFileName(filePath.value), true)
|
||
}
|
||
|
||
// 按钮:显示确认对话框
|
||
showManualSaveDialog(false)
|
||
return false
|
||
}
|
||
|
||
// ========== 重置操作 ==========
|
||
|
||
/**
|
||
* 重置内容到原始状态
|
||
*/
|
||
const resetContent = () => {
|
||
if (onReset) {
|
||
onReset()
|
||
} else {
|
||
fileContent.value = originalContent.value
|
||
}
|
||
}
|
||
|
||
// ========== 编辑模式切换 ==========
|
||
|
||
/**
|
||
* 切换编辑模式
|
||
*/
|
||
const toggleEditMode = () => {
|
||
isEditMode.value = !isEditMode.value
|
||
|
||
// 持久化
|
||
try {
|
||
localStorage.setItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE, isEditMode.value.toString())
|
||
} catch (e) {
|
||
console.warn('[toggleEditMode] 保存编辑模式失败:', e)
|
||
}
|
||
|
||
// 进入编辑模式时,记录原始内容
|
||
if (isEditMode.value) {
|
||
originalContent.value = fileContent.value
|
||
}
|
||
}
|
||
|
||
// ========== 工具函数 ==========
|
||
|
||
/**
|
||
* 从路径获取文件名
|
||
* @param {string} path - 文件路径
|
||
* @returns {string} 文件名
|
||
*/
|
||
const getFileName = (path) => {
|
||
if (!path) return ''
|
||
const parts = path.split(/[/\\]/)
|
||
return parts[parts.length - 1] || path
|
||
}
|
||
|
||
// ========== 监听内容变化 ==========
|
||
|
||
/**
|
||
* 监听文件内容变化,自动保存草稿
|
||
*/
|
||
watch(fileContent, () => {
|
||
if (fileContent.value && fileContent.value !== originalContent.value) {
|
||
saveDraft()
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 监听文件路径变化,更新原始内容
|
||
*/
|
||
watch(filePath, () => {
|
||
originalContent.value = fileContent.value
|
||
})
|
||
|
||
return {
|
||
// 状态
|
||
isSaving,
|
||
isShortcutSave,
|
||
saveSuccessMessage,
|
||
originalContent,
|
||
isEditMode,
|
||
isFileModified,
|
||
canSaveFile,
|
||
canResetContent,
|
||
|
||
// 方法
|
||
saveDraft,
|
||
clearDraft,
|
||
loadDraft,
|
||
handleSaveContent,
|
||
handleSaveAs,
|
||
handleWriteFile,
|
||
resetContent,
|
||
toggleEditMode,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} UseFileEditReturn
|
||
* @property {Ref<boolean>} isSaving - 是否正在保存
|
||
* @property {Ref<boolean>} isShortcutSave - 是否是快捷键触发
|
||
* @property {Ref<string>} saveSuccessMessage - 保存成功提示消息
|
||
* @property {Ref<string>} originalContent - 原始文件内容
|
||
* @property {Ref<boolean>} isEditMode - 是否为编辑模式
|
||
* @property {ComputedRef<boolean>} isFileModified - 文件内容是否已修改
|
||
* @property {ComputedRef<boolean>} canSaveFile - 是否可以保存文件
|
||
* @property {ComputedRef<boolean>} canResetContent - 是否可以重置内容
|
||
* @property {Function} saveDraft - 保存草稿
|
||
* @property {Function} clearDraft - 清除草稿
|
||
* @property {Function} loadDraft - 加载草稿
|
||
* @property {Function} handleSaveContent - 处理保存内容
|
||
* @property {Function} handleSaveAs - 另存为
|
||
* @property {Function} handleWriteFile - 处理写入文件
|
||
* @property {Function} resetContent - 重置内容
|
||
* @property {Function} toggleEditMode - 切换编辑模式
|
||
*/
|