增加盘符切换

This commit is contained in:
小朱
2025-07-17 22:11:53 +08:00
parent 55efb000ca
commit f9643ecbbf
6 changed files with 352 additions and 100 deletions

View File

@@ -15,7 +15,8 @@ import {
Modal,
Progress,
Badge,
Drawer
Drawer,
Select
} from 'antd'
import {
HomeOutlined,
@@ -40,7 +41,8 @@ import {
ClockCircleOutlined,
BellOutlined,
AppstoreOutlined,
UnorderedListOutlined
UnorderedListOutlined,
HddOutlined
} from '@ant-design/icons'
import { useFileStore } from '@/stores/fileStore'
import { useNotificationStore } from '@/stores/notificationStore'
@@ -169,6 +171,88 @@ const FileManagerPage: React.FC = () => {
position: { x: number; y: number }
} | null>(null);
// 盘符选择状态
const [drives, setDrives] = useState<Array<{ label: string; value: string; type: string }>>([])
const [selectedDrive, setSelectedDrive] = useState<string>('')
const [drivesLoading, setDrivesLoading] = useState(false)
// 查找当前路径对应的盘符
const findDriveForPath = useCallback((path: string, driveList: Array<{ label: string; value: string; type: string }>) => {
if (!path || !driveList.length) return null
const normalizedPath = normalizePath(path)
// 对每个盘符进行匹配
for (const drive of driveList) {
const normalizedDriveValue = normalizePath(drive.value)
// 确保盘符路径以 / 结尾进行比较
const driveRoot = normalizedDriveValue.endsWith('/') ? normalizedDriveValue : normalizedDriveValue + '/'
const pathToCheck = normalizedPath.endsWith('/') ? normalizedPath : normalizedPath + '/'
// 检查路径是否以盘符开头
if (pathToCheck.startsWith(driveRoot) || normalizedPath === normalizedDriveValue) {
return drive
}
// 特殊处理:如果是 Windows 盘符格式(如 D: 和 D:/),也要匹配
if (normalizedDriveValue.match(/^[A-Za-z]:\/?\/?$/)) {
const drivePrefix = normalizedDriveValue.charAt(0) + ':'
if (normalizedPath.startsWith(drivePrefix)) {
return drive
}
}
}
return null
}, [])
// 加载系统盘符
const loadDrives = useCallback(async () => {
try {
setDrivesLoading(true)
const driveList = await fileApiClient.getDrives()
setDrives(driveList)
// 如果当前路径匹配某个盘符,设置为选中状态
if (currentPath) {
const currentDrive = findDriveForPath(currentPath, driveList)
if (currentDrive) {
setSelectedDrive(currentDrive.value)
}
}
} catch (error: any) {
console.error('加载盘符失败:', error)
addNotification({
type: 'error',
title: '加载盘符失败',
message: error.message || '无法获取系统盘符'
})
} finally {
setDrivesLoading(false)
}
}, [currentPath, addNotification, findDriveForPath])
// 导航到指定路径
const navigateToPath = useCallback((newPath: string) => {
const normalizedPath = normalizePath(newPath)
// 更新历史记录
const newHistory = history.slice(0, historyIndex + 1)
newHistory.push(normalizedPath)
setHistory(newHistory)
setHistoryIndex(newHistory.length - 1)
// 只更新 URL 参数,让 useEffect 监听 URL 变化来更新状态
navigate(`/files?path=${encodeURIComponent(normalizedPath)}`, { replace: true })
}, [history, historyIndex, navigate])
// 切换盘符
const handleDriveChange = useCallback((driveValue: string) => {
setSelectedDrive(driveValue)
navigateToPath(driveValue)
}, [navigateToPath])
// 初始化
useEffect(() => {
// 检查 URL 参数中的路径
@@ -183,7 +267,20 @@ const FileManagerPage: React.FC = () => {
// 初始加载任务列表
loadActiveTasks()
}, [searchParams, setCurrentPath, loadFiles, loadActiveTasks])
// 加载系统盘符
loadDrives()
}, [searchParams, setCurrentPath, loadFiles])
// 当路径变化时更新选中的盘符
useEffect(() => {
if (drives.length > 0 && currentPath) {
const currentDrive = findDriveForPath(currentPath, drives)
if (currentDrive && currentDrive.value !== selectedDrive) {
setSelectedDrive(currentDrive.value)
}
}
}, [currentPath, drives, selectedDrive, findDriveForPath])
// 定期刷新活动任务
useEffect(() => {
@@ -201,7 +298,7 @@ const FileManagerPage: React.FC = () => {
}, 2000) // 每2秒刷新一次
return () => clearInterval(interval)
}, [activeTasks, loadActiveTasks, loadFiles])
}, [activeTasks, loadFiles])
// 键盘快捷键
useEffect(() => {
@@ -268,30 +365,40 @@ const FileManagerPage: React.FC = () => {
}
}, [error, addNotification, setError])
// 获取显示路径(相对于当前盘符的路径)
const getDisplayPath = () => {
if (selectedDrive && currentPath) {
const normalizedCurrentPath = normalizePath(currentPath)
const normalizedDriveValue = normalizePath(selectedDrive)
// 确保盘符路径以 / 结尾进行比较
const driveRoot = normalizedDriveValue.endsWith('/') ? normalizedDriveValue : normalizedDriveValue + '/'
if (normalizedCurrentPath.startsWith(driveRoot) || normalizedCurrentPath === normalizedDriveValue) {
let relativePath = normalizedCurrentPath.slice(normalizedDriveValue.length)
// 移除开头的斜杠
if (relativePath.startsWith('/')) {
relativePath = relativePath.slice(1)
}
// 如果是根目录,显示盘符
return relativePath || normalizedDriveValue
}
}
return currentPath
}
// 更新路径输入
useEffect(() => {
setPathInput(currentPath)
}, [currentPath])
// 导航到指定路径
const navigateToPath = useCallback((newPath: string) => {
const normalizedPath = normalizePath(newPath)
// 更新历史记录
const newHistory = history.slice(0, historyIndex + 1)
newHistory.push(normalizedPath)
setHistory(newHistory)
setHistoryIndex(newHistory.length - 1)
setCurrentPath(normalizedPath)
}, [history, historyIndex, setCurrentPath])
setPathInput(getDisplayPath())
}, [currentPath, selectedDrive])
// 后退
const goBack = () => {
if (historyIndex > 0) {
const newIndex = historyIndex - 1
setHistoryIndex(newIndex)
setCurrentPath(history[newIndex])
const targetPath = history[newIndex]
navigate(`/files?path=${encodeURIComponent(targetPath)}`, { replace: true })
}
}
@@ -300,7 +407,8 @@ const FileManagerPage: React.FC = () => {
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1
setHistoryIndex(newIndex)
setCurrentPath(history[newIndex])
const targetPath = history[newIndex]
navigate(`/files?path=${encodeURIComponent(targetPath)}`, { replace: true })
}
}
@@ -314,7 +422,19 @@ const FileManagerPage: React.FC = () => {
// 处理路径输入
const handlePathSubmit = () => {
const trimmedInput = normalizePath(pathInput.trim())
let inputPath = pathInput.trim()
// 如果输入的是相对路径且有选中的盘符,转换为绝对路径
if (selectedDrive && !inputPath.includes(':') && !inputPath.startsWith('/')) {
// 确保盘符路径以正确的分隔符结尾
let basePath = selectedDrive
if (!basePath.endsWith('/') && !basePath.endsWith('\\')) {
basePath += '/'
}
inputPath = basePath + inputPath
}
const trimmedInput = normalizePath(inputPath)
const current = normalizePath(currentPath)
if (trimmedInput && trimmedInput !== current) {
navigateToPath(trimmedInput)
@@ -607,21 +727,42 @@ const FileManagerPage: React.FC = () => {
// 生成面包屑
const generateBreadcrumbs = () => {
const parts = currentPath.split('/').filter(Boolean)
// 获取相对于当前盘符的路径
let relativePath = currentPath
let rootTitle = '根目录'
let rootPath = '/'
// 如果有选中的盘符,计算相对路径
if (selectedDrive && currentPath.startsWith(selectedDrive)) {
relativePath = currentPath.slice(selectedDrive.length)
rootTitle = selectedDrive.replace(':', '')
rootPath = selectedDrive
// 移除开头的斜杠或反斜杠
if (relativePath.startsWith('/') || relativePath.startsWith('\\')) {
relativePath = relativePath.slice(1)
}
}
const parts = relativePath.split(/[/\\]/).filter(Boolean)
const items = [
{
title: (
<span className="flex items-center cursor-pointer" onClick={() => navigateToPath('/')}>
<HomeOutlined className="mr-1" />
<span className="flex items-center cursor-pointer" onClick={() => navigateToPath(rootPath)}>
<HddOutlined className="mr-1" />
{rootTitle}
</span>
)
}
]
let currentBreadcrumbPath = ''
let currentBreadcrumbPath = rootPath
parts.forEach((part, index) => {
currentBreadcrumbPath += '/' + part
// 确保路径分隔符正确
if (!currentBreadcrumbPath.endsWith('/') && !currentBreadcrumbPath.endsWith('\\')) {
currentBreadcrumbPath += '/'
}
currentBreadcrumbPath += part
const breadcrumbPath = currentBreadcrumbPath
items.push({
@@ -681,6 +822,20 @@ const FileManagerPage: React.FC = () => {
{/* 工具栏 */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-2">
{/* 盘符选择 */}
<div className="mr-2">
<Select
value={selectedDrive}
onChange={handleDriveChange}
loading={drivesLoading}
placeholder="盘符"
size="small"
style={{ width: 80, height: 25 }}
suffixIcon={<HddOutlined />}
options={drives}
/>
</div>
{/* 导航按钮 */}
<Space>
<Tooltip title="后退">

View File

@@ -89,8 +89,12 @@ export const useFileStore = create<FileStore>((set, get) => ({
// 设置当前路径
setCurrentPath: (path: string) => {
set({ currentPath: path })
get().loadFiles(path)
const currentState = get()
// 只有当路径真正改变时才更新状态和加载文件
if (currentState.currentPath !== path) {
set({ currentPath: path })
get().loadFiles(path)
}
},
// 加载文件列表

View File

@@ -296,6 +296,12 @@ export class FileApiClient {
const response = await this.client.delete(`${API_BASE}/tasks/${taskId}`)
return response.data
}
// 获取系统盘符
async getDrives(): Promise<Array<{ label: string; value: string; type: string }>> {
const response = await this.client.get(`${API_BASE}/drives`)
return response.data.data
}
}
export const fileApiClient = new FileApiClient()

View File

@@ -14,8 +14,11 @@ export function normalizePath(pathStr: string): string {
// 将反斜杠转换为正斜杠
let normalized = pathStr.replace(/\\/g, '/')
// 确保以/开头
if (!normalized.startsWith('/')) {
// 检查是否是 Windows 绝对路径(如 C:/ 或 D:/
const isWindowsAbsolute = /^[A-Za-z]:/.test(normalized)
// 只有在不是 Windows 绝对路径且不以/开头时才添加前缀/
if (!isWindowsAbsolute && !normalized.startsWith('/')) {
normalized = '/' + normalized
}
@@ -37,8 +40,19 @@ export function normalizePath(pathStr: string): string {
}
// 重新构建路径
const result = '/' + stack.join('/')
return result === '/' ? '/' : result
if (isWindowsAbsolute) {
// Windows 绝对路径不需要前缀 /
const result = stack.join('/')
// 确保 Windows 盘符路径以 / 结尾(如 C:/ 而不是 C:
if (result.match(/^[A-Za-z]:$/) && !result.endsWith('/')) {
return result + '/'
}
return result
} else {
// Unix 风格路径需要前缀 /
const result = '/' + stack.join('/')
return result === '/' ? '/' : result
}
}
/**
@@ -50,13 +64,31 @@ export function getDirectoryPath(pathStr: string): string {
if (!pathStr || pathStr === '/') return '/'
const normalized = normalizePath(pathStr)
// 检查是否是 Windows 绝对路径
const isWindowsAbsolute = /^[A-Za-z]:/.test(normalized)
const lastSlashIndex = normalized.lastIndexOf('/')
if (lastSlashIndex === 0) {
return '/'
if (isWindowsAbsolute) {
// Windows 路径处理
if (lastSlashIndex === -1) {
// 如果没有斜杠,返回盘符根目录
return normalized.match(/^[A-Za-z]:/)![0] + '/'
} else if (lastSlashIndex === 2 && normalized.charAt(1) === ':') {
// 如果是盘符根目录(如 C:/),返回自身
return normalized
} else {
// 返回父目录
return normalized.substring(0, lastSlashIndex)
}
} else {
// Unix 风格路径处理
if (lastSlashIndex === 0) {
return '/'
}
return normalized.substring(0, lastSlashIndex)
}
return normalized.substring(0, lastSlashIndex)
}
/**
@@ -118,7 +150,15 @@ export function getExtension(pathStr: string): string {
* @returns 是否为绝对路径
*/
export function isAbsolute(pathStr: string): boolean {
return pathStr && pathStr.startsWith('/')
if (!pathStr) return false
// 检查是否是 Windows 绝对路径(如 C:/ 或 D:/
const isWindowsAbsolute = /^[A-Za-z]:/.test(pathStr)
// 检查是否是 Unix 绝对路径(以 / 开头)
const isUnixAbsolute = pathStr.startsWith('/')
return isWindowsAbsolute || isUnixAbsolute
}
/**

View File

@@ -9,10 +9,14 @@ import unzipper from 'unzipper'
import * as tar from 'tar'
import * as zlib from 'zlib'
import mime from 'mime-types'
import { exec } from 'child_process'
import { promisify } from 'util'
import { authenticateToken } from '../middleware/auth.js'
import { taskManager } from '../modules/task/taskManager.js'
import { compressionWorker } from '../modules/task/compressionWorker.js'
const execAsync = promisify(exec)
const router = Router()
// 处理中文文件名编码的工具函数
@@ -116,7 +120,10 @@ const isValidPath = (filePath: string): boolean => {
// 在Unix系统上绝对路径以 / 开头
const isAbsolute = path.isAbsolute(normalizedPath)
return isAbsolute
// 特殊处理 Windows 盘符路径(如 D: 或 D:/
const isWindowsDrive = process.platform === 'win32' && /^[A-Za-z]:[\\/]?$/.test(normalizedPath)
return isAbsolute || isWindowsDrive
}
// 修复Windows路径格式的工具函数
const fixWindowsPath = (filePath: string): string => {
@@ -130,6 +137,17 @@ const fixWindowsPath = (filePath: string): string => {
decodedPath = decodedPath.substring(1)
}
// 在Windows系统中如果路径是盘符格式如 D: 或 D:/),转换为根目录格式(如 D:\
if (process.platform === 'win32') {
if (/^[A-Za-z]:$/.test(decodedPath)) {
// D: -> D:\
decodedPath = decodedPath + '\\'
} else if (/^[A-Za-z]:\/+$/.test(decodedPath)) {
// D:/ 或 D:/// -> D:\
decodedPath = decodedPath.charAt(0) + decodedPath.charAt(1) + '\\'
}
}
return decodedPath
}
@@ -1827,4 +1845,93 @@ router.delete('/tasks/:taskId', authenticateToken, async (req: Request, res: Res
}
})
// 获取系统盘符
router.get('/drives', authenticateToken, async (req: Request, res: Response) => {
try {
const drives: Array<{ label: string; value: string; type: string }> = []
if (process.platform === 'win32') {
// Windows系统 - 获取所有盘符
try {
const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption,description,drivetype')
const lines = stdout.split('\n').filter(line => line.trim() && !line.includes('Caption'))
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 5) {
const caption = parts[0] // 盘符 (如 C:)
const driveType = parseInt(parts[2]) // 驱动器类型
if (caption && caption.match(/^[A-Z]:$/)) {
let type = 'unknown'
switch (driveType) {
case 2: type = 'removable'; break // 可移动磁盘
case 3: type = 'fixed'; break // 固定磁盘
case 4: type = 'network'; break // 网络磁盘
case 5: type = 'cdrom'; break // 光盘
default: type = 'unknown'; break
}
drives.push({
label: `${caption}\\`,
value: `${caption}\\`,
type: type
})
}
}
}
} catch (error) {
// 如果wmic命令失败使用备用方法
console.warn('wmic命令失败使用备用方法获取盘符:', error)
// 尝试常见的盘符
const commonDrives = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:']
for (const drive of commonDrives) {
try {
const drivePath = `${drive}\\`
await fs.access(drivePath)
drives.push({
label: drivePath,
value: drivePath,
type: 'fixed'
})
} catch (error) {
// 盘符不存在,跳过
}
}
}
} else {
// Linux/Unix系统 - 只显示根目录
drives.push({
label: '根目录 (/)',
value: '/',
type: 'fixed'
})
}
// 如果没有找到任何盘符,至少返回当前工作目录
if (drives.length === 0) {
const cwd = process.cwd()
const rootPath = process.platform === 'win32' ? path.parse(cwd).root : '/'
drives.push({
label: process.platform === 'win32' ? rootPath : '根目录 (/)',
value: rootPath,
type: 'fixed'
})
}
res.json({
status: 'success',
data: drives
})
} catch (error: any) {
console.error('获取盘符失败:', error)
res.status(500).json({
status: 'error',
message: error.message || '获取盘符失败'
})
}
})
export default router

View File

@@ -1,60 +0,0 @@
react-dom.development.js:86
Warning: React has detected a change in the order of Hooks called by GlobalMusicPlayer. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useRef useRef
2. useMemo useMemo
3. useSyncExternalStore useSyncExternalStore
4. useEffect useEffect
5. useDebugValue useDebugValue
6. useDebugValue useDebugValue
7. useState useState
8. useState useState
9. useRef useRef
10. useContext useContext
11. useContext useContext
12. useContext useContext
13. useContext useContext
14. useContext useContext
15. useContext useContext
16. useContext useContext
17. useRef useRef
18. useContext useContext
19. useLayoutEffect useLayoutEffect
20. useCallback useCallback
21. undefined useEffect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at GlobalMusicPlayer (http://localhost:5173/src/components/GlobalMusicPlayer.tsx:40:7)
at div
at div
at App (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:21124:16)
at MotionWrapper (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6541:32)
at ProviderChildren (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6649:5)
at ConfigProvider (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6938:27)
at App (http://localhost:5173/src/App.tsx?t=1752220576201:124:39)
at Router (http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=41c5d98f:4501:15)
at BrowserRouter (http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=41c5d98f:5247:5)
2
react-dom.development.js:15688
Uncaught Error: Rendered more hooks than during the previous render.
at GlobalMusicPlayer (GlobalMusicPlayer.tsx:44:3)
react-dom.development.js:18704
The above error occurred in the <GlobalMusicPlayer> component:
at GlobalMusicPlayer (http://localhost:5173/src/components/GlobalMusicPlayer.tsx:40:7)
at div
at div
at App (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:21124:16)
at MotionWrapper (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6541:32)
at ProviderChildren (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6649:5)
at ConfigProvider (http://localhost:5173/node_modules/.vite/deps/antd.js?v=41c5d98f:6938:27)
at App (http://localhost:5173/src/App.tsx?t=1752220576201:124:39)
at Router (http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=41c5d98f:4501:15)
at BrowserRouter (http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=41c5d98f:5247:5)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
react-dom.development.js:12056
Uncaught Error: Rendered more hooks than during the previous render.