mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-08-28 03:29:35 +08:00
feat: Add sorting functionality to the virtual file system and adapter list methods
This commit is contained in:
@@ -306,10 +306,12 @@ async def browse_fs(
|
|||||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
full_path: str,
|
full_path: str,
|
||||||
page_num: int = Query(1, alias="page", ge=1, description="页码"),
|
page_num: int = Query(1, alias="page", ge=1, description="页码"),
|
||||||
page_size: int = Query(50, ge=1, le=500, description="每页条数")
|
page_size: int = Query(50, ge=1, le=500, description="每页条数"),
|
||||||
|
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
|
||||||
|
sort_order: str = Query("asc", description="排序顺序: asc, desc")
|
||||||
):
|
):
|
||||||
full_path = '/' + full_path if not full_path.startswith('/') else full_path
|
full_path = '/' + full_path if not full_path.startswith('/') else full_path
|
||||||
result = await list_virtual_dir(full_path, page_num, page_size)
|
result = await list_virtual_dir(full_path, page_num, page_size, sort_by, sort_order)
|
||||||
return success({
|
return success({
|
||||||
"path": full_path,
|
"path": full_path,
|
||||||
"entries": result["items"],
|
"entries": result["items"],
|
||||||
@@ -336,6 +338,18 @@ async def api_delete(
|
|||||||
async def root_listing(
|
async def root_listing(
|
||||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
page_num: int = Query(1, alias="page", ge=1, description="页码"),
|
page_num: int = Query(1, alias="page", ge=1, description="页码"),
|
||||||
page_size: int = Query(50, ge=1, le=500, description="每页条数")
|
page_size: int = Query(50, ge=1, le=500, description="每页条数"),
|
||||||
|
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
|
||||||
|
sort_order: str = Query("asc", description="排序顺序: asc, desc")
|
||||||
):
|
):
|
||||||
return await browse_fs("", page_num, page_size)
|
result = await list_virtual_dir("/", page_num, page_size, sort_by, sort_order)
|
||||||
|
return success({
|
||||||
|
"path": "/",
|
||||||
|
"entries": result["items"],
|
||||||
|
"pagination": {
|
||||||
|
"total": result["total"],
|
||||||
|
"page": result["page"],
|
||||||
|
"page_size": result["page_size"],
|
||||||
|
"pages": result["pages"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from models import StorageAdapter
|
|||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class BaseAdapter(Protocol):
|
class BaseAdapter(Protocol):
|
||||||
record: StorageAdapter
|
record: StorageAdapter
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]: ...
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]: ...
|
||||||
async def read_file(self, root: str, rel: str) -> bytes: ...
|
async def read_file(self, root: str, rel: str) -> bytes: ...
|
||||||
async def write_file(self, root: str, rel: str, data: bytes): ...
|
async def write_file(self, root: str, rel: str, data: bytes): ...
|
||||||
async def write_file_stream(self, root: str, rel: str, data_iter: AsyncIterator[bytes]): ...
|
async def write_file_stream(self, root: str, rel: str, data_iter: AsyncIterator[bytes]): ...
|
||||||
|
|||||||
+32
-14
@@ -46,25 +46,18 @@ class LocalAdapter:
|
|||||||
return str(Path(root) / sub_path)
|
return str(Path(root) / sub_path)
|
||||||
return root
|
return root
|
||||||
|
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]:
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]:
|
||||||
rel = rel.strip('/')
|
rel = rel.strip('/')
|
||||||
base = _safe_join(root, rel) if rel else Path(root)
|
base = _safe_join(root, rel) if rel else Path(root)
|
||||||
if not base.exists():
|
if not base.exists():
|
||||||
return [], 0
|
return [], 0
|
||||||
if not base.is_dir():
|
if not base.is_dir():
|
||||||
raise NotADirectoryError(rel)
|
raise NotADirectoryError(rel)
|
||||||
|
|
||||||
# 获取所有文件名并排序
|
all_names = await asyncio.to_thread(os.listdir, base)
|
||||||
all_names = await asyncio.to_thread(lambda: sorted(os.listdir(base), key=str.lower))
|
|
||||||
total_count = len(all_names)
|
|
||||||
|
|
||||||
# 计算分页范围
|
|
||||||
start_idx = (page_num - 1) * page_size
|
|
||||||
end_idx = start_idx + page_size
|
|
||||||
page_names = all_names[start_idx:end_idx]
|
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for name in page_names:
|
for name in all_names:
|
||||||
fp = base / name
|
fp = base / name
|
||||||
try:
|
try:
|
||||||
st = await asyncio.to_thread(fp.stat)
|
st = await asyncio.to_thread(fp.stat)
|
||||||
@@ -79,10 +72,35 @@ class LocalAdapter:
|
|||||||
"mode": stat.S_IMODE(st.st_mode),
|
"mode": stat.S_IMODE(st.st_mode),
|
||||||
"type": "dir" if is_dir else "file",
|
"type": "dir" if is_dir else "file",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 排序
|
||||||
|
reverse = sort_order.lower() == "desc"
|
||||||
|
|
||||||
# 按目录优先排序
|
def get_sort_key(item):
|
||||||
entries.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
|
# 基础排序键,目录优先
|
||||||
return entries, total_count
|
key = (not item["is_dir"],)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item["size"],)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item["mtime"],)
|
||||||
|
else: # 默认按名称
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
|
||||||
|
entries.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
|
total_count = len(entries)
|
||||||
|
|
||||||
|
# 分页
|
||||||
|
start_idx = (page_num - 1) * page_size
|
||||||
|
end_idx = start_idx + page_size
|
||||||
|
page_entries = entries[start_idx:end_idx]
|
||||||
|
|
||||||
|
return page_entries, total_count
|
||||||
|
|
||||||
async def read_file(self, root: str, rel: str) -> bytes:
|
async def read_file(self, root: str, rel: str) -> bytes:
|
||||||
fp = _safe_join(root, rel)
|
fp = _safe_join(root, rel)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class OneDriveAdapter:
|
|||||||
"type": "dir" if is_dir else "file",
|
"type": "dir" if is_dir else "file",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]:
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]:
|
||||||
"""
|
"""
|
||||||
列出目录内容。
|
列出目录内容。
|
||||||
由于 Graph API 不支持基于偏移($skip)的分页,此方法将获取所有项目,
|
由于 Graph API 不支持基于偏移($skip)的分页,此方法将获取所有项目,
|
||||||
@@ -122,6 +122,8 @@ class OneDriveAdapter:
|
|||||||
:param rel: 相对路径。
|
:param rel: 相对路径。
|
||||||
:param page_num: 页码。
|
:param page_num: 页码。
|
||||||
:param page_size: 每页大小。
|
:param page_size: 每页大小。
|
||||||
|
:param sort_by: 排序字段
|
||||||
|
:param sort_order: 排序顺序
|
||||||
:return: 文件/目录列表和总数。
|
:return: 文件/目录列表和总数。
|
||||||
"""
|
"""
|
||||||
api_path = self._get_api_path(rel)
|
api_path = self._get_api_path(rel)
|
||||||
@@ -149,8 +151,23 @@ class OneDriveAdapter:
|
|||||||
resp = await self._request("GET", full_url=next_link)
|
resp = await self._request("GET", full_url=next_link)
|
||||||
|
|
||||||
formatted_items = [self._format_item(item) for item in all_items]
|
formatted_items = [self._format_item(item) for item in all_items]
|
||||||
formatted_items.sort(key=lambda x: (
|
|
||||||
not x["is_dir"], x["name"].lower()))
|
# 排序
|
||||||
|
reverse = sort_order.lower() == "desc"
|
||||||
|
def get_sort_key(item):
|
||||||
|
key = (not item["is_dir"],)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item["size"],)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item["mtime"],)
|
||||||
|
else:
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
formatted_items.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
total_count = len(formatted_items)
|
total_count = len(formatted_items)
|
||||||
start_idx = (page_num - 1) * page_size
|
start_idx = (page_num - 1) * page_size
|
||||||
end_idx = start_idx + page_size
|
end_idx = start_idx + page_size
|
||||||
|
|||||||
+16
-2
@@ -52,7 +52,7 @@ class S3Adapter:
|
|||||||
def _get_client(self):
|
def _get_client(self):
|
||||||
return self.session.client("s3", endpoint_url=self.endpoint_url)
|
return self.session.client("s3", endpoint_url=self.endpoint_url)
|
||||||
|
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]:
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]:
|
||||||
prefix = self._get_s3_key(rel)
|
prefix = self._get_s3_key(rel)
|
||||||
if prefix and not prefix.endswith("/"):
|
if prefix and not prefix.endswith("/"):
|
||||||
prefix += "/"
|
prefix += "/"
|
||||||
@@ -91,7 +91,21 @@ class S3Adapter:
|
|||||||
})
|
})
|
||||||
|
|
||||||
# 在内存中排序和分页
|
# 在内存中排序和分页
|
||||||
all_items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
|
reverse = sort_order.lower() == "desc"
|
||||||
|
def get_sort_key(item):
|
||||||
|
key = (not item["is_dir"],)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item["size"],)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item["mtime"],)
|
||||||
|
else:
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
all_items.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
total_count = len(all_items)
|
total_count = len(all_items)
|
||||||
start_idx = (page_num - 1) * page_size
|
start_idx = (page_num - 1) * page_size
|
||||||
end_idx = start_idx + page_size
|
end_idx = start_idx + page_size
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class TelegramAdapter:
|
|||||||
def get_effective_root(self, sub_path: str | None) -> str:
|
def get_effective_root(self, sub_path: str | None) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]:
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]:
|
||||||
if rel:
|
if rel:
|
||||||
return [], 0
|
return [], 0
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class TelegramAdapter:
|
|||||||
entries = []
|
entries = []
|
||||||
try:
|
try:
|
||||||
await client.connect()
|
await client.connect()
|
||||||
messages = await client.get_messages(self.chat_id, limit=50)
|
messages = await client.get_messages(self.chat_id, limit=200)
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if not message:
|
if not message:
|
||||||
continue
|
continue
|
||||||
@@ -113,7 +113,30 @@ class TelegramAdapter:
|
|||||||
if client.is_connected():
|
if client.is_connected():
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
|
|
||||||
return entries, len(entries)
|
# 排序
|
||||||
|
reverse = sort_order.lower() == "desc"
|
||||||
|
def get_sort_key(item):
|
||||||
|
key = (not item["is_dir"],)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item["size"],)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item["mtime"],)
|
||||||
|
else:
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
entries.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
|
total_count = len(entries)
|
||||||
|
|
||||||
|
# 分页
|
||||||
|
start_idx = (page_num - 1) * page_size
|
||||||
|
end_idx = start_idx + page_size
|
||||||
|
page_entries = entries[start_idx:end_idx]
|
||||||
|
|
||||||
|
return page_entries, total_count
|
||||||
|
|
||||||
async def read_file(self, root: str, rel: str) -> bytes:
|
async def read_file(self, root: str, rel: str) -> bytes:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class WebDAVAdapter:
|
|||||||
rel = rel.strip('/')
|
rel = rel.strip('/')
|
||||||
return self.base_url if not rel else urljoin(self.base_url, quote(rel) + ('/' if rel.endswith('/') else ''))
|
return self.base_url if not rel else urljoin(self.base_url, quote(rel) + ('/' if rel.endswith('/') else ''))
|
||||||
|
|
||||||
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50) -> Tuple[List[Dict], int]:
|
async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Tuple[List[Dict], int]:
|
||||||
raw_url = self._build_url(rel)
|
raw_url = self._build_url(rel)
|
||||||
url = raw_url if raw_url.endswith('/') else raw_url + '/'
|
url = raw_url if raw_url.endswith('/') else raw_url + '/'
|
||||||
depth = "1"
|
depth = "1"
|
||||||
@@ -92,16 +92,39 @@ class WebDAVAdapter:
|
|||||||
"d:collection", NS) is not None if rt_el is not None else href_path.endswith('/')
|
"d:collection", NS) is not None if rt_el is not None else href_path.endswith('/')
|
||||||
size = int(
|
size = int(
|
||||||
size_el.text) if size_el is not None and size_el.text and size_el.text.isdigit() else 0
|
size_el.text) if size_el is not None and size_el.text and size_el.text.isdigit() else 0
|
||||||
|
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
|
mtime = 0
|
||||||
|
if lm_el is not None and lm_el.text:
|
||||||
|
try:
|
||||||
|
mtime = int(parsedate_to_datetime(lm_el.text).timestamp())
|
||||||
|
except Exception:
|
||||||
|
mtime = 0
|
||||||
|
|
||||||
all_entries.append({
|
all_entries.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"is_dir": is_dir,
|
"is_dir": is_dir,
|
||||||
"size": 0 if is_dir else size,
|
"size": 0 if is_dir else size,
|
||||||
"mtime": 0,
|
"mtime": mtime,
|
||||||
"type": "dir" if is_dir else "file",
|
"type": "dir" if is_dir else "file",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 排序所有条目
|
# 排序所有条目
|
||||||
all_entries.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
|
reverse = sort_order.lower() == "desc"
|
||||||
|
def get_sort_key(item):
|
||||||
|
key = (not item["is_dir"],)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item["size"],)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item["mtime"],)
|
||||||
|
else:
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
all_entries.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
total_count = len(all_entries)
|
total_count = len(all_entries)
|
||||||
|
|
||||||
# 应用分页
|
# 应用分页
|
||||||
|
|||||||
+22
-7
@@ -59,7 +59,7 @@ async def _ensure_method(adapter: Any, method: str):
|
|||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
async def list_virtual_dir(path: str, page_num: int = 1, page_size: int = 50) -> Dict:
|
async def list_virtual_dir(path: str, page_num: int = 1, page_size: int = 50, sort_by: str = "name", sort_order: str = "asc") -> Dict:
|
||||||
norm = (path if path.startswith('/') else '/' + path).rstrip('/') or '/'
|
norm = (path if path.startswith('/') else '/' + path).rstrip('/') or '/'
|
||||||
adapters = await StorageAdapter.filter(enabled=True)
|
adapters = await StorageAdapter.filter(enabled=True)
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ async def list_virtual_dir(path: str, page_num: int = 1, page_size: int = 50) ->
|
|||||||
if adapter_model and adapter_instance:
|
if adapter_model and adapter_instance:
|
||||||
list_dir = await _ensure_method(adapter_instance, "list_dir")
|
list_dir = await _ensure_method(adapter_instance, "list_dir")
|
||||||
try:
|
try:
|
||||||
adapter_entries, adapter_total = await list_dir(effective_root, rel, page_num, page_size)
|
adapter_entries, adapter_total = await list_dir(effective_root, rel, page_num, page_size, sort_by, sort_order)
|
||||||
except NotADirectoryError:
|
except NotADirectoryError:
|
||||||
raise HTTPException(400, detail="Not a directory")
|
raise HTTPException(400, detail="Not a directory")
|
||||||
|
|
||||||
@@ -118,17 +118,32 @@ async def list_virtual_dir(path: str, page_num: int = 1, page_size: int = 50) ->
|
|||||||
ent['is_image'] = is_image_filename(ent['name'])
|
ent['is_image'] = is_image_filename(ent['name'])
|
||||||
else:
|
else:
|
||||||
ent['is_image'] = False
|
ent['is_image'] = False
|
||||||
|
|
||||||
all_entries = adapter_entries + mount_entries
|
all_entries = adapter_entries + mount_entries
|
||||||
all_entries.sort(key=lambda x: (not x.get("is_dir"), x["name"].lower()))
|
|
||||||
total_entries = adapter_total + len(mount_entries)
|
|
||||||
if mount_entries:
|
if mount_entries:
|
||||||
|
reverse = sort_order.lower() == "desc"
|
||||||
|
def get_sort_key(item):
|
||||||
|
key = (not item.get("is_dir"),)
|
||||||
|
sort_field = sort_by.lower()
|
||||||
|
if sort_field == "name":
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
elif sort_field == "size":
|
||||||
|
key += (item.get("size", 0),)
|
||||||
|
elif sort_field == "mtime":
|
||||||
|
key += (item.get("mtime", 0),)
|
||||||
|
else:
|
||||||
|
key += (item["name"].lower(),)
|
||||||
|
return key
|
||||||
|
all_entries.sort(key=get_sort_key, reverse=reverse)
|
||||||
|
|
||||||
|
total_entries = adapter_total + len(mount_entries)
|
||||||
start_idx = (page_num - 1) * page_size
|
start_idx = (page_num - 1) * page_size
|
||||||
end_idx = start_idx + page_size
|
end_idx = start_idx + page_size
|
||||||
page_entries = all_entries[start_idx:end_idx]
|
page_entries = all_entries[start_idx:end_idx]
|
||||||
|
|
||||||
return page(page_entries, total_entries, page_num, page_size)
|
return page(page_entries, total_entries, page_num, page_size)
|
||||||
else:
|
|
||||||
return page(adapter_entries, adapter_total, page_num, page_size)
|
return page(adapter_entries, adapter_total, page_num, page_size)
|
||||||
|
|
||||||
|
|
||||||
async def read_file(path: str) -> Union[bytes, Any]:
|
async def read_file(path: str) -> Union[bytes, Any]:
|
||||||
|
|||||||
+4
-2
@@ -27,12 +27,14 @@ export interface SearchResultItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const vfsApi = {
|
export const vfsApi = {
|
||||||
list: (path: string, page: number = 1, pageSize: number = 50) => {
|
list: (path: string, page: number = 1, pageSize: number = 50, sortBy: string = 'name', sortOrder: string = 'asc') => {
|
||||||
const cleaned = path.replace(/\\/g, '/');
|
const cleaned = path.replace(/\\/g, '/');
|
||||||
const trimmed = cleaned === '/' ? '' : cleaned.replace(/^\/+/, '');
|
const trimmed = cleaned === '/' ? '' : cleaned.replace(/^\/+/, '');
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: page.toString(),
|
page: page.toString(),
|
||||||
page_size: pageSize.toString()
|
page_size: pageSize.toString(),
|
||||||
|
sort_by: sortBy,
|
||||||
|
sort_order: sortOrder
|
||||||
});
|
});
|
||||||
return request<DirListing>(`/fs/${encodeURI(trimmed)}?${params}`);
|
return request<DirListing>(`/fs/${encodeURI(trimmed)}?${params}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
const dragCounter = useRef(0);
|
const dragCounter = useRef(0);
|
||||||
|
|
||||||
// --- Hooks ---
|
// --- Hooks ---
|
||||||
const { path, entries, loading, pagination, processorTypes, load, navigateTo, goUp, handlePaginationChange, refresh } = useFileExplorer(navKey);
|
const { path, entries, loading, pagination, processorTypes, sortBy, sortOrder, load, navigateTo, goUp, handlePaginationChange, refresh, handleSortChange } = useFileExplorer(navKey);
|
||||||
const { selectedEntries, handleSelect, handleSelectRange, clearSelection, setSelectedEntries } = useFileSelection();
|
const { selectedEntries, handleSelect, handleSelectRange, clearSelection, setSelectedEntries } = useFileSelection();
|
||||||
const { doCreateDir, doDelete, doRename, doDownload, doShare, doGetDirectLink } = useFileActions({ path, refresh, clearSelection, onShare: (entries) => setSharingEntries(entries), onGetDirectLink: (entry) => setDirectLinkEntry(entry) });
|
const { doCreateDir, doDelete, doRename, doDownload, doShare, doGetDirectLink } = useFileActions({ path, refresh, clearSelection, onShare: (entries) => setSharingEntries(entries), onGetDirectLink: (entry) => setDirectLinkEntry(entry) });
|
||||||
const { appWindows, openFileWithDefaultApp, confirmOpenWithApp, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows(path);
|
const { appWindows, openFileWithDefaultApp, confirmOpenWithApp, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows(path);
|
||||||
@@ -56,8 +56,8 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
// --- Effects ---
|
// --- Effects ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const routeP = '/' + (restPath || '').replace(/^\/+/, '');
|
const routeP = '/' + (restPath || '').replace(/^\/+/, '');
|
||||||
load(routeP, 1, pagination.pageSize);
|
load(routeP, 1, pagination.pageSize, sortBy, sortOrder);
|
||||||
}, [restPath, navKey, load, pagination.pageSize]);
|
}, [restPath, navKey, load, pagination.pageSize, sortBy, sortOrder]);
|
||||||
|
|
||||||
// --- Handlers ---
|
// --- Handlers ---
|
||||||
const handleOpenEntry = (entry: VfsEntry) => {
|
const handleOpenEntry = (entry: VfsEntry) => {
|
||||||
@@ -136,12 +136,15 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
path={path}
|
path={path}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
|
sortBy={sortBy}
|
||||||
|
sortOrder={sortOrder}
|
||||||
onGoUp={goUp}
|
onGoUp={goUp}
|
||||||
onNavigate={navigateTo}
|
onNavigate={navigateTo}
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
onCreateDir={() => setCreatingDir(true)}
|
onCreateDir={() => setCreatingDir(true)}
|
||||||
onUpload={uploader.openModal}
|
onUpload={uploader.openModal}
|
||||||
onSetViewMode={setViewMode}
|
onSetViewMode={setViewMode}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input ref={uploader.fileInputRef} type="file" style={{ display: 'none' }} multiple onChange={uploader.handleFileChange} />
|
<input ref={uploader.fileInputRef} type="file" style={{ display: 'none' }} multiple onChange={uploader.handleFileChange} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Flex, Typography, Divider, Button, Space, Tooltip, Segmented, Breadcrumb, Input, theme } from 'antd';
|
import { Flex, Typography, Divider, Button, Space, Tooltip, Segmented, Breadcrumb, Input, theme } from 'antd';
|
||||||
import { ArrowUpOutlined, ReloadOutlined, PlusOutlined, UploadOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, PlusOutlined, UploadOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||||
|
import { Select } from 'antd';
|
||||||
import type { ViewMode } from '../types';
|
import type { ViewMode } from '../types';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -8,24 +9,30 @@ interface HeaderProps {
|
|||||||
path: string;
|
path: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
|
sortBy: string;
|
||||||
|
sortOrder: string;
|
||||||
onGoUp: () => void;
|
onGoUp: () => void;
|
||||||
onNavigate: (path: string) => void;
|
onNavigate: (path: string) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onCreateDir: () => void;
|
onCreateDir: () => void;
|
||||||
onUpload: () => void;
|
onUpload: () => void;
|
||||||
onSetViewMode: (mode: ViewMode) => void;
|
onSetViewMode: (mode: ViewMode) => void;
|
||||||
|
onSortChange: (sortBy: string, sortOrder: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({
|
export const Header: React.FC<HeaderProps> = ({
|
||||||
path,
|
path,
|
||||||
loading,
|
loading,
|
||||||
viewMode,
|
viewMode,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
onGoUp,
|
onGoUp,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onCreateDir,
|
onCreateDir,
|
||||||
onUpload,
|
onUpload,
|
||||||
onSetViewMode,
|
onSetViewMode,
|
||||||
|
onSortChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const [editingPath, setEditingPath] = useState(false);
|
const [editingPath, setEditingPath] = useState(false);
|
||||||
@@ -100,6 +107,22 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
<Button size="small" icon={<ReloadOutlined />} onClick={onRefresh} loading={loading}>刷新</Button>
|
<Button size="small" icon={<ReloadOutlined />} onClick={onRefresh} loading={loading}>刷新</Button>
|
||||||
<Button size="small" icon={<PlusOutlined />} onClick={onCreateDir}>新建目录</Button>
|
<Button size="small" icon={<PlusOutlined />} onClick={onCreateDir}>新建目录</Button>
|
||||||
<Button size="small" icon={<UploadOutlined />} onClick={onUpload}>上传</Button>
|
<Button size="small" icon={<UploadOutlined />} onClick={onUpload}>上传</Button>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(val) => onSortChange(val, sortOrder)}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
options={[
|
||||||
|
{ value: 'name', label: '名称' },
|
||||||
|
{ value: 'size', label: '大小' },
|
||||||
|
{ value: 'mtime', label: '修改时间' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={sortOrder === 'asc' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||||
|
onClick={() => onSortChange(sortBy, sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||||
|
/>
|
||||||
<Segmented
|
<Segmented
|
||||||
size="small"
|
size="small"
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
|
|||||||
@@ -21,14 +21,16 @@ export function useFileExplorer(navKey: string) {
|
|||||||
showTotal: (total: number, range: [number, number]) => `共 ${total} 项,第 ${range[0]}-${range[1]} 项`,
|
showTotal: (total: number, range: [number, number]) => `共 ${total} 项,第 ${range[0]}-${range[1]} 项`,
|
||||||
pageSizeOptions: ['20', '50', '100', '200']
|
pageSizeOptions: ['20', '50', '100', '200']
|
||||||
});
|
});
|
||||||
|
const [sortBy, setSortBy] = useState('name');
|
||||||
|
const [sortOrder, setSortOrder] = useState('asc');
|
||||||
|
|
||||||
const load = useCallback(async (p: string, page: number = 1, pageSize: number = 50) => {
|
const load = useCallback(async (p: string, page: number = 1, pageSize: number = 50, sb = sortBy, so = sortOrder) => {
|
||||||
const canonical = p === '' ? '/' : (p.startsWith('/') ? p : '/' + p);
|
const canonical = p === '' ? '/' : (p.startsWith('/') ? p : '/' + p);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Load entries and processor types concurrently
|
// Load entries and processor types concurrently
|
||||||
const [res, processors] = await Promise.all([
|
const [res, processors] = await Promise.all([
|
||||||
vfsApi.list(canonical === '/' ? '' : canonical, page, pageSize),
|
vfsApi.list(canonical === '/' ? '' : canonical, page, pageSize, sb, so),
|
||||||
processorsApi.list()
|
processorsApi.list()
|
||||||
]);
|
]);
|
||||||
setEntries(res.entries);
|
setEntries(res.entries);
|
||||||
@@ -45,7 +47,7 @@ export function useFileExplorer(navKey: string) {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [sortBy, sortOrder]);
|
||||||
|
|
||||||
const navigateTo = useCallback((p: string) => {
|
const navigateTo = useCallback((p: string) => {
|
||||||
const canonical = p === '' || p === '/' ? '/' : (p.startsWith('/') ? p : '/' + p);
|
const canonical = p === '' || p === '/' ? '/' : (p.startsWith('/') ? p : '/' + p);
|
||||||
@@ -60,23 +62,32 @@ export function useFileExplorer(navKey: string) {
|
|||||||
}, [path, navigateTo]);
|
}, [path, navigateTo]);
|
||||||
|
|
||||||
const handlePaginationChange = (page: number, pageSize: number) => {
|
const handlePaginationChange = (page: number, pageSize: number) => {
|
||||||
load(path, page, pageSize);
|
load(path, page, pageSize, sortBy, sortOrder);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
load(path, pagination.current, pagination.pageSize);
|
load(path, pagination.current, pagination.pageSize, sortBy, sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSortChange = (sb: string, so: string) => {
|
||||||
|
setSortBy(sb);
|
||||||
|
setSortOrder(so);
|
||||||
|
load(path, 1, pagination.pageSize, sb, so);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
entries,
|
entries,
|
||||||
loading,
|
loading,
|
||||||
pagination,
|
pagination,
|
||||||
processorTypes,
|
processorTypes,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
load,
|
load,
|
||||||
navigateTo,
|
navigateTo,
|
||||||
goUp,
|
goUp,
|
||||||
handlePaginationChange,
|
handlePaginationChange,
|
||||||
refresh,
|
refresh,
|
||||||
|
handleSortChange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user