Private
Public Access
1
0
Files
u-desk/web/src/composables/useFileEdit.js
绝尘 a5d30684ed 重构:文件系统模块化架构,增强 Markdown 渲染
- 拆分 FileSystem.vue 为模块化组件架构
- 新增 Markdown Mermaid 图表渲染支持
- 新增 180+ 编程语言代码高亮
- 修复编辑/预览模式切换渲染问题
- 优化亮色/暗色模式主题适配
- 新增 TypeScript 类型定义
2026-02-04 03:32:46 +08:00

370 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 文件编辑和保存逻辑 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 - 切换编辑模式
*/