优化压缩任务错误提示,增强用户反馈和任务管理

This commit is contained in:
yxsj245
2026-04-05 17:46:36 +08:00
parent 5d3e9f416f
commit 5560dbf042
4 changed files with 135 additions and 41 deletions

View File

@@ -75,7 +75,7 @@ import { ImagePreview } from '@/components/ImagePreview'
import { EncodingConfirmDialog } from '@/components/EncodingConfirmDialog'
import { FileChangedDialog } from '@/components/FileChangedDialog'
import PasteConflictDialog from '@/components/PasteConflictDialog'
import { FileItem } from '@/types/file'
import { FileItem, Task } from '@/types/file'
import socketClient from '@/utils/socket'
import { fileApiClient } from '@/utils/fileApi'
import { isTextFile, isImageFile } from '@/utils/format'
@@ -193,6 +193,7 @@ const FileManagerPage: React.FC = () => {
const [searchResults, setSearchResults] = useState<FileItem[]>([])
const [isSearching, setIsSearching] = useState(false)
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const previousActiveTaskCountRef = useRef(0)
// 历史记录
const [history, setHistory] = useState<string[]>([])
@@ -774,6 +775,7 @@ const FileManagerPage: React.FC = () => {
// 初始加载任务列表
loadActiveTasks()
loadTasks()
// 加载系统盘符
loadDrives()
@@ -783,7 +785,7 @@ const FileManagerPage: React.FC = () => {
// 预加载系统信息(用于右键菜单权限判断)
fetchSystemInfo()
}, [searchParams, setCurrentPath, loadFiles, fetchSystemInfo, loadFavorites])
}, [searchParams, setCurrentPath, loadFiles, loadActiveTasks, loadTasks, fetchSystemInfo, loadFavorites])
// 当路径变化时更新选中的盘符
useEffect(() => {
@@ -795,23 +797,26 @@ const FileManagerPage: React.FC = () => {
}
}, [currentPath, drives, selectedDrive, findDriveForPath])
// 定期刷新活动任务
// 定期刷新任务状态
useEffect(() => {
const interval = setInterval(() => {
if (activeTasks.length > 0) {
if (taskDrawerVisible || activeTasks.length > 0) {
loadActiveTasks()
// 如果有任务完成,刷新文件列表
const hasCompletedTasks = activeTasks.some(task =>
task.status === 'completed' || task.status === 'failed'
)
if (hasCompletedTasks) {
loadFiles(undefined, true) // 重置分页
}
loadTasks()
}
}, 2000) // 每2秒刷新一次
return () => clearInterval(interval)
}, [activeTasks, loadFiles])
}, [activeTasks.length, taskDrawerVisible, loadActiveTasks, loadTasks])
useEffect(() => {
if (previousActiveTaskCountRef.current > 0 && activeTasks.length === 0) {
loadTasks()
loadFiles(undefined, true)
}
previousActiveTaskCountRef.current = activeTasks.length
}, [activeTasks.length, loadFiles, loadTasks])
// 键盘快捷键
useEffect(() => {
@@ -1268,7 +1273,7 @@ const FileManagerPage: React.FC = () => {
const handleCompressConfirm = async (archiveName: string, format: string, compressionLevel: number) => {
const filePaths = compressDialog.files.map(file => file.path)
const success = await compressFiles(filePaths, archiveName, format)
const success = await compressFiles(filePaths, archiveName, format, compressionLevel)
if (success) {
addNotification({
type: 'success',
@@ -1524,6 +1529,42 @@ const FileManagerPage: React.FC = () => {
}
}
const getTaskTypeText = (type: Task['type']) => {
switch (type) {
case 'compress':
return '压缩'
case 'extract':
return '解压'
case 'copy':
return '复制'
case 'move':
return '移动'
case 'download':
return '下载'
default:
return type
}
}
const getTaskTargetText = (task: Task) => {
const archivePath = typeof task.data?.archivePath === 'string' ? task.data.archivePath : ''
const targetPath = typeof task.data?.targetPath === 'string' ? task.data.targetPath : ''
if ((task.type === 'compress' || task.type === 'extract') && archivePath) {
return getBasename(archivePath)
}
if (task.type === 'download' && targetPath) {
return getBasename(targetPath)
}
return ''
}
const visibleTasks = [...tasks]
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
.slice(0, 12)
return (
<div className="h-full flex flex-col bg-white dark:bg-gray-900">
{/* 工具栏 */}
@@ -2426,27 +2467,30 @@ const FileManagerPage: React.FC = () => {
width={400}
>
<div className="space-y-4">
{activeTasks.length === 0 ? (
{visibleTasks.length === 0 ? (
<Empty
description="暂无活动任务"
description="暂无任务"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
activeTasks.map((task) => (
visibleTasks.map((task) => (
<Card key={task.id} size="small" className="mb-2">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
{getTaskStatusIcon(task.status)}
<span className="font-medium">
{task.type === 'compress' ? '压缩' :
task.type === 'extract' ? '解压' :
task.type === 'copy' ? '复制' :
task.type === 'move' ? '移动' :
task.type === 'download' ? '下载' : task.type}
</span>
<span className="text-gray-500">
{getTaskStatusText(task.status)}
</span>
<div>
<div className="flex items-center space-x-2">
{getTaskStatusIcon(task.status)}
<span className="font-medium">
{getTaskTypeText(task.type)}
</span>
<span className="text-gray-500">
{getTaskStatusText(task.status)}
</span>
</div>
{getTaskTargetText(task) && (
<div className="text-xs text-gray-500 mt-1 break-all">
{getTaskTargetText(task)}
</div>
)}
</div>
{(task.status === 'completed' || task.status === 'failed') && (
<Button
@@ -2509,7 +2553,7 @@ const FileManagerPage: React.FC = () => {
))
)}
{activeTasks.length > 0 && (
{visibleTasks.length > 0 && (
<div className="text-center pt-4">
<Button
type="primary"

View File

@@ -73,7 +73,7 @@ interface FileStore {
clearClipboard: () => void
// 压缩解压操作
compressFiles: (filePaths: string[], archiveName: string, format?: string) => Promise<boolean>
compressFiles: (filePaths: string[], archiveName: string, format?: string, compressionLevel?: number) => Promise<boolean>
extractArchive: (archivePath: string) => Promise<boolean>
// 编辑器操作
@@ -614,13 +614,17 @@ export const useFileStore = create<FileStore>((set, get) => ({
},
// 压缩文件
compressFiles: async (filePaths: string[], archiveName: string, format: string = 'zip') => {
compressFiles: async (
filePaths: string[],
archiveName: string,
format: string = 'zip',
compressionLevel: number = 6
) => {
const { currentPath } = get()
try {
const result = await fileApiClient.compressFiles(filePaths, currentPath, archiveName, format)
// 立即刷新活动任务列表
await get().loadActiveTasks()
await fileApiClient.compressFiles(filePaths, currentPath, archiveName, format, compressionLevel)
await Promise.all([get().loadActiveTasks(), get().loadTasks()])
return true
} catch (error: any) {
set({ error: error.message || '压缩文件失败' })
@@ -633,9 +637,8 @@ export const useFileStore = create<FileStore>((set, get) => ({
const { currentPath } = get()
try {
const result = await fileApiClient.extractArchive(archivePath, currentPath)
// 立即刷新活动任务列表
await get().loadActiveTasks()
await fileApiClient.extractArchive(archivePath, currentPath)
await Promise.all([get().loadActiveTasks(), get().loadTasks()])
return true
} catch (error: any) {
set({ error: error.message || '解压文件失败' })

View File

@@ -0,0 +1,23 @@
# 压缩任务错误提示优化说明
## 本次调整
针对文件管理器中的压缩任务,补充了以下优化:
- 压缩工具启动失败时,任务列表会直接显示更明确的中文错误原因
- 当错误属于执行权限不足(`EACCES`)时,会提示为二进制文件添加执行权限,并附带工具路径与参考命令
- 文件管理器任务抽屉不再只显示活动任务,同时会保留最近结束的任务,避免“失败太快导致列表为空”
- 压缩弹窗中选择的压缩级别会实际传递到后端接口
## 典型错误提示
当 Linux 环境中的 `file_zip_linux_x64` 缺少执行权限时,任务列表会显示类似提示:
```text
Zip-Tools 无法启动:压缩工具缺少执行权限,请为该文件添加可执行权限后重试。
参考命令: chmod +x "/path/to/file_zip_linux_x64"
```
## 使用效果
用户在收到“请查看任务列表”提示后,即使压缩任务瞬时失败,也可以在任务抽屉中直接看到失败记录和原因,不再需要优先查看后端日志才能判断问题。

View File

@@ -326,6 +326,30 @@ class ZipToolsManager {
await this.download7z()
}
private buildProcessStartError(
toolName: string,
toolPath: string,
error: NodeJS.ErrnoException
): Error {
const resolvedToolPath = path.resolve(toolPath)
if (error.code === 'EACCES') {
return new Error(
`${toolName} 无法启动:压缩工具缺少执行权限,请为该文件添加可执行权限后重试。参考命令: chmod +x "${resolvedToolPath}"。工具路径: ${resolvedToolPath}`
)
}
if (error.code === 'ENOENT') {
return new Error(
`${toolName} 无法启动:未找到压缩工具文件,请检查文件是否存在或重新下载依赖。工具路径: ${resolvedToolPath}`
)
}
return new Error(
`${toolName} 进程启动失败: ${error.message || '未知错误'}。工具路径: ${resolvedToolPath}`
)
}
/**
* 执行 7z 子进程并等待完成
* 退出码为 0 表示成功,非 0 抛出包含 stderr 的异常
@@ -341,8 +365,8 @@ class ZipToolsManager {
stderr += data.toString()
})
child.on('error', (error: Error) => {
reject(new Error(`7z 进程启动失败: ${error.message}`))
child.on('error', (error: NodeJS.ErrnoException) => {
reject(this.buildProcessStartError('7z', toolPath, error))
})
child.on('close', (code: number | null) => {
@@ -453,8 +477,8 @@ class ZipToolsManager {
stderr += data.toString()
})
child.on('error', (error: Error) => {
reject(new Error(`Zip-Tools 进程启动失败: ${error.message}`))
child.on('error', (error: NodeJS.ErrnoException) => {
reject(this.buildProcessStartError('Zip-Tools', toolPath, error))
})
child.on('close', (code: number | null) => {