Private
Public Access
1
0

新增:Markdown编辑器/数据库优化/安全修复

- Markdown 编辑器:实时预览、PDF 导出、独立查看器
- 数据库优化:动态连接池、查询缓存、Redis Pipeline
- 窗口置顶功能
- 文件系统增强:右键菜单、编辑器集成、收藏夹重构
- 安全修复:XSS 防护、路径穿越、HTML 注入
- 代码质量:正则预编译、缓存锁优化、死代码清理
This commit is contained in:
2026-03-31 09:18:06 +08:00
parent 5f94ccf13b
commit e5dbe89a6f
59 changed files with 5289 additions and 1316 deletions

View File

@@ -20,6 +20,13 @@
</template>
</a-button>
</a-tooltip>
<a-tooltip :content="isPinned ? '取消置顶' : '窗口置顶'">
<a-button type="text" :class="{ 'pin-active': isPinned }" @click="handleTogglePin">
<template #icon>
<IconPushpin :class="{ pinned: isPinned }"/>
</template>
</a-button>
</a-tooltip>
<ThemeToggle/>
<!-- 窗口控制按钮 -->
@@ -71,9 +78,10 @@
</a-layout>
</template>
<script setup>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
import {IconSettings} from '@arco-design/web-vue/es/icon'
import {IconSettings, IconPushpin} from '@arco-design/web-vue/es/icon'
import MarkdownEditor from './views/markdown-editor/index.vue'
import DbCli from './views/db-cli/index.vue'
import ThemeToggle from './components/ThemeToggle.vue'
import FileSystem from './components/FileSystem/index.vue'
@@ -81,7 +89,6 @@ import SettingsPanel from './components/SettingsPanel.vue'
import UpdateNotification from './components/UpdateNotification.vue'
import {useUpdateStore} from './stores/update'
import {useConfigStore} from './stores/config'
import {preloadCommonLanguages} from './utils/codeMirrorLoader'
// 存储键
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
@@ -91,6 +98,7 @@ const savedTab = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
const activeTab = ref((savedTab === 'user' ? 'file-system' : savedTab) || 'file-system')
const showSettings = ref(false)
const isMaximized = ref(false)
const isPinned = ref(false)
// 使用 stores
const updateStore = useUpdateStore()
@@ -129,21 +137,27 @@ const loadConfig = async () => {
const getComponent = (key) => {
const components = {
'file-system': FileSystem,
'db-cli': DbCli
'db-cli': DbCli,
'markdown-editor': MarkdownEditor
}
return components[key] || null
}
// 组件挂载时加载配置
// 禁止 Ctrl+滚轮缩放
const preventZoom = (e: WheelEvent) => {
if (e.ctrlKey) e.preventDefault()
}
onMounted(() => {
loadConfig()
// 预加载常用编辑器语言包
preloadCommonLanguages()
// 设置更新事件监听
updateStore.setupEventListeners()
// 禁止 Ctrl+滚轮缩放
document.addEventListener('wheel', preventZoom, { passive: false })
// 延迟检查更新(启动后 3 秒,静默模式)
setTimeout(() => {
updateStore.checkForUpdates(true)
@@ -152,6 +166,7 @@ onMounted(() => {
// 组件卸载时清理事件监听
onUnmounted(() => {
document.removeEventListener('wheel', preventZoom)
updateStore.removeEventListeners()
})
@@ -166,6 +181,16 @@ const handleMinimize = async () => {
}
}
const handleTogglePin = async () => {
try {
if (window.go?.main?.App?.WindowToggleAlwaysOnTop) {
isPinned.value = await window.go.main.App.WindowToggleAlwaysOnTop()
}
} catch (error) {
console.error('切换置顶失败:', error)
}
}
const handleMaximize = async () => {
try {
if (window.go?.main?.App?.WindowMaximize) {
@@ -282,6 +307,25 @@ watch(activeTab, (newTab) => {
color: #ffffff;
}
.pin-active {
color: rgb(var(--primary-6)) !important;
}
.pin-active :deep(svg) {
transform: none !important;
opacity: 1 !important;
}
.header-actions :deep(.arco-icon-pushpin) {
transform: rotate(45deg);
opacity: 0.6;
}
.header-actions :deep(.arco-icon-pushpin.pinned) {
transform: none;
opacity: 1;
}
.window-control-btn svg {
display: block;
pointer-events: none;

View File

@@ -76,7 +76,7 @@
style="cursor: pointer; margin-bottom: 4px"
>
<template #icon>
<span>{{ fav.is_dir ? '📁' : '📄' }}</span>
<span>{{ fav.isDir ? '📁' : '📄' }}</span>
</template>
{{ fav.name }}
</a-tag>
@@ -504,7 +504,7 @@ const openFavoriteFile = (path) => {
addToHistory(path)
const fav = favoriteFiles.value.find(f => f.path === path)
if (fav && fav.is_dir) {
if (fav && fav.isDir) {
listDirectory()
} else {
readFile()

View File

@@ -1,8 +1,9 @@
<template>
<div
v-if="config.visible"
ref="menuRef"
class="context-menu"
:style="{ left: config.x + 'px', top: config.y + 'px' }"
:style="menuStyle"
@click.stop
>
<!-- 空白区域菜单 -->
@@ -21,6 +22,16 @@
<!-- 文件菜单 -->
<template v-else-if="config.context === 'file' && config.selectedFile">
<div class="context-menu-item" @click="handleCreateFile">
<span class="context-menu-icon">📄</span>
<span>新建文件</span>
<span class="context-menu-shortcut">Ctrl+N</span>
</div>
<div class="context-menu-item" @click="handleCreateDir">
<span class="context-menu-icon">📁</span>
<span>新建文件夹</span>
<span class="context-menu-shortcut">Ctrl+Shift+N</span>
</div>
<div class="context-menu-divider"></div>
<div
v-if="!config.selectedFile.is_dir && isOfficeFile(config.selectedFile.name)"
@@ -46,9 +57,12 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import type { ContextMenuConfig, FileItem } from '@/types/file-system'
import { isOfficeFile } from '@/utils/fileTypeHelpers'
const menuRef = ref<HTMLElement>()
// Props
interface Props {
config: ContextMenuConfig
@@ -64,6 +78,26 @@ interface Emits {
const emit = defineEmits<Emits>()
const menuStyle = computed(() => {
return { left: props.config.x + 'px', top: props.config.y + 'px' }
})
// 位置修正:渲染后检查菜单是否超出视口,自动调整位置
watch(() => props.config.visible, (visible) => {
if (!visible) return
nextTick(() => {
const el = menuRef.value
if (!el) return
const rect = el.getBoundingClientRect()
if (rect.right > window.innerWidth) {
el.style.left = (window.innerWidth - rect.width - 4) + 'px'
}
if (rect.bottom > window.innerHeight) {
el.style.top = (window.innerHeight - rect.height - 4) + 'px'
}
})
})
/**
* 处理菜单项点击
*/

View File

@@ -84,8 +84,8 @@ const error = ref('')
const children = ref<Array<{ name: string; path: string; isDir: boolean }>>([])
const style = ref<{ top: string; left: string }>({ top: '0px', left: '0px' })
const hoverTimer = ref<number | null>(null)
const leaveTimer = ref<number | null>(null)
const hoverTimer = ref<NodeJS.Timeout | null>(null)
const leaveTimer = ref<NodeJS.Timeout | null>(null)
const hoveringMenu = ref(false)
const menuKey = `menu-${props.item.path}-${props.level}`

View File

@@ -1,5 +1,5 @@
<template>
<div class="file-editor-panel" :style="{ width: width + '%' }">
<div ref="panelRef" class="file-editor-panel" :style="{ width: width + '%' }">
<div class="panel-header">
<span class="panel-title">
<template v-if="config.isImageView">🖼 图片预览</template>
@@ -13,7 +13,14 @@
<template v-else-if="config.isCsvFile">📋 CSV 预览</template>
<template v-else>📝 文件内容</template>
</span>
<div v-if="config.currentFileName" class="filename-with-copy">
<div class="header-actions">
<a-tooltip v-if="config.currentFileName" content="全屏预览 (F11)" position="left">
<a-button size="mini" type="text" @click="toggleFullscreen">
<icon-fullscreen v-if="!isFullscreen" />
<icon-fullscreen-exit v-else />
</a-button>
</a-tooltip>
<div v-if="config.currentFileName" class="filename-with-copy">
<a-tooltip :content="config.currentFileFullPath" position="left">
<span
class="panel-filename"
@@ -28,6 +35,7 @@
@click="handleCopyPath"
/>
</div>
</div>
</div>
<div class="editor-content">
@@ -194,6 +202,16 @@
<template #icon><icon-save /></template>
</a-button>
</a-tooltip>
<!-- PDF 导出按钮仅在预览模式显示 -->
<a-tooltip v-if="!config.isEditMode" position="left" content="导出">
<a-button
type="outline"
size="small"
@click="handleExportPDF"
>
<template #icon><icon-file-pdf /></template>
</a-button>
</a-tooltip>
<!-- 预览/编辑切换按钮 -->
<a-tooltip
position="left"
@@ -288,7 +306,7 @@
<script setup lang="ts">
import { computed, watch, nextTick, defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSave, IconEdit, IconEye, IconUndo, IconCopy } from '@arco-design/web-vue/es/icon'
import { IconSave, IconEdit, IconEye, IconUndo, IconCopy, IconFilePdf, IconFullscreen, IconFullscreenExit } from '@arco-design/web-vue/es/icon'
import { getFileName } from '@/utils/fileUtils'
import type { FileEditorPanelConfig } from '@/types/file-system'
import { renderMermaidDiagrams } from '@/utils/markedExtensions'
@@ -309,6 +327,30 @@ const csvPreviewRef = ref<HTMLElement | null>(null)
// Markdown 预览容器引用
const markdownPreviewRef = ref<HTMLElement | null>(null)
// 全屏
const panelRef = ref<HTMLElement | null>(null)
const isFullscreen = ref(false)
function toggleFullscreen() {
if (!panelRef.value) return
if (!document.fullscreenElement) {
panelRef.value.requestFullscreen().then(() => { isFullscreen.value = true })
} else {
document.exitFullscreen().then(() => { isFullscreen.value = false })
}
}
function onFullscreenChange() {
isFullscreen.value = !!document.fullscreenElement
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'F11' && props.config.currentFileName) {
e.preventDefault()
toggleFullscreen()
}
}
// Props
interface Props {
config: FileEditorPanelConfig
@@ -400,6 +442,165 @@ const handleImageError = () => {
emit('imageError')
}
// Markdown PDF 导出处理
const handleExportPDF = async () => {
try {
// 获取 Markdown 预览容器
const markdownContent = markdownPreviewRef.value
if (!markdownContent) {
Message.error('无法获取 Markdown 内容')
return
}
// 打开打印窗口
const printWindow = window.open('', '_blank')
if (!printWindow) {
Message.error('无法打开打印窗口,请检查浏览器设置')
return
}
// 设置打印样式
const style = `
<style>
@media print {
body {
font-size: 12pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 20px;
}
.no-print {
display: none !important;
}
.markdown-content {
max-width: 100%;
margin: 0;
padding: 0;
}
.markdown-content h1 {
font-size: 24pt;
margin-bottom: 12pt;
border-bottom: 2px solid #333;
}
.markdown-content h2 {
font-size: 18pt;
margin-bottom: 10pt;
border-bottom: 1px solid #ccc;
}
.markdown-content h3 {
font-size: 14pt;
margin-bottom: 8pt;
}
.markdown-content p {
margin-bottom: 10pt;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 10pt;
}
.markdown-content li {
margin-bottom: 4pt;
}
.markdown-content table {
border-collapse: collapse;
margin-bottom: 12pt;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.markdown-content th {
background-color: #f5f5f5;
font-weight: bold;
}
.markdown-content img {
max-width: 100%;
height: auto;
}
.markdown-content blockquote {
border-left: 4px solid #ddd;
margin: 16px 0;
padding: 10px 20px;
color: #666;
}
.markdown-content code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
}
.markdown-content pre {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
}
/* 优化屏幕显示 */
.markdown-content {
background: white;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
</style>
`
// 构建打印页面
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${props.config.currentFileName || 'Markdown 导出 PDF'}</title>
${style}
</head>
<body>
<div class="markdown-content">
${markdownContent.innerHTML}
</div>
</body>
</html>
`)
printWindow.document.close()
// 延迟一点时间让样式加载完成
setTimeout(() => {
printWindow.print()
// 不关闭窗口,让用户可以手动关闭或继续打印
}, 500)
Message.success('PDF 导出窗口已打开')
} catch (error) {
console.error('[handleExportPDF] 导出失败:', error)
Message.error('PDF 导出失败,请重试')
}
}
// 监听模式切换,切换到预览模式时渲染 Mermaid 图表
watch(() => props.config.isEditMode, async (newVal, oldVal) => {
// 从编辑模式切换到预览模式
@@ -607,9 +808,11 @@ const handleHtmlIframeMessage = (event: MessageEvent) => {
}
}
// 监听 iframe 的 postMessage
// 监听 iframe 的 postMessage + 全屏事件
onMounted(() => {
window.addEventListener('message', handleHtmlIframeMessage)
document.addEventListener('fullscreenchange', onFullscreenChange)
document.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
@@ -617,6 +820,8 @@ onUnmounted(() => {
markdownPreviewRef.value.removeEventListener('click', handleMarkdownLinkClick)
}
window.removeEventListener('message', handleHtmlIframeMessage)
document.removeEventListener('fullscreenchange', onFullscreenChange)
document.removeEventListener('keydown', onKeyDown)
})
</script>
@@ -628,6 +833,12 @@ onUnmounted(() => {
background: var(--color-bg-1);
}
.file-editor-panel:fullscreen {
width: 100vw !important;
height: 100vh;
z-index: 1000;
}
.panel-header {
display: flex;
align-items: center;
@@ -641,8 +852,25 @@ onUnmounted(() => {
gap: 12px;
}
.panel-header > * {
--wails-draggable: no-drag;
}
/* 仅全屏模式下 header 可拖动窗口 */
.file-editor-panel:fullscreen .panel-header {
--wails-draggable: drag;
}
.panel-title {
color: var(--color-text-1);
flex-shrink: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.filename-with-copy {
@@ -1056,16 +1284,61 @@ onUnmounted(() => {
fill: var(--color-text-1);
}
/* ========== 深色模式适配 ========== */
/* ========== 代码高亮主题色(不依赖 hljs 主题 CSS ========== */
/* Mermaid 图表深色模式 */
body[arco-theme*='dark'] .markdown-preview-content :deep(.mermaid) {
background: rgba(255, 255, 255, 0.05);
/* 亮色模式 - GitHub 配色 */
.markdown-preview-content :deep(.hljs) {
color: #24292e;
background: #f6f8fa;
}
body[arco-theme*='dark'] .markdown-preview-content :deep(.mermaid *) {
color: var(--color-text-1) !important;
stroke: var(--color-text-1) !important;
.markdown-preview-content :deep(.hljs-comment),
.markdown-preview-content :deep(.hljs-quote) { color: #6a737d; font-style: italic; }
.markdown-preview-content :deep(.hljs-keyword),
.markdown-preview-content :deep(.hljs-selector-tag),
.markdown-preview-content :deep(.hljs-subst) { color: #d73a49; }
.markdown-preview-content :deep(.hljs-string),
.markdown-preview-content :deep(.hljs-doctag) { color: #032f62; }
.markdown-preview-content :deep(.hljs-number),
.markdown-preview-content :deep(.hljs-literal),
.markdown-preview-content :deep(.hljs-variable),
.markdown-preview-content :deep(.hljs-template-variable),
.markdown-preview-content :deep(.hljs-tag .hljs-attr) { color: #005cc5; }
.markdown-preview-content :deep(.hljs-title),
.markdown-preview-content :deep(.hljs-section),
.markdown-preview-content :deep(.hljs-selector-id) { color: #6f42c1; font-weight: bold; }
.markdown-preview-content :deep(.hljs-type),
.markdown-preview-content :deep(.hljs-class .hljs-title) { color: #6f42c1; }
.markdown-preview-content :deep(.hljs-tag .hljs-keyword),
.markdown-preview-content :deep(.hljs-tag .hljs-title) { color: #22863a; }
.markdown-preview-content :deep(.hljs-bullet) { color: #e36209; }
.markdown-preview-content :deep(.hljs-symbol) { color: #005cc5; }
.markdown-preview-content :deep(.hljs-built_in),
.markdown-preview-content :deep(.hljs-type) { color: #005cc5; }
.markdown-preview-content :deep(.hljs-attr) { color: #e36209; }
.markdown-preview-content :deep(.hljs-meta) { color: #735c0f; }
.markdown-preview-content :deep(.hljs-addition) { color: #22863a; background-color: #f0fff4; }
.markdown-preview-content :deep(.hljs-deletion) { color: #b31d28; background-color: #ffeef0; }
/* ========== 深色模式适配 ========== */
/* Mermaid 图表深色模式 - 使用原生 dark 主题,仅需背景适配 */
body[arco-theme*='dark'] .markdown-preview-content :deep(.mermaid) {
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
}
/* 代码高亮深色模式 - 使用 CSS 自定义属性 */

View File

@@ -31,7 +31,7 @@
<span v-else class="file-item-name" :title="file.name">{{ file.name }}</span>
<!-- 文件大小 -->
<span v-if="!file.is_dir && !isEditing" class="file-item-size">
<span v-if="!file.isDir && !isEditing" class="file-item-size">
{{ formattedSize }}
</span>
@@ -54,8 +54,7 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue'
import { IconStar, IconStarFill } from '@arco-design/web-vue/es/icon'
import { formatBytes } from '@/utils/fileUtils'
import { getFileIcon } from '@/utils/fileUtils'
import { formatBytes, getFileIcon } from '@/utils/fileUtils'
import type { FileItem } from '@/types/file-system'
// Props

View File

@@ -55,9 +55,8 @@
<script setup lang="ts">
import { ref, computed, watch, provide, type Ref } from 'vue'
import { IconRight, IconFolder, IconFile, IconExclamationCircle } from '@arco-design/web-vue/es/icon'
import { IconRight, IconFolder } from '@arco-design/web-vue/es/icon'
import { listDir } from '@/api/system'
import { getParentPath, joinPath, normalizePathSeparators } from '@/utils/pathHelpers'
import { sortFileList } from '@/utils/fileUtils'
import { useTimeout } from '@/composables/useTimeout'
import DropdownItem from './DropdownItem.vue'
@@ -118,17 +117,22 @@ const segments = computed<PathSegment[]>(() => {
})
const activeIndex = ref<number | null>(null)
const hoverTimer = ref<NodeJS.Timeout | null>(null)
const closeTimer = ref<NodeJS.Timeout | null>(null)
const children = ref<Array<{ name: string; path: string; isDir: boolean }>>([])
const loading = ref(false)
const error = ref('')
const lastLoadedPath = ref('')
const loadChildren = async (path: string) => {
if (path === lastLoadedPath.value) return
loading.value = true
error.value = ''
try {
const files = await listDir(path)
lastLoadedPath.value = path
children.value = sortFileList(files.map(f => ({
name: f.name,
path: f.path,
@@ -150,17 +154,22 @@ const resetAndClose = () => {
const onHover = (segment: PathSegment, index: number) => {
if (index === segments.value.length - 1) return
delay(() => {
if (hoverTimer.value) clearTimeout(hoverTimer.value)
if (closeTimer.value) clearTimeout(closeTimer.value)
hoverTimer.value = delay(() => {
activeIndex.value = index
loadChildren(segment.path)
}, 200)
}
const onMenuEnter = () => {
if (hoverTimer.value) clearTimeout(hoverTimer.value)
if (closeTimer.value) clearTimeout(closeTimer.value)
}
const onMenuLeave = () => {
if (hoverTimer.value) clearTimeout(hoverTimer.value)
closeTimer.value = delay(() => {
resetAndClose()
}, 100)
@@ -184,6 +193,7 @@ const onOpenFile = (path: string) => {
watch(() => props.path, () => {
activeIndex.value = null
children.value = []
lastLoadedPath.value = ''
openMenus.value = new Map()
})
</script>

View File

@@ -84,7 +84,6 @@
<!-- 刷新按钮 -->
<a-button
type="primary"
size="small"
:loading="config.fileLoading"
@click="handleRefresh"
@@ -137,14 +136,6 @@ interface Emits {
const emit = defineEmits<Emits>()
// 事件处理
const handlePathUpdate = (path: string) => {
emit('update:filePath', path)
}
const handlePathSelect = (value: string) => {
emit('goToPath', value)
}
const handleGoToPath = (path: string) => {
emit('goToPath', path)
}

View File

@@ -3,8 +3,10 @@
* 提供收藏文件的添加、删除、排序等功能
*/
import { ref, watch } from 'vue'
import { STORAGE_KEYS } from '@/utils/constants'
import { ref } from 'vue'
import { STORAGE_KEYS, DEFAULTS } from '@/utils/constants'
import { getPathSeparator } from '@/utils/fileUtils'
import { Message } from '@arco-design/web-vue'
import type { FavoriteFile, FileItem, DraggingState } from '@/types/file-system'
export function useFavorites() {
@@ -67,13 +69,23 @@ export function useFavorites() {
}
}
/**
* 标准化路径用于比较Windows 大小写不敏感)
*/
const normalizePath = (path: string): string => {
return path.toLowerCase()
}
/**
* 添加收藏
*/
const addFavorite = (file: FileItem) => {
// 检查是否已存在
const exists = favorites.value.some(fav => fav.path === file.path)
if (exists) {
if (isFavorite(file.path)) {
return false
}
if (favorites.value.length >= DEFAULTS.MAX_FAVORITES_LENGTH) {
Message.warning(`收藏夹已满,最多收藏 ${DEFAULTS.MAX_FAVORITES_LENGTH}`)
return false
}
@@ -81,17 +93,11 @@ export function useFavorites() {
...file,
addedAt: Date.now()
} as FavoriteFile)
sortFavorites()
saveFavorites()
return true
}
/**
* 标准化路径用于比较(后端已统一为 /,直接转小写)
*/
const normalizePath = (path: string): string => {
return path.toLowerCase()
}
/**
* 删除收藏
*/
@@ -108,14 +114,12 @@ export function useFavorites() {
* 切换收藏状态
*/
const toggleFavorite = (file: FileItem) => {
const exists = isFavorite(file.path)
if (exists) {
if (isFavorite(file.path)) {
removeFavorite(file.path)
return false
} else {
addFavorite(file)
return true
}
addFavorite(file)
return true
}
/**
@@ -131,15 +135,9 @@ export function useFavorites() {
*/
const togglePin = (path: string) => {
const normalizedPath = normalizePath(path)
const fav = favorites.value.find(f => normalizePath(f.path) === normalizedPath)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedPath)
if (fav) {
if (fav.pinnedAt) {
// 取消置顶
fav.pinnedAt = undefined
} else {
// 设置置顶
fav.pinnedAt = Date.now()
}
fav.pinnedAt = fav.pinnedAt ? undefined : Date.now()
sortFavorites()
saveFavorites()
}
@@ -150,28 +148,37 @@ export function useFavorites() {
*/
const isPinned = (path: string): boolean => {
const normalizedPath = normalizePath(path)
const fav = favorites.value.find(f => normalizePath(f.path) === normalizedPath)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedPath)
return !!fav?.pinnedAt
}
/**
* 长按开始
* 更新收藏项路径(重命名时使用,保留置顶状态和添加时间)
*/
const onLongPressStart = (event: MouseEvent | TouchEvent, index: number) => {
const isMouse = event instanceof MouseEvent
const isTouch = event instanceof TouchEvent
const updateFavoritePath = (oldPath: string, newName: string) => {
const normalizedOld = normalizePath(oldPath)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedOld)
if (!fav) return
// 只支持鼠标左键或触摸
if (isMouse && event.button !== 0) return
if (!isMouse && !isTouch) return
const separator = getPathSeparator(oldPath)
const parentPath = oldPath.substring(
0,
Math.max(oldPath.lastIndexOf('\\'), oldPath.lastIndexOf('/'))
)
fav.path = parentPath + separator + newName
fav.name = newName
saveFavorites()
}
// 拖拽方法
const onLongPressStart = (event: MouseEvent | TouchEvent, index: number) => {
if (event instanceof MouseEvent && event.button !== 0) return
if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent)) return
draggingState.value.pressedIndex = index
draggingState.value.draggedIndex = index
}
/**
* 长按取消
*/
const onLongPressCancel = () => {
if (!draggingState.value.isDragging) {
draggingState.value.pressedIndex = -1
@@ -179,23 +186,15 @@ export function useFavorites() {
}
}
/**
* 拖拽开始
*/
const onDragStart = (event: DragEvent, index: number) => {
draggingState.value.isDragging = true
draggingState.value.draggedIndex = index
// 设置拖拽数据
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', index.toString())
}
}
/**
* 拖拽经过
*/
const onDragOver = (event: DragEvent) => {
event.preventDefault()
if (event.dataTransfer) {
@@ -203,81 +202,53 @@ export function useFavorites() {
}
}
/**
* 放置
*/
const onDrop = (event: DragEvent, targetIndex: number) => {
event.preventDefault()
const fromIndex = draggingState.value.draggedIndex
const toIndex = targetIndex
if (fromIndex === toIndex || fromIndex === -1) {
if (fromIndex === targetIndex || fromIndex === -1) {
resetDragging()
return
}
// 移动元素
const item = favorites.value.splice(fromIndex, 1)[0]
favorites.value.splice(toIndex, 0, item)
favorites.value.splice(targetIndex, 0, item)
saveFavorites()
resetDragging()
}
/**
* 拖拽结束
*/
const onDragEnd = () => {
resetDragging()
}
/**
* 重置拖拽状态
*/
const resetDragging = () => {
draggingState.value.isDragging = false
draggingState.value.draggedIndex = -1
draggingState.value.pressedIndex = -1
}
/**
* 重新排序
*/
const reorder = (fromIndex: number, toIndex: number) => {
if (fromIndex === toIndex) return
const item = favorites.value.splice(fromIndex, 1)[0]
favorites.value.splice(toIndex, 0, item)
saveFavorites()
}
// 组件挂载时加载收藏列表
loadFavorites()
return {
// 状态
favorites,
draggingState,
// 方法
addFavorite,
removeFavorite,
toggleFavorite,
isFavorite,
togglePin,
isPinned,
updateFavoritePath,
// 拖拽方法
onLongPressStart,
onLongPressCancel,
onDragStart,
onDragOver,
onDrop,
onDragEnd,
reorder,
// 工具方法
loadFavorites,
saveFavorites,
resetDragging

View File

@@ -6,6 +6,12 @@
import { ref, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { STORAGE_KEYS, FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
import { getExt } from '@/utils/fileUtils'
import {
isImageFile, isVideoFile, isAudioFile, isPdfFile,
isExcelFile, isWordFile, isCsvFile,
isTextEditable, isConfigFile
} from '@/utils/fileTypeHelpers'
import { useFileOperations } from './useFileOperations'
export interface UseFileEditOptions {
@@ -63,96 +69,29 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
return ''
}
/**
* 获取文件扩展名
*/
const getFileExtension = (filepath: any): string => {
const path = getFilePath(filepath)
if (!path || typeof path !== 'string') return ''
return path.split('.').pop()?.toLowerCase() || ''
}
/**
* 判断是否为图片文件
*/
const isImageFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
if (!ext) return false
return FILE_EXTENSIONS.IMAGE.includes(ext)
}
/**
* 判断是否为视频文件
*/
const isVideoFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
if (!ext) return false
return FILE_EXTENSIONS.VIDEO.includes(ext)
}
/**
* 判断是否为音频文件
*/
const isAudioFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
if (!ext) return false
return FILE_EXTENSIONS.AUDIO.includes(ext)
}
/**
* 判断是否为 PDF 文件
*/
const isPdfFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
return ext === 'pdf'
}
/**
* 判断是否为 Excel 文件
*/
const isExcelFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
return ['xlsx', 'xls'].includes(ext)
}
/**
* 判断是否为 Word 文件
*/
const isWordFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
return ['docx', 'doc'].includes(ext)
}
/**
* 判断是否为 CSV/TSV 文件
*/
const isCsvFile = (filepath: any): boolean => {
const ext = getFileExtension(filepath)
return ['csv', 'tsv'].includes(ext)
}
/**
* 判断是否为二进制文件(基于扩展名)
* 注意媒体文件图片、视频、音频、PDF不是二进制文件它们可以预览
* 对于无扩展名的文件,返回 null 表示未知,需要内容检测
*/
const isBinaryFileByExt = (filepath: any): boolean | null => {
const ext = getFileExtension(filepath)
const path = getFilePath(filepath)
const ext = getExt(path)
if (!ext) return null // 无扩展名返回 null表示需要进一步检测
// 媒体文件(可预览,不算二进制)
const isMediaFile = FILE_EXTENSIONS.IMAGE.includes(ext) ||
FILE_EXTENSIONS.VIDEO.includes(ext) ||
FILE_EXTENSIONS.AUDIO.includes(ext) ||
['pdf', 'html', 'htm', 'md', 'markdown'].includes(ext)
const isMediaFile = isImageFile(path) ||
isVideoFile(path) ||
isAudioFile(path) ||
isPdfFile(path) ||
['html', 'htm', 'md', 'markdown'].includes(ext)
// Office 文件和 CSV可预览
const isOfficeFile = ['xlsx', 'xls', 'docx', 'doc', 'csv', 'tsv'].includes(ext)
const isOfficeFile = isExcelFile(path) || isWordFile(path) || isCsvFile(path)
// 文本或代码文件(可编辑)
const isTextFile = FILE_EXTENSIONS.TEXT.includes(ext) ||
FILE_EXTENSIONS.CODE.includes(ext) ||
FILE_EXTENSIONS.CONFIG.includes(ext)
const isTextFile = isTextEditable(path) || isConfigFile(path) ||
FILE_EXTENSIONS.CODE.includes(ext)
// 如果是媒体文件、Office 文件或文本文件,就不是二进制
if (isMediaFile || isOfficeFile || isTextFile) return false
@@ -243,7 +182,7 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
// 新内容加载完成后会直接替换旧内容
const filename = getFilePath(path)
const ext = getFileExtension(filename)
const ext = getExt(filename)
// Office 文件和 CSV 文件直接读取内容进行预览,跳过二进制检测
if (isExcelFile(filename) || isWordFile(filename) || isCsvFile(filename)) {
@@ -658,13 +597,6 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
setEditorHeight,
// 文件类型检查
isImageFile,
isVideoFile,
isAudioFile,
isPdfFile,
isExcelFile,
isWordFile,
isCsvFile,
isBinaryFileByExt,
isFileInCurrentDirectory
}

View File

@@ -3,8 +3,8 @@
* 提供文件读取、写入、删除等基础操作,包括 ZIP 文件浏览
*/
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getPathSeparator } from '@/utils/fileUtils'
import {
listDir,
readFile as readFileApi,
@@ -13,12 +13,12 @@ import {
createFile,
createDir,
renamePath as renamePathApi,
listZipContents,
listZipContents as listZipContentsApi,
extractFileFromZip,
extractFileFromZipToTemp,
getFileServerURL
extractFileFromZipToTemp as extractZipToTempApi,
getFileServerURL as getFileServerUrlApi
} from '@/api'
import type { FileOperationResult } from '@/types/file-system'
import type { FileItem, FileOperationResult } from '@/types/file-system'
export interface UseFileOperationsOptions {
onSuccess?: (operation: string, data: any) => void
@@ -133,7 +133,7 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
*/
const rename = async (oldPath: string, newName: string): Promise<FileItem> => {
// 构造新路径
const separator = oldPath.includes('\\') ? '\\' : '/'
const separator = getPathSeparator(oldPath)
const parentPath = oldPath.substring(
0,
Math.max(oldPath.lastIndexOf('\\'), oldPath.lastIndexOf('/'))
@@ -186,7 +186,7 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
*/
const listZipContents = async (zipPath: string): Promise<FileItem[]> => {
try {
const result = await listZipContents(zipPath)
const result = await listZipContentsApi(zipPath)
onSuccess?.('listZipContents', { zipPath, count: result.length })
return result
} catch (error) {
@@ -216,7 +216,7 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
*/
const extractZipFileToTemp = async (zipPath: string, filePath: string): Promise<string> => {
try {
const tempPath = await extractFileFromZipToTemp(zipPath, filePath)
const tempPath = await extractZipToTempApi(zipPath, filePath)
onSuccess?.('extractZipFileToTemp', { zipPath, filePath, tempPath })
return tempPath
} catch (error) {
@@ -231,7 +231,7 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
*/
const getFileServerURL = async (): Promise<string> => {
try {
const url = await getFileServerURL()
const url = await getFileServerUrlApi()
onSuccess?.('getFileServerURL', { url })
return url
} catch (error) {

View File

@@ -3,10 +3,15 @@
* 提供文件预览 URL 生成、媒体元数据获取等功能
*/
import { ref, computed } from 'vue'
import { ref } from 'vue'
import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
import { normalizeFilePath } from '@/utils/fileUtils'
import { normalizeFilePath, getExt } from '@/utils/fileUtils'
import { detectFileTypeByContent } from '@/api/system'
import {
isImageFile, isVideoFile, isAudioFile, isPdfFile,
isHtmlFile, isMarkdownFile, isPreviewable as isPreviewableType,
isTextEditable, isConfigFile
} from '@/utils/fileTypeHelpers'
import type { FilePreviewMetadata, FileType } from '@/types/file-system'
// 内容检测大小限制(与后端一致)
@@ -81,159 +86,42 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
const getFileType = (filename: string): FileType => {
if (!filename || typeof filename !== 'string') return 'Binary' as FileType
const ext = filename.split('.').pop()?.toLowerCase() || ''
if (isImageFile(filename)) return 'Image' as FileType
if (isVideoFile(filename)) return 'Video' as FileType
if (isAudioFile(filename)) return 'Audio' as FileType
if (isPdfFile(filename)) return 'Pdf' as FileType
if (isHtmlFile(filename)) return 'Html' as FileType
if (isMarkdownFile(filename)) return 'Markdown' as FileType
if (FILE_EXTENSIONS.CODE.includes(getExt(filename))) return 'Code' as FileType
if (isConfigFile(filename)) return 'Code' as FileType
if (isTextEditable(filename)) return 'Text' as FileType
// 图片
if (FILE_EXTENSIONS.IMAGE.includes(ext)) {
return 'Image' as FileType
}
// 视频
if (FILE_EXTENSIONS.VIDEO.includes(ext)) {
return 'Video' as FileType
}
// 音频
if (FILE_EXTENSIONS.AUDIO.includes(ext)) {
return 'Audio' as FileType
}
// PDF
if (ext === 'pdf') {
return 'Pdf' as FileType
}
// HTML
if (['html', 'htm'].includes(ext)) {
return 'Html' as FileType
}
// Markdown
if (['md', 'markdown'].includes(ext)) {
return 'Markdown' as FileType
}
// 代码
if (FILE_EXTENSIONS.CODE.includes(ext)) {
return 'Code' as FileType
}
// 配置文件(返回 Code 类型,因为它们也是可编辑的文本格式)
if (FILE_EXTENSIONS.CONFIG.includes(ext)) {
return 'Code' as FileType
}
// 文本
if (FILE_EXTENSIONS.TEXT.includes(ext)) {
return 'Text' as FileType
}
// 默认为二进制
return 'Binary' as FileType
}
/**
* 判断是否为图片文件
*/
const isImageFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.IMAGE.includes(ext)
}
/**
* 判断是否为视频文件
*/
const isVideoFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.VIDEO.includes(ext)
}
/**
* 判断是否为音频文件
*/
const isAudioFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.AUDIO.includes(ext)
}
/**
* 判断是否为 PDF 文件
*/
const isPdfFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return ext === 'pdf'
}
/**
* 判断是否为 HTML 文件
*/
const isHtmlFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return ['html', 'htm'].includes(ext)
}
/**
* 判断是否为 Markdown 文件
*/
const isMarkdownFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return ['md', 'markdown'].includes(ext)
}
/**
* 判断是否为代码文件
*/
const isCodeFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.CODE.includes(ext)
}
/**
* 判断是否为文本文件
*/
const isTextFile = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.TEXT.includes(ext)
}
/**
* 判断文件是否可预览
*/
const isPreviewable = (filename: string): boolean => {
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
return FILE_EXTENSIONS.IMAGE.includes(ext) ||
FILE_EXTENSIONS.VIDEO.includes(ext) ||
FILE_EXTENSIONS.AUDIO.includes(ext) ||
ext === 'pdf' ||
['html', 'htm'].includes(ext) ||
['md', 'markdown'].includes(ext)
return isPreviewableType(filename)
}
/**
* 判断文件是否可编辑
*/
const isEditable = (filename: string, fileSize: number): boolean => {
// 检查文件大小
if (fileSize > FILE_SIZE_THRESHOLDS.BIG_FILE) {
return false
}
// 检查文件类型
if (!filename || typeof filename !== 'string') return false
const ext = filename.split('.').pop()?.toLowerCase() || ''
const ext = getExt(filename)
return FILE_EXTENSIONS.CODE.includes(ext) ||
FILE_EXTENSIONS.TEXT.includes(ext) ||
FILE_EXTENSIONS.CONFIG.includes(ext) ||
['html', 'htm', 'md', 'markdown'].includes(ext)
isTextEditable(filename) ||
isConfigFile(filename) ||
isHtmlFile(filename) ||
isMarkdownFile(filename)
}
/**
@@ -306,8 +194,6 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
isPdfFile,
isHtmlFile,
isMarkdownFile,
isCodeFile,
isTextFile,
isPreviewable,
isEditable,

View File

@@ -5,7 +5,7 @@
import { ref, watch, computed } from 'vue'
import { STORAGE_KEYS } from '@/utils/constants'
import { normalizePathSeparators } from '@/utils/pathHelpers'
import { normalizePathSeparators } from '@/utils/fileUtils'
import type { PathHistory } from '@/types/file-system'
export interface UsePathNavigationOptions {

View File

@@ -99,10 +99,10 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick, watchEffect } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { getPathSeparator } from '@/utils/fileUtils'
import { Message, Modal } from '@arco-design/web-vue'
import { marked, renderMermaidDiagrams } from '@/utils/markedExtensions'
import 'highlight.js/styles/github-dark.css'
// 导入子组件
import Toolbar from './components/Toolbar.vue'
@@ -121,7 +121,6 @@ import { useCommonPaths } from './composables/useCommonPaths'
// 导入工具函数
import { getFileName, sortFileList } from '@/utils/fileUtils'
import { getParentPath } from '@/utils/pathHelpers'
import { isImageFile, isVideoFile, isAudioFile, isPdfFile, isHtmlFile, isMarkdownFile, isExcelFile, isWordFile, isCsvFile } from '@/utils/fileTypeHelpers'
import { listDir } from '@/api/system'
import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS } from '@/utils/constants'
@@ -225,7 +224,7 @@ const fileOps = useFileOperations({
})
// 收藏夹
const { favorites, draggingState, toggleFavorite: toggleFav, removeFavorite: removeFav, isFavorite, togglePin } = useFavorites()
const { favorites, draggingState, toggleFavorite: toggleFav, removeFavorite: removeFav, isFavorite, togglePin, updateFavoritePath, onLongPressStart, onLongPressCancel, onDragStart, onDragOver, onDrop, onDragEnd } = useFavorites()
// 路径导航
const { filePath, history, navigate, back, forward, onPathSelect, onPathEnter, browseDirectory, getParentPath } =
@@ -463,50 +462,31 @@ const handleTogglePin = (path: string) => {
}
const handleLongPressStart = (event: MouseEvent | TouchEvent, index: number) => {
// 拖拽开始
onLongPressStart(event, index)
}
const handleLongPressCancel = () => {
// 拖拽取消
onLongPressCancel()
}
const handleDragStart = (event: DragEvent, index: number) => {
// 拖拽开始
onDragStart(event, index)
}
const handleDragOver = (event: DragEvent) => {
// 拖拽经过
onDragOver(event)
}
const handleDrop = (event: DragEvent, targetIndex: number) => {
// 放置
onDrop(event, targetIndex)
}
const handleDragEnd = () => {
// 拖拽结束
onDragEnd()
}
// 文件列表事件
const handleFileClick = async (file: FileItem) => {
// ZIP 浏览模式 - 暂时禁用
/*
if (false) { // ZIP 浏览模式已禁用
await zipBrowser.handleClick(file.path, fileList.value, {
selectFile: (f: FileItem) => {
selectedFileItem.value = f
},
isImage: isImageFile,
extractAndPreview: extractZipImageAndPreview,
extractAndRead: extractZipTextAndRead,
loadZipContents: loadZipDirectoryContents,
updateFileList: (files: FileItem[]) => {
fileList.value = files
}
})
return
}
*/
// 正常文件系统浏览
if (file.isDir) {
// 目录:使用 navigate 函数,确保历史记录正确更新
@@ -522,25 +502,6 @@ const handleFileDoubleClick = async (file: FileItem) => {
if (file.isDir) {
await navigate(file.path)
} else {
// 检查是否为 ZIP 文件 - 暂时禁用
/*
const ext = file.name.split('.').pop()?.toLowerCase() || ''
if (ext === 'zip' && !zipBrowser.isActive.value) {
// ZIP 文件:进入 ZIP 浏览模式
await zipBrowser.enter(file.path, {
saveBeforePath: () => {
// 保存当前路径
return filePath.value
},
loadZipContents: loadZipDirectoryContents,
updateFileList: (files: FileItem[]) => {
fileList.value = files
}
})
} else {
selectFile(file.path)
}
*/
selectFile(file.path)
}
}
@@ -573,7 +534,8 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
const trimmedName = newName.trim()
// 如果名称没有变化,直接返回
const oldName = oldPath.substring(oldPath.lastIndexOf('\\') + 1)
const lastSep = Math.max(oldPath.lastIndexOf('\\'), oldPath.lastIndexOf('/'))
const oldName = oldPath.substring(lastSep + 1)
if (trimmedName === oldName) {
editingFilePath.value = ''
editingFileName.value = ''
@@ -588,7 +550,7 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
}
// 构造新路径
const separator = oldPath.includes('\\') ? '\\' : '/'
const separator = getPathSeparator(oldPath)
const dirPath = oldPath.substring(0, oldPath.lastIndexOf(separator))
const newPath = dirPath + separator + trimmedName
@@ -649,10 +611,9 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
// 更新文件列表(保留收藏状态)
updateFileInList(oldPath, renamedFile)
// 如果重命名的是收藏的文件,更新收藏夹中的路径
// 如果重命名的是收藏的文件,更新收藏夹中的路径(保留置顶状态)
if (isFavorite(oldPath)) {
removeFav(oldPath)
toggleFav(renamedFile)
updateFavoritePath(oldPath, trimmedName)
}
Message.success(`✓ 重命名成功: ${trimmedName}`)
@@ -669,7 +630,6 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
// 针对常见错误提供友好提示
if (errorMsg.includes('being used by another process') ||
errorMsg.includes('being used by another process') ||
errorMsg.includes('被另一个进程占用')) {
errorMsg = '文件正在被其他程序使用,请先关闭该文件后重试'
if (selectedFileItem.value?.isDir) {
@@ -799,11 +759,6 @@ const handleCreateFile = async () => {
return
}
if (false) { // ZIP 浏览模式已禁用
Message.warning('ZIP 浏览模式下不支持创建文件')
return
}
showInputDialog(
UI_TEXT.CREATE_FILE,
UI_TEXT.ENTER_FILE_NAME,
@@ -831,11 +786,8 @@ const handleCreateFile = async () => {
return
}
// 构建完整路径
const fullPath = `${filePath.value}\\${fileName}`
try {
const newFile = await fileOps.createNewFile(fullPath)
const newFile = await fileOps.createNewFile(filePath.value, fileName)
Message.success(`✓ 文件 "${fileName}" 创建成功`)
addFileToList(newFile)
} catch (error: any) {
@@ -855,11 +807,6 @@ const handleCreateDir = async () => {
return
}
if (false) { // ZIP 浏览模式已禁用
Message.warning('ZIP 浏览模式下不支持创建文件夹')
return
}
showInputDialog(
UI_TEXT.CREATE_FOLDER,
UI_TEXT.ENTER_FOLDER_NAME,
@@ -887,11 +834,8 @@ const handleCreateDir = async () => {
return
}
// 构建完整路径
const fullPath = `${filePath.value}\\${folderName}`
try {
const newDir = await fileOps.createNewDir(fullPath)
const newDir = await fileOps.createNewDir(filePath.value, folderName)
Message.success(`✓ 文件夹 "${folderName}" 创建成功`)
addFileToList(newDir)
} catch (error: any) {
@@ -1033,7 +977,7 @@ const selectFile = async (path: string) => {
name: fileName,
isDir: false,
size: 0,
mod_time: '',
modified_time: '',
is_favorite: isFavorite(path)
}
}
@@ -1150,7 +1094,7 @@ const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Pr
path: f.path,
isDir: f.isDir,
size: f.size || 0,
mod_time: f.mod_time || '',
modified_time: f.modified_time || '',
is_favorite: false
}))
@@ -1206,7 +1150,8 @@ const startResizeHorizontal = (event: MouseEvent) => {
const onMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - startX
const newLeftWidth = Math.max(200, Math.min(containerRect.width - 200, startLeftWidth + deltaX))
const minPx = (DEFAULTS.MIN_PANEL_WIDTH / 100) * containerRect.width
const newLeftWidth = Math.max(minPx, Math.min(containerRect.width - minPx, startLeftWidth + deltaX))
const newLeftPercent = (newLeftWidth / containerRect.width) * 100
panelWidth.value = {

View File

@@ -0,0 +1,563 @@
<template>
<div class="markdown-editor-container">
<div class="editor-header">
<div class="title">
<icon-file />
<span>Markdown 编辑器</span>
<a-divider type="vertical" />
<a-tooltip content="自动保存已启用">
<span class="save-status" :class="{ 'saved': !hasChanges }">
{{ hasChanges ? '未保存' : '已保存' }}
</span>
</a-tooltip>
</div>
<div class="actions">
<a-tooltip content="清空内容">
<a-button size="small" type="outline" @click="clearContent">
<icon-delete />
</a-button>
</a-tooltip>
<a-tooltip content="全屏编辑">
<a-button size="small" type="outline" @click="toggleFullscreen">
<icon-expand />
</a-button>
</a-tooltip>
<PdfExportButton @export-complete="onExportComplete" />
</div>
</div>
<div class="editor-content" :class="{ 'fullscreen': isFullscreen }">
<div class="editor-panel" :class="{ 'expanded': isEditorExpanded }">
<div class="panel-header">
<span>编辑</span>
<div class="panel-controls">
<a-tooltip content="展开编辑器">
<a-button size="small" type="text" @click="toggleEditorExpand">
<icon-align-left v-if="!isEditorExpanded" />
<icon-shrink v-else />
</a-button>
</a-tooltip>
</div>
</div>
<div class="editor-wrapper">
<textarea
ref="textarea"
v-model="markdownContent"
class="markdown-textarea"
placeholder="在这里输入 Markdown 内容...
# 标题
## 二级标题
**粗体** *斜体*
- 列表项 1
- 列表项 2
\`\`\`javascript
console.log('Hello, World!')
\`\`\`
> 引用内容"
@input="handleInput"
@keydown="handleKeydown"
/>
</div>
</div>
<div class="resizer" @mousedown="startResize"></div>
<div class="preview-panel" :class="{ 'expanded': isPreviewExpanded }">
<div class="panel-header">
<span>预览</span>
<div class="panel-controls">
<a-tooltip content="展开预览">
<a-button size="small" type="text" @click="togglePreviewExpand">
<icon-align-left v-if="!isPreviewExpanded" />
<icon-shrink v-else />
</a-button>
</a-tooltip>
<a-tooltip content="刷新预览">
<a-button size="small" type="text" @click="renderPreview">
<icon-sync />
</a-button>
</a-tooltip>
</div>
</div>
<div class="preview-wrapper">
<MarkdownPreview :content="markdownContent" />
</div>
</div>
</div>
<div class="editor-footer">
<div class="status">
<span>{{ wordCount }} 字符 | {{ lineCount }} | {{ readingTime }} 分钟阅读</span>
</div>
<div class="shortcuts">
<a-tooltip content="快捷键: Ctrl + S 保存">
<a-button size="small" @click="saveContent" :disabled="!hasChanges">
<icon-save />
保存
</a-button>
</a-tooltip>
<a-tooltip content="快捷键: Ctrl + / 切换预览">
<a-button size="small" @click="togglePreview">
<icon-eye />
</a-button>
</a-tooltip>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import MarkdownPreview from './MarkdownPreview.vue'
import PdfExportButton from './PdfExportButton.vue'
import IconFile from '@arco-design/web-vue/es/icon/icon-file'
import IconDelete from '@arco-design/web-vue/es/icon/icon-delete'
import IconExpand from '@arco-design/web-vue/es/icon/icon-expand'
import IconShrink from '@arco-design/web-vue/es/icon/icon-shrink'
import IconSync from '@arco-design/web-vue/es/icon/icon-sync'
import IconSave from '@arco-design/web-vue/es/icon/icon-save'
import IconEye from '@arco-design/web-vue/es/icon/icon-eye'
import IconAlignLeft from '@arco-design/web-vue/es/icon/icon-align-left'
export default {
name: 'MarkdownEditor',
components: {
MarkdownPreview,
PdfExportButton,
IconFile,
IconDelete,
IconExpand,
IconShrink,
IconSync,
IconSave,
IconEye,
IconAlignLeft
},
emits: ['content-change', 'update:content', 'save'],
props: {
content: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const markdownContent = ref(props.content)
const textarea = ref(null)
const hasChanges = ref(false)
const lastSavedContent = ref('')
const isFullscreen = ref(false)
const isEditorExpanded = ref(false)
const isPreviewExpanded = ref(false)
const showPreview = ref(true)
// 计算属性
const wordCount = computed(() => {
return markdownContent.value.length
})
const lineCount = computed(() => {
return markdownContent.value.split('\n').length
})
const readingTime = computed(() => {
// 平均阅读速度:每分钟 200 字符
const wordsPerMinute = 200
const minutes = Math.ceil(wordCount.value / wordsPerMinute)
return minutes
})
// 方法
const handleInput = () => {
hasChanges.value = markdownContent.value !== lastSavedContent.value
emit('content-change', markdownContent.value)
emit('update:content', markdownContent.value)
}
const handleKeydown = (event) => {
// Ctrl + S 保存
if (event.ctrlKey && event.key === 's') {
event.preventDefault()
saveContent()
}
// Ctrl + / 切换预览
if (event.ctrlKey && event.key === '/') {
event.preventDefault()
togglePreview()
}
}
const saveContent = () => {
lastSavedContent.value = markdownContent.value
hasChanges.value = false
emit('save', markdownContent.value)
Message.success('内容已保存')
// 保存到本地存储
localStorage.setItem('u-desk-markdown-content', markdownContent.value)
}
const onExportComplete = () => {
Message.success('PDF 导出完成')
}
// 自动调整 textarea 高度
const adjustTextareaHeight = () => {
if (textarea.value) {
textarea.value.style.height = 'auto'
textarea.value.style.height = textarea.value.scrollHeight + 'px'
}
}
// 窗口大小调整
const startResize = (event) => {
if (!showPreview.value) return
const startX = event.clientX
const startWidth = document.querySelector('.editor-panel').offsetWidth
const startPreviewWidth = document.querySelector('.preview-panel').offsetWidth
const doResize = (moveEvent) => {
const deltaX = moveEvent.clientX - startX
const newEditorWidth = startWidth + deltaX
const newPreviewWidth = startPreviewWidth - deltaX
if (newEditorWidth > 100 && newPreviewWidth > 100) {
document.querySelector('.editor-panel').style.width = newEditorWidth + 'px'
document.querySelector('.preview-panel').style.width = newPreviewWidth + 'px'
}
}
const stopResize = () => {
document.removeEventListener('mousemove', doResize)
document.removeEventListener('mouseup', stopResize)
}
document.addEventListener('mousemove', doResize)
document.addEventListener('mouseup', stopResize)
}
// 切换功能
const togglePreview = () => {
showPreview.value = !showPreview.value
if (showPreview.value) {
// 恢复预览时重新调整大小
nextTick(() => {
adjustTextareaHeight()
})
}
}
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
if (isFullscreen.value) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
const toggleEditorExpand = () => {
isEditorExpanded.value = !isEditorExpanded.value
if (isEditorExpanded.value && isPreviewExpanded.value) {
isPreviewExpanded.value = false
}
nextTick(() => {
adjustTextareaHeight()
})
}
const togglePreviewExpand = () => {
isPreviewExpanded.value = !isPreviewExpanded.value
if (isPreviewExpanded.value && isEditorExpanded.value) {
isEditorExpanded.value = false
}
}
const clearContent = () => {
Modal.confirm({
title: '确认清空',
content: '确定要清空所有内容吗?此操作不可恢复。',
okButtonProps: { status: 'danger' },
onOk: () => {
markdownContent.value = ''
hasChanges.value = true
lastSavedContent.value = ''
emit('content-change', '')
Message.success('内容已清空')
}
})
}
const renderPreview = () => {
// 强制重新渲染预览
const previewElement = document.querySelector('.preview-wrapper')
if (previewElement) {
previewElement.style.opacity = '0'
nextTick(() => {
previewElement.style.opacity = '1'
})
}
}
// 自动保存定时器
let autoSaveTimer = null
// 监听内容变化:自动保存 + 调整高度
watch(markdownContent, () => {
// 自动保存
if (hasChanges.value) {
clearTimeout(autoSaveTimer)
autoSaveTimer = setTimeout(() => {
saveContent()
}, 5000)
}
// 调整高度
nextTick(() => {
adjustTextareaHeight()
})
}, { deep: true })
// 初始化
onMounted(() => {
nextTick(() => {
adjustTextareaHeight()
})
})
onUnmounted(() => {
if (autoSaveTimer) {
clearTimeout(autoSaveTimer)
}
})
// 导出方法
const getMarkdownContent = () => {
return markdownContent.value
}
const setMarkdownContent = (content) => {
markdownContent.value = content
hasChanges.value = content !== lastSavedContent.value
}
return {
markdownContent,
textarea,
hasChanges,
wordCount,
lineCount,
readingTime,
isFullscreen,
isEditorExpanded,
isPreviewExpanded,
showPreview,
handleInput,
handleKeydown,
saveContent,
onExportComplete,
getMarkdownContent,
setMarkdownContent,
startResize,
togglePreview,
toggleFullscreen,
toggleEditorExpand,
togglePreviewExpand,
clearContent,
renderPreview
}
}
}
</script>
<style scoped>
.markdown-editor-container {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-2);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
.markdown-editor-container.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
border-radius: 0;
height: 100vh;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--color-bg-1);
border-bottom: 1px solid var(--color-border);
}
.title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--color-text-1);
}
.save-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
background: var(--color-warning-light-1);
color: var(--color-warning-6);
}
.save-status.saved {
background: var(--color-success-light-1);
color: var(--color-success-6);
}
.actions {
display: flex;
gap: 8px;
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-panel,
.preview-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 100px;
}
.editor-panel.expanded {
flex: 2;
}
.preview-panel.expanded {
flex: 2;
}
.editor-panel {
border-right: 1px solid var(--color-border);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--color-fill-2);
border-bottom: 1px solid var(--color-border);
font-size: 12px;
font-weight: 500;
color: var(--color-text-2);
}
.panel-controls {
display: flex;
gap: 4px;
}
.editor-wrapper {
flex: 1;
padding: 16px;
overflow: auto;
}
.preview-wrapper {
flex: 1;
padding: 16px;
overflow: auto;
background: var(--color-bg-1);
transition: opacity 0.2s;
}
.resizer {
width: 4px;
cursor: col-resize;
background: var(--color-border);
transition: background-color 0.2s;
}
.resizer:hover {
background: var(--color-primary-light-3);
}
.markdown-textarea {
width: 100%;
height: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
resize: none;
outline: none;
background: var(--color-bg-1);
color: var(--color-text-1);
}
.markdown-textarea:focus {
border-color: var(--color-primary-6);
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2);
}
.markdown-textarea::placeholder {
color: var(--color-text-3);
}
.editor-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--color-bg-1);
border-top: 1px solid var(--color-border);
}
.status {
font-size: 12px;
color: var(--color-text-2);
}
.shortcuts {
display: flex;
gap: 8px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.editor-content {
flex-direction: column;
}
.editor-panel {
border-right: none;
border-bottom: 1px solid var(--color-border);
}
.resizer {
height: 4px;
width: 100%;
cursor: row-resize;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="md-preview">
<div v-html="renderedMarkdown" class="markdown-body"></div>
</div>
</template>
<script>
import { marked } from '@/utils/markedExtensions'
function sanitizeHtml(html) {
return html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<script[^>]*>/gi, '')
.replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, '')
.replace(/javascript\s*:/gi, 'blocked:')
.replace(/<iframe[\s\S]*?<\/iframe>/gi, '')
.replace(/<object[\s\S]*?<\/object>/gi, '')
.replace(/<embed[^>]*>/gi, '')
.replace(/<form[\s\S]*?<\/form>/gi, '')
}
export default {
name: 'MarkdownPreview',
props: {
content: {
type: String,
default: ''
}
},
computed: {
renderedMarkdown() {
return sanitizeHtml(marked(this.content))
}
}
}
</script>
<style scoped>
.md-preview {
padding: 20px;
background: white;
border-radius: 8px;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,262 @@
<template>
<a-tooltip content="导出" position="bottom">
<a-button
size="small"
type="outline"
@click="exportPDF"
:loading="exporting"
>
<template #icon>
<icon-file-pdf />
</template>
</a-button>
</a-tooltip>
</template>
<script>
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
export default {
name: 'PdfExportButton',
emits: ['export-start', 'export-complete'],
props: {
title: {
type: String,
default: '文档'
},
containerSelector: {
type: String,
default: '.markdown-body'
}
},
setup(props, { emit }) {
const exporting = ref(false)
function escapeHtml(str) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }
return str.replace(/[&<>"']/g, c => map[c])
}
function stripScripts(html) {
return html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<script[^>]*>/gi, '')
.replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, '')
.replace(/<iframe[\s\S]*?<\/iframe>/gi, '')
.replace(/<object[\s\S]*?<\/object>/gi, '')
.replace(/<embed[^>]*>/gi, '')
}
const exportPDF = async () => {
if (exporting.value) return
exporting.value = true
emit('export-start')
try {
// 获取渲染后的 Markdown 内容
const contentElement = document.querySelector(props.containerSelector)
if (!contentElement) {
Message.error('没有可导出的内容')
exporting.value = false
return
}
const htmlContent = stripScripts(contentElement.innerHTML)
if (!htmlContent || !htmlContent.trim()) {
Message.error('内容为空,无法导出')
exporting.value = false
return
}
// 打开打印窗口
const printWindow = window.open('', '_blank', 'width=800,height=600')
if (!printWindow) {
Message.error('无法打开打印窗口,请检查浏览器弹窗设置')
exporting.value = false
return
}
// 写入打印内容
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${escapeHtml(props.title)}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
padding: 40px;
max-width: 800px;
margin: 0 auto;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
page-break-after: avoid;
}
h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1em; }
h5 { font-size: 0.875em; }
h6 { font-size: 0.85em; color: #6a737d; }
p {
margin-top: 0;
margin-bottom: 16px;
}
ul, ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
li {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
}
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
margin-top: 0;
margin-bottom: 16px;
page-break-inside: avoid;
}
pre code {
padding: 0;
margin: 0;
background-color: transparent;
border: 0;
}
table {
border-spacing: 0;
border-collapse: collapse;
margin-top: 0;
margin-bottom: 16px;
width: 100%;
page-break-inside: avoid;
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
table th {
font-weight: 600;
background-color: #f6f8fa;
}
table tr:nth-child(even) {
background-color: #f8f8f8;
}
img {
max-width: 100%;
page-break-inside: avoid;
}
hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
@media print {
body {
padding: 0;
}
@page {
margin: 15mm;
size: A4;
}
}
</style>
</head>
<body>
${htmlContent}
</body>
</html>
`)
printWindow.document.close()
// 等待内容加载完成后自动打印
let printTriggered = false
printWindow.onload = () => {
printTriggered = true
printWindow.print()
}
// 兼容性处理:如果 onload 未触发
setTimeout(() => {
if (!printTriggered && printWindow && !printWindow.closed) {
printWindow.print()
}
}, 500)
Message.success('请在打印对话框中选择"另存为 PDF"')
emit('export-complete')
} catch (error) {
console.error('PDF导出失败:', error)
Message.error(`PDF导出失败${error.message || '未知错误'}`)
} finally {
exporting.value = false
}
}
return {
exporting,
exportPDF
}
}
}
</script>

View File

@@ -66,8 +66,8 @@ export function useFavoriteFiles(storageKey, options = {}) {
return
}
favoriteFiles.value.sort((a, b) => {
const timeA = a.created_at || 0
const timeB = b.created_at || 0
const timeA = a.addedAt || 0
const timeB = b.addedAt || 0
return timeB - timeA // 倒序:最新的在上面
})
}
@@ -106,8 +106,8 @@ export function useFavoriteFiles(storageKey, options = {}) {
favoriteFiles.value.push({
path: item.path,
name: item.name,
is_dir: item.is_dir || false,
created_at: Date.now(), // 添加时间戳(用于 getSortedFavorites
isDir: item.isDir || false,
addedAt: Date.now(),
})
save(favoriteFiles.value) // 直接保存,不重新排序(新项目添加到末尾)
@@ -201,8 +201,8 @@ export function useFavoriteFiles(storageKey, options = {}) {
const getSortedFavorites = (order = 'desc') => {
const sorted = [...favoriteFiles.value]
sorted.sort((a, b) => {
const timeA = a.created_at || 0
const timeB = b.created_at || 0
const timeA = a.addedAt || 0
const timeB = b.addedAt || 0
return order === 'desc' ? timeB - timeA : timeA - timeB
})
return sorted
@@ -255,9 +255,27 @@ export function useFavoriteFiles(storageKey, options = {}) {
return true
}
// 组件挂载时加载数据(不自动排序,保持用户拖拽的顺序
// 旧字段名 → 新字段名迁移(一次性,迁移后自动回写
const migrateFieldNames = (list) => {
if (!Array.isArray(list)) return
const map = { is_dir: 'isDir', created_at: 'addedAt' }
let changed = false
list.forEach(item => {
for (const [old, newKey] of Object.entries(map)) {
if (old in item) {
if (!(newKey in item)) item[newKey] = item[old]
delete item[old]
changed = true
}
}
})
if (changed) save(list)
}
// 组件挂载时加载数据并迁移旧字段
onMounted(() => {
load()
migrateFieldNames(favoriteFiles.value)
})
return {

View File

@@ -1,34 +0,0 @@
/**
* LocalStorage composable
* 通用的 localStorage 操作
*/
import { watch, type Ref } from 'vue'
export function useLocalStorage<T>(
key: string,
defaultValue: T,
storage: Storage = localStorage
): [Ref<T>, (value: T) => void, () => void] {
const stored = storage.getItem(key)
const value = ref<T>(stored ? JSON.parse(stored) : defaultValue)
const setValue = (newValue: T) => {
value.value = newValue
}
const clearValue = () => {
value.value = defaultValue
storage.removeItem(key)
}
watch(value, (newValue) => {
try {
storage.setItem(key, JSON.stringify(newValue))
} catch (e) {
console.warn(`Failed to save ${key} to localStorage:`, e)
}
}, { deep: true })
return [value, setValue, clearValue]
}

View File

@@ -66,12 +66,20 @@ export const useConfigStore = defineStore('config', () => {
const defaultTab = computed(() => appConfig.value.defaultTab)
// ==================== 核心方法 ====================
let _retryCount = 0
const MAX_RETRIES = 30 // 最多重试30次约30秒
/**
* 加载配置
*/
const loadConfig = async () => {
if (!window.go?.main?.App) {
console.warn('Wails 绑定未准备好1秒后重试')
_retryCount++
if (_retryCount > MAX_RETRIES) {
console.error('Wails 绑定初始化超时,使用默认配置')
useDefaultConfig()
return
}
setTimeout(loadConfig, 1000)
return
}
@@ -104,9 +112,10 @@ export const useConfigStore = defineStore('config', () => {
appConfig.value = {
tabs: [
{ key: 'file-system', title: '文件管理', visible: true, enabled: true },
{ key: 'db-cli', title: '数据库', visible: true, enabled: true }
{ key: 'db-cli', title: '数据库', visible: true, enhanced: true },
{ key: 'markdown-editor', title: 'Markdown 编辑器', visible: true, enabled: true }
],
visibleTabs: ['file-system', 'db-cli'],
visibleTabs: ['file-system', 'db-cli', 'markdown-editor'],
defaultTab: 'file-system'
}
}

View File

@@ -50,6 +50,292 @@ body {
scrollbar-color: var(--color-border-2, rgba(0, 0, 0, 0.1)) transparent;
}
/* Highlight.js CSS */
.hljs {
display: block;
overflow-x: auto;
padding: 1em;
background: #f6f8fa;
border-radius: 6px;
color: #333;
}
.hljs-comment,
.hljs-quote {
color: #6a737d;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
color: #d73a49;
}
.hljs-number,
.hljs-string,
.hljs-meta .hljs-string,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: #032f62;
}
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #6f42c1;
}
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-class .hljs-title,
.hljs-type {
color: #005cc5;
}
.hljs-symbol,
.hljs-bullet,
.hljs-subst,
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-link {
color: #735c0f;
}
.hljs-built_in,
.hljs-deletion {
color: #d73a49;
}
.hljs-formula {
background: #f6f8fa;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
/* GitHub 风格的 Markdown 预览样式 */
.markdown-body {
line-height: 1.6;
color: #333;
font-size: 14px;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
.markdown-body h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
.markdown-body h3 { font-size: 1.25em; }
.markdown-body h4 { font-size: 1em; }
.markdown-body h5 { font-size: 0.875em; }
.markdown-body h6 { font-size: 0.85em; color: #6a737d; }
.markdown-body p {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body li {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.markdown-body blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
.markdown-body code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
}
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body pre code {
padding: 0;
margin: 0;
background-color: transparent;
border: 0;
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
margin-top: 0;
margin-bottom: 16px;
display: block;
width: 100%;
overflow: auto;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-body table th {
font-weight: 600;
background-color: #f6f8fa;
}
.markdown-body table tr:nth-child(even) {
background-color: #f8f8f8;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-body hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
/* 打印样式 */
@media print {
.no-print {
display: none !important;
}
.markdown-body {
max-width: 100%;
margin: 0;
padding: 20px;
font-size: 12pt;
line-height: 1.4;
}
.markdown-body h1 {
font-size: 24pt;
margin-bottom: 12pt;
border-bottom: 1px solid #ccc;
}
.markdown-body h2 {
font-size: 18pt;
margin-bottom: 10pt;
border-bottom: 1px solid #eee;
}
.markdown-body h3 {
font-size: 14pt;
margin-bottom: 8pt;
}
.markdown-body p {
margin-bottom: 10pt;
}
.markdown-body ul,
.markdown-body ol {
margin-bottom: 10pt;
}
.markdown-body li {
margin-bottom: 4pt;
}
.markdown-body table {
border-collapse: collapse;
margin-bottom: 12pt;
width: 100%;
}
.markdown-body th,
.markdown-body td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.markdown-body th {
background-color: #f5f5f5;
font-weight: bold;
}
.markdown-body img {
max-width: 100%;
height: auto;
}
.markdown-body blockquote {
border-left: 4px solid #ddd;
margin: 16px 0;
padding: 10px 20px;
color: #666;
}
.markdown-body code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-body pre {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
.markdown-body pre code {
background-color: transparent;
padding: 0;
}
}
/* Markdown 标题锚点链接样式 */
.heading {
position: relative;
@@ -83,4 +369,34 @@ body {
/* 平滑滚动 */
html {
scroll-behavior: smooth;
}
/* Tooltip 全局样式 */
.arco-tooltip {
font-size: 12px !important;
}
.arco-tooltip-content {
background: var(--color-bg-5) !important;
color: var(--color-text-1) !important;
padding: 6px 10px !important;
border-radius: 6px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
max-width: 240px;
line-height: 1.5;
white-space: nowrap;
}
.arco-tooltip-content::before {
background: var(--color-bg-5) !important;
}
.arco-tooltip-content-white {
background: var(--color-bg-1) !important;
border: 1px solid var(--color-border-2) !important;
color: var(--color-text-1) !important;
}
.arco-tooltip-content-white::before {
background: var(--color-bg-1) !important;
}

View File

@@ -6,8 +6,9 @@
import {
javascript, json, yaml, html, css,
cpp, rust, go, python, php, sql, markdown, java,
shell, StreamLanguage
shell, powerShell, dart, StreamLanguage
} from './codemirrorExports'
import { getCmLanguage } from './languageMap'
const languageCache = new Map()
@@ -17,14 +18,12 @@ const languageCache = new Map()
* @returns {Extension|null} CodeMirror 语言扩展
*/
export function loadLanguageExtension(language) {
// 检查缓存
if (languageCache.has(language)) {
return languageCache.get(language)
}
let extension = null
// 使用静态导入的语言包
switch (language) {
case 'javascript':
extension = javascript({ jsx: true })
@@ -74,6 +73,12 @@ export function loadLanguageExtension(language) {
case 'sh':
extension = StreamLanguage.define(shell)
break
case 'powershell':
extension = StreamLanguage.define(powerShell)
break
case 'dart':
extension = StreamLanguage.define(dart)
break
default:
return null
}
@@ -90,34 +95,5 @@ export function loadLanguageExtension(language) {
* @returns {string} 语言名称
*/
export function getLanguageFromExtension(extension) {
const ext = extension.toLowerCase()
const langMap = {
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
ts: 'typescript', tsx: 'typescript',
json: 'json',
yaml: 'yaml', yml: 'yaml',
html: 'html', htm: 'html',
css: 'css', scss: 'css', sass: 'css', less: 'css',
cpp: 'cpp', c: 'c', cc: 'cpp', cxx: 'cpp', h: 'cpp', hpp: 'cpp', hxx: 'cpp',
rust: 'rust', rs: 'rust',
go: 'go',
python: 'python', py: 'python', pyw: 'python',
php: 'php',
sql: 'sql',
markdown: 'markdown', md: 'markdown',
java: 'java',
sh: 'shell', bash: 'shell', shell: 'shell', zsh: 'shell'
}
return langMap[ext] || 'text'
}
/**
* 预加载常用语言包
* 用于在应用启动时预热缓存
*/
export function preloadCommonLanguages() {
// 现在是同步的,不需要 Promise.all
;['javascript', 'json', 'markdown', 'python', 'sql'].forEach(loadLanguageExtension)
return getCmLanguage(extension)
}

View File

@@ -25,5 +25,7 @@ export { sql } from '@codemirror/lang-sql'
export { markdown } from '@codemirror/lang-markdown'
export { java } from '@codemirror/lang-java'
// Legacy language modes (shell)
// Legacy language modes (shell, powershell, dart)
export { shell } from '@codemirror/legacy-modes/mode/shell'
export { powerShell } from '@codemirror/legacy-modes/mode/powershell'
export { dart } from '@codemirror/legacy-modes/mode/clike'

View File

@@ -73,9 +73,9 @@ export const FILE_EXTENSIONS = {
CODE: [
'js', 'ts', 'jsx', 'tsx', 'cts', 'mts', 'cjs', 'mjs',
'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt',
'scala', 'css', 'scss', 'sass', 'less', 'sql', 'sh', 'bat', 'ps1',
'scala', 'dart', 'css', 'scss', 'sass', 'less', 'sql', 'sh', 'bat', 'ps1',
'flow', 'pch', 'cc', 'cxx', 'hpp', 'hxx', 'tcc', 'defs', 'makefile', 'mk', 'cmake',
'tex', 'm', 'r', 'matlab', 'latex', 'rst', 'adoc'
'm', 'r', 'matlab'
],
// 配置文件(可编辑的文本格式)
@@ -154,6 +154,7 @@ export const FILE_ICONS = {
RUST: '🦀',
PHP: '🐘',
RUBY: '💎',
DART: '🎯',
// 数据库
DATABASE: '🗄️',
@@ -266,6 +267,8 @@ const initIconMap = () => {
'gem': FILE_ICONS.RUBY,
// SQL
'sql': FILE_ICONS.SQL,
// Dart
'dart': FILE_ICONS.DART,
}
Object.keys(langIcons).forEach(ext => FILE_ICON_MAP.set(ext, langIcons[ext]))

View File

@@ -1,41 +0,0 @@
/**
* 文件类型工具函数
*/
import { FILE_EXTENSIONS } from './constants'
// 获取文件扩展名
export const getExt = (path) => {
if (!path) return ''
const dot = path.lastIndexOf('.')
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
if (dot === -1 || dot < slash) return ''
return path.slice(dot + 1).toLowerCase()
}
// 文件类型检查
export const isImage = (path) => FILE_EXTENSIONS.IMAGE.includes(getExt(path))
export const isVideo = (path) => FILE_EXTENSIONS.VIDEO_BROWSER.includes(getExt(path))
export const isAudio = (path) => FILE_EXTENSIONS.AUDIO.includes(getExt(path))
export const isPdf = (path) => getExt(path) === 'pdf'
export const isHtml = (path) => { const e = getExt(path); return e === 'html' || e === 'htm' }
export const isMarkdown = (path) => { const e = getExt(path); return e === 'md' || e === 'markdown' }
export const isCode = (path) => FILE_EXTENSIONS.CODE.includes(getExt(path))
export const isArchive = (path) => FILE_EXTENSIONS.ARCHIVE.includes(getExt(path))
export const isDatabase = (path) => FILE_EXTENSIONS.DATABASE.includes(getExt(path))
export const isExecutable = (path) => FILE_EXTENSIONS.EXECUTABLE.includes(getExt(path))
// 复合检查
export const isVideoAny = (path) => {
const e = getExt(path)
return FILE_EXTENSIONS.VIDEO_BROWSER.includes(e) || FILE_EXTENSIONS.VIDEO_EXTERNAL.includes(e)
}
export const isEditableDoc = (path) => {
const e = getExt(path)
return FILE_EXTENSIONS.DOCUMENT.includes(e) && e !== 'pdf'
}
export const isBinary = (path) => isVideoAny(path) || isAudio(path) || isArchive(path) || isExecutable(path)
export const canPreview = (path) => isImage(path) || isVideo(path) || isAudio(path) || isPdf(path)
export const canEdit = (path) => !isBinary(path) && !isImage(path)

View File

@@ -2,10 +2,8 @@
* Office 文件预览处理器
*/
// 获取文件扩展名(统一方法)
function getExt(fileName) {
return fileName?.split('.').pop()?.toLowerCase() || ''
}
import { escapeHtml } from './fileUtils'
import { isExcelFile, isWordFile, isOfficeFile, isCsvFile } from './fileTypeHelpers'
// 每批加载行数
const BATCH_ROWS = 200
@@ -37,10 +35,10 @@ export async function previewExcel(file, container) {
.excel-info{padding:4px 12px;background:var(--color-fill-1);font-size:12px;color:var(--color-text-3);border-bottom:1px solid var(--color-border-2)}
.excel-content{flex:1;overflow:auto;padding:12px}
.excel-content table{width:100%;border-collapse:collapse;font-size:13px}
.excel-content td,.excel-content th{border:1px solid var(--color-border-2);padding:6px 10px;text-align:left;white-space:nowrap}
.excel-content th{background:var(--color-fill-2);font-weight:600;position:sticky;top:0;left:0;z-index:2}
.excel-content td,.excel-content th{border:1px solid var(--color-border-2);padding:6px 10px;text-align:left;white-space:nowrap;color:var(--color-text-1)}
.excel-content th{background:var(--color-fill-2);font-weight:600;position:sticky;top:0;z-index:2}
.excel-content .row-num{background:var(--color-fill-1);color:var(--color-text-3);text-align:center;min-width:10px;position:sticky;left:0;z-index:1}
.excel-content th.row-num{z-index:3}
.excel-content th.row-num{z-index:3;top:0;left:0}
.excel-content tr:hover td{background:var(--color-fill-1)}
.excel-content tr:hover .row-num{background:var(--color-fill-2)}
</style>
@@ -61,8 +59,6 @@ export async function previewExcel(file, container) {
// 渲染表格(带行号)
const renderTable = (data, startRow = 0) => {
const escapeHtml = (str) => str == null ? '' : String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
let html = '<table><thead><tr><th class="row-num">#</th>'
if (data[0]) {
data[0].forEach((cell, i) => {
@@ -86,7 +82,6 @@ export async function previewExcel(file, container) {
// 追加行
const appendRows = (data, fromRow, toRow) => {
const escapeHtml = (str) => str == null ? '' : String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const tbody = contentEl.querySelector('tbody')
if (!tbody) return
@@ -191,15 +186,8 @@ export async function previewWord(file, container) {
return { success: true }
}
// 文件类型判断
const OFFICE_EXTS = { xlsx: 1, xls: 1, docx: 1, doc: 1 }
const EXCEL_EXTS = { xlsx: 1, xls: 1 }
const WORD_EXTS = { docx: 1, doc: 1 }
export const isOfficeFile = (name) => OFFICE_EXTS[getExt(name)] || false
export const isExcelFile = (name) => EXCEL_EXTS[getExt(name)] || false
export const isWordFile = (name) => WORD_EXTS[getExt(name)] || false
export const isCsvFile = (name) => ['csv', 'tsv'].includes(getExt(name))
// 文件类型判断(从 fileTypeHelpers 导入)
export { isOfficeFile, isExcelFile, isWordFile, isCsvFile }
// CSV/TSV 预览处理器(原生实现,支持滚动加载)
export async function previewCsv(file, container) {
@@ -243,8 +231,6 @@ export async function previewCsv(file, container) {
const delimiter = file.name.endsWith('.tsv') ? '\t' : ','
const rows = lines.map(line => parseLine(line, delimiter))
const escapeHtml = (str) => str == null ? '' : String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
container.innerHTML = `
<div class="csv-preview">
<div class="csv-info">📋 ${file.name}</div>
@@ -255,10 +241,10 @@ export async function previewCsv(file, container) {
.csv-info{padding:4px 12px;background:var(--color-fill-1);font-size:12px;color:var(--color-text-3);border-bottom:1px solid var(--color-border-2)}
.csv-content{flex:1;overflow:auto;padding:12px}
.csv-content table{width:100%;border-collapse:collapse;font-size:13px}
.csv-content td,.csv-content th{border:1px solid var(--color-border-2);padding:6px 10px;text-align:left;white-space:nowrap}
.csv-content th{background:var(--color-fill-2);font-weight:600;position:sticky;top:0;left:0;z-index:2}
.csv-content td,.csv-content th{border:1px solid var(--color-border-2);padding:6px 10px;text-align:left;white-space:nowrap;color:var(--color-text-1)}
.csv-content th{background:var(--color-fill-2);font-weight:600;position:sticky;top:0;z-index:2}
.csv-content .row-num{background:var(--color-fill-1);color:var(--color-text-3);text-align:center;min-width:10px;position:sticky;left:0;z-index:1}
.csv-content th.row-num{z-index:3}
.csv-content th.row-num{z-index:3;top:0;left:0}
.csv-content tr:hover td{background:var(--color-fill-1)}
.csv-content tr:hover .row-num{background:var(--color-fill-2)}
</style>

View File

@@ -6,7 +6,7 @@
*/
import { FILE_EXTENSIONS } from './constants'
import { getExt } from './pathHelpers'
import { getExt } from './fileUtils'
/**
* 可预览的文件类型(有专门的预览处理)

View File

@@ -5,8 +5,51 @@
* @description 提供文件相关的通用工具函数,避免代码重复
*/
import { normalizePathSeparators } from './pathHelpers.js'
import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT, FILE_EXTENSIONS } from './constants'
import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT } from './constants'
/**
* 路径分隔符正则(匹配 Windows 和 Unix 风格)
* @type {RegExp}
*/
export const PATH_SEPARATOR_REGEX = /[/\\]/
/**
* 规范化路径分隔符(统一为正斜杠)
* @param {string} path - 文件路径
* @returns {string} 规范化后的路径
*/
export const normalizePathSeparators = (path) => {
if (!path) return ''
return path.replace(/\\/g, '/')
}
/**
* HTML 转义,防止 XSS
* @param {string} str - 原始字符串
* @returns {string} 转义后的字符串
*/
export const escapeHtml = (str) => {
if (str == null) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
/**
* 获取文件扩展名(路径安全)
* @param {string} path - 文件路径
* @returns {string} 扩展名(小写,不含点号)
*/
export const getExt = (path) => {
if (!path) return ''
const dot = path.lastIndexOf('.')
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
if (dot === -1 || dot < slash) return ''
return path.slice(dot + 1).toLowerCase()
}
/**
* 格式化文件大小
@@ -46,34 +89,29 @@ export function formatBytes(bytes) {
*/
export function getFileName(path) {
if (!path) return ''
// 后端已统一返回 / 路径,直接分割
const parts = path.split('/')
const parts = path.split(PATH_SEPARATOR_REGEX)
return parts[parts.length - 1] || path
}
/**
* 从文件路径中提取文件扩展名
* 分割路径为多个部分
* @param {string} path - 文件路径
* @returns {string} 扩展名(小写,不含点号)
*
* @example
* getFileExtension('/path/to/file.txt') // "txt"
* getFileExtension('/path/to/file.TXT') // "txt"
* getFileExtension('/path/to/file') // ""
* @returns {string[]} 路径数组
*/
export function getFileExtension(path) {
if (!path) return ''
export const splitPath = (path) => {
if (!path) return []
return path.split(PATH_SEPARATOR_REGEX)
}
/**
* 获取文件名(不含扩展名)
* @param {string} path - 文件路径
* @returns {string} 文件名(不含扩展名)
*/
export const getFileNameWithoutExt = (path) => {
const fileName = getFileName(path)
const lastDotIndex = fileName.lastIndexOf('.')
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
return ''
}
return fileName.substring(lastDotIndex + 1).toLowerCase()
const lastDot = fileName.lastIndexOf('.')
return lastDot > 0 ? fileName.substring(0, lastDot) : fileName
}
/**
@@ -97,51 +135,12 @@ export function getFileIcon(fileInfo) {
}
// 获取文件扩展名
const ext = getFileExtension(fileInfo.name)
const ext = getExt(fileInfo.name)
// 从映射表中查找图标
return FILE_ICON_MAP.get(ext) || FILE_ICONS.FILE
}
/**
* 判断文件是否为图片
* @param {string} path - 文件路径
* @returns {boolean} 是否为图片文件
*/
export function isImageFile(path) {
const ext = getFileExtension(path)
return FILE_EXTENSIONS.IMAGE.includes(ext)
}
/**
* 判断文件是否为视频
* @param {string} path - 文件路径
* @returns {boolean} 是否为视频文件
*/
export function isVideoFile(path) {
const ext = getFileExtension(path)
return [...FILE_EXTENSIONS.VIDEO_BROWSER, ...FILE_EXTENSIONS.VIDEO_EXTERNAL].includes(ext)
}
/**
* 判断文件是否为音频
* @param {string} path - 文件路径
* @returns {boolean} 是否为音频文件
*/
export function isAudioFile(path) {
const ext = getFileExtension(path)
return FILE_EXTENSIONS.AUDIO.includes(ext)
}
/**
* 判断文件是否为PDF
* @param {string} path - 文件路径
* @returns {boolean} 是否为PDF文件
*/
export function isPdfFile(path) {
return getFileExtension(path) === 'pdf'
}
/**
* 规范化文件路径将反斜杠转换为正斜杠并进行URL编码
* @param {string} path - 原始路径
@@ -189,7 +188,7 @@ export function normalizeFilePath(path, encode = false) {
* getFileTypeName('unknown.xyz') // "XYZ文件"
*/
export function getFileTypeName(path) {
const ext = getFileExtension(path)
const ext = getExt(path)
const extUpper = ext.toUpperCase()
// 图片
@@ -226,23 +225,6 @@ export function getFileTypeName(path) {
return ext ? `${extUpper}文件` : '文件'
}
/**
* 判断文件是否为二进制文件
* @param {string} path - 文件路径
* @returns {boolean} 是否为二进制文件
*/
export function isBinaryFile(path) {
const ext = getFileExtension(path)
const binaryExtensions = [
'exe', 'dll', 'so', 'dylib', // 可执行文件
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', // 压缩文件
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', // Office文档
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'mp3', 'mp4', // 媒体文件
'eot', 'ttf', 'otf', 'woff', 'woff2', // 字体文件
]
return binaryExtensions.includes(ext)
}
/**
* 检查路径是否为绝对路径
* @param {string} path - 文件路径
@@ -278,6 +260,15 @@ export function joinPaths(...parts) {
return parts.join('/').replace(/\/+/g, '/')
}
/**
* 获取路径使用的分隔符Windows 反斜杠或 Unix 正斜杠)
* @param {string} path - 文件路径
* @returns {string} 分隔符 '\\' 或 '/'
*/
export function getPathSeparator(path) {
return path.includes('\\') ? '\\' : '/'
}
/**
* 获取父目录路径
* @param {string} path - 文件路径
@@ -290,14 +281,24 @@ export function joinPaths(...parts) {
export function getParentPath(path) {
if (!path) return ''
const normalizedPath = normalizeFilePath(path)
const normalizedPath = normalizePathSeparators(path)
const lastSlashIndex = normalizedPath.lastIndexOf('/')
if (lastSlashIndex <= 0) {
return '/' // 根目录
if (/^[A-Za-z]:$/.test(normalizedPath)) {
return normalizedPath + '/'
}
return normalizedPath || '/'
}
return normalizedPath.substring(0, lastSlashIndex)
const parentPath = normalizedPath.substring(0, lastSlashIndex)
// 盘符根目录下文件E:/file.txt → E:/
if (/^[A-Za-z]:$/.test(parentPath)) {
return parentPath + '/'
}
return parentPath || '/'
}
/**

View File

@@ -0,0 +1,129 @@
/**
* 统一语言映射
* 供 highlight.jsMarkdown 预览)和 CodeMirror代码编辑器共用
*/
/**
* 文件扩展名/缩写 → 语言标识符
* - hljs: 用于 markedExtensions.ts 的代码块高亮
* - cm: 用于 codeMirrorLoader.js 的编辑器语言
* 值为 false 表示该扩展名不对应任何编程语言
*/
const extensionToLanguage: Record<string, { hljs?: string; cm?: string }> = {
// === JavaScript / TypeScript ===
js: { hljs: 'javascript', cm: 'javascript' },
jsx: { hljs: 'javascript', cm: 'javascript' },
mjs: { hljs: 'javascript', cm: 'javascript' },
cjs: { hljs: 'javascript', cm: 'javascript' },
ts: { hljs: 'typescript', cm: 'typescript' },
tsx: { hljs: 'typescript', cm: 'typescript' },
cts: { hljs: 'typescript', cm: 'typescript' },
mts: { hljs: 'typescript', cm: 'typescript' },
// === Web ===
html: { hljs: 'xml', cm: 'html' },
htm: { hljs: 'xml', cm: 'html' },
css: { hljs: 'css', cm: 'css' },
scss: { hljs: 'scss', cm: 'css' },
sass: { hljs: 'scss', cm: 'css' },
less: { hljs: 'less', cm: 'css' },
vue: { hljs: 'xml', cm: 'html' },
// === 数据格式 ===
json: { hljs: 'json', cm: 'json' },
xml: { hljs: 'xml', cm: 'html' },
yaml: { hljs: 'yaml', cm: 'yaml' },
yml: { hljs: 'yaml', cm: 'yaml' },
toml: { cm: 'text' },
csv: { cm: 'text' },
tsv: { cm: 'text' },
// === C / C++ / 系统编程 ===
c: { hljs: 'c', cm: 'cpp' },
cpp: { hljs: 'cpp', cm: 'cpp' },
cc: { hljs: 'cpp', cm: 'cpp' },
cxx: { hljs: 'cpp', cm: 'cpp' },
h: { hljs: 'cpp', cm: 'cpp' },
hpp: { hljs: 'cpp', cm: 'cpp' },
hxx: { hljs: 'cpp', cm: 'cpp' },
cs: { hljs: 'csharp', cm: 'text' },
swift: { hljs: 'swift', cm: 'text' },
kt: { hljs: 'kotlin', cm: 'text' },
rs: { hljs: 'rust', cm: 'rust' },
go: { hljs: 'go', cm: 'go' },
java: { hljs: 'java', cm: 'java' },
pch: { hljs: 'cpp', cm: 'cpp' },
tcc: { hljs: 'cpp', cm: 'cpp' },
// === 脚本 ===
py: { hljs: 'python', cm: 'python' },
pyw: { hljs: 'python', cm: 'python' },
rb: { hljs: 'ruby', cm: 'text' },
php: { hljs: 'php', cm: 'php' },
sh: { hljs: 'bash', cm: 'shell' },
bash: { hljs: 'bash', cm: 'shell' },
shell: { hljs: 'bash', cm: 'shell' },
zsh: { hljs: 'bash', cm: 'shell' },
ps1: { hljs: 'powershell', cm: 'powershell' },
bat: { hljs: 'dos', cm: 'text' },
ahk: { hljs: 'autohotkey', cm: 'text' },
lua: { hljs: 'lua', cm: 'text' },
r: { hljs: 'r', cm: 'text' },
m: { hljs: 'objectivec', cm: 'text' },
scala: { hljs: 'scala', cm: 'text' },
dart: { hljs: 'dart', cm: 'dart' },
// === 数据库 / 标记 ===
sql: { hljs: 'sql', cm: 'sql' },
md: { hljs: 'markdown', cm: 'markdown' },
markdown: { hljs: 'markdown', cm: 'markdown' },
tex: { hljs: 'latex', cm: 'text' },
rst: { hljs: 'plaintext', cm: 'text' },
adoc: { hljs: 'plaintext', cm: 'text' },
// === 构建工具 / 配置 ===
dockerfile: { hljs: 'dockerfile', cm: 'text' },
makefile: { hljs: 'makefile', cm: 'text' },
mk: { hljs: 'makefile', cm: 'text' },
cmake: { hljs: 'cmake', cm: 'text' },
ini: { hljs: 'ini', cm: 'text' },
cfg: { hljs: 'ini', cm: 'text' },
conf: { hljs: 'ini', cm: 'text' },
env: { cm: 'text' },
props: { cm: 'text' },
manifest: { cm: 'text' },
lock: { cm: 'text' },
ignore: { cm: 'text' },
// === 纯文本 ===
txt: { cm: 'text' },
text: { cm: 'text' },
log: { cm: 'text' },
msg: { cm: 'text' },
}
/**
* 获取 hljs 语言标识(带别名解析)
*/
export function getHljsLanguage(langOrExt: string): string {
if (!langOrExt) return 'plaintext'
const lower = langOrExt.toLowerCase()
// 先查扩展名映射
const mapped = extensionToLanguage[lower]
if (mapped?.hljs) return mapped.hljs
// 再查 hljs 直接注册名
if (typeof hljs !== 'undefined' && hljs.getLanguage(lower)) return lower
return 'plaintext'
}
/**
* 获取 CodeMirror 语言标识
*/
export function getCmLanguage(extension: string): string {
if (!extension) return 'text'
const lower = extension.toLowerCase()
return extensionToLanguage[lower]?.cm || 'text'
}

View File

@@ -1,38 +1,66 @@
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/lib/common'
// 额外导入 common 包不包含的语言
import 'highlight.js/lib/languages/bash'
import 'highlight.js/lib/languages/go'
import 'highlight.js/styles/github-dark.css'
import 'highlight.js/styles/github.css'
// 语言别名映射sh -> bash 等)
const languageAliases: Record<string, string> = {
'sh': 'bash',
'shell': 'bash',
'zsh': 'bash',
'ksh': 'bash',
'ts': 'typescript',
'js': 'javascript',
'py': 'python',
'rb': 'ruby',
'yml': 'yaml',
'md': 'markdown'
}
// 按需导入 common 包不包含的语言
import 'highlight.js/lib/languages/powershell'
import 'highlight.js/lib/languages/dos'
import 'highlight.js/lib/languages/autohotkey'
import 'highlight.js/lib/languages/latex'
import 'highlight.js/lib/languages/dockerfile'
import 'highlight.js/lib/languages/cmake'
import 'highlight.js/lib/languages/scala'
import 'highlight.js/lib/languages/dart'
import { getHljsLanguage } from './languageMap'
let mermaidInstance: typeof import('mermaid').default | null = null
let mermaidTheme: string | null = null
// 检测当前是否为暗色主题
function isDarkTheme(): boolean {
if (typeof document === 'undefined') return false
return document.body.getAttribute('arco-theme')?.includes('dark') ?? false
}
async function loadMermaid() {
if (mermaidInstance) return mermaidInstance
const currentTheme = isDarkTheme() ? 'dark' : 'default'
if (mermaidInstance && mermaidTheme === currentTheme) {
return mermaidInstance
}
try {
const mermaid = await import('mermaid')
mermaid.default.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
theme: currentTheme,
securityLevel: 'strict',
themeVariables: currentTheme === 'dark' ? {
primaryColor: '#165DFF',
primaryTextColor: '#ffffff',
primaryBorderColor: '#4080FF',
lineColor: '#4E5969',
secondaryColor: '#0E42D2',
tertiaryColor: '#0FC6C2',
mainBkg: '#17171A',
nodeBorder: '#165DFF',
clusterBkg: '#232324',
titleColor: '#FFFFFF',
edgeLabelBackground: '#232324'
} : {
primaryColor: '#165DFF',
primaryTextColor: '#ffffff',
primaryBorderColor: '#4080FF',
lineColor: '#86909C',
secondaryColor: '#E8F3FF',
tertiaryColor: '#722ED1',
mainBkg: '#F2F3F5',
nodeBorder: '#165DFF',
clusterBkg: '#F7F8FA',
titleColor: '#1D2129',
edgeLabelBackground: '#F2F3F5'
}
})
mermaidTheme = currentTheme
mermaidInstance = mermaid.default
return mermaidInstance
} catch {
@@ -47,14 +75,7 @@ renderer.code = function(token: any) {
return `<pre class="mermaid">${token.text}</pre>`
}
// 获取语言,支持别名
let lang = token.lang || 'plaintext'
lang = languageAliases[lang] || lang
// 检查语言是否支持
if (!hljs.getLanguage(lang)) {
lang = 'plaintext'
}
const lang = getHljsLanguage(token.lang)
const highlighted = hljs.highlight(token.text, { language: lang }).value
return `<pre><code class="hljs language-${lang}" data-theme="auto">${highlighted}</code></pre>`
@@ -81,34 +102,26 @@ renderer.heading = function(token: any) {
// 判断是否为本地文件链接(相对路径或本地绝对路径)
const isLocalFileLink = (href: string): boolean => {
if (!href) return false
// 排除 http/https/ftp/mailto 等外部链接
if (/^(https?|ftp|mailto|tel|data):/i.test(href)) return false
// 排除锚点链接
if (href.startsWith('#')) return false
// 相对路径或本地路径(如 ./file.md, ../file.md, /path/to/file, C:\path\file
return true
}
// 自定义链接渲染器 - 支持本地文件链接
renderer.link = function(token: any) {
const href = token.href || ''
// 解析链接文本中的内联元素(如加粗、斜体等)
const text = this.parser.parseInline(token.tokens) || token.text || ''
const title = token.title || ''
const titleAttr = title ? ` title="${title}"` : ''
// 锚点链接 - 保持原样,页面内跳转
if (href.startsWith('#')) {
return `<a href="${href}"${titleAttr}>${text}</a>`
}
// 判断是否为本地文件链接
if (isLocalFileLink(href)) {
return `<a href="javascript:void(0)" data-local-link="${href}" class="local-file-link"${titleAttr}>${text}</a>`
}
// 外部链接使用默认行为
return `<a href="${href}" target="_blank" rel="noopener noreferrer"${titleAttr}>${text}</a>`
}
@@ -122,5 +135,3 @@ export async function renderMermaidDiagrams() {
await mermaid.run()
}
}

View File

@@ -1,118 +0,0 @@
/**
* 路径处理工具函数
*
* @module utils/pathHelpers
* @description 统一路径分割、文件名获取等操作,避免重复代码
*/
import { getExt as getExtFromFileHelpers } from './fileHelpers'
// 重新导出 getExt避免重复定义
export const getExt = getExtFromFileHelpers
/**
* 路径分隔符正则(匹配 Windows 和 Unix 风格)
* @type {RegExp}
*/
export const PATH_SEPARATOR_REGEX = /[/\\]/
/**
* 分割路径为多个部分
* @param {string} path - 文件路径
* @returns {string[]} 路径数组
* @example
* splitPath('C:\\Users\\file.txt') // ['C:', 'Users', 'file.txt']
* splitPath('/home/user/file.txt') // ['home', 'user', 'file.txt']
*/
export const splitPath = (path) => {
if (!path) return []
return path.split(PATH_SEPARATOR_REGEX)
}
/**
* 获取文件名(含扩展名)
* @param {string} path - 文件路径
* @returns {string} 文件名
* @example
* getFileName('C:\\Users\\file.txt') // 'file.txt'
* getFileName('/home/user/file.txt') // 'file.txt'
*/
export const getFileName = (path) => {
if (!path) return ''
const parts = splitPath(path)
return parts[parts.length - 1] || path
}
/**
* 获取父目录路径
* @param {string} path - 文件或目录路径
* @returns {string} 父目录路径
* @example
* getParentPath('C:\\Users\\file.txt') // 'C:\\Users'
* getParentPath('/home/user/file.txt') // '/home/user'
* getParentPath('E:/file.txt') // 'E:/'
*/
export const getParentPath = (path) => {
if (!path) return ''
// 规范化路径分隔符
const normalizedPath = path.replace(/\\/g, '/')
// 查找最后一个分隔符的位置
const lastSep = normalizedPath.lastIndexOf('/')
if (lastSep <= 0) {
// 没有分隔符或分隔符在开头,返回根目录(对于盘符情况)
if (/^[A-Za-z]:$/.test(normalizedPath)) {
return normalizedPath + '/' // E: 转换为 E:/
}
return normalizedPath
}
const parentPath = normalizedPath.substring(0, lastSep)
// 特殊处理如果是盘符根目录下的文件E:/file.txt -> E:/
if (/^[A-Za-z]:$/.test(parentPath)) {
return parentPath + '/' // 确保根目录带斜杠
}
return parentPath || '/'
}
/**
* 获取文件名(不含扩展名)
* @param {string} path - 文件路径
* @returns {string} 文件名(不含扩展名)
* @example
* getFileNameWithoutExt('file.txt') // 'file'
* getFileNameWithoutExt('archive.tar.gz') // 'archive.tar'
*/
export const getFileNameWithoutExt = (path) => {
const fileName = getFileName(path)
const lastDot = fileName.lastIndexOf('.')
return lastDot > 0 ? fileName.substring(0, lastDot) : fileName
}
/**
* 规范化路径分隔符(统一为正斜杠)
* @param {string} path - 文件路径
* @returns {string} 规范化后的路径
*/
export const normalizePathSeparators = (path) => {
if (!path) return ''
return path.replace(/\\/g, '/')
}
/**
* 连接路径片段
* @param {...string} parts - 路径片段
* @returns {string} 连接后的路径
* @example
* joinPath('C:', 'Users', 'file.txt') // 'C:/Users/file.txt'
*/
export const joinPath = (...parts) => {
return parts
.filter(part => part && part !== '')
.map(part => part.replace(/[\/\\]+$/, '').replace(/^[\/\\]+/, ''))
.join('/')
}

View File

@@ -0,0 +1,220 @@
<template>
<div class="markdown-viewer-container">
<div class="viewer-header">
<div class="title">
<icon-file-text />
<span>{{ currentFile?.name || 'Markdown 文档' }}</span>
</div>
<div class="actions">
<PdfExportButton @export-complete="onExportComplete" />
<a-button @click="handleBackToList" type="outline">
<icon-arrow-left />
返回列表
</a-button>
</div>
</div>
<div class="viewer-content">
<MarkdownEditor
:content="fileContent"
@content-change="handleContentChange"
@save="handleSave"
/>
</div>
<!-- 底部状态栏 -->
<div class="status-bar">
<div class="file-info">
<span>{{ currentFile?.path }}</span>
</div>
<div class="content-info">
<span>{{ wordCount }} 字符 | {{ lineCount }} </span>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import MarkdownEditor from '@/components/MarkdownEditor.vue'
import PdfExportButton from '@/components/PdfExportButton.vue'
import { useFileOperations } from '@/composables/useFileOperations'
export default {
name: 'MarkdownViewer',
components: {
MarkdownEditor,
PdfExportButton
},
props: {
filePath: {
type: String,
required: true
}
},
emits: ['back'],
setup(props, { emit }) {
const fileOperations = useFileOperations()
const fileContent = ref('')
const currentFile = ref(null)
const hasChanges = ref(false)
const lastSavedContent = ref('')
// 计算属性
const wordCount = computed(() => {
return fileContent.value.length
})
const lineCount = computed(() => {
return fileContent.value.split('\n').length
})
// 方法
const loadFile = async () => {
try {
const response = await fileOperations.readFile(props.filePath)
fileContent.value = response.content
lastSavedContent.value = response.content
hasChanges.value = false
// 获取文件信息
const fileName = props.filePath.split('/').pop() || props.filePath.split('\\').pop()
currentFile.value = {
name: fileName,
path: props.filePath
}
} catch (error) {
Message.error(`读取文件失败: ${error?.message ?? String(error)}`)
}
}
const handleContentChange = (content) => {
hasChanges.value = content !== lastSavedContent.value
}
const handleSave = async () => {
try {
await fileOperations.saveFile(props.filePath, fileContent.value)
lastSavedContent.value = fileContent.value
hasChanges.value = false
Message.success('文件已保存')
} catch (error) {
Message.error(`保存文件失败: ${error?.message ?? String(error)}`)
}
}
const onExportComplete = () => {
Message.success('PDF 导出完成')
}
const handleBackToList = () => {
emit('back')
}
// 初始化
onMounted(() => {
loadFile()
})
return {
fileContent,
currentFile,
hasChanges,
wordCount,
lineCount,
handleContentChange,
handleSave,
onExportComplete,
handleBackToList
}
}
}
</script>
<style scoped>
.markdown-viewer-container {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
}
.viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: white;
border-bottom: 1px solid #e8e8e8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.actions {
display: flex;
gap: 8px;
align-items: center;
}
.viewer-content {
flex: 1;
overflow: auto;
background: white;
margin: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background: white;
border-top: 1px solid #e8e8e8;
font-size: 12px;
color: #666;
}
.file-info {
font-family: monospace;
color: #999;
}
.content-info {
color: #666;
}
/* 响应式设计 */
@media (max-width: 768px) {
.viewer-header {
padding: 12px 16px;
}
.title {
font-size: 14px;
}
.actions {
gap: 4px;
}
.viewer-content {
margin: 8px;
}
.status-bar {
padding: 6px 16px;
font-size: 11px;
}
}
</style>

View File

@@ -495,6 +495,7 @@ import { Input, Select, Checkbox, InputGroup, Button, Option, Optgroup, Tooltip
import MySQLCreate from './MySQLCreate.vue'
import { STORAGE_KEYS } from '../constants/storage'
import { useResultHistory, type ResultHistoryItem } from '../composables/useResultHistory'
import { escapeHtml } from '@/utils/fileUtils'
// MySQL 数据类型选项
const mysqlDataTypeOptions = [
@@ -1125,13 +1126,7 @@ const formatJSON = (data: unknown): string => JSON.stringify(data, null, 2)
const highlightJSON = (data: unknown): string => {
const json = JSON.stringify(data, null, 2)
if (!json) return ''
// 转义 HTML 特殊字符
const escapeHtml = (str: string) => str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 语法高亮正则替换
return escapeHtml(json)
// 字符串值(双引号包围,不是键名)

View File

@@ -6,17 +6,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { escapeHtml } from '@/utils/fileUtils'
const props = defineProps<{
data: any[]
}>()
// 转义 HTML
const escapeHtml = (str: string) => str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// JSON 高亮
const highlightedJson = computed(() => {
const json = JSON.stringify(props.data, null, 2)

View File

@@ -3,6 +3,8 @@
* 支持 CSV、JSON、Excel 格式
*/
import { escapeHtml } from '@/utils/fileUtils'
/**
* 导出为 CSV
*/
@@ -183,20 +185,6 @@ function downloadFile(content, filename, mimeType) {
URL.revokeObjectURL(url)
}
/**
* HTML 转义
*/
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, m => map[m])
}
/**
* 复制到剪贴板
*/

View File

@@ -0,0 +1,113 @@
<template>
<div class="markdown-editor-page">
<div class="editor-container">
<MarkdownEditor
v-model:content="markdownContent"
@content-change="handleContentChange"
@save="handleSave"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import MarkdownEditor from '../../components/MarkdownEditor.vue'
import { Message } from '@arco-design/web-vue'
const markdownContent = ref('')
// 初始化示例内容
const initSampleContent = () => {
markdownContent.value = `# 欢迎使用 Markdown 编辑器
这是一个功能强大的 Markdown 编辑器,支持实时预览和 PDF 导出功能。
## 功能特性
- ✅ **实时预览** - 输入内容即时显示预览效果
- ✅ **语法高亮** - 支持 GitHub 风格的 Markdown 语法
- ✅ **PDF 导出** - 一键导出为格式化的 PDF 文档
- ✅ **自动保存** - 支持 Ctrl + S 快捷键保存
- ✅ **字数统计** - 实时显示字符数和行数
## 使用说明
### 基本语法
\`\`\`markdown
# 一级标题
## 二级标题
### 三级标题
**粗体文本** 和 *斜体文本*
- 无序列表项 1
- 无序列表项 2
- 嵌套列表项 1
1. 有序列表项 1
2. 有序列表项 2
\`\`\`
### 代码块
\`\`\`javascript
function hello() {
console.log('Hello, World!')
}
\`\`\`
### 表格
| 列 1 | 列 2 | 列 3 |
|------|------|------|
| 数据 1 | 数据 2 | 数据 3 |
| 数据 4 | 数据 5 | 数据 6 |
### 引用
> 这是一个引用示例
> 可以包含多行内容
---
**开始创作吧!**`
}
const handleContentChange = (content) => {
// 内容变化时的处理
}
const handleSave = (content) => {
// 保存处理
console.log('Content saved:', content)
Message.success('内容已保存到本地存储')
}
onMounted(() => {
// 从本地存储加载之前保存的内容
const savedContent = localStorage.getItem('u-desk-markdown-content')
if (savedContent) {
markdownContent.value = savedContent
} else {
// 没有保存的内容时显示示例内容
initSampleContent()
}
})
</script>
<style scoped>
.markdown-editor-page {
height: 100%;
width: 100%;
padding: 20px;
background: var(--color-bg-1);
}
.editor-container {
height: 100%;
width: 100%;
}
</style>

View File

@@ -28,6 +28,10 @@ export function EmptyRecycleBin():Promise<void>;
export function ExecuteSQL(arg1:number,arg2:string,arg3:string):Promise<Record<string, any>>;
export function ExportMarkdownToPDF(arg1:string):Promise<string>;
export function ExportPDF(arg1:string,arg2:string,arg3:string,arg4:number,arg5:number,arg6:number):Promise<Record<string, any>>;
export function ExtractFileFromZip(arg1:string,arg2:string):Promise<string>;
export function ExtractFileFromZipToTemp(arg1:string,arg2:string):Promise<string>;
@@ -56,6 +60,12 @@ export function GetIndexes(arg1:number,arg2:string,arg3:string):Promise<Array<Re
export function GetMemoryInfo():Promise<Record<string, any>>;
export function GetRecycleBinEntries():Promise<Array<Record<string, any>>>;
export function GetResultHistory(arg1:any,arg2:string,arg3:number,arg4:number):Promise<Record<string, any>>;
@@ -108,12 +118,16 @@ export function SaveAppConfig(arg1:main.SaveAppConfigRequest):Promise<Record<str
export function SaveDbConnection(arg1:api.SaveConnectionRequest):Promise<void>;
export function SaveResult(arg1:number,arg2:string,arg3:string,arg4:string,arg5:any,arg6:Array<string>,arg7:number,arg8:number):Promise<Record<string, any>>;
export function SaveSqlTabs(arg1:Array<Record<string, any>>):Promise<void>;
export function SelectPDFSaveDirectory():Promise<string>;
export function SetUpdateConfig(arg1:boolean,arg2:number,arg3:string):Promise<Record<string, any>>;
export function TestDbConnection(arg1:number):Promise<void>;
export function TestDbConnectionWithParams(arg1:api.TestConnectionRequest):Promise<void>;
@@ -130,4 +144,6 @@ export function WindowMaximize():Promise<void>;
export function WindowMinimize():Promise<void>;
export function WindowToggleAlwaysOnTop():Promise<boolean>;
export function WriteFile(arg1:main.WriteFileRequest):Promise<void>;

View File

@@ -50,6 +50,14 @@ export function ExecuteSQL(arg1, arg2, arg3) {
return window['go']['main']['App']['ExecuteSQL'](arg1, arg2, arg3);
}
export function ExportMarkdownToPDF(arg1) {
return window['go']['main']['App']['ExportMarkdownToPDF'](arg1);
}
export function ExportPDF(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['ExportPDF'](arg1, arg2, arg3, arg4, arg5, arg6);
}
export function ExtractFileFromZip(arg1, arg2) {
return window['go']['main']['App']['ExtractFileFromZip'](arg1, arg2);
}
@@ -106,6 +114,30 @@ export function GetMemoryInfo() {
return window['go']['main']['App']['GetMemoryInfo']();
}
export function GetOpenClawConfig() {
return window['go']['main']['App']['GetOpenClawConfig']();
}
export function GetOpenClawModelUsage() {
return window['go']['main']['App']['GetOpenClawModelUsage']();
}
export function GetOpenClawSessionHistory(arg1, arg2) {
return window['go']['main']['App']['GetOpenClawSessionHistory'](arg1, arg2);
}
export function GetOpenClawSessions() {
return window['go']['main']['App']['GetOpenClawSessions']();
}
export function GetOpenClawStatus() {
return window['go']['main']['App']['GetOpenClawStatus']();
}
export function GetOpenClawSystemUsage() {
return window['go']['main']['App']['GetOpenClawSystemUsage']();
}
export function GetRecycleBinEntries() {
return window['go']['main']['App']['GetRecycleBinEntries']();
}
@@ -210,6 +242,10 @@ export function SaveDbConnection(arg1) {
return window['go']['main']['App']['SaveDbConnection'](arg1);
}
export function SaveOpenClawConfig(arg1) {
return window['go']['main']['App']['SaveOpenClawConfig'](arg1);
}
export function SaveResult(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
return window['go']['main']['App']['SaveResult'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
}
@@ -218,10 +254,18 @@ export function SaveSqlTabs(arg1) {
return window['go']['main']['App']['SaveSqlTabs'](arg1);
}
export function SelectPDFSaveDirectory() {
return window['go']['main']['App']['SelectPDFSaveDirectory']();
}
export function SetUpdateConfig(arg1, arg2, arg3) {
return window['go']['main']['App']['SetUpdateConfig'](arg1, arg2, arg3);
}
export function SwitchOpenClawSession(arg1) {
return window['go']['main']['App']['SwitchOpenClawSession'](arg1);
}
export function TestDbConnection(arg1) {
return window['go']['main']['App']['TestDbConnection'](arg1);
}
@@ -254,6 +298,10 @@ export function WindowMinimize() {
return window['go']['main']['App']['WindowMinimize']();
}
export function WindowToggleAlwaysOnTop() {
return window['go']['main']['App']['WindowToggleAlwaysOnTop']();
}
export function WriteFile(arg1) {
return window['go']['main']['App']['WriteFile'](arg1);
}