重构:文件系统模块化架构,增强 Markdown 渲染
- 拆分 FileSystem.vue 为模块化组件架构 - 新增 Markdown Mermaid 图表渲染支持 - 新增 180+ 编程语言代码高亮 - 修复编辑/预览模式切换渲染问题 - 优化亮色/暗色模式主题适配 - 新增 TypeScript 类型定义
This commit is contained in:
369
web/src/composables/useFileEdit.js
Normal file
369
web/src/composables/useFileEdit.js
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 文件编辑和保存逻辑 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 - 切换编辑模式
|
||||
*/
|
||||
Reference in New Issue
Block a user