Private
Public Access
1
0

优化:CSV编辑模式/PDF导出重构/收藏夹bug修复/移除useLocalStorage

- FileEditorPanel: CSV新增预览/编辑切换、PDF导出;提取openPrintWindow公共函数
- useFavorites: 修复find回调中fav变量遮蔽bug(f.path)、sort改为副本排序
- useFavoriteFiles/DeviceTest: 移除useLocalStorage抽象层,直接管理localStorage
- system.ts: createDir/createFile签名改为(parent, name)两参数拼接
- useFileOperations: createNewFile移除无用content参数
- 清理OpenClaw相关Wails绑定
This commit is contained in:
2026-04-07 11:58:42 +08:00
parent fb12ec48e8
commit efc042fcd3
8 changed files with 204 additions and 231 deletions

View File

@@ -110,23 +110,25 @@ export async function deletePath(path: string): Promise<any> {
}
/**
* 创建目录
* 创建目录parentPath + dirname 拼接为完整路径)
*/
export async function createDir(path: string): Promise<any> {
export async function createDir(parentPath: string, dirname: string): Promise<any> {
if (!window.go?.main?.App?.CreateDir) {
throw new Error('CreateDir API 不可用')
}
return await window.go.main.App.CreateDir(path)
const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
return await window.go.main.App.CreateDir(fullPath)
}
/**
* 创建文件
* 创建文件dirPath + filename 拼接为完整路径)
*/
export async function createFile(path: string): Promise<any> {
export async function createFile(dirPath: string, filename: string): Promise<any> {
if (!window.go?.main?.App?.CreateFile) {
throw new Error('CreateFile API 不可用')
}
return await window.go.main.App.CreateFile(path)
const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
return await window.go.main.App.CreateFile(fullPath)
}
/**

View File

@@ -221,7 +221,6 @@ import { formatBytes, sortFileList } from '@/utils/fileUtils'
// 导入 composables
import { useFileOperations } from '@/composables/useFileOperations'
import { useFavoriteFiles } from '@/composables/useFavoriteFiles'
import { useLocalStorage } from '@/composables/useLocalStorage'
// ========== 使用 Composables ==========
@@ -251,20 +250,21 @@ const {
} = useFavoriteFiles(STORAGE_KEYS.DEVICE_TEST.FAVORITE_FILES)
// localStorage管理
const { storedValue: fileContentHeight } = useLocalStorage(
STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT_HEIGHT,
DEFAULTS.DEFAULT_CONTENT_HEIGHT
)
const fileContentHeight = ref(DEFAULTS.DEFAULT_CONTENT_HEIGHT)
const filePanelWidth = ref({ left: 50, right: 50 })
const pathHistory = ref([])
const { storedValue: filePanelWidth } = useLocalStorage(
STORAGE_KEYS.DEVICE_TEST.PANEL_WIDTH,
{ left: 50, right: 50 }
)
const { storedValue: pathHistory } = useLocalStorage(
STORAGE_KEYS.DEVICE_TEST.PATH_HISTORY,
[]
)
// 从 localStorage 恢复
try {
const h = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT_HEIGHT)
if (h) fileContentHeight.value = JSON.parse(h)
const w = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.PANEL_WIDTH)
if (w) filePanelWidth.value = JSON.parse(w)
const p = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.PATH_HISTORY)
if (p) pathHistory.value = JSON.parse(p)
} catch (e) {
console.error('[DeviceTest] 加载 localStorage 失败:', e)
}
// ========== 立即清理旧的文件内容缓存 ==========
// 在组件初始化之前清理,防止加载大文件导致空白
@@ -387,6 +387,11 @@ const addToHistory = (path) => {
if (pathHistory.value.length > 20) {
pathHistory.value = pathHistory.value.slice(0, 20)
}
try {
localStorage.setItem(STORAGE_KEYS.DEVICE_TEST.PATH_HISTORY, JSON.stringify(pathHistory.value))
} catch (e) {
console.error('[DeviceTest] 保存路径历史失败:', e)
}
}
// ========== 文件选择(重写以添加历史记录) ==========

View File

@@ -108,14 +108,54 @@
</div>
</div>
<!-- CSV 预览 -->
<div v-else-if="config.isCsvFile" class="office-preview">
<div class="office-preview-container" ref="csvPreviewRef">
<!-- CSV 预览/编辑 -->
<div v-else-if="config.isCsvFile" class="csv-preview-wrapper">
<div class="preview-mode-switch">
<a-tooltip v-if="config.isEditMode && config.canResetContent" position="left" content="恢复原始内容">
<a-button type="outline" size="small" @click="handleReset">
<template #icon><icon-undo /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="config.canSaveFile" position="left" content="保存 (Ctrl+S)">
<a-button type="primary" size="small" @click="handleSave">
<template #icon><icon-save /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="!config.isEditMode" position="left" content="导出 PDF">
<a-button type="outline" size="small" @click="handleExportCsvPDF">
<template #icon><icon-file-pdf /></template>
</a-button>
</a-tooltip>
<a-tooltip position="left" :content="getModeSwitchTooltip()">
<a-button type="primary" size="small" @click="handleToggleEditMode">
<template #icon>
<icon-eye v-if="config.isEditMode" />
<icon-edit v-else />
</template>
</a-button>
</a-tooltip>
</div>
<!-- 预览模式 -->
<div v-if="!config.isEditMode" class="office-preview-container" ref="csvPreviewRef">
<a-spin v-if="config.officeLoading" :loading="true" tip="加载中...">
<div class="loading-placeholder"></div>
</a-spin>
<a-alert v-else-if="config.officeError" type="error" :message="config.officeError" />
</div>
<!-- 编辑模式 -->
<div v-else class="csv-edit-wrapper">
<AsyncCodeEditor
:model-value="config.fileContent"
:file-extension="config.currentFileExtension"
@update:model-value="handleContentUpdate"
class="code-editor"
/>
<div class="resize-handle-v" @mousedown="handleStartResize" title="拖拽调整高度">
<div class="resize-dots"></div>
</div>
</div>
</div>
<!-- HTML 预览/编辑 -->
@@ -307,7 +347,7 @@
import { computed, watch, nextTick, defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSave, IconEdit, IconEye, IconUndo, IconCopy, IconFilePdf, IconFullscreen, IconFullscreenExit } from '@arco-design/web-vue/es/icon'
import { getFileName } from '@/utils/fileUtils'
import { getFileName, escapeHtml } from '@/utils/fileUtils'
import type { FileEditorPanelConfig } from '@/types/file-system'
import { renderMermaidDiagrams } from '@/utils/markedExtensions'
import { previewExcel, previewWord, previewCsv } from '@/utils/filePreviewHandlers'
@@ -442,163 +482,98 @@ const handleImageError = () => {
emit('imageError')
}
// Markdown PDF 导出处理
const handleExportPDF = async () => {
try {
// 获取 Markdown 预览容器
const markdownContent = markdownPreviewRef.value
if (!markdownContent) {
Message.error('无法获取 Markdown 内容')
return
}
// 打印窗口导出 PDF 公共函数
const openPrintWindow = (title: string, bodyHtml: string, extraStyle = '') => {
const printWindow = window.open('', '_blank')
if (!printWindow) {
Message.error('无法打开打印窗口,请检查浏览器设置')
return
}
// 打开打印窗口
const printWindow = window.open('', '_blank')
if (!printWindow) {
Message.error('无法打开打印窗口,请检查浏览器设置')
return
}
// 设置打印样式
const style = `
<style>
@media print {
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${escapeHtml(title)}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-size: 12pt;
line-height: 1.4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
line-height: 1.5;
color: #333;
margin: 0;
padding: 20px;
}
.no-print {
display: none !important;
@media print {
body { padding: 0; }
@page { margin: 15mm; size: A4; }
}
${extraStyle}
</style>
</head>
<body>${bodyHtml}</body>
</html>
`)
printWindow.document.close()
.markdown-content {
max-width: 100%;
margin: 0;
padding: 0;
}
setTimeout(() => { printWindow.print() }, 500)
Message.success('PDF 导出窗口已打开')
}
.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 导出失败,请重试')
// Markdown PDF 导出处理
const handleExportPDF = async () => {
const markdownContent = markdownPreviewRef.value
if (!markdownContent) {
Message.error('无法获取 Markdown 内容')
return
}
openPrintWindow(
props.config.currentFileName || 'Markdown 导出 PDF',
`<div class="markdown-content">${markdownContent.innerHTML}</div>`,
`
.markdown-content { background: white; padding: 20px; max-width: 100%; }
@media print {
.markdown-content { 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; }
}
`
)
}
// CSV PDF 导出处理
const handleExportCsvPDF = async () => {
const csvContent = csvPreviewRef.value?.querySelector('.csv-content table')
if (!csvContent) {
Message.error('无法获取 CSV 内容')
return
}
openPrintWindow(
props.config.currentFileName || 'CSV 导出 PDF',
csvContent.outerHTML,
`
table { border-collapse: collapse; width: 100%; margin-bottom: 12pt; page-break-inside: auto; }
tr { page-break-inside: avoid; page-break-after: auto; }
th, td { border: 1px solid #dfe2e5; padding: 6px 10px; text-align: left; white-space: nowrap; }
th { background-color: #f6f8fa; font-weight: 600; }
tr:nth-child(even) { background-color: #f8f8f8; }
`
)
}
// 监听模式切换,切换到预览模式时渲染 Mermaid 图表
@@ -628,9 +603,9 @@ watch(() => [props.config.isWordFile, props.config.currentFileFullPath] as const
}
}, { immediate: true })
// 监听 CSV 文件变化,触发预览
watch(() => [props.config.isCsvFile, props.config.currentFileFullPath] as const, async ([isCsv, filePath]) => {
if (isCsv && filePath) {
// 监听 CSV 文件变化或编辑模式切换,触发预览
watch(() => [props.config.isCsvFile, props.config.currentFileFullPath, props.config.isEditMode] as const, async ([isCsv, filePath, isEditMode]) => {
if (isCsv && filePath && !isEditMode) {
await nextTick()
if (csvPreviewRef.value) {
await loadCsvPreview(filePath)
@@ -704,9 +679,9 @@ const loadCsvPreview = async (filePath: string) => {
try {
csvPreviewRef.value.innerHTML = '<div class="loading-hint">加载中...</div>'
const fileUrl = props.config.previewUrl
const response = await fetch(fileUrl)
const blob = await response.blob()
const blob = props.config.fileContent && !props.config.isBinaryFile
? new Blob([props.config.fileContent], { type: 'text/csv' })
: await (await fetch(props.config.previewUrl)).blob()
const file = new File([blob], getFileName(filePath), { type: 'text/csv' })
const result = await previewCsv(file, csvPreviewRef.value)
@@ -1203,6 +1178,22 @@ onUnmounted(() => {
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
}
/* CSV 预览/编辑 */
.csv-preview-wrapper {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.csv-edit-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.office-preview-container {

View File

@@ -24,7 +24,7 @@ export function useFavorites() {
* 排序收藏列表:置顶项在前(按 pinnedAt 降序),非置顶项按添加时间降序
*/
const sortFavorites = () => {
favorites.value.sort((a, b) => {
favorites.value = [...favorites.value].sort((a, b) => {
// 置顶项优先
if (a.pinnedAt && !b.pinnedAt) return -1
if (!a.pinnedAt && b.pinnedAt) return 1
@@ -135,7 +135,7 @@ export function useFavorites() {
*/
const togglePin = (path: string) => {
const normalizedPath = normalizePath(path)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedPath)
const fav = favorites.value.find(f => normalizePath(f.path) === normalizedPath)
if (fav) {
fav.pinnedAt = fav.pinnedAt ? undefined : Date.now()
sortFavorites()
@@ -148,7 +148,7 @@ export function useFavorites() {
*/
const isPinned = (path: string): boolean => {
const normalizedPath = normalizePath(path)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedPath)
const fav = favorites.value.find(f => normalizePath(f.path) === normalizedPath)
return !!fav?.pinnedAt
}
@@ -157,7 +157,7 @@ export function useFavorites() {
*/
const updateFavoritePath = (oldPath: string, newName: string) => {
const normalizedOld = normalizePath(oldPath)
const fav = favorites.value.find(f => normalizePath(fav.path) === normalizedOld)
const fav = favorites.value.find(f => normalizePath(f.path) === normalizedOld)
if (!fav) return
const separator = getPathSeparator(oldPath)

View File

@@ -99,11 +99,10 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
*/
const createNewFile = async (
dirPath: string,
filename: string,
content: string = ''
filename: string
): Promise<FileItem> => {
try {
const result = await createFile(dirPath, filename, content)
const result = await createFile(dirPath, filename)
onSuccess?.('createFile', { dirPath, filename, result })
return result as FileItem
} catch (error) {

View File

@@ -7,7 +7,6 @@
import { ref, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useLocalStorage } from './useLocalStorage'
/**
* 收藏夹 composable
@@ -40,11 +39,27 @@ export function useFavoriteFiles(storageKey, options = {}) {
onRemove = () => {},
} = options
// 使用 localStorage composable 管理收藏列表
const { storedValue: favoriteFiles, load, save } = useLocalStorage(
storageKey,
[]
)
// 收藏列表
const favoriteFiles = ref([])
const load = () => {
try {
const stored = localStorage.getItem(storageKey)
if (stored) {
favoriteFiles.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载收藏列表失败:', e)
}
}
const save = (data) => {
try {
localStorage.setItem(storageKey, JSON.stringify(data || favoriteFiles.value))
} catch (e) {
console.error('保存收藏列表失败:', e)
}
}
/**
* 判断文件/目录是否已收藏
@@ -70,6 +85,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
const timeB = b.addedAt || 0
return timeB - timeA // 倒序:最新的在上面
})
save()
}
/**

View File

@@ -60,12 +60,6 @@ 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>>;
@@ -118,7 +112,6 @@ 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>;
@@ -127,7 +120,6 @@ 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>;

View File

@@ -114,30 +114,6 @@ 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']();
}
@@ -242,10 +218,6 @@ 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);
}
@@ -262,10 +234,6 @@ 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);
}